diff --git a/src/application/UpdateMaintenance.ts b/src/application/UpdateMaintenance.ts index a5e06ef2ec..2dec719567 100644 --- a/src/application/UpdateMaintenance.ts +++ b/src/application/UpdateMaintenance.ts @@ -3,7 +3,6 @@ import { RegularFileCleanupFailure } from "./RegularFileRead.js"; import { classifyClientRegistrationCommand } from "./ClientRegistrationIdentity.js"; import { readClientRegistrationInspections } from "./ClientRegistrationStatus.js"; -import { supportedClients } from "./SupportedClients.js"; import { existingSkillDestinations } from "./SetupSkill.js"; import type { SetupAction } from "./SetupTypes.js"; import type { Result } from "../domain/result.js"; @@ -77,32 +76,14 @@ export const existingMaintenanceScope = async ( continue; clients.push(registration.client); } - // One shared file is one maintenance decision: a shared-registration client - // defers to another selected client covering the same file, so the owned - // row is refreshed once through the applicable product instead of twice - // through two views of the same registration. - const shared = new Map( - supportedClients(home, process.platform, environment).map((client) => [ - client.name, - client.sharedRegistration === true, - ]), - ); - const covered = new Set( - registrations - .filter( - ({ status }) => - clients.includes(status.client) && shared.get(status.client) !== true, - ) - .map(({ status }) => status.config_path), - ); - const deduped = clients.filter( - (client) => - shared.get(client) !== true || - !registrations.some( - ({ status }) => - status.client === client && covered.has(status.config_path), - ), - ); + // Each setup action rewrites only that client's own configuration path, so + // maintenance keeps every selected client instead of deferring one view of + // a registration to another client's inspection: ZCode writes + // ~/.zcode/cli/config.json and deliberately leaves the ~/.agents/mcp.json + // fallback untouched (docs/client-configuration.md), while FreeBuff writes + // the shared file itself. Dropping FreeBuff because ZCode reads that + // fallback would leave the row both products load pinned to the old + // version. const skillDestinations = await existingSkillDestinations( home, environment, @@ -110,7 +91,7 @@ export const existingMaintenanceScope = async ( resources, ); return { - clients: deduped, + clients, skill: skillDestinations.length > 0, skillDestinations, }; diff --git a/tests/process-global/updateMaintenance.test.ts b/tests/process-global/updateMaintenance.test.ts index f25770f9a3..5c44093386 100644 --- a/tests/process-global/updateMaintenance.test.ts +++ b/tests/process-global/updateMaintenance.test.ts @@ -158,9 +158,11 @@ describe("maintenance activation and layered configuration", () => { }), ); + // The fallback row is also FreeBuff's registration, and FreeBuff is the + // setup action that rewrites it, so both views stay in maintenance scope. await expect( existingMaintenanceScope(home, entryPoint, process.env), - ).resolves.toMatchObject({ clients: ["zcode"] }); + ).resolves.toMatchObject({ clients: ["freebuff", "zcode"] }); }); it("selects an enabled registration inherited through layered OpenCode files", async () => { @@ -355,6 +357,30 @@ describe("shared registration maintenance", () => { }); }); + it("retains FreeBuff when ZCode reads the shared fallback", async () => { + // Both products load the one shared row, but their setup actions write + // different files: ZCode writes ~/.zcode/cli/config.json and deliberately + // leaves ~/.agents/mcp.json untouched (docs/client-configuration.md), so + // its fallback view cannot cover FreeBuff's registration. The shared row + // would stay pinned to the old version without FreeBuff's own action. + await writeClient("zcode", JSON.stringify({ mcp: { servers: {} } })); + await mkdir(join(home, ".config", "freebuff-desktop"), { + recursive: true, + }); + await writeClient( + "freebuff", + JSON.stringify({ + mcpServers: { + rea: { command: "npx", args: ["-y", "rea-agents@0.1.0", "mcp"] }, + }, + }), + ); + + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toMatchObject({ clients: ["freebuff", "zcode"] }); + }); + it("leaves a foreign shared registration out of maintenance", async () => { await writeClient( "freebuff",