fix(server): isolate paseo.json parse failures so they don't block fetch_workspaces

A malformed paseo.json (e.g. git-conflict markers) in any registered
workspace caused readPaseoConfig to throw, which propagated through
getScriptConfigs -> buildWorkspaceScriptPayloads ->
Session.describeWorkspaceRecord and failed the entire
fetch_workspaces_request with code=fetch_workspaces_failed. The client
showed no workspaces at all.

Reshape: separate loading (mechanism) from policy (per-caller).

- readPaseoConfig now returns a discriminated result
  ({ ok: true; config } | { ok: false; configPath; error }). Pure load,
  no logging, no opinions.
- Workspace projection owns its own policy via readPaseoConfigForProjection
  in script-status-projection.ts: warn via pino with configPath +
  workspaceDirectory + err, treat workspace as having no scripts, keep the
  list alive.
- spawnWorkspaceScript throws with cause so the existing
  handleStartWorkspaceScriptRequest catch logs it once with full context.
- The setup/teardown/terminal wrappers retain their pre-existing silent
  degradation on missing/broken config.
- getScriptConfigs takes a parsed PaseoConfig | null instead of repoRoot
  so the projection can pass the result through without double-reading.

Closes https://github.com/getpaseo/paseo/issues/598
This commit is contained in:
Mohamed Boudra
2026-04-28 19:48:24 +07:00
parent fbf10ba4c6
commit 00219cfd04
7 changed files with 139 additions and 22 deletions
+10 -3
View File
@@ -11,6 +11,7 @@ import {
isServiceScript,
isPaseoOwnedWorktreeCwd,
listPaseoWorktrees,
readPaseoConfig,
resolveWorktreeRuntimeEnv,
type WorktreeSetupCommandProgressEvent,
runWorktreeSetupCommands,
@@ -18,6 +19,12 @@ import {
type CreateWorktreeOptions,
type WorktreeConfig,
} from "./worktree";
import type { PaseoConfig } from "./paseo-config-schema.js";
function loadConfigForTest(repoRoot: string): PaseoConfig | null {
const result = readPaseoConfig(repoRoot);
return result.ok ? result.config : null;
}
import { getPaseoWorktreeMetadataPath } from "./worktree-metadata.js";
import { execSync } from "child_process";
import {
@@ -707,7 +714,7 @@ describe.skipIf(process.platform === "win32")("createWorktree", () => {
}),
);
const scriptConfigs = getScriptConfigs(repoDir);
const scriptConfigs = getScriptConfigs(loadConfigForTest(repoDir));
const typecheck = scriptConfigs.get("typecheck");
expect(typecheck).toEqual({
@@ -731,7 +738,7 @@ describe.skipIf(process.platform === "win32")("createWorktree", () => {
}),
);
const scriptConfigs = getScriptConfigs(repoDir);
const scriptConfigs = getScriptConfigs(loadConfigForTest(repoDir));
const server = scriptConfigs.get("server");
expect(server).toEqual({
@@ -771,7 +778,7 @@ describe.skipIf(process.platform === "win32")("createWorktree", () => {
}),
);
expect(getScriptConfigs(repoDir)).toEqual(
expect(getScriptConfigs(loadConfigForTest(repoDir))).toEqual(
new Map([
["valid", { command: "npm run valid" }],
["invalidType", { command: "npm run worker" }],