oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1package memoria
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "amadan.net/oficina/suite"
10
11 "amadan.net/oficina/memoria/internal/memoria/sibling"
12)
13
14// breakerClock is a fixed "now" so the day arithmetic is testable without
15// waiting a week. Midday, so nothing sits on a boundary by accident.
16var breakerClock = time.Date(2026, 9, 7, 12, 0, 0, 0, time.UTC)
17
18func daysBefore(n int) time.Time { return breakerClock.AddDate(0, 0, -n) }
19
20// countOn files answers for one person on one day.
21func countOn(t *testing.T, s *Store, app, person string, when time.Time, visible, notVisible int) {
22 t.Helper()
23 answers := make([]suite.Authorization, 0, visible+notVisible)
24 for i := 0; i < visible; i++ {
25 answers = append(answers, suite.VISIBLE)
26 }
27 for i := 0; i < notVisible; i++ {
28 answers = append(answers, suite.NOT_VISIBLE)
29 }
30 if len(answers) == 0 {
31 return
32 }
33 asker := suite.Subject{Kind: suite.SubjectAddress, Value: person}
34 if err := s.recordAuthorizationsAt(context.Background(), app, asker, answers, when); err != nil {
35 t.Fatalf("record: %v", err)
36 }
37}
38
39// breakerStore is a store that has been counting long enough to have a
40// full baseline, so a test is about the predicate rather than about the
41// instance's age.
42func breakerStore(t *testing.T) *Store {
43 t.Helper()
44 s := storeForTest(t)
45 if err := s.StartCounting(context.Background(), "Sheets", daysBefore(30)); err != nil {
46 t.Fatalf("StartCounting: %v", err)
47 }
48 return s
49}
50
51func verdict(t *testing.T, s *Store) Breaker {
52 t.Helper()
53 b, err := s.RefusalBreaker(context.Background(), "Sheets", breakerClock)
54 if err != nil {
55 t.Fatalf("RefusalBreaker: %v", err)
56 }
57 return b
58}
59
60// Gate 8: **five named cases, and three of them are cases where it must
61// stay silent.**
62//
63// The silence is the hard part. A notice that fires for an ordinary person
64// is a notice owners learn to skip, and an owner who skips it is in the
65// same position as one who was never told — so the three quiet cases are
66// not edge cases around the interesting one, they *are* the design. Draft
67// 3's predicate fired on the routine case; draft 4 wrote the right
68// sentence and kept a predicate that was a property of whoever queried
69// that day.
70//
71// MUTATION-TESTED 2026-09-07 with the four the spec itself names, each
72// verified red then reverted:
73//
74// (i) replace the per-asker transition with "zero VISIBLE in the
75// window" -> the Docs-only case AND the outage case go red
76// (ii) count VISIBLE absence instead of NOT_VISIBLE presence
77// -> the outage case goes red alone
78// (iii) let the baseline overlap the window
79// -> the fires case goes red, because every firing asker is then
80// their own baseline
81// (iv) return "no anomaly" when the counters are missing
82// -> the cannot-determine case goes red
83func TestTheRefusalBreaker(t *testing.T) {
84 t.Run("fires: three people who could see, now refused", func(t *testing.T) {
85 s := breakerStore(t)
86 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
87 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0) // baseline: allowed
88 countOn(t, s, "Sheets", p, breakerClock, 0, 4) // window: refused
89 }
90 b := verdict(t, s)
91 if b.Verdict != BreakerFiring {
92 t.Errorf("verdict = %v, want firing (askers=%d)", b.Verdict, b.Askers)
93 }
94 if b.Askers != 3 {
95 t.Errorf("askers = %d, want 3", b.Askers)
96 }
97 })
98
99 t.Run("silent: three Docs-only members", func(t *testing.T) {
100 // The case draft 4's predicate got wrong. One person uses Sheets
101 // and three do not; the three query hard and are refused
102 // wholesale, and the Sheets user does not query that day. None of
103 // the three is in the baseline set, so no number of them can trip
104 // it — that is what makes the predicate a property of the sibling.
105 s := breakerStore(t)
106 countOn(t, s, "Sheets", "dana@x", daysBefore(3), 9, 0) // the Sheets user, baseline only
107 for _, p := range []string{"ed@x", "fay@x", "gil@x"} {
108 countOn(t, s, "Sheets", p, breakerClock, 0, 50)
109 }
110 if b := verdict(t, s); b.Verdict != BreakerQuiet {
111 t.Errorf("verdict = %v, want quiet: three people who never had "+
112 "access to Sheets being refused by Sheets is Tuesday, and a "+
113 "notice that fires for it is one owners learn to skip", b.Verdict)
114 }
115 })
116
117 t.Run("silent: a full-day outage", func(t *testing.T) {
118 // Sheets answers UNKNOWN to everything, including to baseline
119 // askers. Zero VISIBLE, and — this is the clause that does the
120 // work — no NOT_VISIBLE at all. §6.5's partial-page notice reports
121 // the outage; a breaker whose whole subject is refusal must not,
122 // or an availability problem is reported as a permissions anomaly
123 // on the day the operator most needs to read the notice correctly.
124 s := breakerStore(t)
125 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
126 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
127 // An outage writes nothing: UNKNOWN is counted as neither.
128 asker := suite.Subject{Kind: suite.SubjectAddress, Value: p}
129 answers := []suite.Authorization{suite.UNKNOWN, suite.UNKNOWN, suite.UNKNOWN}
130 if err := s.recordAuthorizationsAt(context.Background(), "Sheets", asker, answers, breakerClock); err != nil {
131 t.Fatalf("record: %v", err)
132 }
133 }
134 if b := verdict(t, s); b.Verdict != BreakerQuiet {
135 t.Errorf("verdict = %v, want quiet: an outage is all UNKNOWN and "+
136 "no NOT_VISIBLE, and this breaker is about refusal", b.Verdict)
137 }
138 })
139
140 t.Run("silent: below the asker floor", func(t *testing.T) {
141 s := breakerStore(t)
142 for _, p := range []string{"ana@x", "ben@x"} {
143 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
144 countOn(t, s, "Sheets", p, breakerClock, 0, 4)
145 }
146 b := verdict(t, s)
147 if b.Verdict != BreakerQuiet {
148 t.Errorf("verdict = %v, want quiet: two is below the floor", b.Verdict)
149 }
150 // And the margin is reported, so an owner can see it was two
151 // rather than none.
152 if b.Askers != 2 {
153 t.Errorf("askers = %d, want 2", b.Askers)
154 }
155 })
156
157 t.Run("silent: a baseline asker who did not ask", func(t *testing.T) {
158 // Absence is not refusal. Three people had access last week and
159 // simply did not query today; without the "asked in the window"
160 // clause, a quiet day reads as a wholesale refusal.
161 s := breakerStore(t)
162 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
163 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
164 }
165 // Somebody else is refused, so a NOT_VISIBLE exists in the window
166 // and only the per-asker clause can keep this quiet.
167 countOn(t, s, "Sheets", "zoe@x", breakerClock, 0, 1)
168 if b := verdict(t, s); b.Verdict != BreakerQuiet {
169 t.Errorf("verdict = %v, want quiet: three people not querying is "+
170 "not three people being refused", b.Verdict)
171 }
172 })
173
174 t.Run("cannot determine: a fresh instance names what it lacks", func(t *testing.T) {
175 // §8.1: this is the one place where mistaking "I cannot tell" for
176 // "no anomaly" would silently disable a guard rather than
177 // mis-render a page.
178 s := storeForTest(t)
179 if err := s.StartCounting(context.Background(), "Sheets", daysBefore(2)); err != nil {
180 t.Fatalf("StartCounting: %v", err)
181 }
182 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
183 countOn(t, s, "Sheets", p, daysBefore(1), 5, 0)
184 countOn(t, s, "Sheets", p, breakerClock, 0, 4)
185 }
186 b := verdict(t, s)
187 if b.Verdict != BreakerCannotDetermine {
188 t.Fatalf("verdict = %v, want cannot-determine: two days of history "+
189 "cannot answer a question about seven", b.Verdict)
190 }
191 if !strings.Contains(strings.Join(b.Lacking, " "), "baseline") {
192 t.Errorf("lacking = %v, want it to name the baseline", b.Lacking)
193 }
194 })
195
196 t.Run("cannot determine: no counters at all", func(t *testing.T) {
197 s := storeForTest(t)
198 b := verdict(t, s)
199 if b.Verdict != BreakerCannotDetermine {
200 t.Errorf("verdict = %v: a sibling nothing has ever counted must "+
201 "not report 'no anomaly'", b.Verdict)
202 }
203 if len(b.Lacking) == 0 {
204 t.Error("cannot-determine named nothing; §10 gate 8 requires the " +
205 "page to say WHICH of window and baseline it lacks")
206 }
207 })
208}
209
210// The counters survive a restart, which is the assertion that they are
211// durable rather than in memory.
212//
213// memoria hibernates, so an in-memory window resets on every wake and a
214// breaker that can never accumulate seven days of baseline is a breaker
215// that is off — reading, on the owner's page, as "no anomaly". That is the
216// failure the whole storage choice exists to prevent.
217//
218// MUTATION-TESTED 2026-09-07: hold the tallies in a map on the Store and
219// this goes red on the second open. Reverted.
220func TestBreakerCountersSurviveARestart(t *testing.T) {
221 ctx := context.Background()
222 dir := t.TempDir()
223
224 first := storeAt(t, dir)
225 if err := first.StartCounting(ctx, "Sheets", daysBefore(30)); err != nil {
226 t.Fatalf("StartCounting: %v", err)
227 }
228 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
229 countOn(t, first, "Sheets", p, daysBefore(3), 5, 0)
230 countOn(t, first, "Sheets", p, breakerClock, 0, 4)
231 }
232 if b := verdict(t, first); b.Verdict != BreakerFiring {
233 t.Fatalf("the fixture did not fire before the restart: %v", b.Verdict)
234 }
235
236 // Stop and start: a new Store over the same database file, with the
237 // same DEK, exactly as a wake does.
238 second := storeAt(t, dir)
239 b, err := second.RefusalBreaker(ctx, "Sheets", breakerClock)
240 if err != nil {
241 t.Fatalf("RefusalBreaker after restart: %v", err)
242 }
243 if b.Verdict != BreakerFiring {
244 t.Errorf("verdict after restart = %v, want firing: the baseline did "+
245 "not survive, so this instance can never accumulate one and the "+
246 "breaker is permanently off while reading as 'no anomaly'", b.Verdict)
247 }
248}
249
250// The counters are retained 8 days and no longer.
251//
252// §8.1's "no longer" is the load-bearing half: this is the only per-person
253// record memoria keeps, and a retention rule nothing enforces is a
254// sentence in a document.
255//
256// TWO-SIDED: the rows inside the window must survive, or "delete
257// everything" passes.
258//
259// MUTATION-TESTED 2026-09-07, each verified red then reverted:
260//
261// prune nothing -> the old-rows assertion red
262// prune everything -> the kept-rows assertion red
263// off-by-one on the cutoff -> the boundary assertion red
264func TestTalliesAreRetainedEightDaysAndNoLonger(t *testing.T) {
265 ctx := context.Background()
266 s := breakerStore(t)
267 for _, d := range []int{0, 1, 7, 8, 9, 30} {
268 countOn(t, s, "Sheets", "ana@x", daysBefore(d), 1, 0)
269 }
270 if got := tallyDays(t, s); len(got) != 6 {
271 t.Fatalf("%d tally rows seeded, want 6", len(got))
272 }
273
274 n, err := s.PruneTallies(ctx, breakerClock)
275 if err != nil {
276 t.Fatalf("PruneTallies: %v", err)
277 }
278 if n != 2 {
279 t.Errorf("pruned %d rows, want 2 (day 9 and day 30)", n)
280 }
281 got := tallyDays(t, s)
282 if len(got) != 4 {
283 t.Fatalf("%d rows left, want 4 (days 0, 1, 7 and 8)", len(got))
284 }
285
286 // **Day 7 is the oldest day anything reads** — the baseline runs
287 // `[today-7, today-1]` — and day 8 is the spare.
288 //
289 // An earlier version of this said day 8 was "the oldest baseline day",
290 // which is simply false, and a review caught the test explaining
291 // itself with the wrong reason. §8.1 says "retained 8 days and no
292 // longer" and in the same breath calls that "the window plus the
293 // baseline plus one", which is 1 + 7 + 1 = 9 distinct days. The two
294 // numbers in that sentence disagree; the code keeps nine, because the
295 // spare day is what the "plus one" is for and losing it to a prune
296 // that runs a minute before midnight would shorten a real baseline.
297 if !got[daysBefore(7).Format("2006-01-02")] {
298 t.Error("the oldest baseline day was pruned; every window is then " +
299 "computed against less history than the predicate assumes")
300 }
301 if !got[daysBefore(8).Format("2006-01-02")] {
302 t.Error("the spare day was pruned; a prune running just before " +
303 "midnight would take a day the next baseline needs")
304 }
305 if got[daysBefore(9).Format("2006-01-02")] {
306 t.Error("the day-9 row survived; this is the only per-person record " +
307 "memoria keeps and the retention rule is what bounds it")
308 }
309}
310
311func tallyDays(t *testing.T, s *Store) map[string]bool {
312 t.Helper()
313 var rows []SiblingTally
314 if err := s.db.G.Find(&rows).Error; err != nil {
315 t.Fatalf("read tallies: %v", err)
316 }
317 out := map[string]bool{}
318 for _, r := range rows {
319 out[r.Day] = true
320 }
321 return out
322}
323
324// The counters are sealed, and the asker is one-way.
325//
326// This is the only per-person record memoria keeps — "Ana was refused 40
327// times by Sheets on Tuesday" is a fact about Ana that nothing else in the
328// store carries. §3.2's leak list names it; this is what holds it to what
329// the list says.
330//
331// PRESENCE FIRST: the row really does hold those numbers and that person,
332// so the absence checks are assertions rather than descriptions of an
333// empty table.
334//
335// MUTATION-TESTED 2026-09-07, each verified red then reverted:
336//
337// store the counts as plain columns -> the sealed check red
338// store the asker address verbatim -> the fingerprint check red
339// use Seal rather than Fingerprint for the asker
340// -> green on this test, and it is NOT a finding here: it would
341// still be unreadable in the bucket. It is refused in the code
342// for a different reason, stated at seal.PurposeAsker — a
343// reversible id makes this table a roster of who reads what, and
344// nothing in the predicate needs the address back.
345func TestBreakerCountersAreSealedAndTheAskerIsOneWay(t *testing.T) {
346 s := breakerStore(t)
347 countOn(t, s, "Sheets", "ana@example.com", breakerClock, 3, 41)
348
349 var rows []SiblingTally
350 if err := s.db.G.Find(&rows).Error; err != nil {
351 t.Fatalf("read: %v", err)
352 }
353 if len(rows) != 1 {
354 t.Fatalf("%d rows, want 1", len(rows))
355 }
356 // Presence: the numbers really are in there.
357 got, err := s.openTally(rows[0].SealedCounts)
358 if err != nil {
359 t.Fatalf("open: %v", err)
360 }
361 if got.Visible != 3 || got.NotVisible != 41 {
362 t.Fatalf("tally = %+v, want {3 41}", got)
363 }
364
365 blob := string(rows[0].SealedCounts)
366 if strings.Contains(blob, "41") || strings.Contains(blob, `"n"`) {
367 t.Error("the counts are legible at rest: how much one person was " +
368 "refused is a fact about that person")
369 }
370 if strings.Contains(rows[0].AskerPrint, "ana@example.com") {
371 t.Error("the asker's address is in the row; this table would be a " +
372 "roster of who reads what")
373 }
374 // And the fingerprint is stable, or the same person counts as two and
375 // the per-asker transition can never see anybody twice.
376 countOn(t, s, "Sheets", "ana@example.com", breakerClock, 1, 0)
377 if err := s.db.G.Find(&rows).Error; err != nil {
378 t.Fatalf("read: %v", err)
379 }
380 if len(rows) != 1 {
381 t.Errorf("%d rows after a second call from the same person, want 1: "+
382 "an unstable fingerprint makes one person look like many, and "+
383 "the per-asker transition then never sees anyone twice", len(rows))
384 }
385}
386
387// The baseline and the window do not overlap, asserted on the boundary
388// itself rather than through the predicate.
389//
390// §10 gate 8's third mutation is "let the baseline overlap the window",
391// and it expects the fires case to go red because every firing asker is
392// then their own baseline. **That mutation survives the predicate here,
393// and the reason is worth stating rather than hiding**: with day-granular
394// counters an asker cannot both have a VISIBLE and count as refused on the
395// same day — the row is one aggregate, so `visible > 0` excludes them
396// whichever period the day belongs to. The spec's mutation is written
397// against a rolling window, where intra-day ordering exists and today's
398// own grant can serve as its own baseline.
399//
400// That is the same day-granularity tension the window comment describes,
401// surfacing as a mutation the predicate cannot see. So the property is
402// asserted where it IS visible: the two ranges, directly. Overlapping them
403// goes red here even though it does not there.
404//
405// MUTATION-TESTED 2026-09-07, each verified red then reverted:
406//
407// baseTo becomes today -> red
408// baseFrom becomes today - 6 -> red (the baseline is 7 days)
409// the window becomes yesterday -> red
410func TestTheBaselineAndTheWindowDoNotOverlap(t *testing.T) {
411 window := breakerWindow(breakerClock)
412 from, to := breakerBaseline(breakerClock)
413
414 if to >= window {
415 t.Errorf("baseline ends %s and the window is %s: they overlap, so an "+
416 "asker can be their own baseline and the predicate is vacuous",
417 to, window)
418 }
419 if want := breakerClock.AddDate(0, 0, -1).Format("2006-01-02"); to != want {
420 t.Errorf("baseline ends %s, want %s — the day before the window, with "+
421 "no gap: a gap would silently shorten the baseline", to, want)
422 }
423 if want := breakerClock.AddDate(0, 0, -breakerBaselineDays).Format("2006-01-02"); from != want {
424 t.Errorf("baseline starts %s, want %s (%d days)", from, want, breakerBaselineDays)
425 }
426 if want := breakerClock.Format("2006-01-02"); window != want {
427 t.Errorf("window = %s, want %s", window, want)
428 }
429}
430
431// An unreadable counter makes the breaker say so, not go quiet.
432//
433// A seal failure, a partial restore, a truncated row: whatever the cause,
434// the period cannot be read, and a period that cannot be read is not a
435// period with nothing in it. Skipping the row instead would make the
436// window look quieter than it was, and quieter is the direction that turns
437// a firing breaker into a silent one — §8.1's "a count that fails is not a
438// count of zero", on the one guard where that mistake disables rather than
439// mis-renders.
440//
441// PRESENCE FIRST: the fixture fires before the row is corrupted, so this
442// is a statement about the corruption and not about an empty table.
443//
444// MUTATION-TESTED 2026-09-07, each verified red then reverted:
445//
446// tallyRange skips a row it cannot open -> red, the breaker goes quiet
447// RefusalBreaker reports Quiet on a range error -> red
448func TestAnUnreadableCounterIsNotAQuietOne(t *testing.T) {
449 s := breakerStore(t)
450 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
451 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
452 countOn(t, s, "Sheets", p, breakerClock, 0, 4)
453 }
454 if b := verdict(t, s); b.Verdict != BreakerFiring {
455 t.Fatalf("the fixture did not fire before corruption: %v", b.Verdict)
456 }
457
458 // Spoil one baseline row.
459 var row SiblingTally
460 if err := s.db.G.Where("day = ?", daysBefore(3).Format("2006-01-02")).First(&row).Error; err != nil {
461 t.Fatalf("find a row: %v", err)
462 }
463 if err := s.db.G.Model(&SiblingTally{}).Where("id = ?", row.ID).
464 Update("sealed_counts", []byte("not a sealed value")).Error; err != nil {
465 t.Fatalf("corrupt: %v", err)
466 }
467
468 b := verdict(t, s)
469 if b.Verdict == BreakerQuiet {
470 t.Error("a period the breaker cannot read was reported as 'no " +
471 "anomaly'; that is the one mistake §8.1 says disables the guard " +
472 "rather than mis-rendering the page")
473 }
474 if b.Verdict != BreakerCannotDetermine {
475 t.Errorf("verdict = %v, want cannot-determine", b.Verdict)
476 }
477 if len(b.Lacking) == 0 {
478 t.Error("cannot-determine named nothing")
479 }
480}
481
482// Gate 8's placement half: **the firing notice reaches the owner and no
483// reader.**
484//
485// §8.1 is explicit about why. A message telling one reader how much they
486// were not allowed to see is a disclosure with a number in it, and §4.1
487// makes NOT_VISIBLE the one value that may be silent. The reader's page
488// says what could not be shown and from which app; only the owner's says
489// how many people a sibling stopped answering for.
490//
491// This is the half a unit test on the predicate cannot make: `RefusalBreaker`
492// returning the right verdict says nothing about where the sentence lands.
493//
494// MUTATION-TESTED 2026-09-07, each verified red then reverted:
495//
496// render the breaker sentence into results.html
497// -> the reader's-page case red
498// drop siblingRows from the connectors handler
499// -> the owner's-page case red
500// move the sources route outside requireOwner
501// -> gate 17's route enumeration red, and this test's own
502// owner-only assertion red
503func TestTheBreakerNoticeReachesTheOwnerAndNoReader(t *testing.T) {
504 s := breakerStore(t)
505 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
506 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
507 countOn(t, s, "Sheets", p, breakerClock, 0, 4)
508 }
509 s.custody = &Siblings{
510 client: sibling.NewForTest(),
511 byName: map[string]sibling.Sibling{
512 "Sheets": {Name: "Sheets", BaseURL: "https://acme.sheets.oficina.works", Token: "t"},
513 },
514 }
515
516 b, err := s.RefusalBreaker(context.Background(), "Sheets", breakerClock)
517 if err != nil {
518 t.Fatalf("RefusalBreaker: %v", err)
519 }
520 if b.Verdict != BreakerFiring {
521 t.Fatalf("the fixture did not fire: %v — nothing below is about "+
522 "placement if there is no notice to place", b.Verdict)
523 }
524 sentence, alarming := breakerSentence(b)
525
526 // It names the anomaly rather than gesturing at it: an owner has to be
527 // able to act on this without opening a log.
528 if !strings.Contains(sentence, "Sheets") {
529 t.Errorf("the notice does not name the app: %q", sentence)
530 }
531 if !strings.Contains(sentence, "3") {
532 t.Errorf("the notice does not say how many people: %q", sentence)
533 }
534 // And it says nothing was deleted, because that is the question an
535 // owner reading "refusing people it used to allow" will ask next, and
536 // §8.1 promises all three breakers purge nothing.
537 if !strings.Contains(strings.ToLower(sentence), "deleted") {
538 t.Errorf("the notice does not say nothing was deleted: %q", sentence)
539 }
540 if !alarming {
541 t.Error("a firing breaker did not mark its row for attention")
542 }
543
544 // **The placement half lives in TestTheFiringCountReachesOnlyTheOwner**,
545 // which runs a real search and reads the notices it actually produced.
546 //
547 // It was here, and it was worthless: it built a `Page` in the test,
548 // wrote a notice into it by hand, and then checked that the string it
549 // had just written did not contain the breaker's words. A review
550 // rendered the firing sentence straight into results.html and this
551 // stayed green, which is exactly what an assertion about your own
552 // fixture is worth.
553}
554
555// "Cannot determine" says which half it lacks, and §10 gate 8 asserts the
556// string.
557//
558// An owner told only "cannot determine" cannot tell a fresh instance from
559// a broken one, and the two want different actions: one is wait, the other
560// is investigate.
561//
562// MUTATION-TESTED 2026-09-07: return the bare phrase without the Lacking
563// list, and this goes red. Reverted.
564func TestCannotDetermineSaysWhichHalfIsMissing(t *testing.T) {
565 // The two combinations `RefusalBreaker` can actually produce. A
566 // window-only lack is unreachable — counting cannot begin after today
567 // without also beginning after the baseline — and a review pointed
568 // out that testing it describes a state rather than asserting one.
569 // The singular/plural split is still worth pinning, so the
570 // baseline-only case carries it.
571 for _, c := range []struct {
572 lacking []string
573 want []string
574 }{
575 {[]string{"the baseline"}, []string{"baseline", "is missing"}},
576 {[]string{"the baseline", "the window"}, []string{"baseline", "window", "are missing"}},
577 } {
578 got, alarming := breakerSentence(Breaker{
579 App: "Sheets", Verdict: BreakerCannotDetermine, Lacking: c.lacking,
580 })
581 for _, want := range c.want {
582 if !strings.Contains(got, want) {
583 t.Errorf("lacking %v produced %q, want it to contain %q",
584 c.lacking, got, want)
585 }
586 }
587 // It must never read as reassurance.
588 if strings.Contains(got, "No refusal anomaly") {
589 t.Errorf("cannot-determine read as no-anomaly: %q", got)
590 }
591 if !strings.Contains(got, "not the same as no anomaly") {
592 t.Errorf("the notice does not distinguish itself from 'no "+
593 "anomaly', which is the one confusion §8.1 says disables "+
594 "the guard: %q", got)
595 }
596 if !alarming {
597 t.Errorf("cannot-determine did not mark its row for attention: %q", got)
598 }
599 }
600}
601
602// A row with nothing wrong carries no alarm, and the two lines are toned
603// independently.
604//
605// **This exists because looking at the rendered page found two bugs the
606// whole Go suite passed over**, on markup every template test agreed was
607// correct:
608//
609// 1. one `Alarming` flag carried both lines, so a sibling that had merely
610// not been polled yet rendered "No refusal anomaly." inside a warning
611// callout — a warning box with reassurance in it;
612// 2. "not polled yet" was itself alarming, and memoria hibernates, so
613// every row of every freshly-woken instance warned about the ordinary
614// case. §8.1's argument about a breaker that fires for ordinary people,
615// arriving in the freshness line instead.
616//
617// Both are about what a person sees and neither changes a string a test
618// was checking for. What this pins is the property underneath: the alarm
619// is reserved for something actually being wrong.
620//
621// MUTATION-TESTED 2026-09-07, each verified red then reverted:
622//
623// breakerSentence takes an incoming flag again -> quiet-row case red
624// "not polled yet" sets CurrentAlarming -> quiet-row case red
625// a firing breaker returns false -> firing case red
626// a cannot-determine returns false -> cannot-determine red
627func TestOnlyAWrongThingIsAlarming(t *testing.T) {
628 for _, c := range []struct {
629 name string
630 b Breaker
631 alarm bool
632 }{
633 {"quiet", Breaker{App: "Docs", Verdict: BreakerQuiet}, false},
634 {"firing", Breaker{App: "Sheets", Verdict: BreakerFiring, Askers: 3}, true},
635 {"cannot determine", Breaker{App: "Cal", Verdict: BreakerCannotDetermine,
636 Lacking: []string{"the baseline"}}, true},
637 } {
638 _, alarm := breakerSentence(c.b)
639 if alarm != c.alarm {
640 t.Errorf("%s: alarming = %v, want %v", c.name, alarm, c.alarm)
641 }
642 }
643
644 // And the two lines cannot infect each other: a quiet breaker stays
645 // quiet whatever the freshness line is doing. Asserted by the
646 // signature — breakerSentence takes no incoming flag — and here by
647 // the value, because a signature is not a test.
648 if _, alarm := breakerSentence(Breaker{App: "Docs", Verdict: BreakerQuiet}); alarm {
649 t.Error("a quiet breaker reported an alarm; the first version " +
650 "inherited one from the freshness line and rendered " +
651 "\"No refusal anomaly.\" in a warning box")
652 }
653}
654
655// **An outage plus one ordinary refusal must not fire**, and the
656// window-global reading of §8.1's third clause let it.
657//
658// This is §13's finding, found again by a review that built the fixture:
659// Sheets is down and answers UNKNOWN to every baseline asker, while one
660// Docs-only member gets a single routine NOT_VISIBLE. Read globally, "the
661// window contains at least one NOT_VISIBLE" is satisfied by that one
662// query — so a 24-hour outage fires the refusal breaker, which is the
663// exact case §8.1's prose says is excluded.
664//
665// Per counted asker it cannot: each of the three has zero NOT_VISIBLE of
666// their own, so none is counted, and the stranger is not in the baseline.
667//
668// MUTATION-TESTED 2026-09-07, each verified red then reverted:
669//
670// restore the window-global anyNotVisible clause -> red, it fires
671// count `t.Visible == 0` without requiring a NOT_VISIBLE
672// -> red, and this is the same bug by a shorter route
673func TestAnOutageWithOneRoutineRefusalDoesNotFire(t *testing.T) {
674 ctx := context.Background()
675 s := breakerStore(t)
676
677 // Three people who could read from Sheets last week.
678 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
679 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
680 // Today Sheets is down: they ask, and get UNKNOWN. Nothing this
681 // breaker counts, so no row — which reads the same as not having
682 // asked, and that is correct: an outage tells you nothing about
683 // permissions either way.
684 asker := suite.Subject{Kind: suite.SubjectAddress, Value: p}
685 answers := []suite.Authorization{suite.UNKNOWN, suite.UNKNOWN, suite.UNKNOWN}
686 if err := s.recordAuthorizationsAt(ctx, "Sheets", asker, answers, breakerClock); err != nil {
687 t.Fatalf("record: %v", err)
688 }
689 }
690
691 // And one person who never had Sheets access gets one ordinary
692 // refusal — the single NOT_VISIBLE that the window-global clause
693 // accepted on everybody else's behalf.
694 countOn(t, s, "Sheets", "stranger@x", breakerClock, 0, 1)
695
696 b := verdict(t, s)
697 if b.Verdict == BreakerFiring {
698 t.Errorf("an outage plus one routine refusal fired the breaker "+
699 "(askers=%d): the NOT_VISIBLE clause has to be a property of "+
700 "each counted asker, or one stranger's ordinary Tuesday speaks "+
701 "for three people who were not refused at all", b.Askers)
702 }
703 if b.Verdict != BreakerQuiet {
704 t.Errorf("verdict = %v, want quiet", b.Verdict)
705 }
706 if b.Askers != 0 {
707 t.Errorf("askers = %d, want 0: nobody was refused", b.Askers)
708 }
709}
710
711// The baseline is people the sibling ALLOWED, not people who queried.
712//
713// `t.Visible > 0` is the whole of it. Relaxed to `>= 0` the baseline
714// becomes "anyone who asked last week", and three Docs-only members who
715// happened to query last week and are refused today trip it — draft 4's
716// failure with a week's delay. A review found the clause unpinned.
717//
718// MUTATION-TESTED 2026-09-07: `t.Visible > 0` -> `t.Visible >= 0`. Red.
719// Reverted.
720func TestTheBaselineIsWhoWasAllowedNotWhoAsked(t *testing.T) {
721 s := breakerStore(t)
722 // Three people who asked Sheets last week and were refused every
723 // time. They are not in the baseline, because Sheets never allowed
724 // them anything.
725 for _, p := range []string{"ed@x", "fay@x", "gil@x"} {
726 countOn(t, s, "Sheets", p, daysBefore(3), 0, 6)
727 countOn(t, s, "Sheets", p, breakerClock, 0, 4)
728 }
729 if b := verdict(t, s); b.Verdict != BreakerQuiet {
730 t.Errorf("verdict = %v, want quiet: being refused this week the same "+
731 "way as last week is not a transition, and a breaker that fires "+
732 "for it fires forever", b.Verdict)
733 }
734}
735
736// A mixed day is not a refusal.
737//
738// Somebody who saw four passages and was refused one had an ordinary day.
739// Counting them makes the predicate "was refused at all" rather than "was
740// refused everything", and three ordinary days then fire the breaker. A
741// review found this clause unpinned too.
742//
743// MUTATION-TESTED 2026-09-07: `t.Visible == 0` -> `true`. Red. Reverted.
744func TestAMixedDayIsNotARefusal(t *testing.T) {
745 s := breakerStore(t)
746 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
747 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
748 countOn(t, s, "Sheets", p, breakerClock, 4, 1)
749 }
750 if b := verdict(t, s); b.Verdict != BreakerQuiet {
751 t.Errorf("verdict = %v, want quiet: seeing four things and being "+
752 "refused one is a Tuesday, not an anomaly", b.Verdict)
753 }
754}
755