fix(update): retain FreeBuff when ZCode reads the shared fallback

Maintenance deduplicated clients on the path each inspection read, so ZCode's fallback view of ~/.agents/mcp.json marked that file "covered" and dropped FreeBuff from the scope. ZCode setup writes ~/.zcode/cli/config.json and deliberately leaves the fallback untouched (docs/client-configuration.md), so the emitted command refreshed only ZCode while the shared row both products load stayed pinned to the old package version. Each setup action rewrites only its own configuration path and no other action rewrites the shared row, so every selected client keeps its own maintenance action; the read-path covered/deduped filter is removed.

Covers the Codex P2 on UpdateMaintenance.ts:104. Validation: the new regression fails on e2fdec384 with {clients:["zcode"]} and passes with {clients:["freebuff","zcode"]}; updateMaintenance 31/31, setupClientConfigCleanupOwnership, cliUpdateAlias, updateOutput, cliStartupImports green; tsc --noEmit, oxlint (0 errors) and oxfmt --check clean; clientRegistrationStatus keeps the same 4 symlink/stale-vs-aligned failures this Windows host has on base e2fdec384 (verified by stashing the change).

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
0xLLVEL
2026-10-19 13:36:26 +07:00
parent e2fdec3847
commit 4dbfdf1a9a
2 changed files with 36 additions and 29 deletions
+9 -28
View File
@@ -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,
};
+27 -1
View File
@@ -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",