fleetd #446: make exhaustedPattern hot, name the fix, report it in fleet_profiles #457

Closed
agent wants to merge 0 commits from worker/446-hot-exhausted-pattern-0af580-6 into main
Member

Fixes fleetd #446: the model gate can be turned off at runtime, but its usage-limit detector could not be turned on at runtime.

What changed

  1. exhaustedPattern is now HOT. New LiveExhaustedPatterns (fleetd/src/main/java/dev/ltms/fleet/inject/LiveExhaustedPatterns.java) reads the pattern off the live config supplier per lookup, instead of Fleetd.main compiling it once into a frozen Map<String,Pattern> at startup. Compiled Patterns are cached by profile name, not pattern text — pattern text would grow unboundedly as an operator tunes a regex across several reload cycles; profile names are bounded by the small, restart-gated set of configured profiles (adding/removing a profile is itself a deferred key). patternFor() backs CompletionResolver's classification; armed() backs fleet_profiles' exhaustionDetectionArmed — both read the SAME object, the fleetd #404 single-accessor rule CompositePeerLauncher.modelGateState()'s javadoc states for the model gate's own armed/off pair.

  2. WARNING names the fix. In Fleetd.java, on a BACKEND_EXHAUSTED detection, a new log.warn names the profile's model and the exact edit (enabled: false on that model's models.allow entry — hot, no restart — remove it again once the window resets), or, when the profile has no model: configured, says quarantine alone is holding it off.

  3. Observable in fleet_profiles. FleetMcp.QuarantineSource gains modelFor/reasonFor; a quarantined profile's row in fleet_profiles now carries model and reason fields, so a lead can see why a limit hit and which model it points to without reading the daemon log. fleet_list's capacityView is intentionally NOT touched — scoped to fleet_profiles only.

Deliberately not done (per ticket)

  • No automatic backoff probe.
  • No automatic re-enable — turning a model back on is a manual revert of enabled: false, since models: is already hot.
  • No real exhaustedPattern/errorPattern values added to any profile (fleetd.yaml is gitignored/live and untouched).
  • BackendQuarantine itself is untouched (~15+ existing test call sites use its 1-arg quarantine(credentialId)); the exhaustion reason is tracked in a new side-channel Map<String,String> quarantineReasonByCredential local to Fleetd.java, populated at the same call site as the existing quarantine.quarantine(...) call.
  • errorPattern stays Deferred on purpose — out of scope.

Tests

  • LiveExhaustedPatternsTest (new): direct unit proof of hotness + caching (read, mutate the backing config, read again, see the change; assertSame/assertNotSame prove the cache hits on unchanged text and recompiles on changed text).
  • FleetdExhaustionDetectionArmedWiringTest (rewritten): builds one QuarantineSource BEFORE a ConfigRef.reload(), re-invokes the SAME exhaustedPatternArmed() function AFTER the reload (no new object), asserts the answer flips false→true with no restart. This is the inverse of the pre-#446 version of this file, which asserted assertFalse on the identical scenario.
  • ConfigRefTest/ConfigRefProfileCoverageTest: updated for exhaustedPattern moving from Deferred to LAUNCH_SETTINGS_EXCLUDED/Hot.

Mutation proof (criterion 1): reverted ONLY the compile-site fix — LiveExhaustedPatterns snapshotting profiles.get() once at construction instead of reading it live per call, simulating the pre-#446 Fleetd.main startup compile — and re-ran LiveExhaustedPatternsTest + FleetdExhaustionDetectionArmedWiringTest: 5 failures (Tests run: 10, Failures: 5, BUILD FAILURE). Restored the fix: same two classes back to 10/10 green, BUILD SUCCESS.

Full suite: mvn -B clean test → Tests run: 1586, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.

Scope note

Swept for other fields with the same shape as #404/#446 (a reported/observable state computed from a different source than the enforced behaviour) — grepped FleetMcp.java and CompositePeerLauncher.java for armed/Armed. Found only the two already-known pairs (modelGateArmed #404, exhaustionDetectionArmed #446), both now following the single-accessor rule. No new instances found in this quick sweep; not an exhaustive audit of the whole config surface.


