| 1 | package sibling |
| 2 | |
| 3 | import ( |
| 4 | "reflect" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "amadan.net/oficina/suite" |
| 9 | ) |
| 10 | |
| 11 | // Gate 6, first half: **every S2 result is handled, and exactly one of them |
| 12 | // is allowed to be silent.** |
| 13 | // |
| 14 | // §4.1's table row by row. The interesting assertion is not that each value |
| 15 | // maps somewhere — a `return Withhold` for everything satisfies that — but |
| 16 | // that the *silence* is where the table puts it. `NOT_VISIBLE` is silent |
| 17 | // because a notice there reports the existence of a passage this person may |
| 18 | // not have. Every other value is loud because a shorter page that looks |
| 19 | // complete is the plausible empty list. |
| 20 | // |
| 21 | // TWO-SIDED in both directions: the table below pins what renders (so |
| 22 | // "withhold everything" is red) and what stays quiet (so "notice |
| 23 | // everything" is red). |
| 24 | // |
| 25 | // MUTATION-TESTED 2026-09-06, each verified red then reverted: |
| 26 | // |
| 27 | // STALE -> OmitSilently (draft 2's actual bug) -> red |
| 28 | // UNKNOWN -> OmitSilently -> red |
| 29 | // NOT_VISIBLE -> Withhold -> red |
| 30 | // the default arm -> OmitSilently -> red on PERHAPS |
| 31 | // Notices() returns true for OmitSilently -> red |
| 32 | // Renders() returns true for Withhold -> red |
| 33 | // |
| 34 | // And the one a review found, which needed two edits at once and so was |
| 35 | // invisible to every single-edit mutation: |
| 36 | // |
| 37 | // Notices() written as `switch { case <the loud three>: true }` |
| 38 | // PLUS a sixth constant added to the block |
| 39 | // -> red on the quiet-count assertion. Either edit alone is |
| 40 | // harmless: the switch form agrees with the current form for the |
| 41 | // five values that exist, and a sixth constant under the current |
| 42 | // form is loud by default. Together they produce a disposition |
| 43 | // that neither renders nor notices — a second value permitted to |
| 44 | // hide a passage without admitting to it — and the AHEAD tripwire |
| 45 | // instructs the next person to make exactly one of the two. |
| 46 | func TestEveryS2ResultIsHandled(t *testing.T) { |
| 47 | for _, c := range []struct { |
| 48 | result suite.Authorization |
| 49 | want Disposition |
| 50 | renders bool |
| 51 | notices bool |
| 52 | }{ |
| 53 | {suite.VISIBLE, Render, true, false}, |
| 54 | {suite.NOT_VISIBLE, OmitSilently, false, false}, |
| 55 | {suite.STALE, WithholdAndRefetch, false, true}, |
| 56 | {suite.UNKNOWN, Withhold, false, true}, |
| 57 | |
| 58 | // Gate 6's named mutation: a sixth value the client does not know. |
| 59 | // It must withhold loudly, never fall through to omission — an |
| 60 | // unrecognised word is a sibling this client does not understand, |
| 61 | // and omission is the one outcome a reader cannot tell from an |
| 62 | // honest absence. |
| 63 | {suite.Authorization("PERHAPS"), Withhold, false, true}, |
| 64 | |
| 65 | // And the two shapes a value arrives in when something upstream went |
| 66 | // wrong rather than something being said: an empty string, and the |
| 67 | // value with the right letters in the wrong case. Neither renders. |
| 68 | {suite.Authorization(""), Withhold, false, true}, |
| 69 | {suite.Authorization("visible"), Withhold, false, true}, |
| 70 | } { |
| 71 | got := Decide(c.result) |
| 72 | if got != c.want { |
| 73 | t.Errorf("Decide(%q) = %v, want %v", c.result, got, c.want) |
| 74 | } |
| 75 | if got.Renders() != c.renders { |
| 76 | t.Errorf("Decide(%q).Renders() = %v, want %v", c.result, got.Renders(), c.renders) |
| 77 | } |
| 78 | if got.Notices() != c.notices { |
| 79 | t.Errorf("Decide(%q).Notices() = %v, want %v", c.result, got.Notices(), c.notices) |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | // Gate 6, second half: **the zero value is the safe one.** |
| 85 | // |
| 86 | // Go hands an unset field, a missing map entry and a variable nobody |
| 87 | // assigned the same thing, and this asserts what that thing is. If Render |
| 88 | // sat at iota's first position, each of those accidents would speak a |
| 89 | // passage nobody authorised. |
| 90 | // |
| 91 | // MUTATION-TESTED 2026-09-06: move Render above Withhold in the const |
| 92 | // block; red here and red in TestEveryS2ResultIsHandled. Reverted. |
| 93 | func TestTheZeroDispositionWithholds(t *testing.T) { |
| 94 | var unset Disposition |
| 95 | if unset.Renders() { |
| 96 | t.Error("the zero Disposition renders: a struct field nobody set, a " + |
| 97 | "map miss, or a forgotten assignment now speaks a passage") |
| 98 | } |
| 99 | if !unset.Notices() { |
| 100 | t.Error("the zero Disposition is silent: an outcome nobody assigned " + |
| 101 | "is not the one value permitted to say nothing") |
| 102 | } |
| 103 | |
| 104 | // And exactly one disposition is quiet without rendering, because "the |
| 105 | // only value that may be silent" is a claim about the whole set. |
| 106 | // |
| 107 | // **Iterated to numDispositions rather than over a literal**, because |
| 108 | // the literal version made that claim about the five values its author |
| 109 | // remembered: a reviewer appended a sixth constant, touched nothing |
| 110 | // else, and got a disposition that neither rendered nor noticed while |
| 111 | // this test stayed green. The set has to enumerate itself. |
| 112 | quiet := 0 |
| 113 | for d := Disposition(0); d < numDispositions; d++ { |
| 114 | if !d.Renders() && !d.Notices() { |
| 115 | quiet++ |
| 116 | if d != OmitSilently { |
| 117 | t.Errorf("%v neither renders nor notices, and is not OmitSilently: "+ |
| 118 | "a second silent disposition is a second way to serve a "+ |
| 119 | "short page that looks complete", d) |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | if quiet != 1 { |
| 124 | t.Errorf("%d silent dispositions, want exactly 1", quiet) |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | // The page's notices. §6.5: "the page names what is missing and from which |
| 129 | // app", and §8.1's copy: *"Sheets did not answer; 7 results withheld."* |
| 130 | // |
| 131 | // The structural half of this is not asserted here because it cannot fail: |
| 132 | // Record takes an app and a disposition and **no item**, so no title, id or |
| 133 | // snippet can reach a notice line. That is gate 4's rule about content in |
| 134 | // logs, made by leaving out the parameter rather than by grepping for what |
| 135 | // went through it. |
| 136 | // |
| 137 | // MUTATION-TESTED 2026-09-06, each verified red then reverted: |
| 138 | // |
| 139 | // Record files rendered and silently-omitted rulings too |
| 140 | // -> red on the stored state. **This one SURVIVED the first version of |
| 141 | // this test**, which read only the rendered lines: Lines() has no arm |
| 142 | // for the quiet dispositions, so the page was identical while the |
| 143 | // Outcome quietly grew a per-app count of what the reader may not see. |
| 144 | // The assertion that catches it reads o.counts directly. |
| 145 | // Lines() iterates the map instead of the order slice |
| 146 | // -> flaky, then red on the ordering assertion (10 runs, -count=10) |
| 147 | // the reconciling line gains a count |
| 148 | // -> red |
| 149 | // results() drops the singular case |
| 150 | // -> red on "1 result" |
| 151 | func TestTheNoticesNameTheAppAndTheShapeOfTheLoss(t *testing.T) { |
| 152 | var o Outcome |
| 153 | o.Record("Sheets", Decide(suite.UNKNOWN)) |
| 154 | o.Record("Sheets", Decide(suite.UNKNOWN)) |
| 155 | o.Record("Sheets", Decide(suite.VISIBLE)) // renders; no notice |
| 156 | o.Record("Sheets", Decide(suite.NOT_VISIBLE)) // silent; no notice |
| 157 | o.Record("Docs", Decide(suite.STALE)) |
| 158 | o.Record("Calendar", WithholdAndReannounce) |
| 159 | |
| 160 | got := o.Lines() |
| 161 | want := []string{ |
| 162 | "Sheets did not answer; 2 results withheld.", |
| 163 | "Docs has moved on from 1 result; refetching.", |
| 164 | "Calendar is reconciling after a restore; its results are not on this page.", |
| 165 | } |
| 166 | if !reflect.DeepEqual(got, want) { |
| 167 | t.Errorf("notices =\n %s\nwant\n %s", |
| 168 | strings.Join(got, "\n "), strings.Join(want, "\n ")) |
| 169 | } |
| 170 | |
| 171 | // **The silent ruling must not have been stored, not merely not |
| 172 | // printed**, and this assertion exists because the copy-only version of |
| 173 | // it survived its mutation. |
| 174 | // |
| 175 | // `Record` filing everything and `Lines` having no arm for the quiet |
| 176 | // dispositions renders identically — every assertion above stays green — |
| 177 | // while `counts` grows a per-app tally of passages this person may not |
| 178 | // see. Nothing displays it today. It is one switch arm, one debug print |
| 179 | // or one metric label away from being exactly the count §4.1 forbids, |
| 180 | // and the version of this test that only read the rendered lines could |
| 181 | // not tell the two apart. So it reads the state. |
| 182 | for k := range o.counts { |
| 183 | if k.kind == Render || k.kind == OmitSilently { |
| 184 | t.Errorf("Outcome stored a %v ruling for %q: a count of what a "+ |
| 185 | "person may not see, held per app, awaiting a line to print it", |
| 186 | k.kind, k.app) |
| 187 | } |
| 188 | } |
| 189 | if len(o.counts) != 3 { |
| 190 | t.Errorf("Outcome holds %d keys, want 3 — one per loud (app, kind) pair "+ |
| 191 | "and nothing else", len(o.counts)) |
| 192 | } |
| 193 | |
| 194 | if reannounce := o.SourcesToReannounce(); !reflect.DeepEqual(reannounce, []string{"Calendar"}) { |
| 195 | t.Errorf("SourcesToReannounce() = %v, want [Calendar]", reannounce) |
| 196 | } |
| 197 | |
| 198 | t.Run("a page with nothing withheld says nothing", func(t *testing.T) { |
| 199 | var clean Outcome |
| 200 | clean.Record("Sheets", Decide(suite.VISIBLE)) |
| 201 | clean.Record("Sheets", Decide(suite.NOT_VISIBLE)) |
| 202 | if lines := clean.Lines(); len(lines) != 0 { |
| 203 | t.Errorf("a fully-answered page carried notices: %v", lines) |
| 204 | } |
| 205 | if apps := clean.SourcesToReannounce(); len(apps) != 0 { |
| 206 | t.Errorf("a fully-answered page marked sources for re-announce: %v", apps) |
| 207 | } |
| 208 | }) |
| 209 | } |
| 210 | |
| 211 | // **AHEAD is adopted and not yet on the wire, and this is the tripwire.** |
| 212 | // |
| 213 | // §4.1: suite doctrine revision 10 settled `AHEAD` on 2026-09-06, on |
| 214 | // memoria's argument, and the module carries it on a branch that has not |
| 215 | // shipped — held by suite CI being unconfigured (§13). The version this |
| 216 | // repository pins has four constants. So a sibling answering "AHEAD" today |
| 217 | // is folded to UNKNOWN by the module and withheld loudly here: safe, and |
| 218 | // lossy. What is lost is the marking for re-announce. |
| 219 | // |
| 220 | // A shipping dependency nothing checks is a shipping dependency nobody |
| 221 | // notices has arrived. **This test goes red the day the module gains the |
| 222 | // value** — DecodeS2 will stop folding it — and whoever bumps the |
| 223 | // dependency then owes `Decide` its fifth arm and policy.go the deletion of |
| 224 | // the paragraph explaining the lag. |
| 225 | // |
| 226 | // It is deliberately written as an assertion about the *module*, not about |
| 227 | // memoria: memoria's own behaviour here is correct either way, and the |
| 228 | // thing that changes underfoot is upstream. |
| 229 | func TestAheadIsStillAShippingLag(t *testing.T) { |
| 230 | asked := []suite.ItemRevision{{ItemID: "doc-a", Revision: 7}} |
| 231 | body := []byte(`{"results":[{"item_id":"doc-a","revision":7,"result":"AHEAD"}]}`) |
| 232 | |
| 233 | got := suite.DecodeS2(asked, 200, body) |
| 234 | if len(got) != 1 { |
| 235 | t.Fatalf("DecodeS2 returned %d answers for 1 pair", len(got)) |
| 236 | } |
| 237 | if got[0] != suite.UNKNOWN { |
| 238 | t.Fatalf("the pinned suite module now understands %q (it answered %q).\n\n"+ |
| 239 | "That is the shipping lag ending, not a bug. What is owed:\n"+ |
| 240 | " - Decide() gains a `case suite.AHEAD: return WithholdAndReannounce`\n"+ |
| 241 | " - policy.go's aheadNotYetOnTheWire paragraph is deleted\n"+ |
| 242 | " - spec §4.1's \"four on the wire today\" and §13's dependency close\n"+ |
| 243 | " - this test is replaced by the AHEAD row in gate 6's table", |
| 244 | aheadNotYetOnTheWire, got[0]) |
| 245 | } |
| 246 | |
| 247 | // And the memoria side of it, which is correct either way: whatever |
| 248 | // arrives, it withholds loudly and it does not omit. |
| 249 | d := Decide(got[0]) |
| 250 | if d.Renders() || !d.Notices() { |
| 251 | t.Errorf("a folded AHEAD = %v; want withheld loudly. A sibling that "+ |
| 252 | "lost history must not read as a sibling with nothing to say", d) |
| 253 | } |
| 254 | } |
| 255 | |