CB-201: cover losing completion CAS
This commit is contained in:
@@ -349,6 +349,51 @@ class SessionManagerTest {
|
|||||||
"onDelivered must refuse a backend-error member");
|
"onDelivered must refuse a backend-error member");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void losingCompletionDoesNotReleaseOrClearABackendErrorMember() {
|
||||||
|
assertLosingCompletionLeavesBackendErrorIntact(1, false,
|
||||||
|
"a stale completion must not release a backend-error member at the context cap");
|
||||||
|
assertLosingCompletionLeavesBackendErrorIntact(0, true,
|
||||||
|
"a stale completion must not clear the context of a backend-error member");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertLosingCompletionLeavesBackendErrorIntact(int contextCap, boolean clearAfterTurn,
|
||||||
|
String protectedAction) {
|
||||||
|
FakeHerdr herdr = new FakeHerdr();
|
||||||
|
FleetConfig.Profile cfg = new FleetConfig.Profile(
|
||||||
|
"ltms-local", "http://gx00.gw:8000", "coder", null, "FLEETD_WORKER_TOKEN",
|
||||||
|
List.of("ccs", "ltms-local"), "tab", "fleetd-workers",
|
||||||
|
"worker: {profile} #{n}", null, null, null);
|
||||||
|
ClaudeCodeLauncher delegate = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
|
||||||
|
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null);
|
||||||
|
ClearContextSpyLauncher launcher = new ClearContextSpyLauncher(delegate);
|
||||||
|
java.util.concurrent.atomic.AtomicReference<SessionManager> manager = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
java.util.concurrent.atomic.AtomicReference<String> terminal = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
java.util.concurrent.atomic.AtomicBoolean armed = new java.util.concurrent.atomic.AtomicBoolean();
|
||||||
|
SessionManager sessions = new SessionManager(launcher, new RecordingWorktrees(), () -> {
|
||||||
|
if (armed.compareAndSet(true, false)) {
|
||||||
|
manager.get().onBackendError(terminal.get(), "backend exited during completion");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}, contextCap, clearAfterTurn);
|
||||||
|
manager.set(sessions);
|
||||||
|
MemberSession session = sessions.acquire("ltms-local", null, "/caller", "term_primary");
|
||||||
|
terminal.set(session.terminalId());
|
||||||
|
sessions.asPresence().markPresent(session.terminalId());
|
||||||
|
sessions.onDelivered(session.terminalId(), TestTurnTokens.inert(session.terminalId()));
|
||||||
|
armed.set(true);
|
||||||
|
|
||||||
|
assertFalse(sessions.onTurnCompleteWithPostAction(session.terminalId()),
|
||||||
|
"the completion CAS loses after the injected backend error");
|
||||||
|
|
||||||
|
assertTrue(sessions.get(session.paneId()).isPresent(), protectedAction);
|
||||||
|
MemberSession updated = sessions.get(session.paneId()).orElseThrow();
|
||||||
|
assertEquals(MemberSession.State.BACKEND_ERROR, updated.state());
|
||||||
|
assertEquals("backend exited during completion", updated.failureReason());
|
||||||
|
assertFalse(herdr.called("pane.close"), protectedAction);
|
||||||
|
assertEquals(0, launcher.clearContextCalls(), protectedAction);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void backendErrorForUnknownTargetIsWarnedAndDoesNotCreateASession() {
|
void backendErrorForUnknownTargetIsWarnedAndDoesNotCreateASession() {
|
||||||
LoggerContext ctx = (LoggerContext) LoggerFactory.getILoggerFactory();
|
LoggerContext ctx = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||||
@@ -1104,6 +1149,76 @@ class SessionManagerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Delegates all launcher work while recording context clears. */
|
||||||
|
private static final class ClearContextSpyLauncher implements PeerLauncher {
|
||||||
|
private final PeerLauncher delegate;
|
||||||
|
private int clearContextCalls;
|
||||||
|
|
||||||
|
ClearContextSpyLauncher(PeerLauncher delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Capability> capabilities() {
|
||||||
|
return delegate.capabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Capability> capabilitiesFor(String profileName) {
|
||||||
|
return delegate.capabilitiesFor(profileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerHandle spawn(SpawnRequest req) {
|
||||||
|
return delegate.spawn(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> profiles() {
|
||||||
|
return delegate.profiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String defaultProfile() {
|
||||||
|
return delegate.defaultProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String effectiveCwd(SpawnRequest req) {
|
||||||
|
return delegate.effectiveCwd(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> parityOverlay(String profileName) {
|
||||||
|
return delegate.parityOverlay(profileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<?> list() {
|
||||||
|
return delegate.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int reapOrphanWorkers() {
|
||||||
|
return delegate.reapOrphanWorkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop(String id) {
|
||||||
|
delegate.stop(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean clearContext(String id) {
|
||||||
|
clearContextCalls++;
|
||||||
|
return delegate.clearContext(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
int clearContextCalls() {
|
||||||
|
return clearContextCalls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void acquireRecordsTheAgentSessionIdFromTheHandle() {
|
void acquireRecordsTheAgentSessionIdFromTheHandle() {
|
||||||
FakeHerdr herdr = new FakeHerdr();
|
FakeHerdr herdr = new FakeHerdr();
|
||||||
|
|||||||
Reference in New Issue
Block a user