Follow-up (commit 30d6872): pin criteria 2 and 3 with mutation-tested assertions

The lead ran a mutation battery against the merged PR (d02dd1b) and found criterion 1
solidly pinned, but criteria 2 and 3 shipped correct and untested: deleting
row.put("model", model) or row.put("reason", reason) in FleetMcp.profilesView,
or renaming the "usage-limit fix:" log tag, all left the full 1586-test suite green.

What changed

  • Extracted the exhaustionSink WARNING text out of two inline SLF4J {}-placeholder
    log.warn calls into two static methods — Fleetd.usageLimitFixWarning(profile, model)
    and Fleetd.usageLimitFixWarningNoModel(profile, quarantineCooldownSeconds) — the same
    extracted-static-method idiom as exhaustedPatternCoverageLine/errorPatternCoverageLine.
    log.warn now takes each method's return value as a single, already-formatted argument, so
    a test asserts on the exact string fleetd.out receives. No behaviour change: same text,
    same two branches, same call site — proved by reverting the extraction (git diff empty
    vs. committed content) and confirming both formatted strings identical before/after.
  • New FleetdUsageLimitFixWarningTest: pins the leading "usage-limit fix:" grep tag plus
    three specific facts (profile name, model name, enabled: false under models.allow) —
    not the whole sentence — and the no-model fallback branch.
  • New FleetProfilesQuarantineModelReasonFieldsTest: exercises QuarantineSource with a
    modelFor/reasonFor that actually return values (every prior test used .none() or the
    3-arg form defaulting both to null), asserting model/reason are present when supplied,
    and absent (not null, not blank) when the functions return null or a blank string.

Break-and-restore proof (all three, verbatim)

M1 — delete row.put("model", model); → FleetProfilesQuarantineModelReasonFieldsTest fails:

[ERROR] Tests run: 3, Failures: 1
AssertionFailedError: the quarantined row must name the model the fix applies to: {...,"quarantined":{"terra":{"credentialId":"cred-terra","quarantinedForSeconds":1800,"reason":"The usage limit has been reached"}},...} ==> expected: <true> but was: <false>

Restored → Tests run: 3, Failures: 0, BUILD SUCCESS.

M2 — delete row.put("reason", reason); → same test fails:

[ERROR] Tests run: 3, Failures: 1
AssertionFailedError: the quarantined row must name why it was quarantined: {...,"quarantined":{"terra":{"credentialId":"cred-terra","quarantinedForSeconds":1800,"model":"claude-opus-5"}},...} ==> expected: <true> but was: <false>

Restored → Tests run: 3, Failures: 0, BUILD SUCCESS.

M4 — rename "usage-limit fix:" to "MUTANT no fix named:" at both call sites →
FleetdUsageLimitFixWarningTest fails:

[ERROR] Tests run: 3, Failures: 2
AssertionFailedError: must carry the grep-able identifying tag: MUTANT no fix named: profile 'terra' runs model 'claude-opus-5' — set `enabled: false` on that model's entry under models.allow ... ==> expected: <true> but was: <false>
AssertionFailedError: must carry the grep-able identifying tag: MUTANT no fix named: profile 'gx' has no model: configured, ... ==> expected: <true> but was: <false>

Restored → Tests run: 3, Failures: 0, BUILD SUCCESS.

Full suite after restore

mvn -B clean test, redirected to a file (not piped), exit code checked separately from the
output: exit=0, Tests run: 1592, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS
(1586 prior + 6 new).

Log-pinning idiom chosen: extracted static method returning a String, not ListAppender

Chose the exhaustedPatternCoverageLine idiom over ExhaustedPatternGapReportTest's
ListAppender capture. This warning is built once, at one call site, from a single method with
no branching inside the message itself — there's no real log plumbing (multiple call sites,
conditional formatting inside the logger call) left to prove once the text is extracted, so an
appender would only add setup/teardown around the identical assertion the extracted method
already gives for free.


Round 3 (final round)

