Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions tests/broker-subscriptions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ test("broker rejects an explicit unsubscribe that would queue behind a hung clea
});

test("broker rolls back child threads inherited through a failed provisional claim", async (t) => {
const broker = startBroker("with-delayed-subagent");
const broker = startBroker("with-resume-inherited-subagent");
t.after(() => broker.stop());
assert.equal(await broker.listening(), true, `broker never listened: ${broker.stderr()}`);

Expand All @@ -754,8 +754,9 @@ test("broker rolls back child threads inherited through a failed provisional cla
firstClient.destroy();
await waitForUnsubscribes(broker.statePath, [threadId]);

// The resume fails after 250 ms; the delayed child arrives at 100 ms while the
// claim is still open, so the claiming socket inherits it.
// The child, and the grandchild under it, arrive while this resume's claim is open, and
// the resume fails only after both — a chain the fixture drives off the resume itself, so
// no part of it depends on how fast this client got here.
const secondClient = await connectClient(broker.socketPath);
await assert.rejects(
secondClient.request("thread/resume", { threadId, persistFullHistory: true }),
Expand Down
32 changes: 32 additions & 0 deletions tests/fake-codex-fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,38 @@ rl.on("line", (line) => {
if (BEHAVIOR === "permission-profiles-unsupported" && message.params.config) {
throw new Error("unknown field config.default_permissions");
}
if (BEHAVIOR === "with-resume-inherited-subagent" && message.params.persistFullHistory === true) {
// A causal chain, not a stopwatch: the child arrives while THIS resume's claim is
// open, the grandchild under it, and only then does the resume fail. Hanging any of
// it off turn/start instead made the test depend on the client getting from
// turn/start to resume inside 100ms, which a loaded runner does not promise.
const parentThread = ensureThread(state, message.params.threadId);
setTimeout(() => {
const childState = loadState();
const child = nextThread(childState, parentThread.cwd, true, parentThread.sandbox, { parentThreadId: parentThread.id });
const childRecord = ensureThread(childState, child.id);
childRecord.name = "delayed-design-challenger";
childState.subscriptions = [...new Set([...(childState.subscriptions || []), child.id])];
saveState(childState);
const childTurnId = nextTurnId(childState);
send({ method: "thread/started", params: { thread: { ...buildThread(childRecord), name: childRecord.name, agentNickname: childRecord.name } } });
send({ method: "turn/started", params: { threadId: child.id, turn: buildTurn(childTurnId) } });
send({ method: "turn/completed", params: { threadId: child.id, turn: buildTurn(childTurnId, "completed") } });
setTimeout(() => {
const nestedState = loadState();
const grandchild = nextThread(nestedState, parentThread.cwd, true, parentThread.sandbox, { parentThreadId: child.id });
const grandchildRecord = ensureThread(nestedState, grandchild.id);
grandchildRecord.name = "delayed-design-grandchild";
nestedState.subscriptions = [...new Set([...(nestedState.subscriptions || []), grandchild.id])];
saveState(nestedState);
send({ method: "thread/started", params: { thread: { ...buildThread(grandchildRecord), name: grandchildRecord.name, agentNickname: grandchildRecord.name } } });
setTimeout(() => {
send({ id: message.id, error: { code: -32000, message: "forced resume failure after child arrival" } });
}, 50);
}, 50);
}, 50);
break;
}
if (BEHAVIOR === "with-delayed-subagent" && message.params.persistFullHistory === true) {
state.nestedSubagentRequested = true;
saveState(state);
Expand Down
7 changes: 6 additions & 1 deletion tests/locking.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,13 @@ test("stale legacy lock without process identity does not follow a reused PID fo
const oldTime = new Date(Date.now() - 60000);
fs.utimesSync(lockDir, oldTime, oldTime);

// Returning at all is the assertion, as above: with staleMs at 30s, a successor can only
// exist because the legacy lock was reclaimed on the spot. The budget is deliberately far
// wider than the work needs -- acquireLockSync() checks its deadline after a failed attempt,
// so under a loaded runner a single stalled attempt used to expire a 200ms budget in the
// moment between the reclaim and the retry that would have succeeded.
const successor = acquireLockSync(lockDir, {
timeoutMs: 200,
timeoutMs: 5000,
staleMs: 30000,
retryDelayMs: 5,
isProcessRunning: () => true
Expand Down
Loading