| 1 | package memoria |
| 2 | |
| 3 | import ( |
| 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. |
| 26 | const 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. |
| 57 | func 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. |
| 126 | func 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. |
| 199 | func 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. |
| 259 | func 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. |
| 295 | func 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 |
| 321 | func 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 | |