274afafde6dca5817d70307560280d18b7273064
377 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
274afafde6 |
fleetd #498: awaitHerdr distinguishes deadline-passed from interrupted, with measured elapsed time
- awaitHerdr now returns a HerdrAwaitOutcome(HerdrWaitResult, elapsedNanos) instead of a bare boolean, so 'the wait budget genuinely ran out' and 'the waiting thread was interrupted' are two distinct, named states instead of the same false (fleetd #497's shape). - awaitHerdr takes the clock (LongSupplier) and the per-poll sleep (Runnable) as required parameters, with no defaulted overload (fleetd #415), so a test can drive it. - The startup call site is extracted into logHerdrWaitOutcomeAndShouldReap, since main() itself cannot be driven from a unit test; it logs a distinct message per outcome, always printing the measured elapsed time next to the configured budget, never the budget alone. - Adds FleetdAwaitHerdrTest covering the seam (all three outcomes, plus the preserved interrupt flag) and the call site (the three distinct log messages), using ListAppender. |
||
|
|
f687046450 |
fleetd #489 follow-up: fix stale class javadoc, off-by-one nudge count, weak test
Three review corrections on top of the previous commit:
1. The class javadoc's four-step continuation list (lines 47-58) was stale.
Step 1 said "report an injectable state", but waitUntilAtTurnBoundary's
own javadoc excludes BLOCKED - fixed to say IDLE or DONE. Step 3 still
described the old plain re-check ("the original, pre-correction wait...
still here") - fixed to describe what waitForClearPickupAndSettle
actually does: nudge while unpicked-up, then wait for a real WORKING ->
IDLE/DONE boundary, releasing rather than wedging if WORKING never shows.
2. PICKUP_GRACE_POLLS=8 bounds the number of consecutive not-yet-picked-up
polls, not the number of nudges - the 8th poll releases instead of
nudging again, so 8 polls produce 7 nudges. The log.info in the release
branch and two javadoc spots said "8 nudges"; fixed all three to state
the poll count and the nudge count separately and correctly. Behavior
and the constant are unchanged.
3. pickupSeenStopsNudgingAndBootstrapTextIsSent asserted only promptCallCount
and sendKeysCallCount, both of which a return-true stub also satisfies.
Added an assertion on the already-tracked postClearGetCalls counter
(>= 2), which only a real post-/clear poll loop can produce - this is
what makes the test fail against a return-true mutant.
|
||
|
|
c6058652be |
fleetd #489: nudge the /clear submit keystroke before bootstrapText
LeadRollover.runRollover's second wait (after /clear) was a no-op: it polled for IDLE/DONE, which /clear itself never leaves since it starts no real turn, so it always returned true on the first poll. Combined with a direct agents.send bypassing Injector (deliberate, to avoid wedging the pane), the submit Enter that accompanies /clear could race the paste and leave it unsubmitted — bootstrapText then landed concatenated onto the same input line, exactly as measured live on 2026-09-12. Replace that second wait with waitForClearPickupAndSettle, which copies the pickup-nudge pattern Injector already ships for its own post-turn /clear housekeeping (fleetd #306): nudge agents.submit while the pane hasn't reported WORKING yet, release after PICKUP_GRACE_POLLS=8 nudges rather than wedge, and require a real WORKING -> IDLE/DONE boundary once a pickup is observed. BLOCKED stays excluded from both the nudge and the boundary check, same as the (unchanged) first wait — a paused live turn is not settled, and nudging Enter into an open prompt could wrongly answer it. Adds four tests to LeadRolloverTest covering the paste-race regression (nudge ordered between /clear and bootstrapText), a confirmed pickup, a deadline expiry with no boundary ever reached, and a throwing submit(). |
||
|
|
261aa056f9 |
fleetd #480 follow-up correction 2: guarantee resolveHandoverPath is always absolute
LeadRollover.resolveHandoverPath's relative branch resolved the configured
handoverPath against leadWorkspace.apply(...) (fleet.leaders.<name>.cwd) but
never forced the result absolute. If an operator writes a RELATIVE cwd, the
returned path stays relative, silently breaking the "always absolute"
contract documented on PendingRollover.
Fix: call toAbsolutePath() unconditionally on both branches (the
already-absolute input branch, where it is a no-op, and the relative
branch), so neither branch trusts isAbsolute() alone to already imply what
toAbsolutePath() enforces. Method javadoc now states the absolute result is
guaranteed, not merely usual.
Added a test: a lead with a RELATIVE cwd and a relative handoverPath still
yields an absolute PendingRollover.handoverPath. Asserts both isAbsolute()
and the exact resolved value, since isAbsolute() alone would also pass for a
path resolved against the wrong base.
Proved the test discriminates: reverting the toAbsolutePath() calls (keeping
the test) made it fail with an AssertionFailedError ("expected: <true> but
was: <false>"); restoring the fix made it pass again.
Note: FleetConfig has no validation on fleet.leaders.<name>.cwd at config
load (grep across every validate* method: 0 matches for .cwd()) — a relative
cwd is silently accepted. Not adding validation here per instruction; that
is a separate ticket.
|
||
|
|
042b8c99dd |
fleetd #480 follow-up correction: cover Fleetd.leadRollover(...)'s own wiring behaviourally
Add FleetdLeadRolloverWorkspaceLookupTest, calling the package-private Fleetd.leadRollover(...) factory directly (with a real ConfigRef built from a temp fleetd.yaml, never the gitignored live one) to prove the terminal -> lead-name -> Leader.cwd() lookup it builds actually works: a relative handoverPath resolves against the calling lead's configured cwd; a terminal absent from the live lead-terminal map falls back to user.dir; and the lookup is read live, not snapshotted at construction time (a lead discovered by the tab scan after leadRollover(...) was built still resolves correctly). Proved this closes the gap: mutating the factory's lambda body (String leadName = null;, always "no lead found", which forces the daemon-cwd fallback this ticket exists to fix) left the full 1669-test suite green before this commit. With the new test added, the same one-line mutation now fails 2 of its 3 cases; reverting it goes green again (3/3). Mutation applied/reverted only during verification and is not part of this commit (git diff on Fleetd.java is empty). FleetdLeadRolloverWiringTest's class javadoc corrected: it previously claimed no behavioural test could catch this wiring dropping out, which was true only before this commit and only covered the factory's own body, not its call site. Restated what each test class actually covers: the source- text pin covers the call site's argument list; the new behavioural test covers the lambda's body. |
||
|
|
4bfab6b718 |
fleetd #480 follow-up: resolve a relative leadRollover.handoverPath against the calling lead's workspace
LeadRollover.open() now resolves handoverPath to an absolute path exactly once, against the calling lead's fleet.leaders.<name>.cwd (falling back to the daemon's own user.dir when that lead has none configured), matching the LeadLauncher#launch precedent. PendingRollover stores only the resolved absolute path, so checkHandover's exists/empty/fresh checks, the path handed back to the lead in the fleet_handover open response, and the default bootstrapText sentence all see the same absolute location instead of a value resolved against whatever directory the daemon process happened to start in. FleetConfig.LeadRollover.bootstrapText is no longer defaulted in the compact constructor (it would otherwise still bake in the raw, possibly-relative handoverPath); a new bootstrapTextFor (resolvedHandoverPath) method builds the default sentence from the resolved path instead. Fleetd.leadRollover(...) gains a required liveLeadTerminals parameter to build the terminal to lead-name to Leader.cwd lookup, read live through the existing `leads` supplier and ConfigRef on every call, never off a startup snapshot. |
||
|
|
9494a6b99a |
Merge #485: fleetd #480 Unit C — the fleet_handover MCP tool
fleet_handover{action: "open"|"confirm"|"cancel"} drives LeadRollover, which #483 and
#484 landed with nothing calling it. Primary-only via a new Authz.Action.HANDOVER, on
the same case line as SPAWN/STOP/DRAIN.
The tool has NO terminal, session or leadTerminal parameter of any kind — the pane is
always callerTerminal(exchange), resolved from the connection. A lead can therefore only
ever roll itself, never another lead. That is charter invariant 3, and it is the second
of the two corrections recorded in LeadRollover's class javadoc.
Registered unconditionally, so the tool surface does not vary with config: with
leadRollover: absent, every action returns a clean NOT_CONFIGURED refusal instead of
failing, and open()'s IllegalStateException (config removed by a hot reload after
construction) is caught and turned into the same refusal. A config-dependent tool set
would have collided with #474's charter tool-surface gate and McpContractDocTest.
Correction round applied before merge, and it is the reason this took two passes.
The unit first shipped with a defaulted 15-argument FleetMcp constructor delegating to
the new 16-argument one with leadRollover = null. I mutated the wiring rather than
reasoning about it: deleting just the leadRollover argument from Fleetd.main's FleetMcp
call compiled with 0 errors and passed all 1659 tests, BUILD SUCCESS — while the live
daemon would have answered NOT_CONFIGURED to every fleet_handover call for ever.
Neither FleetMcpHandoverTest (it builds its own FleetMcp) nor FleetdLeadRolloverWiringTest
(it pins that LeadRollover is constructed, not that it is passed on) could see it.
The worker then found the defect was wider than I had named: all five shorter
constructors (11/12/13/14/15-arg) formed one defaulting chain into the 16-arg one, each
silently supplying another feature's "off" value — leadChannel, outage, leadSeats, peers,
and finally leadRollover. All five are deleted. FleetMcp now has exactly one public
constructor, so every one of those features is compile-enforced at its call site, not
just this one.
Verified by the lead before merge, on PR head merged with current main (0176378):
- CI run 1728 green on
|
||
|
|
eb0557621e |
fleetd #480 correction round: collapse FleetMcp to one required constructor
FleetMcp had a defaulted 15-argument constructor that delegated to the new 16-argument one with an implicit null for leadRollover. Dropping the leadRollover argument from Fleetd.main's FleetMcp(...) call fell back to that shorter overload, compiled fine, and left all 1659 tests green — the live daemon would then answer NOT_CONFIGURED to fleet_handover forever with nothing going red. Delete every overload that could reach the 16-arg constructor with a silently-defaulted leadRollover (11/12/13/14/15-arg forms all chained to it), leaving the 16-arg constructor as FleetMcp's sole public constructor. Update FleetMcpAuthzTest's call site to pass every parameter explicitly (leadChannel null, OutageSource.none(), LeadSeatSource.none(), List.of(), leadRollover null) — Fleetd.java and FleetMcpHandoverTest already called the full form. Proved with mvn -o -q compile: removing the leadRollover argument from Fleetd.main now fails to compile instead of silently defaulting. No behaviour changes — NOT_CONFIGURED refusals are unchanged. |
||
|
|
e2a91e883e |
fleetd #480 Unit E correction: retire "injectable" wording from the log lines
Both waitUntilAtTurnBoundary guard messages still said "never went idle" / "did not become injectable" — the old mental model the rename was meant to retire. Made both say what the code now actually waits for: a turn boundary (IDLE or DONE). |
||
|
|
62646957ea |
fleetd #480 Unit C: wire fleet_handover MCP tool onto LeadRollover
Adds the fleet_handover tool (open/confirm/cancel) as a thin adapter over LeadRollover, registered unconditionally so the charter tool-surface gate sees a stable set regardless of whether leadRollover: is configured. With a null LeadRollover every action degrades to a clean NOT_CONFIGURED refusal instead of throwing. Gated on a new Authz.Action.HANDOVER (primary-only, same as SPAWN/STOP/DRAIN). The caller's own connection-resolved terminal is the only lead identity ever used — the tool's input schema carries no terminal/session/leadTerminal parameter, so a lead can only ever roll itself. Fleetd.main now passes its existing leadRollover local into FleetMcp via a new trailing constructor parameter. |
||
|
|
802c0ab701 |
fleetd #480 Unit E: BLOCKED is not a settled turn boundary
LeadRollover's waitUntilInjectable used AgentStatus#injectable(), which accepts BLOCKED. A BLOCKED pane is paused mid-turn on a prompt, not settled — reusing injectable() let /clear (or the bootstrap text after it) fire into an open approval prompt within the 20s settle window, destroying the lead's live context. Renamed the helper to waitUntilAtTurnBoundary and restricted both waits to IDLE or DONE only, with a comment explaining why this class does not reuse injectable() (it answers "may I deliver", not "has the turn ended"). Added tests for BLOCKED-forever on both waits (zero sends / exactly one send) and for DONE still completing the full roll. |
||
|
|
a94262271b |
fleetd #480 correction round: defer the roll, and gate it on caller identity
Two defects found after the fact, both from the original brief, both fixed here. 1. confirm() is called FROM the calling lead's own turn, so its pane is still WORKING and can never report injectable inside that same call. The old confirm() sent /clear before polling for that — the poll always timed out, but only after /clear had already fired and queued, destroying the lead's context with no fresh session ever started and a refusal return that lied about what had happened. Fix: confirm() now only validates and, if every gate passes, hands a one-shot continuation to a new continuationRunner (a real virtual thread in production, Runnable::run in tests) and returns RollDecision.approved() immediately - "scheduled", not "rolled". The continuation itself does the actual work, once the calling turn has ended: wait for the SAME pane to report injectable again (new turnSettleSeconds config key, default 20) - if this never happens, /clear is NEVER sent, at all - then /clear, then wait again (clearSettleSeconds, as before), then bootstrapText. The "no timer/scheduler, only confirm() can roll" invariant is restated precisely in LeadRollover's class javadoc: it is about initiative, not synchronicity - a single-shot continuation of an already-approved confirm() call still satisfies it; a recurring background loop would not. 2. confirm() resolved the pane to clear via PrimaryRegistry.primaryTerminal(), a single-slot lookup that is correct for a background loop with no caller but wrong here: on a daemon with more than one labelled lead tab, lead X's confirm() could clear lead Y's pane, violating the charter's "identity comes from the connection, never an argument" invariant. Fix: open() and confirm() now take the caller's terminal id as a parameter (resolved by the MCP layer from the connection - the later MCP-tool unit must pass it in, never accept it as a request field). confirm() refuses with a new NOT_YOUR_ROLLOVER reason unless it matches the terminal open() recorded. LeadRollover no longer depends on PrimaryRegistry at all. Also: renamed RollResult to RollDecision (rolled -> accepted) to reflect the new meaning - approved and scheduled, not necessarily cleared yet. Added turnSettleSeconds to the leadRollover: config block (documented in fleetd.example.yaml alongside the existing keys) and updated Fleetd.java's leadRollover(...) factory to drop the primaryRegistry parameter, with FleetdLeadRolloverWiringTest's source-text pin updated to match. New tests: turnThatNeverSettlesSendsNoClearAtAll (the branch that matters most - a turn that never ends means /clear is never sent) and aDifferentLeadTerminalCannotConfirmAnotherLeadsRollover (NOT_YOUR_ROLLOVER), plus a settle-after-clear timeout test and an open() input-validation test. LeadRolloverTest: 11 -> 14 tests. |
||
|
|
5c12865c25 |
fleetd #480 Unit A: lead rollover core (config block + executor)
Adds the opt-in leadRollover: config block and LeadRollover, the executor a later unit's MCP tool will call. A lead writes a handover file, then open() records a token and confirm() verifies it (exists, non-empty, fresh) and an operator confirmation before clearing the lead's own pane via /clear (sent directly through AgentControl, bypassing Injector, same as ClaudeCodeLauncher#clearContext) and bootstrapping a fresh session. Nothing but an explicit confirm() call can ever roll a pane - no timer, no heartbeat, no background thread anywhere in this class. Wired into Fleetd.java exactly like LeadHeartbeatLoop: constructed only when leadRollover: is present at startup, and nothing calls it yet - the MCP tool is a separate, later unit. Classified leadRollover: as HOT in ConfigRef (joins placement/ memberCredentials/memberLoginShell/models): the executor holds Supplier<FleetConfig.LeadRollover> and reads every field fresh per call, unlike LeadHeartbeatLoop's frozen final fields. The one caveat: the object's construction is still gated on presence in the startup config snapshot, so a freshly-added block needs a restart before anything exists to call. Tests: LeadRolloverTest (14 cases covering the 6 hard requirements - no object without the config block, only confirm() can roll, missing/empty/ stale handover file each refuse by name, requireOperatorConfirm gating, and the injected wall-clock supplier) and FleetdLeadRolloverWiringTest (source- text pin on Fleetd.main's construction call, mirroring FleetdCompletionResolverWiringTest). Also updated the existing FleetConfigValidateAllTest, FleetConfigWithDefaultsPreservesEveryComponentTest, ConfigRefTopLevelCoverageTest and ConfigRefTopLevelReportingCoverageTest to account for the new record component. |
||
|
|
72d6a6878b |
fleetd #474 follow-up: pin main's config wiring against M2
Fleetd.main's own choice of the three-argument ConfigRef constructor (with Fleetd::assertChartersNameOnlyRegisteredTools as extraValidation) was unpinned. Reverting Fleetd.java:154 to the plain two-argument constructor compiled with 0 errors and left the whole suite green, because ConfigRefTest and FleetdConfigRefCharterToolSurfaceWiringTest each build their own ConfigRef directly rather than through main. Adds FleetdConfigRefWiringTest, a source-text check on Fleetd.java following the FleetdBackendQuarantineWiringTest/FleetdLeadSeatWiringTest/ FleetdCompletionResolverWiringTest precedent: asserts the exact three-argument construction is present, asserts the plain two-argument form is absent, and guards against a vacuous pass on a broken/empty source read by first asserting an unrelated anchor is present. |
||
|
|
4466ee0ef2 |
fleetd #474: ConfigRef.reload() runs the charter tool-surface gate too
A charter naming an MCP tool the server does not register refused Fleetd.main at startup but slipped through ConfigRef.reload(), because reload() only ran FleetConfig.validateAll(), which never looks at what a charter's text names. CharterToolSurface stays in the mcp package (config must not depend on it), so ConfigRef now accepts the check as a Consumer<FleetConfig> extraValidation, run inside reload()'s same try/catch as validateAll(). Fleetd.main wires a new package-private adapter, Fleetd.assertChartersNameOnlyRegisteredTools, into both the startup call site and ConfigRef's constructor, so the two call sites can never check different things. Tests: ConfigRefTest (reload refuses/accepts, via a locally-built equivalent consumer since Fleetd's method is package-private to dev.ltms.fleet) and the new FleetdConfigRefCharterToolSurfaceWiringTest (same proof through the exact Fleetd::assertChartersNameOnlyRegisteredTools reference production uses). Verified deleting the new extraValidation.accept(fresh) call site fails both new "refuses" tests by name. (cherry picked from commit |
||
|
|
25ba7f16bb |
Merge #473: fleet_profiles and fleet_list report which attempt a quarantine is on (fleetd #466 item 2)
The escalating cooldown landed in
|
||
|
|
5ba69c9cf2 |
fleetd #393: correct a comment that claimed two tests pin a call they cannot
The comment above the charter writer said flipping that call back to putArray was "proven load-bearing" and pointed at two named tests. I mutated exactly that, on the merge commit, and it SURVIVED at 1618 green - so neither named test covers it, and neither can. The reason is structural, not a missing test: that writer runs first against an empty array, so putArray has nothing to replace and the two idioms are equivalent there. No test can distinguish them. The worker measured the same thing independently and said so in their report; the comment was left over from the pre-fix state, where the mutation being described was a DIFFERENT one (a later writer destroying the charter). A comment that names tests which do not cover the line is worse than no comment. The next person mutates the line, sees green, and concludes the tests are broken. The corrected version states what each cell actually measures: this line is unpinnable and why, the later writers ARE pinned and by which test names, and deleting this line entirely fails the charter-reaches-the-member assertion - the hole that predates #393. |
||
|
|
17052bb515 |
Merge #471: seeded skills reach an opencode member, and instructions[] stops depending on write order (fleetd #393)
memberSkills: copied skill folders into every provisioned worktree's .claude/skills/ and stopped there. Claude Code reads that directory natively; opencode never does. So the feature was INERT for opencode members rather than broken: the copy succeeded, the files were correct, and nothing ever read them. No test failed because there was nothing to fail - the feature worked at the only layer it implemented. An opencode member's only channel for static guidance is the instructions[] array in its generated config. OpenCodeLauncher now adds each seeded skill's SKILL.md there. A folder with no SKILL.md is never delivered and the log names it. The second half is an ordering hazard fleet01 found by reading, and that I then measured. Three writers append to instructions[]: the role charter, the seeded skills, and the IDE rules. The charter used putArray (CREATE-OR-REPLACE) while the other two used withArray (get-or-create). That was safe only because the charter ran first against an empty array - an undeclared constraint that nothing tested. Measured on the earlier merge: making the skills writer use putArray left 1603 tests green while silently deleting the charter entry, so an opencode member would launch with no role contract at all. Worse than the bug being fixed, and invisible. All three writers now use withArray, and three tests pin the array's CONTENTS (never its size - a size assertion passes when putArray swaps two entries for two others) across the combinations that matter: charter-only, charter+ide, charter+skills+ide. WHY NOTHING CAUGHT IT, MEASURED RATHER THAN ASSUMED. fleet01 first said the missing axis was the COMBINATION of writers, then revised that to a stronger claim: that nothing asserted the charter reaches an opencode member at all. I checked the second claim on this merge and it is FALSE. Deleting the charter writer outright fails 6 tests, and 3 of those existed before #393: writesRemoteMcpConfigAndCharterInstructionsWhenMcpUrlSet, roleCharterWithoutMcpOrCustomProviderStillWritesAConfig, and aPinnedEndpointAndTheFleetMcpCoexistInOneConfig. The charter reaching a member was already pinned. So their FIRST diagnosis was the right one. The surviving mutation did not delete the charter write; it made a LATER writer replace the whole array. Every pre-existing test had exactly one writer active, and with one writer putArray and withArray are indistinguishable. The gap was never "is the charter delivered" - it was "are two writers ever active at once", which is the combination axis. Recording this because the stronger claim is the more quotable one and it would have sent the next reader looking for a hole that is not there. One honest residue: the charter writer's own idiom cannot be pinned. Flipping it back to putArray leaves the suite green, and always will, because it runs first against an empty array where the two idioms are equivalent. The edit removes an undeclared constraint for the next person to add a writer; it is not a change any test can detect. The comment in the source claimed two named tests cover it - that was wrong, and the commit after this one corrects it rather than leaving a false claim beside the code. What this does NOT do: opencode has no equivalent of Claude Code's Skill tool, so the content is static system-prompt text present from spawn, not something a member can invoke by name. This closes the DELIVERY gap and cannot close the ACTIVATION gap. That residue is opencode's design. Numbers and my own mutation battery are on the ticket, measured on this merge commit. |
||
|
|
e95ed99bf7 |
fleetd #466 scope item 2: report the quarantine repeat count, not only the seconds
Add BackendQuarantine#status(credentialId) -> Optional<Status>, a single QuarantineState read that answers both remainingSeconds and repeatCount together -- the same "one accessor" pattern CompositePeerLauncher. modelGateState() already uses, so the two facts can never disagree. fleet_profiles/REST GET /profiles and fleet_list's capacity rows (FleetMcp.profilesView/capacityView) now call status() instead of remainingSeconds() and add a "quarantineAttempt" field beside "quarantinedForSeconds": 1 for a first occurrence, 2 for the second in a row, and so on. No change to the escalation, ceiling, or reset logic itself -- this unit is reporting only. |
||
|
|
1477e4358a |
Merge #472: one canonical tool-name set, and charters are checked against it (fleetd #469)
A role charter is free text in config that tells a member which tools to call, and nothing checked that those tools exist. A charter naming bridge_send - a name CB-634 removed - started the daemon cleanly, and the member found out at run time by calling something that was not there. #464 shipped a test for this, but it wrote its own charter into a @TempDir, so nothing anyone put in the real config could fail it. That was a defect in my acceptance criteria, not in that work. Now: FleetTool is one enum of the 11 registered wire names, and every reader goes through it. - FleetMcp's schema builders pass FleetTool.X.wireName() instead of a literal. - FleetMcp's constructor asserts at startup that what it registers with the SDK equals FleetTool.wireNames() exactly, in both directions. - toolAction(String, Map) resolves arbitrary wire input against FleetTool.byWireName() and keeps its run-time throw, which is necessary - network input has no closed compile-time form. It then hands off to authzAction(FleetTool, Map), a switch over the enum with NO default, so a new tool is a compile error at that layer. - CharterToolSurface lives in mcp, not config, and Fleetd.main calls it right after validateAll(). Config must not depend on the MCP server: config loads before the server exists. My brief undercounted the problem and the worker corrected it. I said there were two tool-name inventories plus a test fixture. There were FIVE: the registrations, the authz switch, and three separate source-text scrapes of FleetMcp.java in CharterToolSurfaceTest, FleetMcpAuthzTest and McpContractDocTest - none of which the ticket mentioned. Fixing those three was required, not scope creep: once the literals moved into FleetTool their regexes matched zero names, so one would have failed on its vacuity guard and the other two would have gone quietly vacuous. Inventories after: one. Verified independently on origin/main before accepting the wider diff: three test files did read FleetMcp.java as source text, with a control file at zero to prove the search discriminated. Build number and my own mutation results are on the ticket and the PR, measured on this merge commit rather than on the branch. |
||
|
|
9e4e423ad6 |
fleetd #393 follow-up: remove the instructions[] writer-ordering hazard
OpenCodeLauncher.writeConfig has three writers into the instructions[] array (charter, seeded skills, IDE rules). The charter writer used putArray (create-or-REPLACE) instead of withArray (get-or-create), which "worked" only because it happened to run first against a still-empty array — an undeclared ordering dependency nothing tested. Found by the fleet01 lead and verified on this branch's merge: flipping the skills writer to putArray left the full 1603-test suite green while silently deleting the charter entry, which would launch an opencode member with no role contract at all. Fix: charter's putArray -> withArray (one-word change, behavior-identical today). Add three tests asserting instructions[] CONTENT as an exact ordered list (not size) across writer combinations: charter only, charter + IDE rules, and charter + IDE rules + seeded skills. Mutation testing (see PR body) shows the skills and IDE-rules writers are each independently detectable by name; the charter writer's own mutation is not detectable by any test, because it structurally always runs first against an empty array, so putArray and withArray are equivalent there. |
||
|
|
6c2d6e93cb |
fleetd #469: one canonical FleetTool set backs registration, authz and charter checks
FleetConfig.validateCharters() only checked that a charter key is a role wire name and its text is non-blank; #464's CharterToolSurfaceTest compared charter text against the registered tool surface, but wrote its own charter into a @TempDir fixture, so nothing anyone wrote into the live fleetd.yaml could ever fail it. Add FleetTool, an enum in dev.ltms.fleet.mcp holding the one canonical set of registered tool wire names. FleetMcp's tool schemas now derive their names from it, its constructor asserts at startup that what it actually registers with the SDK equals FleetTool.wireNames() exactly, and its authz dispatch (toolAction/authzAction) resolves the wire string against FleetTool before switching on the enum itself with no default -- adding a tool without pinning its Authz.Action is now a compile error, not just a test gap. Add CharterToolSurface (mcp package, not config -- config loads before the MCP server exists) and call it from Fleetd.main right after cfg.validateAll(), so a charter naming a tool the server does not register refuses the daemon's startup, naming both the charter key and the unknown tool. FleetdStartupValidationTest proves this through Fleetd.main itself against a live-shaped config fixture (bridge_send, CB-634's own removed name). CharterToolSurfaceTest, FleetMcpAuthzTest and McpContractDocTest each kept an independent regex scrape of FleetMcp.java's source for the registered side of their own comparison -- three more copies of the same list nothing tied together. All three now read FleetTool.wireNames() instead. Proved canonical by removal: deleting FleetTool.ACK while ackTool() still referenced it broke mvn compile in two places (FleetMcp.java:940,:1898); registering a schema under a literal not backed by FleetTool ("fleet_ack_v2") failed FleetMcp's new startup assertion in every test that constructs it (12 errors, IllegalStateException at FleetMcp.<init>). Both reverted before this commit. |
||
|
|
01462c9695 |
fleetd #466 follow-up: pin main's choice of the escalating quarantine factory
BackendQuarantineTest proves BackendQuarantine.withEscalation itself escalates, is ceilinged, and resets. Nothing proved Fleetd.main still calls it: reverting main to the flat two-argument constructor compiled with 0 errors and left the full 1608-test suite green, because every other test builds its own BackendQuarantine directly. Adds FleetdBackendQuarantineWiringTest, a source-text assertion on Fleetd.java (same idiom as FleetdLeadSeatWiringTest and FleetdCompletionResolverWiringTest) pinning that main's declaration is built from BackendQuarantine.withEscalation(...), not `new BackendQuarantine(...)`. Measured: reverting to the flat constructor fails it (expected true, was false); renaming the anchored local variable while keeping behaviour identical also fails it loudly, not silently. The class doc states plainly this checks source text only, not that the call executes or that escalation reaches a real backend. |
||
|
|
d4a2cd720c |
fleetd #393: deliver memberSkills to opencode members, and stop overclaiming seeding success
GitWorktrees.seedSkills copies memberSkills:-seeded skill folders into every provisioned worktree's .claude/skills/ and logged "skill seeding: N of M" as if that were success — but .claude/skills/ is a Claude Code CLI convention. opencode has no such discovery, so a kind: opencode member never actually read a seeded skill even though the log said N of M succeeded. Two changes, both required: 1. Deliver it. OpenCodeLauncher.skillInstructionFiles scans <cwd>/.claude/skills/*/SKILL.md at spawn time (the one point the launcher knows both the kind and the cwd) and appends each to the generated opencode.json's instructions[] array, the same channel already used for the member charter and IDE rules. A skill folder with no SKILL.md is named and skipped rather than silently dropped. 2. Stop claiming it where the claim can't be verified. GitWorktrees.seedSkills' log now says explicitly that consumption depends on the member's kind and points at the launcher's own log; OpenCodeLauncher logs its own kind-aware "skill delivery: M of N ..." line once the kind is actually known, naming any folder it could not turn into an instructions[] entry. fleetd.example.yaml's memberSkills: doc previously claimed "Claude Code members only; an opencode member reads a different path (.opencode/agent) this key does not touch" — false as of this fix, corrected to name both kinds and how each consumes it. Tests: OpenCodeLauncherTest gains two cases driving the real GitWorktrees#add seeding path (not a hand-built fixture) into an opencode-kind spawn — one asserting a seeded skill's SKILL.md lands in instructions[] plus the honest log line, one covering a skill folder without SKILL.md (delivered skills still flow, the malformed one is named in the log and excluded from instructions[]). ClaudeCodeLauncher is untouched — its native .claude/skills/ discovery already worked and is out of scope. mvn -B clean test: Tests run: 1603, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS |
||
|
|
5a467e1f8b |
fleetd #466: escalate BackendQuarantine's cooldown on repeated exhaustion
A flat 30-minute quarantine retries a weekly subscription limit about 336 times before the window resets. BackendQuarantine now doubles the cooldown on each consecutive exhaustion of the same credential (no more than one base cooldown after the previous quarantine's deadline), capped at 12x the base cooldown (~6h at the 1800s default), and resets back to the base cooldown once a base-cooldown's worth of quiet has passed with no further exhaustion. The flat two-argument constructor is unchanged (equivalent to multiplier 1.0 / ceiling == base), so all ~20 existing call sites keep their current shape and behaviour. Production wiring (Fleetd.main) switches to the new BackendQuarantine.withEscalation factory. This only touches the exhaustion path (BackendQuarantine's one production caller is Fleetd.exhaustionSink, fired on BACKEND_EXHAUSTED alone) and never the separate, unescalated cooling-off mechanism (BackendOutagePolicy, fixed 60s) that guards against a transient backend-error storm. |
||
|
|
1fb6176783 |
Merge #457: exhaustedPattern goes hot, and the warning names the fix (fleetd #446)
Three rounds. Round 1 made exhaustedPattern a hot config key, made the quarantine warning name the fix instead of only the fact, and added model/reason to fleet_profiles' quarantined rows. Round 2 extracted the warning text into usageLimitFixWarning/usageLimitFixWarningNoModel and pinned both. Round 3 extracted the sink itself into a static exhaustionSink(...) factory and pinned what it actually logs, using a ListAppender on this class's own logger. Where the mutation numbers below come from, stated exactly. The battery ran on merge commit 3d2d521, tree |
||
|
|
49df79203c |
Merge #464: a test that charter text names only registered tools (fleetd #464)
CharterToolSurfaceTest extracts every fleet_* / bridge_* token from configured
launch charters and every tool("fleet_...") FleetMcp registers, then asserts the
first set is a subset of the second.
Verified on the merge commit. Its three acceptance criteria are met:
- Catches the ticket's own example. Fixture charter naming bridge_send, the tool
CB-634 renamed away: KILLED.
- Fails loudly with no charter text. Fixture stripped of every tool name: KILLED
by its named.isEmpty() guard, not a silent pass.
- Fails loudly with no registered tools. The tool("...") scrape broken so it
matches nothing: KILLED by its registered.isEmpty() guard.
- And one cell of my own: the server stops registering fleet_reply, which the
fixture names. KILLED. This is what proves the 'registered' half reads real
production source and is not a second fixture.
The scrape finds 11 registered tools: ack, ask, list, poll, profiles, reply,
send, spawn, status, stop, whoami. An independent count of every "fleet_x"
literal in FleetMcp.java is also 11.
WHAT THIS DOES NOT CLOSE, and it is the ticket's actual gap. The charter half is
a @TempDir fixture the test writes itself, so no charter text anyone writes can
make this test fail. Measured: the test mentions fleetd.yaml 0 times, and the
commit changes 0 production files -- FleetConfig.validateCharters() still never
reads charter text (0 lines of its body mention a tool name). So this pins the
comparison logic and acts as a rename tripwire for the two tools the fixture
names. It does not check the live config. That needs a production-side check and
is filed as a follow-up.
That residue is my ticket's fault, not the worker's: the three criteria I wrote
are exactly the three it met.
A note on my own battery, because it nearly published four false kills. The
first run showed rc=1 on all four mutation cells and I would have read that as
four kills. It was zsh: unquoted parameters are NOT word-split, so
'mvn -B $scope test' passed '-Dtest=X -DfailIfNoTests=false' as ONE argument and
surefire ran zero tests. The tell was a missing 'Tests run:' line. The rerun
proves the harness first -- selector alone must report 'Tests run: 1' -- and
every cell now prints its surefire summary count so a void cell cannot pass for
a kill.
|
||
|
|
7772b41993 |
fleetd #446 round 3: extract exhaustionSink and pin its caller (Cell A/B)
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). |
||
|
|
c4e23eebad | fleetd #464: guard charter tool names | ||
|
|
7df503dfc2 |
fleetd #463: default listFleet's callerIsPrimary to false, fail closed
The compat overload at FleetMcp.java:1373 defaulted callerIsPrimary to a literal true, so a caller that forgot the argument silently got the coordinator row (this daemon's coord-id, mailbox state, held-mail previews, peer reachability) -- lead-to-lead state fleetd #439 just gated. Flip the default to false: a forgotten argument now yields a missing row instead of a leaked one. Six FleetMcpTest methods relied on the implicit true to see the coordinator row at all; they now pass true explicitly through the canonical overload. listIsByteForByteUnchangedForThePrimaryCaller's own premise (comparing the implicit-default path against an explicit-true path) was the shape of the bug, so it now only exercises the explicit-true path. Added listCompatOverloadWithNoCallerIsPrimaryArgumentOmitsTheCoordinatorKey to pin the new default: a compat overload called with no callerIsPrimary argument, against a fully-configured lead channel, must produce a result with the coordinator key absent -- not empty, not redacted, absent. |
||
|
|
92a96fcbd8 |
Merge #462: gate the coordinator row on a named predicate the handler must consult (fleetd #439)
Verified by me on a local merge of |
||
|
|
13482872bb |
contract test: keep the loaded run's passes, discard only its failure
The javadoc threw away the whole loaded run as "not clean evidence". The
fixed cell order makes that too strong in one direction. A cell that runs
last on a climbing load has a free explanation for FAILING. It has no free
explanation for PASSING: surviving a worse condition than a fair order
would have given it is evidence in the safe direction. So the old version's
0 of 3 is still discarded, and the three passes are kept with the load each
one ran at.
Also names the cell that tests the swallow explanation head-on, which the
old text left as "nobody has managed that yet". SHELL_READY_TIMEOUT_MS at 0
types input at once — the worst case for "typed before the prompt" — and it
passed 3 of 3 at load 18.42 to 23.65. The wider read window cannot explain
that away, because a swallowed keystroke is lost, not late: the command
never runs, so no amount of polling makes its output appear.
And a warning not to carry the raw load average to another host. Load
average counts differently per core and per operating system, so only load
per core compares. I broke that rule myself when comparing this run with
another host's numbers.
Both points came from the fleet01 lead reviewing
|
||
|
|
c1ca6273fc |
fleetd #439: pin the caller at the fleet_list call site, not just the gate
Review of PR #462 found M2: the coordinatorVisibleTo gate (then an inline principal(exchange).isPrimary() check) could survive a mutation that replaced the argument with a literal true at the one production call site, because every existing test drove listFleet directly and supplied the boolean itself -- nothing exercised the handler's own call. - Name the decision: FleetMcp.coordinatorVisibleTo(Principal), a small package-private predicate next to denyFor/recordPrimarySingleton. The fleet_list handler now calls coordinatorVisibleTo(principal(exchange)) instead of inlining .isPrimary(). - Pin the predicate's role table in FleetMcpAuthzTest (onlyThePrimaryMaySeeTheCoordinatorRow), covering primary/worker/ architect and, newly, anonymous. - Add a source-reading detector at the boundary (theFleetListHandlerActuallyConsultsCoordinatorVisibleTo), same idiom as toolsTheServerRegisters/everyRegisteredToolHasItsHandlerActionPinned: it reads FleetMcp.java, isolates the listHandler block, asserts (as a control) that the block actually contains a listFleet( call, then asserts the call's trailing boolean argument is exactly coordinatorVisibleTo(principal(exchange)) -- not a literal true/false. Both mutations from the review were reproduced and killed by these tests, then reverted; see the PR body for the full break-and-restore transcript. |
||
|
|
30d6872779 |
fleetd #446 follow-up: pin the WARNING text and the fleet_profiles model/reason fields
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. |
||
|
|
e54e3d87ea |
fleetd #439: omit fleet_list's coordinator key for non-primary callers
The coordinator row is lead-to-lead coordination state (coord-ids, mailbox facts, held-message previews). fleet_list returned it to every caller, including a worker or an architect, because coordinatorView() had no way to know who was asking. Gate at the call site inside listFleet: a new overload takes callerIsPrimary and only assembles/attaches the coordinator row when it is true, so the key is absent (not empty) for a worker or an architect. The MCP handler now passes principal(exchange).isPrimary(); every other listFleet overload keeps passing true, so callers with no caller identity (existing unit tests, the no-op wrappers) are unaffected -- confirmed by a byte-for-byte comparison test against the pre-fix overload. Authz's READ case is untouched: it stays shared by fleet_status, fleet_profiles and fleet_whoami, and the gate here is purely inside fleet_list's own result assembly. |
||
|
|
3c5873dfe2 |
fleetd #453: point the new javadoc at the right javadoc
#456's first paragraph said "HerdrPeerLauncher's own {@link
#spawn(SpawnRequest, PlacementDecision)} javadoc". That link resolves to this
interface's own abstract declaration, not to HerdrPeerLauncher's override, so a
reader who follows it lands on the wrong text. The second paragraph already
used the plain {@code HerdrPeerLauncher.spawn(...)} form; both now match.
Also says who is actually forced to read the paragraph, because #456's
reasoning rests on it and the two cases differ. A class that implements this
interface directly must write a body for spawn(SpawnRequest,
PlacementDecision) - it is abstract here - so it reads this javadoc. A
subclass of HerdrPeerLauncher does not: HerdrPeerLauncher already implements
that method (member/HerdrPeerLauncher.java:611) and the subclass inherits the
body. For a subclass the paragraph is advice, not a gate.
javadoc -Ddoclint=reference: 5 "reference not found", the same 5 in the same 5
untouched files as origin/main at
|
||
|
|
e29227d5f4 |
Merge #456: document the override obligation on PeerLauncher.defaultProfileFor/place (fleetd #453)
Doc-only, one file, 17 added lines. Verified by me on a local merge of |
||
|
|
45aca9eb3e |
fleetd #446: make exhaustedPattern hot, name the fix in the warning, report it in fleet_profiles
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. |
||
|
|
2af13ab1ff |
fleetd #449: name the decisive cell and the load the claim was measured at
The previous comment named a cause with no cell behind it. fleet01's review made the point: a confident wrong mechanism gets copied, and a confident under-determined one gets copied the same way. So the comment now names the cell that settles it. Hold the old 1000ms write sleep and change only the read - 800ms fixed sleep becomes a 5s poll - and the test goes 0 of 3 to 3 of 3. The read deadline was the whole story. It also says where: a 12-core macOS host near idle (load 2.6 to 5.9). The loaded run agreed, but its load climbed from 7 to 50 while the cells ran and the old version ran last, so it is not clean evidence and the comment says so. Comment only. No test or production code changed. |
||
|
|
0788d84be8 |
fleetd #453: document the override obligation on PeerLauncher.defaultProfileFor/place
Decision: leave both as default methods (option 1), not abstract. Neither default is a live defect today — HerdrPeerLauncher is the sole single-profile implementer and the degenerate answer (ignore role, always defaultProfile()) is correct for it. Making them abstract would force ~10 boilerplate one-line overrides across 5 unrelated PeerLauncher test doubles (NeverSpawnsLauncher, RaceLauncher, NoResumeLauncher, ClearContextSpyLauncher, LazyIdLauncher) that never call either method, for a risk that is speculative (no multi-profile HerdrPeerLauncher subclass exists or is planned). Strengthens both javadocs with an explicit MUST-override warning and cross- references HerdrPeerLauncher.spawn(SpawnRequest, PlacementDecision)'s existing #450 javadoc, which already names both methods as "unoverridden here" and ties that to being a single-profile adapter -- the concrete place a future multi-profile launcher author would read, since #450 made that method abstract and any subclass must write its body. |
||
|
|
20c1094cbf |
fleetd #449: say what the timing fix actually proved, not what it assumed
The polling fix that landed in #452 is right, but its javadoc named a mechanism nobody measured: that input typed before the shell's prompt was swallowed by the shell's own startup. I mutated the settle poll away — SHELL_READY_TIMEOUT_MS = 0, so input is typed at once with no wait — and the test passed 3 of 3. So waitForText is the load-bearing half, and the proven cause is the old 800ms READ deadline, not the 1000ms write delay. The direction is the point: typing at 0ms works where typing at 1000ms failed. If early input were swallowed, 0ms would be worse than 1000ms. It is better, so the swallow explanation is unsupported. waitUntilSettled stays as cheap insurance, now labelled as insurance rather than as the fix. Comment-only; AgentControlContractTest still green. |
||
|
|
9011c59b9f |
Merge #452: run the contract tag in CI, fix the stale protocol 14 assertion (fleetd #449)
Verified on a locally built merge onto |
||
|
|
c11ad71ed0 |
Merge #448: fleet_ack errors instead of claiming success on a miss (fleetd #437)
Verified by the lead on head |
||
|
|
d4f93a7b13 |
fleetd #449: fix stale herdr protocol 14 javadocs/assertion, diagnose and fix the timing-raced AgentControlContractTest, select contract tests by tag in CI
- HerdrClient.java, HerdrCodec.java, HerdrContractTest.java: the herdr port to
protocol 19 (CB-521) left the client javadoc and the contract test's own
assertion still saying protocol 14 / herdr 0.7.0. Updated to 19 / 0.8.0 and
renamed pingReturnsProtocol14 -> pingReturnsProtocol19. Verified the
assertion is real by temporarily changing the expected value to 20 (fails),
then restoring 19 (passes).
- AgentControlContractTest.java: tabCreateInjectsEnvIntoTheSeedShell was
failing, not skipping, on a host with a live herdr socket. Diagnosed with a
temporary instrumented run (not committed) that polled the pane every
200ms before and after sending input: the seed shell reliably takes ~2.5s
to reach its prompt (measured 3x), while the test's fixed 1000ms sleep
raced that startup. Input typed too early was swallowed by the shell's own
startup, leaving the typed line followed by the "Restored session" banner
and no command output — indistinguishable at a glance from the env map
never reaching the shell. Once the shell was actually ready, the injected
env value showed up in ~200ms, ruling out an env-seam defect. Replaced both
fixed sleeps with bounded polling on the actual conditions (pane text
settling, then the expected output appearing). Ran the fixed test 3x
standalone, all green.
- .gitea/workflows/ci.yml: the "Contract tests" step ran exactly one class by
name (-Dtest=AmqpReplyInboxContractTest), silently excluding every other
@Tag("contract") test from CI including the herdr ones above -- which is
how the stale protocol 14 assertion went unnoticed. Changed to
-Dgroups=contract, which selects the whole tagged group and picks up
future contract tests automatically.
|
||
|
|
cfebc575ea |
fleetd #450: make PeerLauncher.spawn(SpawnRequest, PlacementDecision) abstract
The default re-entered the single-argument spawn(SpawnRequest), which re-runs
checks that can refuse the profile place() just chose (#444's window). Only
CompositePeerLauncher overrode it; a future placement-doing launcher could
have inherited the wrong body silently.
Give every current implementer an explicit override, chosen by what it does:
- HerdrPeerLauncher (base of ClaudeCodeLauncher/OpenCodeLauncher, neither of
which overrides spawn(req) or place()) does no placement filtering of its
own, so it gets the re-entering form.
- CompositePeerLauncher's routing-form override is untouched.
- 5 test-fake PeerLauncher implementers (SessionManagerTest, FleetdBackendErrorSinkTest)
get overrides matching their existing spawn(SpawnRequest) shape: delegating
wrappers delegate, unreachable stubs throw, the single-profile fake re-enters.
ConfigRef does not implement PeerLauncher at all (confirmed in this tree at
|
||
|
|
5289eb509f |
fleetd #437: pin the ack hit/miss contract in the AMQP contract test
AmqpReplyInboxContractTest is the one contract-group class CI actually runs, and it never asserted on ack()'s return value at all — so the exact defect this ticket fixes (reporting success for an ack that removed nothing) was unpinned in the adapter fleetd runs live. Add ackReportsHitVsMissAgainstARealBroker: a msgId never held for an owned target returns false without throwing, a real held reply returns true and is removed, and acking the same msgId again returns false. Ran against both broker modes the class supports: Testcontainers (AMQP_URI unset) and an external broker via AMQP_URI (the CI shape, using a disposable container — not the shared local LavinMQ instance). |
||
|
|
e4c703a51a |
fleetd #444: separate the adapter's fallback default from the decided profile
Review found the fixture's StubLauncher fell back to 'sol' too — the
same profile place() decides — so an UNSTAMPED request could land on
spawnCount('sol') by coincidence, and the assertion's claim that the
request 'actually carried sol' was unproven. Dropping the stamping
(SpawnRequest routedReq = req) while keeping the routing survived the
test unchanged.
Fix: give the adapter 'b' as its own fallback default instead, so an
unstamped request counts against 'b', not 'sol'. Verified both
mutations against the single test in isolation:
- drop-stamping (routedReq = req): RED, expected <sol> but was <b>
- re-entering (return spawn(req.withProfile(decision.profile())))
i.e. M1 from the first round: still RED, PlacementException
naming the now-quarantined 'sol'
Restored both; full suite green at 1575 tests.
No changes to src/main — PeerLauncher's javadoc from the first round
is unchanged.
|
||
|
|
703a05db41 |
fleetd #437: fleet_ack errors instead of claiming success on a miss
ReplyInbox.ack now returns boolean (true = removed, false = nothing to
remove) instead of void, so FleetMcp.ack can finally tell a hit from a
miss. FleetMcp.ack returns an error when the boolean is false, naming
fleet_poll{coordId} for held peer mail, which has no route through this
call. MessageService.ackReply propagates the boolean; drainReplies keeps
ignoring it (its own javadoc already documents that loss window as
deliberate). Updated the tool schema's target description to match.
Rewrote FleetMcpTest's ack tests to publish a real message before
asserting success, and added tests for a never-queued id and a coord-id
target, both now erroring. Added boolean assertions to
InMemoryReplyInboxTest's existing ack cases.
|
||
|
|
3f036b2a62 |
fleetd #444: pin the place()-to-spawn() window PlacementDecision closes
Add a test that resolves place(role) while nothing is quarantined, then quarantines the resolved profile's credential BEFORE spawning against the held PlacementDecision. CompositePeerLauncher.spawn(req, decision) must still honor the decision and land on the quarantined profile, since it never re-runs the explicit-profile enforce* checks. Verified the test kills the regression: with the override's body replaced by the re-entering spawn(req.withProfile(...)) form, this exact test goes RED with a PlacementException naming the now- quarantined profile; restored, the full suite is green (1575 tests). Also documents on PeerLauncher's default spawn(req, decision) that a launcher routing across more than one profile MUST override it, naming the four enforce* checks the default's re-entry re-applies. |
||
|
|
82fae94c55 |
Merge #445: pin every startup report call in Fleetd.main (fleetd #442)
Test written by a worker whose backend died before it could report; evidence re-run by the lead against the merged tree. Verified: merge of current main clean (0 conflicts); full build 1574 tests, 0 failures, BUILD SUCCESS, 0 compile errors; control green; deleting each of reportGitHostShape, reportMemberTrustModel, reportMemberCredentialsGap and reportExhaustedPatternGap from main() is KILLED by mainReportsEveryStartupGapBeforeValidationAborts. |