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 <ogyshjsm@naver.com>
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>
This commit is contained in:
paseo-bot[bot]
2026-10-04 12:20:44 +02:00
committed by GitHub
co-authored by 3ae3ae Bae Seongmin
parent 485221b739
commit 409b8f9312
6 changed files with 38 additions and 36 deletions
@@ -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-");
@@ -806,7 +806,6 @@ function createPaseoWorktreeForMcpTest(options: {
emit: () => {},
sessionLogger: createTestLogger(),
terminalManager: null,
archiveWorkspaceRecord: async () => {},
serviceProxy: null,
scriptRuntimeStore: null,
getDaemonTcpPort: null,
-1
View File
@@ -1170,7 +1170,6 @@ export async function createPaseoDaemon(
emit: emitExternalSessionMessage,
sessionLogger: logger,
terminalManager,
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
serviceProxy,
scriptRuntimeStore,
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
-2
View File
@@ -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,
@@ -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<string, unknown>();
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<string, unknown>();
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,
@@ -108,7 +108,6 @@ interface CreatePaseoWorktreeInBackgroundDependencies {
emit: EmitSessionMessage;
sessionLogger: Logger;
terminalManager: TerminalManager | null;
archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
serviceProxy: ServiceProxySubsystem | null;
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
getDaemonTcpPort: (() => number | null) | null;
@@ -825,7 +824,6 @@ export async function runWorktreeSetupInBackground(
): Promise<void> {
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",
);