diff --git a/src/cli/secrets-cli.test.ts b/src/cli/secrets-cli.test.ts index d10849de0445..0ca43924d7d5 100644 --- a/src/cli/secrets-cli.test.ts +++ b/src/cli/secrets-cli.test.ts @@ -125,12 +125,15 @@ function createSecretsApplyResult(options?: { }; } -async function withPlanFile(run: (planPath: string) => Promise) { +async function withPlanFile( + run: (planPath: string) => Promise, + contents = `${JSON.stringify(createManualSecretsPlan())}\n`, +) { const planPath = path.join( os.tmpdir(), `openclaw-secrets-cli-test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`, ); - await fs.writeFile(planPath, `${JSON.stringify(createManualSecretsPlan())}\n`, "utf8"); + await fs.writeFile(planPath, contents, "utf8"); try { await run(planPath); } finally { @@ -385,6 +388,17 @@ describe("secrets CLI", () => { }); }); + it("shows a user-friendly error when the secrets plan file is malformed JSON", async () => { + await withPlanFile(async (planPath) => { + await expect( + createProgram().parseAsync(["secrets", "apply", "--from", planPath], { from: "user" }), + ).rejects.toThrow("__exit__:1"); + + expect(runtimeErrors.at(-1)).toContain(`Malformed JSON in secrets plan file: ${planPath}`); + expect(runSecretsApply).not.toHaveBeenCalled(); + }, "{invalid json"); + }); + it("does not print skipped-exec note when apply dry-run skippedExecRefs is zero", async () => { await withPlanFile(async (planPath) => { runSecretsApply.mockResolvedValue(createSecretsApplyResult({ resolvabilityComplete: false })); diff --git a/src/cli/secrets-cli.ts b/src/cli/secrets-cli.ts index 65083a11335d..23a4db508af5 100644 --- a/src/cli/secrets-cli.ts +++ b/src/cli/secrets-cli.ts @@ -53,7 +53,12 @@ async function readPlanFile(pathname: string): Promise { import("../secrets/plan.js"), ]); const raw = readFileSync(pathname, "utf8"); - const parsed = JSON.parse(raw) as unknown; + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error(`Malformed JSON in secrets plan file: ${pathname}`, { cause: err }); + } if (!isSecretsApplyPlan(parsed)) { throw new Error( `Invalid secrets plan file: ${pathname}. Generate a fresh plan with ${formatCliCommand("openclaw secrets configure --plan-out ")}.`,