Round 2's mutation battery (M4a, M1, M2) was re-run against the merged PR and all three stayed
KILLED. A new mutant, M5, SURVIVED: replacing exhaustionSink's log.warn ternary result with a
literal string left the full 1592-test suite green, because nothing exercised the real call site
that builds the quarantine ExhaustionSink main() wires — only the two extracted static methods
in isolation (FleetdUsageLimitFixWarningTest).

Fix: extracted the inline lambda in main() into a new static Fleetd.exhaustionSink(...)
factory (same testability-refactor class the lead approved in round 2), behaviourally unchanged.
Added FleetdExhaustionSinkWarningTest, which drives this factory's return value directly and
asserts on the real text a ListAppender attached to Fleetd's own logger captures — chosen over
a source-text assertion (the FleetMcpAuthzTest idiom) because it answers both of the two gaps the
lead named from one mechanism, by reading what the sink actually logged for each shape of profile:

  • Cell A — does the caller invoke either extracted method at all? Applied the exact M5 mutation
    (ternary result → "usage-limit fix: MUTANT"). Full clean suite: Tests run: 1594, Failures: 1, Errors: 0, Skipped: 0, BUILD FAILURE, exit=1 — failure at
    FleetdExhaustionSinkWarningTest.profileWithModelGetsTheActionableFix:140. Restored; full clean
    suite: Tests run: 1594, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS, exit=0.
  • Cell B — does the caller pick the correct branch? Swapped the ternary's two branches. Full
    clean suite: Tests run: 1594, Failures: 2, Errors: 0, Skipped: 0, BUILD FAILURE, exit=1 — both
    new test methods independently failed (the with-model case got the no-model fallback text and
    vice versa). Restored; full clean suite: Tests run: 1594, Failures: 0, Errors: 0, Skipped: 0,
    BUILD SUCCESS, exit=0.

Kept the round-2 extraction (usageLimitFixWarning/usageLimitFixWarningNoModel) and its test
unchanged, per the lead's instruction — M4a proved they still earn their place.

Files changed this round: fleetd/src/main/java/dev/ltms/fleet/Fleetd.java (extraction only, no
behaviour change), fleetd/src/test/java/dev/ltms/fleet/FleetdExhaustionSinkWarningTest.java (new).

