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 "net/http"
8 "net/http/httptest"
9 "strings"
10 "sync/atomic"
11 "testing"
12 "time"
13
14 "amadan.net/oficina/suite"
15
16 "amadan.net/oficina/memoria/internal/memoria/sibling"
17)
18
19// warmWindow is gate 24's N: how long after the app comes up every
20// configured sibling must have been polled, unprompted.
21//
22// **A stated constant in the low seconds, and deliberately not a race
23// against anything.** Two seconds is far longer than three local polls
24// need and far shorter than "eventually", so it fails a warm-up that was
25// deleted or made lazy without failing on a loaded machine.
26const warmWindow = 2 * time.Second
27
28// Gate 24: **every configured sibling is polled on the wake, with no
29// query issued at all.**
30//
31// §6.5: a cold sibling is a wake, and a wake is slower than a deadline, so
32// the first search after a quiet period reliably withholds unless somebody
33// has already paid that cost. memoria pays it on its own wake, where
34// nobody is waiting.
35//
36// **The assertion is unprompted presence, not an ordering**, and the spec
37// works through why the ordering version cannot be written. Asynchronous,
38// "before the handler served a request" is a race between goroutines and
39// the gate is flaky. Synchronous, satisfying it means the first query
40// waits for three sibling wakes — which puts the latency back on the
41// reader and undoes the entire reason for warming. So: no query is issued
42// here, and the claim is that the polls happen on their own.
43//
44// MUTATION-TESTED 2026-09-07, each verified red then reverted:
45//
46// WarmSiblings returns without starting anything
47// -> every fixture unpolled; red
48// the warm-up fires lazily on the first query instead
49// -> red, because this test issues no query — which is the
50// assertion the ordering version could not make
51// Warm polls only the first sibling in the roster
52// -> red on the other two
53// Warm calls Authorize instead of Poll
54// -> red: S2 needs an asker and there is nobody on a wake, so the
55// fixtures record no S1 at all. This is the mistake draft 3 made
56// and the one no fixture could have received.
57func TestEveryConfiguredSiblingIsPolledOnTheWake(t *testing.T) {
58 var polled [3]int64
59 apps := make([]sibling.Sibling, 0, 3)
60 for i, name := range []string{"Docs", "Sheets", "Calendar"} {
61 counter := &polled[i]
62 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63 if r.URL.Path != suite.RouteS1 {
64 t.Errorf("%s received %s %s on a wake; the warm-up is S1, and "+
65 "S2 and S7 both need a person who does not exist here",
66 name, r.Method, r.URL.Path)
67 w.WriteHeader(http.StatusNotFound)
68 return
69 }
70 atomic.AddInt64(counter, 1)
71 _, _ = w.Write([]byte(`{"cursor":"c1","rows":[]}`))
72 }))
73 t.Cleanup(srv.Close)
74 apps = append(apps, sibling.Sibling{Name: name, BaseURL: srv.URL, Token: "t"})
75 }
76
77 s := storeForTest(t)
78 custody := &Siblings{client: sibling.NewForTest(), byName: map[string]sibling.Sibling{}}
79 for _, a := range apps {
80 custody.byName[a.Name] = a
81 }
82 s.custody = custody
83
84 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
85 start := time.Now()
86 go custody.Warm(context.Background(), s, logger)
87
88 // **No query is issued anywhere in this test.** That is the whole
89 // point: a lazily-fired warm-up has nothing to fire on.
90 deadline := time.Now().Add(warmWindow)
91 for {
92 all := true
93 for i := range polled {
94 if atomic.LoadInt64(&polled[i]) == 0 {
95 all = false
96 }
97 }
98 if all {
99 break
100 }
101 if time.Now().After(deadline) {
102 for i, name := range []string{"Docs", "Sheets", "Calendar"} {
103 if atomic.LoadInt64(&polled[i]) == 0 {
104 t.Errorf("%s was never polled within %v of the wake: the "+
105 "first reader after a quiet period pays that sibling's "+
106 "cold start and gets a withholding notice for it",
107 name, warmWindow)
108 }
109 }
110 return
111 }
112 time.Sleep(5 * time.Millisecond)
113 }
114 t.Logf("three siblings warmed in %v", time.Since(start))
115}
116
117// The warm-up survives a sibling that is down, because one unreachable
118// app must not stop the others being warmed.
119//
120// The failure is not returned and not fatal: there is nobody to return it
121// to on a wake, and the reader learns about it from the withholding notice
122// on the page, which is where §6.5 puts it.
123//
124// MUTATION-TESTED 2026-09-07: return from Warm on the first error and the
125// reachable sibling goes unpolled. Reverted.
126func TestOneDeadSiblingDoesNotStopTheWarmUp(t *testing.T) {
127 // One server per app, so the assertion is "which siblings were
128 // reached" and not "how many calls arrived". A first poll costs two S1
129 // calls — one to take the cursor forward, one to confirm nothing
130 // follows it — so a call count answers a different question than the
131 // one this test asks.
132 var docs, also int64
133 serve := func(counter *int64) string {
134 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
135 atomic.AddInt64(counter, 1)
136 _, _ = w.Write([]byte(`{"cursor":"c1","rows":[]}`))
137 }))
138 t.Cleanup(srv.Close)
139 return srv.URL
140 }
141
142 custody := &Siblings{
143 client: sibling.NewForTest(),
144 byName: map[string]sibling.Sibling{
145 // Port 1 is reserved and nothing listens there.
146 "Dead": {Name: "Dead", BaseURL: "http://127.0.0.1:1", Token: "t"},
147 "Docs": {Name: "Docs", BaseURL: serve(&docs), Token: "t"},
148 "AlsoUp": {Name: "AlsoUp", BaseURL: serve(&also), Token: "t"},
149 },
150 }
151 s := storeForTest(t)
152 s.custody = custody
153
154 custody.Warm(context.Background(), s, slog.New(slog.NewTextHandler(io.Discard, nil)))
155
156 if atomic.LoadInt64(&docs) == 0 || atomic.LoadInt64(&also) == 0 {
157 t.Errorf("Docs polled %d times, AlsoUp %d: one app being down must not "+
158 "leave the others cold, and Warm returning on the first error is "+
159 "how that happens", docs, also)
160 }
161}
162
163// **A sibling's response body is content, and it must not reach a log.**
164//
165// AGENTS.md states this for a model provider's 4xx, which can echo the
166// input that caused it; a sibling's error body is the same shape of
167// hazard and closer to home, because the thing it can echo is a passage.
168// `carlos logs` is platform-held storage we cannot grep, so a body that
169// lands there is a disclosure nobody can retract.
170//
171// The warm-up is where this bites: it is the one path that handles a poll
172// error with nobody to return it to, so the tempting thing to do with the
173// error is log it. A review's mutation added `"err", err` to that line and
174// every test stayed green.
175//
176// PRESENCE FIRST, per §10's second rule: the sibling really does put the
177// canary in a body memoria really does read, so the absence check below is
178// an assertion rather than a description of an empty world.
179//
180// MUTATION-TESTED 2026-09-07:
181//
182// the error is wrapped with the response body before it is logged
183// -> red, the canary in the log
184//
185// **And one that SURVIVES, recorded rather than hidden**, because a gate's
186// coverage is a claim like any other:
187//
188// logger.Warn(..., "err", err) in Warm
189// -> GREEN, and it is not a defect. Every error the module produces
190// from a bad S1 or S3 names a status, a field or a row index and
191// never the body — checked, in s1.go and s3.go, rather than
192// assumed. So logging the error today leaks nothing.
193//
194// The error is still not logged, for the reason AGENTS.md gives about a
195// provider's 4xx: the safety here belongs to somebody else's discretion
196// about their own error strings, and this side should not depend on it.
197// What this test defends is memoria's own handling; the module's part of
198// it is the module's to keep.
199func TestASiblingsErrorBodyNeverReachesTheLog(t *testing.T) {
200 const canary = "SALARY-BANDS-Q3-CONFIDENTIAL"
201
202 var served int64
203 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
204 atomic.AddInt64(&served, 1)
205 // A sibling erroring while echoing content back. Not exotic: an
206 // OpenAI-compatible 4xx does exactly this, and so does any handler
207 // that renders the request into its error page.
208 w.WriteHeader(http.StatusBadRequest)
209 _, _ = w.Write([]byte(`{"error":"could not index ` + canary + `"}`))
210 }))
211 t.Cleanup(srv.Close)
212
213 var logged strings.Builder
214 logger := slog.New(slog.NewTextHandler(&logged, &slog.HandlerOptions{Level: slog.LevelDebug}))
215
216 custody := &Siblings{
217 client: sibling.NewForTest(),
218 byName: map[string]sibling.Sibling{
219 "Docs": {Name: "Docs", BaseURL: srv.URL, Token: "t"},
220 },
221 }
222 s := storeForTest(t)
223 s.custody = custody
224 custody.Warm(context.Background(), s, logger)
225
226 // (a) the body was really served and really read.
227 if atomic.LoadInt64(&served) == 0 {
228 t.Fatal("the sibling was never called, so nothing below is an assertion")
229 }
230 if logged.Len() == 0 {
231 t.Fatal("nothing was logged at all; the absence check below would " +
232 "pass against a logger that swallowed everything")
233 }
234 if !strings.Contains(logged.String(), "Docs") {
235 t.Error("the failure was not reported at all — silence is not the fix")
236 }
237
238 // (b) and the canary is not in it.
239 if strings.Contains(logged.String(), canary) {
240 t.Errorf("a sibling's error body reached the log:\n%s", logged.String())
241 }
242}
243
244// WarmSiblings starts the poll, and this is what says so.
245//
246// Gate 24 above drives `Siblings.Warm` directly, which leaves the method
247// production actually calls untested: a review's mutation emptied
248// `WarmSiblings` and every warm-up test stayed green, because none of them
249// went through it.
250//
251// MUTATION-TESTED 2026-09-07, each verified red then reverted:
252//
253// WarmSiblings returns without starting the goroutine
254// -> red
255// WarmSiblings polls synchronously
256// -> green, and deliberately not a finding: this asserts the poll
257// happens, not how. §6.5's reason for asynchrony is latency, and
258// gate 23 is where latency is held.
259func TestWarmSiblingsStartsThePoll(t *testing.T) {
260 var polled int64
261 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
262 atomic.AddInt64(&polled, 1)
263 _, _ = w.Write([]byte(`{"cursor":"c1","rows":[]}`))
264 }))
265 t.Cleanup(srv.Close)
266
267 s := storeForTest(t)
268 s.custody = &Siblings{
269 client: sibling.NewForTest(),
270 byName: map[string]sibling.Sibling{
271 "Docs": {Name: "Docs", BaseURL: srv.URL, Token: "t"},
272 },
273 }
274 a := &app{store: s, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
275 a.WarmSiblings(context.Background())
276
277 deadline := time.Now().Add(warmWindow)
278 for atomic.LoadInt64(&polled) == 0 {
279 if time.Now().After(deadline) {
280 t.Fatalf("WarmSiblings started nothing within %v", warmWindow)
281 }
282 time.Sleep(5 * time.Millisecond)
283 }
284}
285
286// An instance with no siblings warms nothing and does not panic.
287//
288// Slice 0's wiring: custody is nil. `WireSiblings` returns before warming
289// in that case, and this covers the method being called anyway — which is
290// what a test doing its own wiring will do.
291//
292// This comment used to say main called WarmSiblings "unconditionally",
293// which was not true of the code it described: main guarded it on a
294// non-empty roster. It is now behind WireSiblings, which is tested.
295func TestWarmSiblingsWithNoRosterIsQuiet(t *testing.T) {
296 s := storeForTest(t)
297 a := &app{store: s, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
298 a.WarmSiblings(context.Background())
299}
300
301// WireSiblings is where the roster actually becomes an app's custody, and
302// this is the test main.go could not have.
303//
304// A review defeated the gate over `cmd/` twice — `if len(apps) > 1`
305// around the wiring, and `app.UseAuth` inside a closure nobody invoked —
306// both green, because an AST walk over one file sees that a call is
307// spelled and not that it is reached. The answer is not a cleverer walk;
308// it is to leave nothing in main worth getting wrong. So the logic is
309// here, and here it can be run.
310//
311// MUTATION-TESTED 2026-09-07, each verified red then reverted:
312//
313// guard UseSiblings on len(cfg.Apps) > 1
314// -> the one-sibling case red, and it is the ordinary case
315// return nil before UseSiblings
316// -> the one-sibling case red
317// swallow the LoadSiblings error
318// -> the bad-roster case red
319// log cfg.Apps rather than cfg.Names()
320// -> the token case red
321func TestWireSiblings(t *testing.T) {
322 good := map[string]string{
323 "MEMORIA_TEAM": "acme",
324 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works",
325 "MEMORIA_SIBLING_TOKEN_DOCS": "docs-token",
326 }
327
328 t.Run("one sibling is enough to get custody", func(t *testing.T) {
329 // **One, not two.** A review's mutation guarded the wiring on
330 // `len(apps) > 1`, which every multi-sibling fixture satisfies and
331 // which leaves the ordinary case — a team that has adopted its
332 // first source — running with nil custody, withholding everything
333 // from the one app it has.
334 s := storeForTest(t)
335 a := &app{store: s, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
336 if err := WireSiblings(a, env(good), nil); err != nil {
337 t.Fatalf("WireSiblings: %v", err)
338 }
339 if s.custody == nil {
340 t.Fatal("custody is nil after wiring one sibling: every passage " +
341 "that app owns would be withheld, and the notice would blame it")
342 }
343 if _, ok := s.custody.Sibling("Docs"); !ok {
344 t.Error("the configured app does not resolve from a document's source")
345 }
346 })
347
348 t.Run("an empty roster is not an error and leaves custody nil", func(t *testing.T) {
349 s := storeForTest(t)
350 a := &app{store: s, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
351 if err := WireSiblings(a, env(nil), nil); err != nil {
352 t.Fatalf("an instance with no siblings refused to wire: %v", err)
353 }
354 if s.custody != nil {
355 t.Error("custody was set from an empty roster")
356 }
357 })
358
359 t.Run("a bad roster is an error, not a quiet nil custody", func(t *testing.T) {
360 s := storeForTest(t)
361 a := &app{store: s, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
362 err := WireSiblings(a, env(map[string]string{
363 "MEMORIA_TEAM": "acme",
364 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works",
365 }), nil)
366 if err == nil {
367 t.Fatal("a sibling with no token was wired; every passage it owns " +
368 "would be withheld with a notice blaming a healthy app")
369 }
370 if s.custody != nil {
371 t.Error("custody was set from a roster that failed to load")
372 }
373 })
374
375 t.Run("the log line carries no token", func(t *testing.T) {
376 var logged strings.Builder
377 s := storeForTest(t)
378 a := &app{store: s, logger: slog.New(slog.NewTextHandler(&logged,
379 &slog.HandlerOptions{Level: slog.LevelDebug}))}
380 if err := WireSiblings(a, env(good), nil); err != nil {
381 t.Fatalf("WireSiblings: %v", err)
382 }
383 // Presence first: something was logged and it names the app, so
384 // the absence below is an assertion.
385 if !strings.Contains(logged.String(), "Docs") {
386 t.Fatal("nothing identifying was logged, so the check below proves nothing")
387 }
388 if strings.Contains(logged.String(), "docs-token") {
389 t.Errorf("a consumer token reached the log:\\n%s", logged.String())
390 }
391 })
392}
393