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
49 changes: 49 additions & 0 deletions apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,55 @@ it.effect("cancels tracked setup before provider work is released", () =>
}),
);

it.effect("stops an in-flight branch rename when tracked setup is cancelled", () =>
Effect.gen(function* () {
const setupEntered = yield* Deferred.make<void>();
const branchNameStarted = yield* Deferred.make<void>();
const allowBranchName = yield* Deferred.make<void>();
const branchNameInterrupted = yield* Deferred.make<void>();
const renameAttempted = yield* Deferred.make<void>();
const harness = makeHarness({
renameBranch: (input) =>
Deferred.succeed(renameAttempted, undefined).pipe(Effect.as({ branch: input.newBranch })),
runSetup: () => Deferred.succeed(setupEntered, undefined).pipe(Effect.andThen(Effect.never)),
generateBranchName: () =>
Deferred.succeed(branchNameStarted, undefined).pipe(
Effect.andThen(Deferred.await(allowBranchName)),
Effect.as({ branch: "generated-branch" }),
Effect.onInterrupt(() => Deferred.succeed(branchNameInterrupted, undefined)),
),
});
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const tracker = yield* WorktreeSetupTracker.WorktreeSetupTracker;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "launch:cancel-renaming",
thread: "thread:cancel-renaming",
message: "Start",
workspace: { type: "worktree", baseRef: "main" },
}),
);
yield* Deferred.await(setupEntered);
yield* Deferred.await(branchNameStarted);
assert.isTrue(yield* tracker.cancel(launched.threadId));
// Let a rename that survived the cancel finish, then check it never ran.
yield* Deferred.succeed(allowBranchName, undefined);
yield* Effect.raceFirst(
Deferred.await(branchNameInterrupted),
Deferred.await(renameAttempted),
);
assert.isTrue(
yield* Deferred.isDone(branchNameInterrupted),
"the cancelled setup's branch rename kept running",
);
assert.equal(harness.renameBranch.mock.calls.length, 0);
assert.isNull((yield* threads.getThreadProjection(launched.threadId)).thread.worktreePath);
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect.each([0, 1])("releases an async setup before its completion with exit %s", (exitCode) =>
Effect.gen(function* () {
const completion = yield* Deferred.make<{ exitCode: number | null; durationMs: number }>();
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/orchestration-v2/ThreadLaunchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
Expand Down Expand Up @@ -320,6 +321,9 @@ const make = Effect.gen(function* () {
let setupTerminalId: string | null = null;
let workspaceRecorded = false;
let runReleased = false;
// The background branch rename records the worktree again when it lands,
// so cleanup stops it before forgetting a removed worktree.
let branchRename: Fiber.Fiber<unknown> | null = null;
if (input.workspaceStrategy.type === "worktree") {
yield* setupTracker.begin({
threadId,
Expand Down Expand Up @@ -508,7 +512,7 @@ const make = Effect.gen(function* () {
) {
const oldBranch = branch;
const worktreeCwd = worktreePath;
yield* generateBranchNameFor(worktreeCwd, initialMessage).pipe(
branchRename = yield* generateBranchNameFor(worktreeCwd, initialMessage).pipe(
Effect.flatMap(({ branch: newBranch, exactName }) =>
git.renameBranch({
cwd: worktreeCwd,
Expand Down Expand Up @@ -657,6 +661,7 @@ const make = Effect.gen(function* () {
createdWorktreePath &&
((cancelled && !runReleased) || !workspaceRecorded)
) {
if (branchRename !== null) yield* Fiber.interrupt(branchRename);
const removedPath = createdWorktreePath;
// The thread forgets the worktree only once it is gone; a failed
// removal leaves the directory for the user to clean up rather than
Expand Down
Loading