diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts index 80599fac4d9ac..278b7786293dc 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts @@ -1401,6 +1401,69 @@ it.effect("shows the fetch diagnosis when preparing a worktree from origin fails }).pipe(Effect.provide(harness.layer)); }); +it.effect("names the missing base branch when a worktree cannot be created from it", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const git = yield* GitVcsDriver.GitVcsDriver; + const directory = yield* fs.makeTempDirectoryScoped({ prefix: "launch-missing-base-" }); + const cwd = `${directory}/repo`; + const remote = `${directory}/remote.git`; + yield* fs.makeDirectory(cwd); + const command = (at: string, args: string[]) => + git.execute({ operation: "launch-test", cwd: at, args }); + yield* command(cwd, ["init", "-b", "dev"]); + yield* command(cwd, ["config", "user.name", "Test"]); + yield* command(cwd, ["config", "user.email", "test@example.test"]); + yield* command(cwd, ["commit", "--allow-empty", "-m", "initial"]); + yield* command(cwd, ["init", "--bare", remote]); + yield* command(cwd, ["remote", "add", "origin", remote]); + yield* command(cwd, ["push", "-u", "origin", "dev"]); + + const harness = makeHarness({ + git: { + resolveWorktreeBaseRemote: (input) => git.resolveWorktreeBaseRemote({ ...input, cwd }), + }, + fetchWorktreeBaseCommit: (input) => git.fetchWorktreeBaseCommit({ ...input, cwd }), + createWorktree: (input, options) => + git.createWorktree({ ...input, cwd, path: `${directory}/worktree` }, options), + }); + yield* Effect.gen(function* () { + const launches = yield* ThreadLaunch.ThreadLaunchService; + const threads = yield* ThreadManagement.ThreadManagementService; + const launched = yield* launches.launch( + launchInput({ + command: "command:launch:missing-base", + thread: "thread:launch:missing-base", + message: "Start from master", + workspace: { type: "worktree", baseRef: "master", startFromOrigin: true }, + }), + ); + yield* threads.streamStoredEventsFrom({ threadId: launched.threadId }).pipe( + Stream.filter( + (stored) => + stored.event.type === "run.updated" && stored.event.payload.status === "failed", + ), + Stream.runHead, + ); + const projection = yield* threads.getThreadProjection(launched.threadId); + assert.equal(projection.runs[0]?.status, "failed"); + assert.include( + projection.turnItems.find((item) => item.type === "error")?.failure.message ?? "", + `Base ref "master" does not resolve to a commit in project "Project" (/repo).`, + ); + }).pipe(Effect.provide(harness.layer)); + }).pipe( + Effect.provide( + GitVcsDriver.layer.pipe( + Layer.provide( + ServerConfig.layerTest(process.cwd(), { prefix: "launch-missing-base-git-" }), + ), + Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)), + ), + ), + ), +); + it.effect("rejects a preparation retry after the project disables its provider", () => { let fetchAttempts = 0; const harness = makeHarness({ diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.ts index 18688ebd4b5b6..848ab478d55d1 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.ts @@ -164,6 +164,7 @@ export class ThreadLaunchBaseRefError extends Schema.TaggedError error.reason === "ref_not_found", + (cause) => + Effect.fail( + new ThreadLaunchBaseRefError({ + projectId: project.id, + projectTitle: project.title, + workspaceRoot: project.workspaceRoot, + baseRef: requestedBaseRef, + cause, + }), + ), + ), + Effect.mapError(mapError(input, "provision-worktree", threadId)), + ); worktreePath = worktree.worktree.path; branch = worktree.worktree.refName; createdWorktreePath = worktreePath;