diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index df0328d924..f745678ca9 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -437,7 +437,8 @@ try { ) throw new Error("packaged stale-skill upgrade was not isolated"); - await writeFile(cursorConfig, "malformed"); + await writeFile(claudeConfig, "malformed"); + await writeFile(cursorConfig, "{}\n"); const failedExecution = await runWithStatus( cli, ["setup", "--yes", "--json"], @@ -447,10 +448,17 @@ try { if ( failed.status !== "needs_human" || failedExecution.status !== 1 || - (await readFile(cursorConfig, "utf8")) !== "malformed" + failed.clients?.claude_desktop?.status !== "failed" || + failed.clients?.cursor?.status !== "configured" || + !failed.appliedActions.includes("configured_cursor") || + (await readFile(claudeConfig, "utf8")) !== "malformed" || + json(await readFile(cursorConfig, "utf8")).mcpServers?.rea?.command !== + cli ) - throw new Error("packaged setup failure recovery did not preserve input"); - await writeFile(cursorConfig, '{"existing":true}\n'); + throw new Error( + `packaged setup did not continue after an earlier client failure: ${JSON.stringify(failed)}`, + ); + await writeFile(claudeConfig, "{}\n"); const recoveredExecution = await runWithStatus( cli, ["setup", "--yes", "--json"], diff --git a/src/application/SetupClients.ts b/src/application/SetupClients.ts index d8f1eab626..8554fe8c60 100644 --- a/src/application/SetupClients.ts +++ b/src/application/SetupClients.ts @@ -16,7 +16,7 @@ const failedConfigurationMessage = ( return "Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup."; }; -/** Configure each detected agent, stopping after the first failed transaction. */ +/** Configure every detected agent and report the first failed transaction. */ export const configureDetectedClients = async (options: { readonly host: SetupHost; readonly detectedClients: readonly SetupClient[]; @@ -25,6 +25,7 @@ export const configureDetectedClients = async (options: { readonly clients: Record; readonly appliedActions: string[]; }): Promise => { + let failure: string | undefined; for (const client of options.detectedClients) { const result = await options.host.configureClient( client, @@ -33,9 +34,9 @@ export const configureDetectedClients = async (options: { ); options.clients[client.name] = result; if (result.status === "failed") - return failedConfigurationMessage(result.reason); - if (result.status === "configured") + failure ??= failedConfigurationMessage(result.reason); + else if (result.status === "configured") options.appliedActions.push(`configured_${client.name}`); } - return undefined; + return failure; }; diff --git a/tests/setup.test.ts b/tests/setup.test.ts index 2635be5edc..e4df957338 100644 --- a/tests/setup.test.ts +++ b/tests/setup.test.ts @@ -301,6 +301,31 @@ describe("setup workflow", () => { expect(result.remediation).not.toContain("internal_client_key"); }); + it("records every detected client outcome after an earlier failure", async () => { + const host = new FakeSetupHost(); + host.hopper = "/Applications/Hopper"; + host.clients = [ + { name: "first", configPath: "/first.json" }, + { name: "second", configPath: "/second.json" }, + { name: "third", configPath: "/third.json" }, + ]; + host.clientResults.set("first", { status: "failed", reason: "write" }); + host.clientResults.set("second", { status: "configured" }); + host.clientResults.set("third", { status: "failed", reason: "readback" }); + + const result = await runSetup(options(true), host); + + expect(host.configurations).toBe(3); + expect(result.clients).toEqual({ + first: { status: "failed", reason: "write" }, + second: { status: "configured" }, + third: { status: "failed", reason: "readback" }, + }); + expect(result.appliedActions).toEqual(["configured_second"]); + expect(result.status).toBe("needs_human"); + expect(result.remediation).toContain("could not be updated"); + }); + it("explains skill installation recovery", async () => { const host = new FakeSetupHost(); host.hopper = "/Applications/Hopper";