From 409b8f9312ae9e2e0b4a0ae086f64d1fa2abc6bb Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:20:44 +0200 Subject: [PATCH] fix(server): keep a new worktree open when its paseo.json cannot be parsed (#6036) * fix(server): keep workspaces available after setup config errors * refactor(server): drop the archive dependency from worktree setup Setup no longer archives a workspace when it fails, so the setup workflows no longer take archiveWorkspaceRecord. Co-authored-by: Bae Seongmin <83870598+3ae3ae@users.noreply.github.com> * test(server): stop passing the removed archive dependency to the worktree workflow * test(server): check paseo.json survives without comparing line endings Git checks the fixture out with CRLF on Windows, so comparing its exact contents failed there. --------- Co-authored-by: 3ae3ae Co-authored-by: Bae Seongmin <83870598+3ae3ae@users.noreply.github.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../browser/workspace-setup-streaming.spec.ts | 35 +++++++++++++++++++ .../src/server/agent/mcp-server.test.ts | 1 - packages/server/src/server/bootstrap.ts | 1 - packages/server/src/server/session.ts | 2 -- .../src/server/worktree-session.test.ts | 26 ++------------ .../server/src/server/worktree-session.ts | 9 ----- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/packages/app/e2e/browser/workspace-setup-streaming.spec.ts b/packages/app/e2e/browser/workspace-setup-streaming.spec.ts index 875ec5197d..7fe8ce1814 100644 --- a/packages/app/e2e/browser/workspace-setup-streaming.spec.ts +++ b/packages/app/e2e/browser/workspace-setup-streaming.spec.ts @@ -203,6 +203,41 @@ test.describe("Workspace setup streaming", () => { } }); + test("keeps a workspace usable when its setup config cannot be parsed", async ({ page }) => { + const client = await connectWorkspaceSetupClient(); + const repo = await createTempGitRepo("setup-invalid-config-", { + files: [{ path: "paseo.json", content: "{ invalid json\n" }], + }); + + try { + await seedProjectForWorkspaceSetup(client, repo.path); + const failed = waitForWorkspaceSetupProgress( + client, + (payload) => payload.status === "failed", + ); + const workspace = await createWorkspaceThroughDaemon(client, { + cwd: repo.path, + worktreeSlug: "workspace-setup-invalid-config", + }); + const failedPayload = await failed; + expect(failedPayload.error).toContain("Failed to parse paseo.json"); + expect(failedPayload.detail.commands).toEqual([]); + + await openHomeWithProject(page, repo.path); + await navigateToWorkspaceViaSidebar(page, workspace.id); + await expectFailedSetupTabSeededInMainPane(page, workspace.id); + await expect(page.getByText(/Failed to parse paseo\.json at/)).toBeVisible(); + await closeSetupTab(page, workspace.id); + await clickNewChat(page); + await expectComposerVisible(page); + await openFileExplorer(page); + await expectExplorerEntryVisible(page, "paseo.json"); + } finally { + await client.close(); + await repo.cleanup(); + } + }); + daemonTest("emits a completed empty snapshot when no setup commands exist", async () => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-none-"); diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index dbe0885d5c..ad2c4babd8 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -806,7 +806,6 @@ function createPaseoWorktreeForMcpTest(options: { emit: () => {}, sessionLogger: createTestLogger(), terminalManager: null, - archiveWorkspaceRecord: async () => {}, serviceProxy: null, scriptRuntimeStore: null, getDaemonTcpPort: null, diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 8ace188b51..3f49f41a19 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -1170,7 +1170,6 @@ export async function createPaseoDaemon( emit: emitExternalSessionMessage, sessionLogger: logger, terminalManager, - archiveWorkspaceRecord: archiveWorkspaceRecordExternal, serviceProxy, scriptRuntimeStore, getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null), diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 4e57ee0578..3e6813e148 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -7253,7 +7253,6 @@ export class Session { emit: (message) => this.emit(message), sessionLogger: this.sessionLogger, terminalManager: this.terminalManager, - archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId), serviceProxy: this.serviceProxy, scriptRuntimeStore: this.scriptRuntimeStore, getDaemonTcpPort: this.getDaemonTcpPort, @@ -7300,7 +7299,6 @@ export class Session { emit: (message) => this.emit(message), sessionLogger: this.sessionLogger, terminalManager: this.terminalManager, - archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId), serviceProxy: this.serviceProxy, scriptRuntimeStore: this.scriptRuntimeStore, getDaemonTcpPort: this.getDaemonTcpPort, diff --git a/packages/server/src/server/worktree-session.test.ts b/packages/server/src/server/worktree-session.test.ts index f28718c0f5..744ba0726a 100644 --- a/packages/server/src/server/worktree-session.test.ts +++ b/packages/server/src/server/worktree-session.test.ts @@ -115,7 +115,6 @@ function createWorkflowForRequestTest(options: { emit: () => {}, sessionLogger: createLogger(), terminalManager: null, - archiveWorkspaceRecord: async () => {}, serviceProxy: null, scriptRuntimeStore: null, getDaemonTcpPort: null, @@ -489,7 +488,6 @@ describe("create-agent worktree setup boundary", () => { emit: (message) => emitted.push(message), sessionLogger: createLogger(), terminalManager: createTerminalManagerStub().manager, - archiveWorkspaceRecord: async () => {}, serviceProxy: null, scriptRuntimeStore: null, getDaemonTcpPort: null, @@ -540,7 +538,6 @@ describe("create-agent worktree setup boundary", () => { emit: (message) => workspaceSetupEvents.push(message), sessionLogger: createLogger(), terminalManager: null, - archiveWorkspaceRecord: async () => {}, serviceProxy: null, scriptRuntimeStore: null, getDaemonTcpPort: null, @@ -714,7 +711,6 @@ describe("runWorktreeSetupInBackground", () => { emit: () => {}, sessionLogger: createLogger(), terminalManager: null, - archiveWorkspaceRecord: async () => {}, }, { requestCwd: sourceWorkspaceCwd, @@ -759,7 +755,6 @@ describe("runWorktreeSetupInBackground", () => { const logger = createLogger(); const terminalManager = createTerminalManagerStub(); const emitWorkspaceUpdateForWorkspaceId = vi.fn(async () => {}); - const archiveWorkspaceRecord = vi.fn(async () => {}); await runWorktreeSetupInBackground( { @@ -770,7 +765,6 @@ describe("runWorktreeSetupInBackground", () => { emit: (message) => emitted.push(message), sessionLogger: logger, terminalManager: terminalManager.manager, - archiveWorkspaceRecord, }, { requestCwd: repoDir, @@ -828,11 +822,10 @@ describe("runWorktreeSetupInBackground", () => { }); expect(terminalManager.terminals).toHaveLength(0); - expect(archiveWorkspaceRecord).not.toHaveBeenCalled(); expect(emitWorkspaceUpdateForWorkspaceId).toHaveBeenCalledWith("42"); }); - test("archives the pending workspace and emits a failed snapshot when setup cannot start", async () => { + test("keeps the workspace available and emits a failed snapshot when setup cannot start", async () => { const { tempDir, repoDir } = createGitRepo(); cleanupPaths.push(tempDir); @@ -857,7 +850,6 @@ describe("runWorktreeSetupInBackground", () => { const snapshots = new Map(); const logger = createLogger(); const emitWorkspaceUpdateForWorkspaceId = vi.fn(async () => {}); - const archiveWorkspaceRecord = vi.fn(async () => {}); const workspaceId = "ws-broken-feature"; await runWorktreeSetupInBackground( @@ -869,7 +861,6 @@ describe("runWorktreeSetupInBackground", () => { emit: (message) => emitted.push(message), sessionLogger: logger, terminalManager: null, - archiveWorkspaceRecord, }, { requestCwd: repoDir, @@ -901,7 +892,8 @@ describe("runWorktreeSetupInBackground", () => { status: "failed", error: expect.stringMatching(/Failed to parse paseo\.json at .*paseo\.json/), }); - expect(archiveWorkspaceRecord).toHaveBeenCalledWith(workspaceId); + expect(existsSync(worktreePath)).toBe(true); + expect(existsSync(path.join(worktreePath, "paseo.json"))).toBe(true); expect(emitWorkspaceUpdateForWorkspaceId).toHaveBeenCalledWith(workspaceId); }); @@ -932,7 +924,6 @@ describe("runWorktreeSetupInBackground", () => { const snapshots = new Map(); const logger = createLogger(); const emitWorkspaceUpdateForWorkspaceId = vi.fn(async () => {}); - const archiveWorkspaceRecord = vi.fn(async () => {}); await runWorktreeSetupInBackground( { @@ -943,7 +934,6 @@ describe("runWorktreeSetupInBackground", () => { emit: (message) => emitted.push(message), sessionLogger: logger, terminalManager: null, - archiveWorkspaceRecord, }, { requestCwd: repoDir, @@ -1056,7 +1046,6 @@ describe("runWorktreeSetupInBackground", () => { const logger = createLogger(); const terminalManager = createTerminalManagerStub(); const emitWorkspaceUpdateForWorkspaceId = vi.fn(async () => {}); - const archiveWorkspaceRecord = vi.fn(async () => {}); await runWorktreeSetupInBackground( { @@ -1067,7 +1056,6 @@ describe("runWorktreeSetupInBackground", () => { emit: (message) => emitted.push(message), sessionLogger: logger, terminalManager: terminalManager.manager, - archiveWorkspaceRecord, }, { requestCwd: repoDir, @@ -1116,7 +1104,6 @@ describe("runWorktreeSetupInBackground", () => { status: "completed", error: null, }); - expect(archiveWorkspaceRecord).not.toHaveBeenCalled(); expect(emitWorkspaceUpdateForWorkspaceId).toHaveBeenCalledWith("44"); }); @@ -1151,7 +1138,6 @@ describe("runWorktreeSetupInBackground", () => { }, }); const emitWorkspaceUpdateForWorkspaceId = vi.fn(async () => {}); - const archiveWorkspaceRecord = vi.fn(async () => {}); await runWorktreeSetupInBackground( { @@ -1162,7 +1148,6 @@ describe("runWorktreeSetupInBackground", () => { emit: (message) => emitted.push(message), sessionLogger: logger, terminalManager: terminalManager.manager, - archiveWorkspaceRecord, }, { requestCwd: repoDir, @@ -1202,7 +1187,6 @@ describe("runWorktreeSetupInBackground", () => { status: "completed", error: null, }); - expect(archiveWorkspaceRecord).not.toHaveBeenCalled(); expect(emitWorkspaceUpdateForWorkspaceId).toHaveBeenCalledWith("45"); }); @@ -1233,7 +1217,6 @@ describe("runWorktreeSetupInBackground", () => { const logger = createLogger(); const terminalManager = createTerminalManagerStub(); const emitWorkspaceUpdateForWorkspaceId = vi.fn(async () => {}); - const archiveWorkspaceRecord = vi.fn(async () => {}); await runWorktreeSetupInBackground( { @@ -1244,7 +1227,6 @@ describe("runWorktreeSetupInBackground", () => { emit: (message) => emitted.push(message), sessionLogger: logger, terminalManager: terminalManager.manager, - archiveWorkspaceRecord, }, { requestCwd: repoDir, @@ -1265,7 +1247,6 @@ describe("runWorktreeSetupInBackground", () => { status: "completed", error: null, }); - expect(archiveWorkspaceRecord).not.toHaveBeenCalled(); expect(emitWorkspaceUpdateForWorkspaceId).toHaveBeenCalledWith("46"); }); @@ -1427,7 +1408,6 @@ describe("runWorktreeSetupInBackground", () => { emit: (message: SessionOutboundMessage) => emitted.push(message), sessionLogger: createLogger(), terminalManager: terminalManager.manager, - archiveWorkspaceRecord: vi.fn(async () => {}), serviceProxy: null, scriptRuntimeStore: null, getDaemonTcpPort: null, diff --git a/packages/server/src/server/worktree-session.ts b/packages/server/src/server/worktree-session.ts index c693bc5380..2b9f8a0ffd 100644 --- a/packages/server/src/server/worktree-session.ts +++ b/packages/server/src/server/worktree-session.ts @@ -108,7 +108,6 @@ interface CreatePaseoWorktreeInBackgroundDependencies { emit: EmitSessionMessage; sessionLogger: Logger; terminalManager: TerminalManager | null; - archiveWorkspaceRecord: (workspaceId: string) => Promise; serviceProxy: ServiceProxySubsystem | null; scriptRuntimeStore: WorkspaceScriptRuntimeStore | null; getDaemonTcpPort: (() => number | null) | null; @@ -825,7 +824,6 @@ export async function runWorktreeSetupInBackground( ): Promise { let worktree: WorktreeConfig = options.worktree; let setupResults: WorktreeSetupCommandResult[] = []; - let setupStarted = false; const progressAccumulator = createWorktreeSetupProgressAccumulator(); const workspaceId = options.workspaceId; @@ -862,7 +860,6 @@ export async function runWorktreeSetupInBackground( const workspaceCwd = options.workspaceCwd ?? worktree.worktreePath; const setupCommands = getWorktreeSetupCommands(workspaceCwd); if (setupCommands.length === 0) { - setupStarted = true; emitSetupProgress("completed", null); } else { const runtimeEnv = await resolveWorktreeRuntimeEnv({ @@ -874,7 +871,6 @@ export async function runWorktreeSetupInBackground( cwd: workspaceCwd, env: runtimeEnv, }); - setupStarted = true; setupResults = await runWorktreeSetupCommands({ worktreePath: workspaceCwd, branchName: worktree.branchName, @@ -906,10 +902,6 @@ export async function runWorktreeSetupInBackground( const message = error instanceof Error ? error.message : String(error); emitSetupProgress("failed", message); - if (!setupStarted) { - await dependencies.archiveWorkspaceRecord(options.workspaceId); - } - dependencies.sessionLogger.error( { err: error, @@ -917,7 +909,6 @@ export async function runWorktreeSetupInBackground( repoRoot: options.repoRoot, worktreeSlug: worktree.branchName, worktreePath: worktree.worktreePath, - setupStarted, }, "Background worktree setup failed", );