fix(server): keep setup/teardown/terminal action paths failing loudly on bad paseo.json

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.
This commit is contained in:
Mohamed Boudra
2026-04-28 20:38:11 +07:00
parent 283390e6d5
commit 17b1d8f732
4 changed files with 25 additions and 12 deletions
@@ -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",
@@ -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);
@@ -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);
+18 -6
View File
@@ -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 [];
}