CB-401 Stage A follow-up: rename WorkerService -> ClaudeCodeLauncher

Name the first-class Claude Code adapter explicitly, per the Peer Launcher SPI:
WorkerService was the de-facto Claude-Code launcher; as an in-tree PeerLauncher impl
it should say so. Pure IDE rename (class + file + WorkerServiceTest) plus stale
Javadoc/comment mentions swept to the new name. No behaviour change.

Gate: IDE diagnostics 0/0 on touched files; mvn clean install BUILD SUCCESS,
MVN_EXIT=0, 183 tests pass. Deferral #1 from issue #3 cleared.
This commit is contained in:
Dai Ha
2026-07-18 14:39:56 +02:00
parent e056c7e1fa
commit 3aa69a9e32
11 changed files with 60 additions and 60 deletions
@@ -22,7 +22,7 @@ import dev.ltms.bridged.session.GitWorktrees;
import dev.ltms.bridged.session.SessionManager; import dev.ltms.bridged.session.SessionManager;
import dev.ltms.bridged.peer.PeerLauncher; import dev.ltms.bridged.peer.PeerLauncher;
import dev.ltms.bridged.session.SessionReaper; import dev.ltms.bridged.session.SessionReaper;
import dev.ltms.bridged.worker.WorkerService; import dev.ltms.bridged.worker.ClaudeCodeLauncher;
import io.javalin.Javalin; import io.javalin.Javalin;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -57,13 +57,13 @@ public final class Bridged {
AgentControl agents = new AgentControl(herdr); AgentControl agents = new AgentControl(herdr);
WorkspaceControl spaces = new WorkspaceControl(herdr); WorkspaceControl spaces = new WorkspaceControl(herdr);
PeerLauncher workers = new WorkerService(agents, spaces, guard, PeerLauncher workers = new ClaudeCodeLauncher(agents, spaces, guard,
cfg.workerProfiles(), cfg.defaultProfile(), System::getenv); cfg.workerProfiles(), cfg.defaultProfile(), System::getenv);
// CB-117: herdr keeps worker panes alive across a daemon restart, and their ids died with // CB-117: herdr keeps worker panes alive across a daemon restart, and their ids died with
// the previous process — reap those leaked orphans now, before we start serving. // the previous process — reap those leaked orphans now, before we start serving.
workers.reapOrphanWorkers(); workers.reapOrphanWorkers();
// CB-301: authoritative session registry + lifecycle FSM on top of WorkerService. // CB-301: authoritative session registry + lifecycle FSM on top of ClaudeCodeLauncher.
// CB-301-ext: worktree provisioning seam, optionally rooted at a configured directory. // CB-301-ext: worktree provisioning seam, optionally rooted at a configured directory.
// CB-303 part 2: context cap is opt-in and disabled (0) when absent/null. // CB-303 part 2: context cap is opt-in and disabled (0) when absent/null.
int contextCap = 0; int contextCap = 0;
@@ -121,8 +121,8 @@ public final class Bridged {
// Caller identity is resolved from the connection (peer PID → herdr pane), not arguments. // Caller identity is resolved from the connection (peer PID → herdr pane), not arguments.
ConnectionIdentity identity = new ConnectionIdentity( ConnectionIdentity identity = new ConnectionIdentity(
new PaneLocator(herdr), new LsofPeerPidLookup(), new LsofProcessCwdLookup()); new PaneLocator(herdr), new LsofPeerPidLookup(), new LsofProcessCwdLookup());
// Cast to WorkerService: BridgeMcp is not yet migrated to PeerLauncher (Stage A scope). // Cast to ClaudeCodeLauncher: BridgeMcp is not yet migrated to PeerLauncher (Stage A scope).
BridgeMcp mcp = new BridgeMcp(messages, rendezvous, (WorkerService) workers, sessions, identity, presence); BridgeMcp mcp = new BridgeMcp(messages, rendezvous, (ClaudeCodeLauncher) workers, sessions, identity, presence);
// CB-303 part 3: single ordered shutdown hook. Drain sessions first while herdr is still // CB-303 part 3: single ordered shutdown hook. Drain sessions first while herdr is still
// open (so releases reach the daemon), then stop poller/message/mcp/reaper, and close herdr // open (so releases reach the daemon), then stop poller/message/mcp/reaper, and close herdr
@@ -136,7 +136,7 @@ public final class Bridged {
herdr.close(); herdr.close();
})); }));
Javalin app = new BridgedApp(herdr, (WorkerService) workers, sessions, messages, rendezvous, presence, mcp.servlet()).build(); Javalin app = new BridgedApp(herdr, (ClaudeCodeLauncher) workers, sessions, messages, rendezvous, presence, mcp.servlet()).build();
app.start(cfg.bind().host(), cfg.bind().port()); app.start(cfg.bind().host(), cfg.bind().port());
log.info("bridged listening on {}:{}, herdr socket {}", log.info("bridged listening on {}:{}, herdr socket {}",
cfg.bind().host(), cfg.bind().port(), socket); cfg.bind().host(), cfg.bind().port(), socket);
@@ -12,7 +12,7 @@ import java.util.Map;
* *
* <p>herdr owns the PID→pane truth: {@code pane.process_info} reports each pane's {@code shell_pid} * <p>herdr owns the PID→pane truth: {@code pane.process_info} reports each pane's {@code shell_pid}
* and foreground process PIDs. This scans agent panes; a spawn-time {@code pid→terminal} cache is * and foreground process PIDs. This scans agent panes; a spawn-time {@code pid→terminal} cache is
* the obvious optimization once wired into {@code WorkerService}. * the obvious optimization once wired into {@code ClaudeCodeLauncher}.
*/ */
public final class PaneLocator { public final class PaneLocator {
@@ -9,7 +9,7 @@ import dev.ltms.bridged.msg.Rendezvous;
import dev.ltms.bridged.session.SessionManager; import dev.ltms.bridged.session.SessionManager;
import dev.ltms.bridged.session.WorkerSession; import dev.ltms.bridged.session.WorkerSession;
import dev.ltms.bridged.session.WorktreeRequest; import dev.ltms.bridged.session.WorktreeRequest;
import dev.ltms.bridged.worker.WorkerService; import dev.ltms.bridged.worker.ClaudeCodeLauncher;
import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapperSupplier; import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapperSupplier;
@@ -34,8 +34,8 @@ import java.util.stream.Collectors;
* / {@code bridge_status}; the worker calls {@code bridge_reply}. * / {@code bridge_status}; the worker calls {@code bridge_reply}.
* *
* <p>Beyond delegation the primary also manages the fleet here (CB-108): {@code bridge_spawn} / * <p>Beyond delegation the primary also manages the fleet here (CB-108): {@code bridge_spawn} /
* {@code bridge_list} / {@code bridge_stop} adapt {@link WorkerService} so a worker's whole lifecycle * {@code bridge_list} / {@code bridge_stop} adapt {@link ClaudeCodeLauncher} so a worker's whole lifecycle
* is driven through MCP, with the subscription boundary still enforced inside {@code WorkerService}. * is driven through MCP, with the subscription boundary still enforced inside {@code ClaudeCodeLauncher}.
* *
* <p>The tool <em>logic</em> lives in package-private static methods returning a * <p>The tool <em>logic</em> lives in package-private static methods returning a
* {@link McpSchema.CallToolResult}, so it is unit-testable without standing up the HTTP transport; * {@link McpSchema.CallToolResult}, so it is unit-testable without standing up the HTTP transport;
@@ -59,7 +59,7 @@ public final class BridgeMcp {
private final HttpServletStreamableServerTransportProvider transport; private final HttpServletStreamableServerTransportProvider transport;
private final McpSyncServer server; private final McpSyncServer server;
public BridgeMcp(MessageService messages, Rendezvous rendezvous, WorkerService workers, public BridgeMcp(MessageService messages, Rendezvous rendezvous, ClaudeCodeLauncher workers,
SessionManager sessions, ConnectionIdentity identity, WorkerPresence presence) { SessionManager sessions, ConnectionIdentity identity, WorkerPresence presence) {
McpJsonMapper json = new JacksonMcpJsonMapperSupplier().get(); McpJsonMapper json = new JacksonMcpJsonMapperSupplier().get();
this.transport = HttpServletStreamableServerTransportProvider.builder() this.transport = HttpServletStreamableServerTransportProvider.builder()
@@ -103,7 +103,7 @@ public final class BridgeMcp {
status(messages, str(req.arguments(), "sessionId"))) status(messages, str(req.arguments(), "sessionId")))
.toolCall(pollTool(), (_, req) -> .toolCall(pollTool(), (_, req) ->
poll(messages, str(req.arguments(), "ticket"))) poll(messages, str(req.arguments(), "ticket")))
// Fleet management (CB-108): spawn/list/stop over WorkerService. // Fleet management (CB-108): spawn/list/stop over ClaudeCodeLauncher.
.toolCall(spawnTool(), (exchange, req) -> { .toolCall(spawnTool(), (exchange, req) -> {
Map<String, Object> a = req.arguments(); Map<String, Object> a = req.arguments();
// CB-112: worker inherits the primary's cwd unless the call pins one. // CB-112: worker inherits the primary's cwd unless the call pins one.
@@ -342,14 +342,14 @@ public final class BridgeMcp {
} }
/** {@code bridge_profiles}: the configured worker profiles and the default. */ /** {@code bridge_profiles}: the configured worker profiles and the default. */
static McpSchema.CallToolResult profiles(WorkerService workers) { static McpSchema.CallToolResult profiles(ClaudeCodeLauncher workers) {
return text(json(Map.of( return text(json(Map.of(
"profiles", workers.profiles(), "profiles", workers.profiles(),
"default", workers.defaultProfile() == null ? "" : workers.defaultProfile()))); "default", workers.defaultProfile() == null ? "" : workers.defaultProfile())));
} }
/** {@code bridge_list}: bridge-owned roster merged with live herdr status by paneId. */ /** {@code bridge_list}: bridge-owned roster merged with live herdr status by paneId. */
static McpSchema.CallToolResult listWorkers(WorkerService workers, SessionManager sessions) { static McpSchema.CallToolResult listWorkers(ClaudeCodeLauncher workers, SessionManager sessions) {
try { try {
Map<String, Agent> live = workers.list().stream() Map<String, Agent> live = workers.list().stream()
.filter(a -> a.paneId() != null) .filter(a -> a.paneId() != null)
@@ -6,7 +6,7 @@ import java.util.Set;
/** /**
* SPI for materializing a connected peer — the only way the bridge core creates or tears down * SPI for materializing a connected peer — the only way the bridge core creates or tears down
* a peer process. Every launcher is a first-party, in-tree adapter selected by (future) profile * a peer process. Every launcher is a first-party, in-tree adapter selected by (future) profile
* config; today's single adapter is the {@code WorkerService} / Claude Code over herdr. * config; today's single adapter is the {@code ClaudeCodeLauncher} / Claude Code over herdr.
* *
* <p>The core delegates spawn and teardown to this interface without knowing how the peer is set * <p>The core delegates spawn and teardown to this interface without knowing how the peer is set
* up. Environment variables, CLI flags, subscription guards, transport (herdr tab/pane) layout, * up. Environment variables, CLI flags, subscription guards, transport (herdr tab/pane) layout,
@@ -12,7 +12,7 @@ import dev.ltms.bridged.msg.Rendezvous;
import dev.ltms.bridged.session.SessionManager; import dev.ltms.bridged.session.SessionManager;
import dev.ltms.bridged.session.WorkerSession; import dev.ltms.bridged.session.WorkerSession;
import dev.ltms.bridged.session.WorktreeRequest; import dev.ltms.bridged.session.WorktreeRequest;
import dev.ltms.bridged.worker.WorkerService; import dev.ltms.bridged.worker.ClaudeCodeLauncher;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context; import io.javalin.http.Context;
import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServlet;
@@ -44,7 +44,7 @@ public final class BridgedApp {
private static final long MAX_ASK_TIMEOUT_MS = 115_000; private static final long MAX_ASK_TIMEOUT_MS = 115_000;
private final HerdrClient herdr; private final HerdrClient herdr;
private final WorkerService workers; private final ClaudeCodeLauncher workers;
private final SessionManager sessions; // CB-301: authoritative session registry private final SessionManager sessions; // CB-301: authoritative session registry
private final MessageService messages; private final MessageService messages;
private final Rendezvous rendezvous; private final Rendezvous rendezvous;
@@ -52,7 +52,7 @@ public final class BridgedApp {
private final HttpServlet mcpServlet; // MCP Streamable-HTTP endpoint, mounted at /mcp (nullable) private final HttpServlet mcpServlet; // MCP Streamable-HTTP endpoint, mounted at /mcp (nullable)
private final ObjectMapper mapper = new ObjectMapper(); private final ObjectMapper mapper = new ObjectMapper();
public BridgedApp(HerdrClient herdr, WorkerService workers, SessionManager sessions, public BridgedApp(HerdrClient herdr, ClaudeCodeLauncher workers, SessionManager sessions,
MessageService messages, Rendezvous rendezvous, WorkerPresence presence, MessageService messages, Rendezvous rendezvous, WorkerPresence presence,
HttpServlet mcpServlet) { HttpServlet mcpServlet) {
this.herdr = herdr; this.herdr = herdr;
@@ -42,9 +42,9 @@ import java.util.regex.Pattern;
* clutter the user's real work spaces. Teardown removes the worker's pane <em>and</em> its * clutter the user's real work spaces. Teardown removes the worker's pane <em>and</em> its
* now-empty tab, tolerating an already-gone worker so a repeated DELETE is harmless. * now-empty tab, tolerating an already-gone worker so a repeated DELETE is harmless.
*/ */
public final class WorkerService implements PeerLauncher { public final class ClaudeCodeLauncher implements PeerLauncher {
private static final Logger log = LoggerFactory.getLogger(WorkerService.class); private static final Logger log = LoggerFactory.getLogger(ClaudeCodeLauncher.class);
/** herdr rejects a duplicate agent {@code name}; we retry a bumped name this many times. */ /** herdr rejects a duplicate agent {@code name}; we retry a bumped name this many times. */
private static final int NAME_RETRIES = 8; private static final int NAME_RETRIES = 8;
@@ -86,9 +86,9 @@ public final class WorkerService implements PeerLauncher {
// cannot collide with same-profile workers that outlived a restart. See startUniquelyNamed. // cannot collide with same-profile workers that outlived a restart. See startUniquelyNamed.
private final String nameNonce = String.format("%06x", new SecureRandom().nextInt(1 << 24)); private final String nameNonce = String.format("%06x", new SecureRandom().nextInt(1 << 24));
public WorkerService(AgentControl agents, WorkspaceControl spaces, SubscriptionGuard guard, public ClaudeCodeLauncher(AgentControl agents, WorkspaceControl spaces, SubscriptionGuard guard,
Map<String, BridgedConfig.Worker> profiles, String defaultProfile, Map<String, BridgedConfig.Worker> profiles, String defaultProfile,
Function<String, String> env) { Function<String, String> env) {
this.agents = agents; this.agents = agents;
this.spaces = spaces; this.spaces = spaces;
this.guard = guard; this.guard = guard;
@@ -12,7 +12,7 @@ import dev.ltms.bridged.session.FakeWorktrees;
import dev.ltms.bridged.session.SessionManager; import dev.ltms.bridged.session.SessionManager;
import dev.ltms.bridged.session.WorkerSession; import dev.ltms.bridged.session.WorkerSession;
import dev.ltms.bridged.session.WorktreeRequest; import dev.ltms.bridged.session.WorktreeRequest;
import dev.ltms.bridged.worker.WorkerService; import dev.ltms.bridged.worker.ClaudeCodeLauncher;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -39,11 +39,11 @@ class BridgeMcpTest {
return ((McpSchema.TextContent) r.content().getFirst()).text(); return ((McpSchema.TextContent) r.content().getFirst()).text();
} }
private static WorkerService workerService(FakeHerdr h, String baseUrl, Set<String> allow) { private static ClaudeCodeLauncher workerService(FakeHerdr h, String baseUrl, Set<String> allow) {
BridgedConfig.Worker cfg = new BridgedConfig.Worker( BridgedConfig.Worker cfg = new BridgedConfig.Worker(
"ltms-local", baseUrl, "coder", null, "BRIDGED_WORKER_TOKEN", null, "ltms-local", baseUrl, "coder", null, "BRIDGED_WORKER_TOKEN", null,
"tab", "bridged-workers", "worker: {profile} #{n}", null, null, null); "tab", "bridged-workers", "worker: {profile} #{n}", null, null, null);
return new WorkerService(new AgentControl(h), new WorkspaceControl(h), return new ClaudeCodeLauncher(new AgentControl(h), new WorkspaceControl(h),
new SubscriptionGuard(allow), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> "tok"); new SubscriptionGuard(allow), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> "tok");
} }
@@ -16,7 +16,7 @@ import dev.ltms.bridged.session.FakeWorktrees;
import dev.ltms.bridged.session.GitWorktrees; import dev.ltms.bridged.session.GitWorktrees;
import dev.ltms.bridged.session.SessionManager; import dev.ltms.bridged.session.SessionManager;
import dev.ltms.bridged.session.Worktrees; import dev.ltms.bridged.session.Worktrees;
import dev.ltms.bridged.worker.WorkerService; import dev.ltms.bridged.worker.ClaudeCodeLauncher;
import io.javalin.Javalin; import io.javalin.Javalin;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -64,7 +64,7 @@ class BridgedAppTest {
"ltms-local", workerBaseUrl, "coder", null, "BRIDGED_WORKER_TOKEN", null, "ltms-local", workerBaseUrl, "coder", null, "BRIDGED_WORKER_TOKEN", null,
placement, "bridged-workers", "worker: {profile} #{n}", null, null, null); placement, "bridged-workers", "worker: {profile} #{n}", null, null, null);
AgentControl agents = new AgentControl(herdr); AgentControl agents = new AgentControl(herdr);
WorkerService workers = new WorkerService( ClaudeCodeLauncher workers = new ClaudeCodeLauncher(
agents, new WorkspaceControl(herdr), new SubscriptionGuard(allow), agents, new WorkspaceControl(herdr), new SubscriptionGuard(allow),
Map.of(wcfg.profile(), wcfg), wcfg.profile(), Map.of(wcfg.profile(), wcfg), wcfg.profile(),
k -> "BRIDGED_WORKER_TOKEN".equals(k) ? "tok-abc" : null); k -> "BRIDGED_WORKER_TOKEN".equals(k) ? "tok-abc" : null);
@@ -5,7 +5,7 @@ import dev.ltms.bridged.guard.SubscriptionGuard;
import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.AgentControl;
import dev.ltms.bridged.herdr.FakeHerdr; import dev.ltms.bridged.herdr.FakeHerdr;
import dev.ltms.bridged.herdr.WorkspaceControl; import dev.ltms.bridged.herdr.WorkspaceControl;
import dev.ltms.bridged.worker.WorkerService; import dev.ltms.bridged.worker.ClaudeCodeLauncher;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.List; import java.util.List;
@@ -28,7 +28,7 @@ class SessionManagerTest {
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN", "ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "ltms-local"), "tab", "bridged-workers", List.of("ccs", "ltms-local"), "tab", "bridged-workers",
"worker: {profile} #{n}", null, null, null); "worker: {profile} #{n}", null, null, null);
WorkerService workers = new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), ClaudeCodeLauncher workers = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null); new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null);
return new SessionManager(workers); return new SessionManager(workers);
} }
@@ -42,7 +42,7 @@ class SessionManagerTest {
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN", "ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "ltms-local"), "tab", "bridged-workers", List.of("ccs", "ltms-local"), "tab", "bridged-workers",
"worker: {profile} #{n}", null, null, null); "worker: {profile} #{n}", null, null, null);
WorkerService workers = new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), ClaudeCodeLauncher workers = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null); new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null);
return new SessionManager(workers, new GitWorktrees(), clock, contextCap); return new SessionManager(workers, new GitWorktrees(), clock, contextCap);
} }
@@ -5,7 +5,7 @@ import dev.ltms.bridged.guard.SubscriptionGuard;
import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.AgentControl;
import dev.ltms.bridged.herdr.FakeHerdr; import dev.ltms.bridged.herdr.FakeHerdr;
import dev.ltms.bridged.herdr.WorkspaceControl; import dev.ltms.bridged.herdr.WorkspaceControl;
import dev.ltms.bridged.worker.WorkerService; import dev.ltms.bridged.worker.ClaudeCodeLauncher;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.List; import java.util.List;
@@ -21,12 +21,12 @@ import static org.junit.jupiter.api.Assertions.*;
*/ */
class WorktreeSessionManagerTest { class WorktreeSessionManagerTest {
private static WorkerService workerService(FakeHerdr herdr) { private static ClaudeCodeLauncher workerService(FakeHerdr herdr) {
BridgedConfig.Worker cfg = new BridgedConfig.Worker( BridgedConfig.Worker cfg = new BridgedConfig.Worker(
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN", "ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "ltms-local"), "tab", "bridged-workers", List.of("ccs", "ltms-local"), "tab", "bridged-workers",
"worker: {profile} #{n}", null, null, null); "worker: {profile} #{n}", null, null, null);
return new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), return new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null); new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null);
} }
@@ -18,13 +18,13 @@ import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
/** The step-4 launch-flag injection: the bridge MCP + reply charter are appended to the argv. */ /** The step-4 launch-flag injection: the bridge MCP + reply charter are appended to the argv. */
class WorkerServiceTest { class ClaudeCodeLauncherTest {
private WorkerService service(FakeHerdr herdr, List<String> argv, String mcpUrl) { private ClaudeCodeLauncher service(FakeHerdr herdr, List<String> argv, String mcpUrl) {
BridgedConfig.Worker cfg = new BridgedConfig.Worker( BridgedConfig.Worker cfg = new BridgedConfig.Worker(
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN", "ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
argv, "tab", "bridged-workers", "worker: {profile} #{n}", mcpUrl, null, null); argv, "tab", "bridged-workers", "worker: {profile} #{n}", mcpUrl, null, null);
return new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), return new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null); new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null);
} }
@@ -54,12 +54,12 @@ class WorkerServiceTest {
assertEquals(List.of("bash", "-c", "sleep 1"), spawnedArgv(herdr), "argv untouched without mcpUrl"); assertEquals(List.of("bash", "-c", "sleep 1"), spawnedArgv(herdr), "argv untouched without mcpUrl");
} }
private WorkerService multiProfile(FakeHerdr herdr) { private ClaudeCodeLauncher multiProfile(FakeHerdr herdr) {
BridgedConfig.Worker gx10 = new BridgedConfig.Worker("gx10", "http://gx10.gw:8000", "coder", BridgedConfig.Worker gx10 = new BridgedConfig.Worker("gx10", "http://gx10.gw:8000", "coder",
null, "BRIDGED_WORKER_TOKEN", List.of("ccs", "gx10"), "tab", "bridged-workers", "w #{n}", null, null, null); null, "BRIDGED_WORKER_TOKEN", List.of("ccs", "gx10"), "tab", "bridged-workers", "w #{n}", null, null, null);
BridgedConfig.Worker ollama = new BridgedConfig.Worker("ollama", "http://ollama.ltms.dev", null, BridgedConfig.Worker ollama = new BridgedConfig.Worker("ollama", "http://ollama.ltms.dev", null,
null, "BRIDGED_WORKER_TOKEN", List.of("ccs", "ollama"), "tab", "bridged-workers", "w #{n}", null, null, null); null, "BRIDGED_WORKER_TOKEN", List.of("ccs", "ollama"), "tab", "bridged-workers", "w #{n}", null, null, null);
return new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), return new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx10.gw", "ollama.ltms.dev")), new SubscriptionGuard(Set.of("gx10.gw", "ollama.ltms.dev")),
Map.of("gx10", gx10, "ollama", ollama), "gx10", _ -> "tok"); Map.of("gx10", gx10, "ollama", ollama), "gx10", _ -> "tok");
} }
@@ -103,7 +103,7 @@ class WorkerServiceTest {
BridgedConfig.Worker cfg = new BridgedConfig.Worker("ltms-local", "http://gx00.gw:8000", "coder", BridgedConfig.Worker cfg = new BridgedConfig.Worker("ltms-local", "http://gx00.gw:8000", "coder",
null, "BRIDGED_WORKER_TOKEN", List.of("ccs", "ltms-local"), "tab", "bridged-workers", null, "BRIDGED_WORKER_TOKEN", List.of("ccs", "ltms-local"), "tab", "bridged-workers",
"w #{n}", null, "/pinned/dir", null); "w #{n}", null, "/pinned/dir", null);
WorkerService svc = new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), ClaudeCodeLauncher svc = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of("ltms-local", cfg), "ltms-local", _ -> null); new SubscriptionGuard(Set.of("gx00.gw")), Map.of("ltms-local", cfg), "ltms-local", _ -> null);
svc.spawn("ltms-local", null, "/caller/home"); svc.spawn("ltms-local", null, "/caller/home");
assertEquals("/pinned/dir", startCwd(herdr), "a profile-pinned cwd overrides the caller's"); assertEquals("/pinned/dir", startCwd(herdr), "a profile-pinned cwd overrides the caller's");
@@ -135,7 +135,7 @@ class WorkerServiceTest {
case "GITEA_HOST" -> "git.ltms.dev"; case "GITEA_HOST" -> "git.ltms.dev";
default -> null; default -> null;
}; };
new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of("impl", cfg), "impl", host).spawn(); new SubscriptionGuard(Set.of("gx00.gw")), Map.of("impl", cfg), "impl", host).spawn();
Map<String, String> env = startEnv(herdr); Map<String, String> env = startEnv(herdr);
@@ -151,7 +151,7 @@ class WorkerServiceTest {
BridgedConfig.Worker cfg = new BridgedConfig.Worker("ltms-local", "http://gx00.gw:8000", "coder", BridgedConfig.Worker cfg = new BridgedConfig.Worker("ltms-local", "http://gx00.gw:8000", "coder",
null, "BRIDGED_WORKER_TOKEN", List.of("ccs"), "tab", "bridged-workers", "w #{n}", null, "BRIDGED_WORKER_TOKEN", List.of("ccs"), "tab", "bridged-workers", "w #{n}",
null, null, null); null, null, null);
new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of("ltms-local", cfg), "ltms-local", new SubscriptionGuard(Set.of("gx00.gw")), Map.of("ltms-local", cfg), "ltms-local",
_ -> "would-be-secret").spawn(); _ -> "would-be-secret").spawn();
@@ -164,20 +164,20 @@ class WorkerServiceTest {
@Test @Test
void isForeignWorkerMatchesOurSchemeWithANonSelfNonce() { void isForeignWorkerMatchesOurSchemeWithANonSelfNonce() {
assertTrue(WorkerService.isForeignWorker("claude-ollama-be09c2-2", "aaaaaa"), assertTrue(ClaudeCodeLauncher.isForeignWorker("claude-ollama-be09c2-2", "aaaaaa"),
"a bridge worker name with a different nonce is a prior daemon's orphan"); "a bridge worker name with a different nonce is a prior daemon's orphan");
assertTrue(WorkerService.isForeignWorker("claude-gx10-4127af-11", "aaaaaa"), assertTrue(ClaudeCodeLauncher.isForeignWorker("claude-gx10-4127af-11", "aaaaaa"),
"profile and multi-digit seq are still parsed; foreign nonce ⇒ reap"); "profile and multi-digit seq are still parsed; foreign nonce ⇒ reap");
} }
@Test @Test
void isForeignWorkerSparesOurOwnLiveWorkersAndNonWorkers() { void isForeignWorkerSparesOurOwnLiveWorkersAndNonWorkers() {
assertFalse(WorkerService.isForeignWorker("claude-ollama-abcdef-3", "abcdef"), assertFalse(ClaudeCodeLauncher.isForeignWorker("claude-ollama-abcdef-3", "abcdef"),
"a worker with THIS process's nonce is ours and live — never reap it"); "a worker with THIS process's nonce is ours and live — never reap it");
assertFalse(WorkerService.isForeignWorker(null, "abcdef"), "an unnamed agent is not a worker"); assertFalse(ClaudeCodeLauncher.isForeignWorker(null, "abcdef"), "an unnamed agent is not a worker");
assertFalse(WorkerService.isForeignWorker("claude", "abcdef"), "a bare kind name is not a worker"); assertFalse(ClaudeCodeLauncher.isForeignWorker("claude", "abcdef"), "a bare kind name is not a worker");
assertFalse(WorkerService.isForeignWorker("my-repl", "abcdef"), "a user's own label is not a worker"); assertFalse(ClaudeCodeLauncher.isForeignWorker("my-repl", "abcdef"), "a user's own label is not a worker");
assertFalse(WorkerService.isForeignWorker("claude-ollama-XYZ123-2", "abcdef"), assertFalse(ClaudeCodeLauncher.isForeignWorker("claude-ollama-XYZ123-2", "abcdef"),
"a non-hex nonce does not match our scheme"); "a non-hex nonce does not match our scheme");
} }
@@ -193,7 +193,7 @@ class WorkerServiceTest {
@Test @Test
void reapsAForeignOrphanButSparesOurOwnWorkerAndUserSessions() { void reapsAForeignOrphanButSparesOurOwnWorkerAndUserSessions() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = multiProfile(herdr); ClaudeCodeLauncher svc = multiProfile(herdr);
herdr.withAgent("claude-ollama-be09c2-2", "term_orphan", "wQ:pF", "wQ:t8") // prior daemon's leak herdr.withAgent("claude-ollama-be09c2-2", "term_orphan", "wQ:pF", "wQ:t8") // prior daemon's leak
.withAgent("claude-gx10-" + svc.nameNonce() + "-1", "term_mine", "wQ:pMine", "wQ:tMine"); // ours, live .withAgent("claude-gx10-" + svc.nameNonce() + "-1", "term_mine", "wQ:pMine", "wQ:tMine"); // ours, live
// (the fake's default unnamed term_a stands in for a user's own Claude session) // (the fake's default unnamed term_a stands in for a user's own Claude session)
@@ -210,7 +210,7 @@ class WorkerServiceTest {
@Test @Test
void reapCountsAnAlreadyGoneOrphanAsReaped() { void reapCountsAnAlreadyGoneOrphanAsReaped() {
FakeHerdr herdr = new FakeHerdr().paneCloseFailsWith("pane_not_found"); FakeHerdr herdr = new FakeHerdr().paneCloseFailsWith("pane_not_found");
WorkerService svc = multiProfile(herdr); ClaudeCodeLauncher svc = multiProfile(herdr);
herdr.withAgent("claude-ollama-0d856d-3", "term_gone", "wQ:pS", "wQ:tD"); herdr.withAgent("claude-ollama-0d856d-3", "term_gone", "wQ:pS", "wQ:tD");
assertEquals(1, svc.reapOrphanWorkers(), assertEquals(1, svc.reapOrphanWorkers(),
@@ -228,7 +228,7 @@ class WorkerServiceTest {
@Test @Test
void spawnReturnsPeerHandleWithIdEqualToPaneId() { void spawnReturnsPeerHandleWithIdEqualToPaneId() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = service(herdr, List.of("ccs", "ltms-local"), null); ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
PeerHandle handle = svc.spawn(new SpawnRequest(null, null, null)); PeerHandle handle = svc.spawn(new SpawnRequest(null, null, null));
@@ -239,7 +239,7 @@ class WorkerServiceTest {
@Test @Test
void spawnReturnsPeerHandleWithCorrectTerminalId() { void spawnReturnsPeerHandleWithCorrectTerminalId() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = service(herdr, List.of("ccs", "ltms-local"), null); ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
PeerHandle handle = svc.spawn(new SpawnRequest("ltms-local", null, "/caller")); PeerHandle handle = svc.spawn(new SpawnRequest("ltms-local", null, "/caller"));
@@ -249,7 +249,7 @@ class WorkerServiceTest {
@Test @Test
void capabilitiesIncludeMidTurnAskWorktreeOrphanReap() { void capabilitiesIncludeMidTurnAskWorktreeOrphanReap() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = service(herdr, List.of("ccs", "ltms-local"), null); ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
Set<Capability> caps = svc.capabilities(); Set<Capability> caps = svc.capabilities();
@@ -265,7 +265,7 @@ class WorkerServiceTest {
"impl", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN", "impl", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "impl"), "tab", "bridged-workers", "w #{n}", null, null, null, List.of("ccs", "impl"), "tab", "bridged-workers", "w #{n}", null, null, null,
"GITEA_ACCESS_TOKEN", null); "GITEA_ACCESS_TOKEN", null);
WorkerService svc = new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr), ClaudeCodeLauncher svc = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of("impl", cfg), "impl", new SubscriptionGuard(Set.of("gx00.gw")), Map.of("impl", cfg), "impl",
_ -> "tok"); _ -> "tok");
@@ -276,7 +276,7 @@ class WorkerServiceTest {
@Test @Test
void capabilitiesExcludeSelfPrWhenNoGitToken() { void capabilitiesExcludeSelfPrWhenNoGitToken() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = service(herdr, List.of("ccs", "ltms-local"), null); ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
assertFalse(svc.capabilities().contains(Capability.SELF_PR), assertFalse(svc.capabilities().contains(Capability.SELF_PR),
"no git token profile → no SELF_PR capability"); "no git token profile → no SELF_PR capability");
@@ -285,7 +285,7 @@ class WorkerServiceTest {
@Test @Test
void effectiveCwdViaSpawnRequestMatchesExistingResolution() { void effectiveCwdViaSpawnRequestMatchesExistingResolution() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = service(herdr, List.of("ccs", "ltms-local"), null); ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
String cwd = svc.effectiveCwd(new SpawnRequest("ltms-local", "/work/proj", "/caller/home")); String cwd = svc.effectiveCwd(new SpawnRequest("ltms-local", "/work/proj", "/caller/home"));
@@ -295,7 +295,7 @@ class WorkerServiceTest {
@Test @Test
void profilesViaPeerLauncherMatchesExistingApi() { void profilesViaPeerLauncherMatchesExistingApi() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = multiProfile(herdr); ClaudeCodeLauncher svc = multiProfile(herdr);
assertEquals(Set.of("gx10", "ollama"), svc.profiles(), "profiles() via PeerLauncher must match"); assertEquals(Set.of("gx10", "ollama"), svc.profiles(), "profiles() via PeerLauncher must match");
} }
@@ -303,7 +303,7 @@ class WorkerServiceTest {
@Test @Test
void defaultProfileViaPeerLauncherMatches() { void defaultProfileViaPeerLauncherMatches() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = multiProfile(herdr); ClaudeCodeLauncher svc = multiProfile(herdr);
assertEquals("gx10", svc.defaultProfile(), "defaultProfile() via PeerLauncher must match"); assertEquals("gx10", svc.defaultProfile(), "defaultProfile() via PeerLauncher must match");
} }
@@ -311,7 +311,7 @@ class WorkerServiceTest {
@Test @Test
void stopViaPeerLauncherTearsDownByHandleId() { void stopViaPeerLauncherTearsDownByHandleId() {
FakeHerdr herdr = new FakeHerdr(); FakeHerdr herdr = new FakeHerdr();
WorkerService svc = service(herdr, List.of("ccs", "ltms-local"), null); ClaudeCodeLauncher svc = service(herdr, List.of("ccs", "ltms-local"), null);
PeerHandle handle = svc.spawn(new SpawnRequest(null, null, null)); PeerHandle handle = svc.spawn(new SpawnRequest(null, null, null));
svc.stop(handle.id()); svc.stop(handle.id());