From 17b1d8f7328e7df69597f4ae7c5880c8798d4e77 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 28 Apr 2026 20:38:11 +0700 Subject: [PATCH] fix(server): keep setup/teardown/terminal action paths failing loudly on bad paseo.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the projection fix. The mechanism/policy split landed projection on a graceful-degrade path, but I also made the setup, teardown, and terminal wrappers silently absorb parse failures — which broke the existing contract that `runWorktreeSetupInBackground` surfaces "Failed to parse paseo.json" to the user when they invoke setup against a workspace with a malformed config. Restore the throw, but with the path included and the underlying parse error attached as `cause` (Pino expands it; UI surfaces the path + detail). Extract `paseoConfigParseError` so spawnWorkspaceScript and the three wrappers throw the same shape. Also update opencode-agent.full-access.test.ts mode-order assertion that the reorder commit forgot to update. --- .../opencode-agent.full-access.test.ts | 2 +- .../server/src/server/worktree-bootstrap.ts | 5 ++-- .../src/server/worktree-session.test.ts | 6 +++-- packages/server/src/utils/worktree.ts | 24 ++++++++++++++----- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts b/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts index f383ececa..10570b179 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts @@ -132,7 +132,7 @@ describe("OpenCode full-access mode", () => { const client = new OpenCodeAgentClient(createTestLogger()); const modes = await client.listModes({ cwd: "/tmp/project", force: false }); - expect(modes.map((mode) => mode.id)).toEqual(["build", "full-access", "plan", "paseo-custom"]); + expect(modes.map((mode) => mode.id)).toEqual(["build", "plan", "full-access", "paseo-custom"]); expect(modes.find((mode) => mode.id === "full-access")).toMatchObject({ label: "Full Access", description: "Automatically approves all tool permission prompts for the session", diff --git a/packages/server/src/server/worktree-bootstrap.ts b/packages/server/src/server/worktree-bootstrap.ts index c5c871eb8..d453f50c8 100644 --- a/packages/server/src/server/worktree-bootstrap.ts +++ b/packages/server/src/server/worktree-bootstrap.ts @@ -7,6 +7,7 @@ import { getScriptConfigs, getWorktreeTerminalSpecs, isServiceScript, + paseoConfigParseError, processCarriageReturns, readPaseoConfig, resolveWorktreeRuntimeEnv, @@ -835,9 +836,7 @@ export async function spawnWorkspaceScript( } = options; const configResult = readPaseoConfig(repoRoot); if (!configResult.ok) { - throw new Error(`Failed to parse ${configResult.configPath}`, { - cause: configResult.error, - }); + throw paseoConfigParseError(configResult); } const scriptConfigs = getScriptConfigs(configResult.config); const config = scriptConfigs.get(scriptName); diff --git a/packages/server/src/server/worktree-session.test.ts b/packages/server/src/server/worktree-session.test.ts index 32ca2c2ef..68d9bfffa 100644 --- a/packages/server/src/server/worktree-session.test.ts +++ b/packages/server/src/server/worktree-session.test.ts @@ -572,11 +572,13 @@ describe("runWorktreeSetupInBackground", () => { expect(progressMessages[0]?.payload.status).toBe("running"); expect(progressMessages[0]?.payload.error).toBeNull(); expect(progressMessages[1]?.payload.status).toBe("failed"); - expect(progressMessages[1]?.payload.error).toContain("Failed to parse paseo.json"); + expect(progressMessages[1]?.payload.error).toMatch( + /Failed to parse paseo\.json at .*paseo\.json/, + ); expect(progressMessages[1]?.payload.detail.commands).toEqual([]); expect(snapshots.get(workspaceId)).toMatchObject({ status: "failed", - error: expect.stringContaining("Failed to parse paseo.json"), + error: expect.stringMatching(/Failed to parse paseo\.json at .*paseo\.json/), }); expect(archiveWorkspaceRecord).toHaveBeenCalledWith(workspaceId); expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath); diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index 63095d2d2..88ae897ca 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -213,19 +213,31 @@ export function readPaseoConfig(repoRoot: string): ReadPaseoConfigResult { } } -export function getWorktreeSetupCommands(repoRoot: string): string[] { +export function paseoConfigParseError(failure: { configPath: string; error: unknown }): Error { + const detail = failure.error instanceof Error ? failure.error.message : String(failure.error); + return new Error(`Failed to parse paseo.json at ${failure.configPath}: ${detail}`, { + cause: failure.error, + }); +} + +function readPaseoConfigOrThrow(repoRoot: string): PaseoConfig | null { const result = readPaseoConfig(repoRoot); - return result.ok ? (result.config?.worktree?.setup ?? []) : []; + if (!result.ok) { + throw paseoConfigParseError(result); + } + return result.config; +} + +export function getWorktreeSetupCommands(repoRoot: string): string[] { + return readPaseoConfigOrThrow(repoRoot)?.worktree?.setup ?? []; } export function getWorktreeTeardownCommands(repoRoot: string): string[] { - const result = readPaseoConfig(repoRoot); - return result.ok ? (result.config?.worktree?.teardown ?? []) : []; + return readPaseoConfigOrThrow(repoRoot)?.worktree?.teardown ?? []; } export function getWorktreeTerminalSpecs(repoRoot: string): WorktreeTerminalConfig[] { - const result = readPaseoConfig(repoRoot); - const terminals = result.ok ? result.config?.worktree?.terminals : undefined; + const terminals = readPaseoConfigOrThrow(repoRoot)?.worktree?.terminals; if (!Array.isArray(terminals) || terminals.length === 0) { return []; }