Fixes fleetd #446: the model gate can be turned off at runtime, but its usage-limit detector could not be turned on at runtime. ## What changed 1. **`exhaustedPattern` is now HOT.** New `LiveExhaustedPatterns` (`fleetd/src/main/java/dev/ltms/fleet/inject/LiveExhaustedPatterns.java`) reads the pattern off the live config supplier per lookup, instead of `Fleetd.main` compiling it once into a frozen `Map<String,Pattern>` at startup. Compiled `Pattern`s are cached **by profile name**, not pattern text — pattern text would grow unboundedly as an operator tunes a regex across several reload cycles; profile names are bounded by the small, restart-gated set of configured profiles (adding/removing a profile is itself a deferred key). `patternFor()` backs `CompletionResolver`'s classification; `armed()` backs `fleet_profiles`' `exhaustionDetectionArmed` — both read the SAME object, the fleetd #404 single-accessor rule `CompositePeerLauncher.modelGateState()`'s javadoc states for the model gate's own armed/off pair. 2. **WARNING names the fix.** In `Fleetd.java`, on a BACKEND_EXHAUSTED detection, a new log.warn names the profile's model and the exact edit (`enabled: false` on that model's `models.allow` entry — hot, no restart — remove it again once the window resets), or, when the profile has no `model:` configured, says quarantine alone is holding it off. 3. **Observable in `fleet_profiles`.** `FleetMcp.QuarantineSource` gains `modelFor`/`reasonFor`; a quarantined profile's row in `fleet_profiles` now carries `model` and `reason` fields, so a lead can see why a limit hit and which model it points to without reading the daemon log. `fleet_list`'s `capacityView` is intentionally NOT touched — scoped to `fleet_profiles` only. ## Deliberately not done (per ticket) - No automatic backoff probe. - No automatic re-enable — turning a model back on is a manual revert of `enabled: false`, since `models:` is already hot. - No real `exhaustedPattern`/`errorPattern` values added to any profile (`fleetd.yaml` is gitignored/live and untouched). - `BackendQuarantine` itself is untouched (~15+ existing test call sites use its 1-arg `quarantine(credentialId)`); the exhaustion reason is tracked in a new side-channel `Map<String,String> quarantineReasonByCredential` local to `Fleetd.java`, populated at the same call site as the existing `quarantine.quarantine(...)` call. - `errorPattern` stays Deferred on purpose — out of scope. ## Tests - `LiveExhaustedPatternsTest` (new): direct unit proof of hotness + caching (read, mutate the backing config, read again, see the change; `assertSame`/`assertNotSame` prove the cache hits on unchanged text and recompiles on changed text). - `FleetdExhaustionDetectionArmedWiringTest` (rewritten): builds one `QuarantineSource` BEFORE a `ConfigRef.reload()`, re-invokes the SAME `exhaustedPatternArmed()` function AFTER the reload (no new object), asserts the answer flips false→true with no restart. This is the inverse of the pre-#446 version of this file, which asserted `assertFalse` on the identical scenario. - `ConfigRefTest`/`ConfigRefProfileCoverageTest`: updated for `exhaustedPattern` moving from Deferred to `LAUNCH_SETTINGS_EXCLUDED`/Hot. **Mutation proof (criterion 1):** reverted ONLY the compile-site fix — `LiveExhaustedPatterns` snapshotting `profiles.get()` once at construction instead of reading it live per call, simulating the pre-#446 `Fleetd.main` startup compile — and re-ran `LiveExhaustedPatternsTest` + `FleetdExhaustionDetectionArmedWiringTest`: **5 failures** (`Tests run: 10, Failures: 5`, `BUILD FAILURE`). Restored the fix: same two classes back to **10/10 green**, `BUILD SUCCESS`. **Full suite:** `mvn -B clean test` → `Tests run: 1586, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`. ## Scope note Swept for other fields with the same shape as #404/#446 (a reported/observable state computed from a different source than the enforced behaviour) — grepped `FleetMcp.java` and `CompositePeerLauncher.java` for `armed`/`Armed`. Found only the two already-known pairs (`modelGateArmed` #404, `exhaustionDetectionArmed` #446), both now following the single-accessor rule. No new instances found in this quick sweep; not an exhaustive audit of the whole config surface. --- ## Follow-up (commit 30d6872): pin criteria 2 and 3 with mutation-tested assertions The lead ran a mutation battery against the merged PR (d02dd1b) and found criterion 1 solidly pinned, but **criteria 2 and 3 shipped correct and untested**: deleting `row.put("model", model)` or `row.put("reason", reason)` in `FleetMcp.profilesView`, or renaming the `"usage-limit fix:"` log tag, all left the full 1586-test suite green. ### What changed - Extracted the `exhaustionSink` WARNING text out of two inline SLF4J `{}`-placeholder `log.warn` calls into two static methods — `Fleetd.usageLimitFixWarning(profile, model)` and `Fleetd.usageLimitFixWarningNoModel(profile, quarantineCooldownSeconds)` — the same extracted-static-method idiom as `exhaustedPatternCoverageLine`/`errorPatternCoverageLine`. `log.warn` now takes each method's return value as a single, already-formatted argument, so a test asserts on the exact string `fleetd.out` receives. **No behaviour change**: same text, same two branches, same call site — proved by reverting the extraction (`git diff` empty vs. committed content) and confirming both formatted strings identical before/after. - New `FleetdUsageLimitFixWarningTest`: pins the leading `"usage-limit fix:"` grep tag plus three specific facts (profile name, model name, `enabled: false` under `models.allow`) — not the whole sentence — and the no-model fallback branch. - New `FleetProfilesQuarantineModelReasonFieldsTest`: exercises `QuarantineSource` with a `modelFor`/`reasonFor` that actually return values (every prior test used `.none()` or the 3-arg form defaulting both to `null`), asserting `model`/`reason` are present when supplied, and **absent** (not `null`, not blank) when the functions return `null` or a blank string. ### Break-and-restore proof (all three, verbatim) **M1 — delete `row.put("model", model);`** → `FleetProfilesQuarantineModelReasonFieldsTest` fails: ``` [ERROR] Tests run: 3, Failures: 1 AssertionFailedError: the quarantined row must name the model the fix applies to: {...,"quarantined":{"terra":{"credentialId":"cred-terra","quarantinedForSeconds":1800,"reason":"The usage limit has been reached"}},...} ==> expected: <true> but was: <false> ``` Restored → `Tests run: 3, Failures: 0`, `BUILD SUCCESS`. **M2 — delete `row.put("reason", reason);`** → same test fails: ``` [ERROR] Tests run: 3, Failures: 1 AssertionFailedError: the quarantined row must name why it was quarantined: {...,"quarantined":{"terra":{"credentialId":"cred-terra","quarantinedForSeconds":1800,"model":"claude-opus-5"}},...} ==> expected: <true> but was: <false> ``` Restored → `Tests run: 3, Failures: 0`, `BUILD SUCCESS`. **M4 — rename `"usage-limit fix:"` to `"MUTANT no fix named:"` at both call sites** → `FleetdUsageLimitFixWarningTest` fails: ``` [ERROR] Tests run: 3, Failures: 2 AssertionFailedError: must carry the grep-able identifying tag: MUTANT no fix named: profile 'terra' runs model 'claude-opus-5' — set `enabled: false` on that model's entry under models.allow ... ==> expected: <true> but was: <false> AssertionFailedError: must carry the grep-able identifying tag: MUTANT no fix named: profile 'gx' has no model: configured, ... ==> expected: <true> but was: <false> ``` Restored → `Tests run: 3, Failures: 0`, `BUILD SUCCESS`. ### Full suite after restore `mvn -B clean test`, redirected to a file (not piped), exit code checked separately from the output: `exit=0`, `Tests run: 1592, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS` (1586 prior + 6 new). ### Log-pinning idiom chosen: extracted static method returning a `String`, not `ListAppender` Chose the `exhaustedPatternCoverageLine` idiom over `ExhaustedPatternGapReportTest`'s `ListAppender` capture. This warning is built once, at one call site, from a single method with no branching inside the message itself — there's no real log plumbing (multiple call sites, conditional formatting inside the logger call) left to prove once the text is extracted, so an appender would only add setup/teardown around the identical assertion the extracted method already gives for free. --- ## Round 3 (final round) Round 2's mutation battery (M4a, M1, M2) was re-run against the merged PR and all three stayed KILLED. A new mutant, M5, SURVIVED: replacing `exhaustionSink`'s `log.warn` ternary result with a literal string left the full 1592-test suite green, because nothing exercised the real call site that builds the quarantine `ExhaustionSink` main() wires — only the two extracted static methods in isolation (`FleetdUsageLimitFixWarningTest`). **Fix**: extracted the inline lambda in `main()` into a new static `Fleetd.exhaustionSink(...)` factory (same testability-refactor class the lead approved in round 2), behaviourally unchanged. Added `FleetdExhaustionSinkWarningTest`, which drives this factory's return value directly and asserts on the real text a `ListAppender` attached to `Fleetd`'s own logger captures — chosen over a source-text assertion (the `FleetMcpAuthzTest` idiom) because it answers both of the two gaps the lead named from one mechanism, by reading what the sink actually logged for each shape of profile: - **Cell A** — does the caller invoke either extracted method at all? Applied the exact M5 mutation (ternary result → `"usage-limit fix: MUTANT"`). Full clean suite: `Tests run: 1594, Failures: 1, Errors: 0, Skipped: 0`, `BUILD FAILURE`, exit=1 — failure at `FleetdExhaustionSinkWarningTest.profileWithModelGetsTheActionableFix:140`. Restored; full clean suite: `Tests run: 1594, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`, exit=0. - **Cell B** — does the caller pick the correct branch? Swapped the ternary's two branches. Full clean suite: `Tests run: 1594, Failures: 2, Errors: 0, Skipped: 0`, `BUILD FAILURE`, exit=1 — both new test methods independently failed (the with-model case got the no-model fallback text and vice versa). Restored; full clean suite: `Tests run: 1594, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`, exit=0. Kept the round-2 extraction (`usageLimitFixWarning`/`usageLimitFixWarningNoModel`) and its test unchanged, per the lead's instruction — M4a proved they still earn their place. Files changed this round: `fleetd/src/main/java/dev/ltms/fleet/Fleetd.java` (extraction only, no behaviour change), `fleetd/src/test/java/dev/ltms/fleet/FleetdExhaustionSinkWarningTest.java` (new).
agent added 1 commit 2026-09-10 13:02:35 +02:00
fleetd #446: make exhaustedPattern hot, name the fix in the warning, report it in fleet_profiles
CI / contract (pull_request) Successful in 1m30s
CI / build (pull_request) Successful in 1m34s
45aca9eb3e
The model gate can be turned off at runtime (models.allow[].enabled: false, hot
since fleetd #422), but the usage-limit detector it's meant to react to was
compiled once at Fleetd.main startup into a frozen Map<String,Pattern> — arming
or disarming exhaustedPattern needed a daemon restart. Backwards for a feature
meant to react live.

- New LiveExhaustedPatterns: reads exhaustedPattern off the live config supplier
  per lookup (matching CompositePeerLauncher#models0's live-supplier pattern),
  caching compiled Pattern objects by PROFILE NAME (not pattern text — pattern
  text would grow unboundedly as an operator tunes a regex across reloads;
  profile names are bounded by the small, restart-gated set of configured
  profiles). patternFor() backs CompletionResolver's classification; armed()
  backs fleet_profiles' exhaustionDetectionArmed — both read the same object,
  the fleetd #404 single-accessor rule CompositePeerLauncher.modelGateState()
  established for the model gate.
- Fleetd.java: on BACKEND_EXHAUSTED, log a WARNING naming the profile's model
  and the exact fix (enabled: false under models.allow, hot, no restart; remove
  it again once the window resets) — or, when the profile has no model:
  configured, say quarantine is the only thing keeping spawns off it.
- FleetMcp.QuarantineSource gains modelFor/reasonFor; fleet_profiles'
  quarantined rows gain model/reason fields so a lead can see why without
  reading the daemon log. capacityView (fleet_list) intentionally untouched —
  scoped to fleet_profiles only.
- ConfigRef/FleetConfig docs + fleetd.example.yaml updated: exhaustedPattern
  moves from Deferred to Hot. errorPattern stays deferred on purpose (out of
  scope for this ticket).
- Tests: LiveExhaustedPatternsTest (new, unit-level hotness/caching proof),
  FleetdExhaustionDetectionArmedWiringTest (rewritten — same QuarantineSource
  object, read before and after a reload, asserts the answer flips with no
  restart), ConfigRefTest/ConfigRefProfileCoverageTest updated for the new
  Hot/Deferred classification.
agent added 1 commit 2026-09-10 13:22:58 +02:00
fleetd #446 follow-up: pin the WARNING text and the fleet_profiles model/reason fields
CI / contract (pull_request) Successful in 1m14s
CI / build (pull_request) Successful in 2m4s
30d6872779
A mutation battery run against merged PR #457 (d02dd1b) proved criteria 2 and 3
shipped without a test that could catch them breaking: deleting either
row.put("model", model) or row.put("reason", reason) in FleetMcp.profilesView
left all 1586 tests green (rc=0), and renaming the "usage-limit fix:" log tag
to something meaningless did too. Criterion 1's own mutation (LiveExhaustedPatterns
snapshotting instead of reading live) was correctly killed by the existing
FleetdExhaustionDetectionArmedWiringTest/LiveExhaustedPatternsTest — only 2 and 3
were unguarded.

- Extracted the exhaustionSink WARNING text out of two inline SLF4J {}-placeholder
  log.warn calls into two static methods, Fleetd.usageLimitFixWarning(profile, model)
  and Fleetd.usageLimitFixWarningNoModel(profile, quarantineCooldownSeconds), the
  same extracted-static-method + dedicated-test idiom as
  exhaustedPatternCoverageLine/errorPatternCoverageLine. log.warn is now called with
  each method's return value as a single already-formatted argument, so the string a
  test asserts on is byte-identical to what fleetd.out receives. No behaviour change:
  same text, same two branches, same call site.
- New FleetdUsageLimitFixWarningTest pins the leading "usage-limit fix:" grep tag and
  three specific facts (profile name, model name, "enabled: false" under
  models.allow) rather than the whole sentence, plus the no-model fallback's profile
  name and cooldown-seconds substitution and its explicit absence of "enabled: false".
- New FleetProfilesQuarantineModelReasonFieldsTest exercises FleetMcp.QuarantineSource
  with a modelFor/reasonFor that actually return values (every existing test used
  QuarantineSource.none() or a 3-arg form defaulting both to null), asserting the
  quarantined row's model/reason are present when supplied and absent (not null, not
  blank) when modelFor/reasonFor return null or a blank string.
