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
39 changes: 36 additions & 3 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11614,9 +11614,22 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
);

it.effect.each([
{ caseName: "async setup scripts let the turn start before the script exits", async: true },
{ caseName: "sync setup scripts hold the turn until the script exits", async: false },
])("$caseName", ({ async }) =>
{
caseName: "async setup scripts let the turn start before the script exits",
async: true,
cancel: false,
},
{
caseName: "sync setup scripts hold the turn until the script exits",
async: false,
cancel: false,
},
{
caseName: "cancelling worktree setup publishes its outcome and cleans up the thread",
async: false,
cancel: true,
},
])("$caseName", ({ async, cancel }) =>
Effect.gen(function* () {
const dispatchedCommands: Array<OrchestrationCommand> = [];
const scriptExit = yield* Deferred.make<void>();
Expand Down Expand Up @@ -11750,6 +11763,26 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
assert.equal(stageStatus(running, "agent"), "pending");
assert.isFalse(turnStarted());

if (cancel) {
const cancelled = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) => client[WS_METHODS.worktreeSetupCancel]({ threadId })),
);
assert.isTrue(cancelled.cancelled);
assertTrue(dispatchedCommands.some((command) => command.type === "thread.delete"));
const outcome = dispatchedCommands.findLast(
(command) =>
command.type === "thread.activity.append" && command.activity.kind === "worktree-setup",
);
assertTrue(outcome?.type === "thread.activity.append");
assert.propertyVal(outcome.activity.payload, "phase", "cancelled");
const result = yield* Fiber.join(dispatchFiber).pipe(Effect.result);
assertTrue(result._tag === "Failure");
assert.propertyVal(result.failure, "message", "Worktree setup cancelled.");
assert.propertyVal(result.failure, "bootstrapThreadDisposition", "deleted");
assert.isFalse(turnStarted());
return;
}

// The client that sent the message goes away mid-setup (a reload or a
// dropped socket). The bootstrap belongs to the server, not the
// connection: the thread already exists for every client, so it must
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,7 @@ const makeWsRpcLayer = (
);

const settledBootstrapProgram = bootstrapProgram.pipe(
Effect.interruptible,
Effect.catchCause((cause) => {
const dispatchError = toBootstrapDispatchCommandCauseError(cause);
if (Cause.hasInterruptsOnly(cause)) {
Expand Down Expand Up @@ -1687,6 +1688,8 @@ const makeWsRpcLayer = (
),
).pipe(Effect.andThen(cleanupAndFail(cause, dispatchError)));
}),
// Cancellation must finish recording and rollback after the bootstrap is interrupted.
Effect.uninterruptible,
);

// The bootstrap outlives the connection that asked for it: a reload
Expand Down
Loading