oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1package sibling
2
3import (
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.
46func 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.
93func 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"
151func 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.
229func 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