- Each of the three: implemented, broken by hand (row.put deleted / tag renamed),
  confirmed the new test goes red, restored, confirmed green again. See PR body and
  this ticket's fleet_reply for the verbatim failure output of all three.
agent added 1 commit 2026-09-10 14:00:13 +02:00
fleetd #446 round 3: extract exhaustionSink and pin its caller (Cell A/B)
CI / contract (pull_request) Successful in 55s
CI / build (pull_request) Successful in 2m13s
7772b41993
Round 2 pinned usageLimitFixWarning/usageLimitFixWarningNoModel's TEXT via
FleetdUsageLimitFixWarningTest, but a mutation battery against the merged PR
proved two gaps in the caller that builds main()'s real quarantine
ExhaustionSink: nothing proved the sink's log.warn actually invokes either
method (Cell A), and nothing proved it picks the right one for a profile
with vs without a configured model: (Cell B).

Extract the inline lambda into a new static Fleetd.exhaustionSink(...)
factory (same refactor-for-testability class the lead approved in round 2
for the two static warning methods), behaviourally unchanged from the
lambda it replaces. FleetdExhaustionSinkWarningTest drives this factory's
return value directly and asserts on the real text a ListAppender attached
to Fleetd's own logger captures, covering both cells from one mechanism:

- Cell A (ternary result replaced by a literal string): confirmed red,
  1594 run / 1 failure, restored, confirmed green (1594/0).
