| 377 | } |
| 378 | |
| 379 | func (m *SyncMockManager) AddMock(mock *models.Mock) { |
| 380 | // Unification (Phase 1): resolve the live mock's Lifetime immediately |
| 381 | // on entry so the buffered mock carries a correctly-typed |
| 382 | // TestModeInfo.Lifetime into whichever downstream consumer drains |
| 383 | // syncMock next (persistence writer, downstream agent via outChan, |
| 384 | // etc.). Cheap — single map probe — and removes the need for |
| 385 | // downstream code to call DeriveLifetime defensively. |
| 386 | if mock != nil { |
| 387 | mock.DeriveLifetime() |
| 388 | } |
| 389 | m.mu.Lock() |
| 390 | if m.memoryPause { |
| 391 | // Pressure is on at decode time, but this mock may have been decoded |
| 392 | // late for a request that happened during calm — its TC was captured |
| 393 | // at the ingress, so dropping the mock would orphan it. Decide by the |
| 394 | // request time, not now: drop only if the request ITSELF happened |
| 395 | // during pressure (the ingress never captured it, so there is no TC). |
| 396 | if m.pressureActiveAtLocked(mock.Spec.ReqTimestampMock) { |
| 397 | m.mu.Unlock() |
| 398 | m.pressureDropped.Add(1) |
| 399 | return |
| 400 | } |
| 401 | // Request was during calm → its TC was captured → keep this mock; |
| 402 | // fall through to the normal buffer/forward path. |
| 403 | } |
| 404 | // Mock is being kept — count it as successfully added. |
| 405 | m.totalAdded.Add(1) |
| 406 | |
| 407 | // Tag startup-window traffic. A mock is "startup" when it is captured |
| 408 | // either (a) before the first inbound request — classic app-bootstrap |
| 409 | // traffic (e.g. an AWS Secret Manager fetch at boot) that ran before any |
| 410 | // test window exists — or (b) while we are still inside the startup window, |
| 411 | // i.e. fewer than models.StartupMockTestCaseWindow unique test cases have |
| 412 | // been recorded. Case (b) widens the old "before firstReqSeen" rule so the |
| 413 | // boot-through-Nth-test mock corpus is preserved wholesale: the IsStartup |
| 414 | // tag is the single signal every reaper below keys off (dedup DeleteMocks- |
| 415 | // StrictlyBefore, the ResolveRange keep=false / out-of-window / stale-cutoff |
| 416 | // rescues, FlushOwnedWindows, and the memory-pressure wipe), so tagging here |
| 417 | // makes static-dedup pruning a no-op until the (N+1)-th test case. (firstReqSeen |
| 418 | // is subsumed by the count test — count is 0 before the first request — but |
| 419 | // is kept explicit so the boot case still holds if the window is ever 0.) |
| 420 | if mock != nil && (!m.firstReqSeen || m.resolvedTestCount < models.StartupMockTestCaseWindow) { |
| 421 | mock.TestModeInfo.IsStartup = true |
| 422 | } |
| 423 | |
| 424 | // Decide forward vs buffer vs drop under a single snapshot of |
| 425 | // (outChan, outChanClosed). The trio has three legal outcomes: |
| 426 | // |
| 427 | // closed → drop (shutdown in progress, buffer would leak) |
| 428 | // unbound (nil) → buffer (SetOutputChannel hasn't fired yet; |
| 429 | // ResolveRange will emit once bound) |
| 430 | // bound + open → forward via sendToOutChan, unless we're |
| 431 | // past firstReqSeen in which case the mock |
| 432 | // belongs in the dedup buffer for windowing |
| 433 | // |
| 434 | // Session- and connection-scoped mocks (mongo handshake/heartbeat, |
| 435 | // postgres v3 startup, mysql HikariCP COM_PING) follow the same |
| 436 | // branching here — they ride the buffer when firstReqSeen has |