From 432c1d92d159115122b9d0cf260946a608ea23f7 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Thu, 3 Sep 2026 09:17:10 +0700 Subject: [PATCH] fleetd #176 review: cover the namePrefix guard, the adapter-refinement corroboration that had no test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agentType corroboration (guard b) had positive and negative tests, but the per-adapter namePrefix guard (guard a) had none — nothing proved that an opencode pane can never reach StatusRefiner.classify, only that a claude pane with a null agentType is rejected. Since a live opencode pane always reports a non-null agentType ("opencode"), guard (b) alone cannot catch a broken guard (a). Added opencodePaneIsNeverRefinedEvenWhenItsContentLooksLikeAnIdleClaudePrompt to OpenCodeLauncherTest: agentType("opencode") (non-null, satisfies guard b on its own) + pane content containing "❯" (would classify as IDLE) + raw status UNKNOWN throughout. Asserts the gate still times out, and that no agent.read call used source=detection (StatusRefiner.PROBE_SOURCE) — proving the refiner was never even reached, not just that its answer was discarded. A blanket "agent.read is never called" does not hold here: HerdrPeerLauncher.readPaneQuietly reads the pane tail (source=recent) for the timeout log on every timeout, regardless of adapter, so the assertion is scoped to the refiner's own probe source instead. Mutation check performed and reverted before this commit: temporarily changed `if (!"claude".equals(namePrefix))` to `if (false)` in HerdrPeerLauncher.refinedInjectable — the new test failed ("Expected PeerUnreachableException to be thrown, but nothing was thrown"), confirming it actually exercises the guard. Restored the guard and reran — test passes again (1/1). No production code changed in this commit. mvn clean install: Tests run: 1123, Failures: 0, Errors: 0, Skipped: 0 -- BUILD SUCCESS --- .../fleet/member/OpenCodeLauncherTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/fleetd/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java b/fleetd/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java index df70228..3cb9745 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/member/OpenCodeLauncherTest.java @@ -396,6 +396,46 @@ class OpenCodeLauncherTest { assertNotNull(ex.getMessage()); } + /** + * fleetd #176 fix 2's per-adapter guard: {@code StatusRefiner.classify} is written for the + * Claude Code TUI only, and the spawn-readiness gate must never run it against an opencode + * pane. {@code agentType("opencode")} deliberately satisfies the OTHER guard (the corroborating + * liveness check) so it cannot be what makes this test pass — only the {@code namePrefix} + * check can be. If that check were ever removed, this pane's {@code ❯} content would refine + * straight to IDLE and the gate would report ready before the backend actually was. + */ + @Test + void opencodePaneIsNeverRefinedEvenWhenItsContentLooksLikeAnIdleClaudePrompt(@TempDir Path root) { + FakeHerdr herdr = new FakeHerdr(); + herdr.agentStatus("unknown"); // always UNKNOWN + herdr.agentType("opencode"); // non-null — satisfies the liveness guard on its own + herdr.readText("some-host:~ user$ ❯ "); // content StatusRefiner.classify reads as IDLE + long[] clock = {0}; + OpenCodeLauncher svc = new OpenCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr), + Map.of("gemini", opencodeCfg(null, null, null)), "gemini", _ -> null, + 1000, () -> clock[0], () -> clock[0] += 50, root, root); + + assertThrows(PeerUnreachableException.class, + () -> svc.spawn(new SpawnRequest(null, null, null))); + + assertTrue(clock[0] >= 1000, + "an opencode pane must never refine to injectable, however its content reads — " + + "the gate has to wait out the full timeout: clock only reached " + clock[0]); + // A blanket "agent.read is never called" does not hold here: the timeout path itself reads + // the pane tail (source=recent) for its own log message, on every timeout, regardless of + // adapter — see HerdrPeerLauncher.readPaneQuietly. So assert on the refiner's OWN probe + // source (StatusRefiner.PROBE_SOURCE = "detection") instead — that call happens only inside + // StatusRefiner.refine, so its absence proves the refiner itself was never reached for this + // opencode pane, not merely that its answer was discarded. + long detectionReads = herdr.calls.stream() + .filter(c -> c.method().equals("agent.read")) + .filter(c -> "detection".equals(((Map) c.params()).get("source"))) + .count(); + assertEquals(0, detectionReads, + "the refiner's own pane probe (source=detection) must never run against an " + + "opencode pane — the namePrefix guard has to stop it before that call"); + } + @Test void spawnReturnsHandleWhenGateDisabled(@TempDir Path root) { FakeHerdr herdr = new FakeHerdr();