- Cell B (ternary's two branches swapped): confirmed red, 1594 run /
  2 failures (both test methods independently caught it), restored,
  confirmed green (1594/0).
Owner

Merged as 1fb6176. Closing this PR by hand: the merge was made locally with --no-ff and pushed, which did not trip the forge's auto-close.

Proof the branch is in main, not just asserted: git merge-base --is-ancestor origin/worker/446-hot-exhausted-pattern-0af580-6 origin/main succeeds, with branch tip 7772b41. A control branch that is not merged correctly fails the same test.

Full verification, the three mutation cells (M5 killed, M6 killed, M7 survived) and the build on the merged tree — Tests run: 1601, Failures: 0, BUILD SUCCESS — are on #446, now closed. M7's residue is recorded on #460.

Merged as **`1fb6176`**. Closing this PR by hand: the merge was made locally with `--no-ff` and pushed, which did not trip the forge's auto-close. Proof the branch is in `main`, not just asserted: `git merge-base --is-ancestor origin/worker/446-hot-exhausted-pattern-0af580-6 origin/main` succeeds, with branch tip `7772b41`. A control branch that is *not* merged correctly fails the same test. Full verification, the three mutation cells (M5 killed, M6 killed, **M7 survived**) and the build on the merged tree — `Tests run: 1601, Failures: 0`, `BUILD SUCCESS` — are on #446, now closed. M7's residue is recorded on #460.
ltms closed this pull request 2026-09-10 14:20:14 +02:00
Some checks are pending
CI / contract (pull_request) Successful in 55s
CI / build (pull_request) Successful in 2m13s

Pull request closed

Sign in to join this conversation.