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 "io"
6 "log/slog"
7 "strings"
8 "testing"
9 "time"
10
11 "amadan.net/oficina/suite"
12
13 "amadan.net/oficina/memoria/internal/memoria/seal"
14 "amadan.net/oficina/memoria/internal/memoria/sibling"
15)
16
17// countingCustody answers every candidate the same way and is a real
18// Custody, so a search goes through the whole production path: rule(),
19// recordForBreaker(), RecordAuthorizations().
20type countingCustody struct {
21 apps map[string]sibling.Sibling
22 give suite.Authorization
23}
24
25func (c *countingCustody) Sibling(source string) (sibling.Sibling, bool) {
26 s, ok := c.apps[source]
27 return s, ok
28}
29
30func (c *countingCustody) Rule(ctx context.Context, asker suite.Subject, cands []sibling.Candidate) []sibling.Disposition {
31 out := make([]sibling.Disposition, len(cands))
32 for i := range cands {
33 out[i] = sibling.Decide(c.give)
34 }
35 return out
36}
37
38// **A search writes the breaker's counters**, end to end, and nothing
39// pinned it.
40//
41// A review deleted the `recordForBreaker` call from `Store.rule` and every
42// package stayed green: the breaker's own tests write counters directly,
43// so they prove the predicate and say nothing about whether anything in
44// production ever feeds it. The wiring and the arithmetic were tested
45// separately and the join was not tested at all — which is how a breaker
46// ships permanently reading "cannot determine" on a working instance.
47//
48// MUTATION-TESTED 2026-09-07, each verified red then reverted:
49//
50// delete the recordForBreaker call in Store.rule
51// -> red, no counters at all
52// record under the request context rather than WithoutCancel
53// -> green here (the ctx is not cancelled in this test) and NOT a
54// finding this test can make; the rule it enforces is AGENTS.md's
55// about derived state, and its cost shows up under load rather
56// than in a fixture. Listed so the next reader knows it was tried.
57// map Render to NOT_VISIBLE
58// -> red on the visible count
59func TestASearchWritesTheBreakersCounters(t *testing.T) {
60 ctx := context.Background()
61
62 for _, c := range []struct {
63 name string
64 give suite.Authorization
65 visible, refused int
66 wantAnyCounterRow bool
67 }{
68 {"a rendered passage counts as visible", suite.VISIBLE, 1, 0, true},
69 {"a silently omitted one counts as refused", suite.NOT_VISIBLE, 0, 1, true},
70 // An outage counts as neither, and writes nothing at all — which
71 // is what keeps the predicate from reading it as refusal.
72 {"an outage counts as neither", suite.UNKNOWN, 0, 0, false},
73 {"a stale passage counts as neither", suite.STALE, 0, 0, false},
74 } {
75 t.Run(c.name, func(t *testing.T) {
76 s := storeForTest(t)
77 s.custody = &countingCustody{
78 apps: map[string]sibling.Sibling{
79 "Docs": {Name: "Docs", BaseURL: "https://acme.docs.oficina.works", Token: "t"},
80 },
81 give: c.give,
82 }
83 if _, err := s.Ingest(ctx, IngestSpec{
84 Source: "Docs", Key: "docs/a", Title: "A",
85 Body: "The quarterly planning process runs in March.",
86 ItemID: "docs-a",
87 SiblingRevision: 3,
88 }); err != nil {
89 t.Fatalf("ingest: %v", err)
90 }
91
92 if _, err := s.Search(ctx, testAsker, "quarterly planning", 10); err != nil {
93 t.Fatalf("search: %v", err)
94 }
95
96 var rows []SiblingTally
97 if err := s.db.G.Find(&rows).Error; err != nil {
98 t.Fatalf("read tallies: %v", err)
99 }
100 if !c.wantAnyCounterRow {
101 if len(rows) != 0 {
102 t.Errorf("%d counter rows for an answer this breaker does "+
103 "not count; a zero row puts the asker in the window as "+
104 "somebody who asked and got nothing", len(rows))
105 }
106 return
107 }
108 if len(rows) != 1 {
109 t.Fatalf("%d counter rows, want 1: a search that answers from "+
110 "a sibling must feed the breaker, or it reads 'cannot "+
111 "determine' on a working instance forever", len(rows))
112 }
113 got, err := s.openTally(rows[0].SealedCounts)
114 if err != nil {
115 t.Fatalf("open: %v", err)
116 }
117 if got.Visible != c.visible || got.NotVisible != c.refused {
118 t.Errorf("tally = %+v, want {%d %d}", got, c.visible, c.refused)
119 }
120 if rows[0].App != "Docs" {
121 t.Errorf("counted against %q, want Docs", rows[0].App)
122 }
123 })
124 }
125}
126
127// **`WireSiblings` starts the counting clock**, and nothing pinned that
128// either.
129//
130// A review deleted the `StartCounting` loop and the suite stayed green.
131// In production that leaves `CountingSince` zero for every sibling, which
132// `RefusalBreaker` reads as "I have not been looking long enough" — so
133// every source reports cannot-determine forever, on an instance that has
134// been counting perfectly well the whole time.
135//
136// MUTATION-TESTED 2026-09-07, each verified red then reverted:
137//
138// delete the StartCounting loop in WireSiblings -> red
139// StartCounting overwrites an existing timestamp -> red on the second
140// call: a wake must not reset the clock, or an instance that
141// hibernates nightly never accumulates a baseline
142func TestWiringStartsTheCountingClock(t *testing.T) {
143 ctx := context.Background()
144 env := func(pairs map[string]string) func(string) string {
145 return func(k string) string { return pairs[k] }
146 }
147 cfg := map[string]string{
148 "MEMORIA_TEAM": "acme",
149 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works",
150 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
151 }
152
153 s := storeForTest(t)
154 a := &app{store: s, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
155 if err := WireSiblings(a, env(cfg), nil); err != nil {
156 t.Fatalf("WireSiblings: %v", err)
157 }
158
159 var row SiblingFeed
160 if err := s.db.G.Where("app = ?", "Docs").First(&row).Error; err != nil {
161 t.Fatalf("no feed row after wiring: %v", err)
162 }
163 first := row.CountingSince
164 if first.IsZero() {
165 t.Fatal("CountingSince is zero after wiring: every sibling then " +
166 "reports cannot-determine forever on an instance that has been " +
167 "counting the whole time")
168 }
169
170 // A second wake must not reset it. memoria hibernates; an instance
171 // that restarts the clock nightly never accumulates seven days.
172 if err := s.StartCounting(ctx, "Docs", time.Now().UTC().Add(time.Hour)); err != nil {
173 t.Fatalf("StartCounting: %v", err)
174 }
175 if err := s.db.G.Where("app = ?", "Docs").First(&row).Error; err != nil {
176 t.Fatal(err)
177 }
178 if !row.CountingSince.Equal(first) {
179 t.Errorf("a second wake moved CountingSince from %v to %v: an "+
180 "instance that hibernates nightly would never accumulate a "+
181 "baseline, and every source would read cannot-determine",
182 first, row.CountingSince)
183 }
184}
185
186// The firing count reaches the owner's page and no reader's.
187//
188// §8.1: a message telling one reader how much they were not allowed to see
189// is a disclosure with a number in it. The first version of this assertion
190// checked a `Page.Notices` slice the test had written itself — it
191// constrained nothing, and a review rendered the breaker sentence straight
192// into results.html with it still green.
193//
194// This asserts the two real surfaces: what `siblingRows` produces for the
195// owner, and what a search's own notices carry.
196//
197// MUTATION-TESTED 2026-09-07, each verified red then reverted:
198//
199// siblingRows returns no Breaker sentence -> owner case red
200// Store.rule copies the breaker verdict into Page.Notices
201// -> reader case red
202func TestTheFiringCountReachesOnlyTheOwner(t *testing.T) {
203 ctx := context.Background()
204 s := breakerStore(t)
205 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
206 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
207 countOn(t, s, "Sheets", p, breakerClock, 0, 4)
208 }
209 s.custody = &countingCustody{
210 apps: map[string]sibling.Sibling{
211 "Sheets": {Name: "Sheets", BaseURL: "https://acme.sheets.oficina.works", Token: "t"},
212 },
213 give: suite.NOT_VISIBLE,
214 }
215 if _, err := s.Ingest(ctx, IngestSpec{
216 Source: "Sheets", Key: "s/a", Title: "A",
217 Body: "The quarterly planning process runs in March.",
218 ItemID: "s-a", SiblingRevision: 1,
219 }); err != nil {
220 t.Fatalf("ingest: %v", err)
221 }
222
223 // The reader's side: a real search, with a real withholding.
224 page, err := s.Search(ctx, testAsker, "quarterly planning", 10)
225 if err != nil {
226 t.Fatalf("search: %v", err)
227 }
228 joined := strings.Join(page.Notices, " ")
229 for _, forbidden := range []string{"refusing", "used to allow", "people who could read"} {
230 if strings.Contains(strings.ToLower(joined), forbidden) {
231 t.Errorf("a reader's page carried the breaker's words (%q): %q",
232 forbidden, joined)
233 }
234 }
235 // NOT_VISIBLE is the silent value, so this page says nothing at all —
236 // which is also the assertion that no count leaked.
237 if len(page.Notices) != 0 {
238 t.Errorf("a silently-omitted passage produced notices: %v", page.Notices)
239 }
240
241 // The owner's side: the sentence, with its number.
242 b, err := s.RefusalBreaker(ctx, "Sheets", breakerClock)
243 if err != nil {
244 t.Fatalf("RefusalBreaker: %v", err)
245 }
246 if b.Verdict != BreakerFiring {
247 t.Fatalf("the fixture did not fire (%v); nothing above is about "+
248 "placement if there is no notice to place", b.Verdict)
249 }
250 sentence, alarming := breakerSentence(b)
251 if !strings.Contains(sentence, "3") || !strings.Contains(sentence, "Sheets") {
252 t.Errorf("the owner's sentence lacks the app or the count: %q", sentence)
253 }
254 if !alarming {
255 t.Error("a firing breaker did not mark its row")
256 }
257}
258
259// **A zero row is not a refusal**, and this is the only thing that can
260// reach the clause saying so.
261//
262// `RecordAuthorizations` writes no row when an answer set contains neither
263// a VISIBLE nor a NOT_VISIBLE, so nothing on the production path produces
264// `{0, 0}` — which makes `NotVisible >= 1` unreachable through it, and a
265// review's mutation dropping that half stayed green. That is the same
266// shape as the `ASKED` counter it replaced: a guard nothing can reach.
267//
268// The difference is that this one is defending an invariant enforced two
269// functions away, in a different file, on a path a future change could
270// easily alter — write a row to record "this person asked", and every
271// baseline asker in an outage becomes a refusal. So rather than delete the
272// clause, the row is inserted directly here, which is the same remedy
273// `scatter` got in the sibling package: make the branch reachable from a
274// test and pin what it defends.
275//
276// MUTATION-TESTED 2026-09-07, each verified red then reverted:
277//
278// drop `&& t.NotVisible >= 1` -> red, the outage fires
279// count a zero row as an ask -> red, same
280func TestAZeroRowIsNotARefusal(t *testing.T) {
281 s := breakerStore(t)
282
283 // Three people the sibling allowed last week.
284 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
285 countOn(t, s, "Sheets", p, daysBefore(3), 5, 0)
286 }
287
288 // Today each has a row saying they saw nothing and were refused
289 // nothing — the shape an "asked, got UNKNOWN" record would take.
290 // Inserted directly, because no production path writes one.
291 for _, p := range []string{"ana@x", "ben@x", "cleo@x"} {
292 print, err := s.sealer.Fingerprint(seal.PurposeAsker, []byte("asker\x00"+p))
293 if err != nil {
294 t.Fatalf("fingerprint: %v", err)
295 }
296 sealed, err := s.sealTally(tally{})
297 if err != nil {
298 t.Fatalf("seal: %v", err)
299 }
300 if err := s.db.G.Create(&SiblingTally{
301 App: "Sheets", AskerPrint: print,
302 Day: breakerClock.Format("2006-01-02"),
303 SealedCounts: sealed,
304 }).Error; err != nil {
305 t.Fatalf("insert: %v", err)
306 }
307 }
308
309 // The presence half: the rows really are there, so what follows is an
310 // assertion about the predicate and not about an empty window.
311 var n int64
312 if err := s.db.G.Model(&SiblingTally{}).
313 Where("day = ?", breakerClock.Format("2006-01-02")).Count(&n).Error; err != nil {
314 t.Fatal(err)
315 }
316 if n != 3 {
317 t.Fatalf("%d zero rows seeded, want 3", n)
318 }
319
320 if b := verdict(t, s); b.Verdict != BreakerQuiet {
321 t.Errorf("verdict = %v (askers=%d), want quiet: three baseline people "+
322 "with nothing recorded against them today were counted as refused, "+
323 "which is what turns a 24-hour outage into a permissions anomaly",
324 b.Verdict, b.Askers)
325 }
326}
327
328// The wake prunes.
329//
330// `PruneTallies` was written with a comment saying a retention rule
331// nothing enforces is a sentence in a document, and then nothing called
332// it — a review found the sentence describing its own state. §3.2: an
333// unbounded version of these counters is a year-long history of who was
334// locked out of what, and it is the only per-person record memoria keeps.
335//
336// MUTATION-TESTED 2026-09-07: drop the PruneTallies call from
337// WireSiblings and this goes red with the old row still there. Reverted.
338func TestWiringPrunesOldCounters(t *testing.T) {
339 s := storeForTest(t)
340
341 // A row far outside any window, and one inside it.
342 countOn(t, s, "Docs", "ana@x", time.Now().UTC().AddDate(0, 0, -40), 1, 0)
343 countOn(t, s, "Docs", "ana@x", time.Now().UTC(), 1, 0)
344 if got := tallyDays(t, s); len(got) != 2 {
345 t.Fatalf("%d rows seeded, want 2", len(got))
346 }
347
348 a := &app{store: s, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
349 if err := WireSiblings(a, func(k string) string {
350 return map[string]string{
351 "MEMORIA_TEAM": "acme",
352 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works",
353 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
354 }[k]
355 }, nil); err != nil {
356 t.Fatalf("WireSiblings: %v", err)
357 }
358
359 got := tallyDays(t, s)
360 if got[time.Now().UTC().AddDate(0, 0, -40).Format("2006-01-02")] {
361 t.Error("a 40-day-old counter survived the wake: this is the only " +
362 "per-person record memoria keeps, and the retention rule is the " +
363 "whole of what bounds it")
364 }
365 if !got[time.Now().UTC().Format("2006-01-02")] {
366 t.Error("today's counter was pruned; the prune is meant to bound the " +
367 "history, not to empty it")
368 }
369}
370