fix(setup): recognize inherited owned launcher

This commit is contained in:
morluto committed 2026-10-27 11:45:01 +08:00
1 parent f2277454b8
commit d2da713859
3 files changed
+128 -1

No files matched your search

+11 -1
View File
@@ -705,7 +705,17 @@ const registrationOwnershipBlocker = (
value: effective.servers[name],
paths: client.configPaths ?? [client.configPath],
});
if (Object.hasOwn(parsed.servers, name))
const fragment = parsed.servers[name];
// A layered policy fragment inherits its launcher. Classify the effective
// launcher above, but also admit any explicit command this write replaces.
const inheritsLauncher =
effective !== undefined &&
effective !== parsed &&
typeof fragment === "object" &&
fragment !== null &&
!Array.isArray(fragment) &&
!Object.hasOwn(fragment, "command");
if (Object.hasOwn(parsed.servers, name) && !inheritsLauncher)
entries.push({ value: parsed.servers[name], paths: [client.configPath] });
if (legacyClientServerPath(parsed, name) !== undefined) {
entries.push({
@@ -262,3 +262,71 @@ cliTest(
});
},
);
cliTest(
"OpenCode setup refreshes an owned inherited launcher through a policy fragment",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-setup-owned-layer-");
const directory = join(home, ".config", "opencode");
await mkdir(directory, { recursive: true });
const lower = join(directory, "config.json");
const upper = join(directory, "opencode.json");
const originalLower = JSON.stringify({
mcp: { rea: { type: "local", command: ["rea", "mcp"] } },
});
const originalUpper = JSON.stringify({
mcp: { rea: { environment: { CUSTOM: "keep" } } },
});
await writeFile(lower, originalLower);
await writeFile(upper, originalUpper);
const result = await cli.run({
arguments: [
"setup",
"--client",
"opencode",
"--skill=false",
"--yes",
"--json",
],
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
},
});
expect(result.exitCode, result.stdout).toBe(0);
expect(await readFile(lower, "utf8")).toBe(originalLower);
expect(await readFile(`${upper}.rea.backup`, "utf8")).toBe(originalUpper);
expect(JSON.parse(await readFile(upper, "utf8"))).toMatchObject({
mcp: { rea: { type: "local", environment: { CUSTOM: "keep" } } },
});
const foreignFragment = JSON.stringify({
mcp: { rea: { command: ["other", "mcp"] } },
});
await writeFile(upper, foreignFragment);
const blocked = await cli.run({
arguments: [
"setup",
"--client",
"opencode",
"--skill=false",
"--yes",
"--json",
],
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
},
});
expect(blocked.exitCode, blocked.stdout).toBe(1);
expect(blocked.json).toMatchObject({
status: "needs_human",
remediation: expect.stringContaining("foreign ownership"),
});
expect(await readFile(upper, "utf8")).toBe(foreignFragment);
expect(await readFile(`${upper}.rea.backup`, "utf8")).toBe(originalUpper);
},
);
@@ -136,3 +136,52 @@ it("blocks an inherited foreign row before planning creation of a missing write
code: "ENOENT",
});
});
it.each(["other", "rea"])(
"rechecks an inherited %s launcher after preflight for a policy fragment",
async (command) => {
const home = await createTestTempDirectory("rea-setup-layer-recheck-");
const lower = join(home, "config.json");
const configPath = join(home, "opencode.json");
const client: SetupClient = {
name: "opencode",
format: "opencode",
configPath,
configPaths: [lower, configPath],
};
await writeFile(
lower,
'{"mcp":{"rea":{"type":"local","command":["rea","mcp"]}}}',
);
const original = '{"mcp":{"rea":{"environment":{"CUSTOM":"keep"}}}}';
await writeFile(configPath, original);
expect(
await inspectClientConfiguration(client, {}, ["rea", "mcp"]),
).toMatchObject({ status: "update" });
// The write transaction must use a fresh source discovery after preflight.
await writeFile(
lower,
JSON.stringify({
mcp: { rea: { type: "local", command: [command, "mcp"] } },
}),
);
const result = await configureClientConfiguration(client, {}, [
"rea",
"mcp",
]);
if (command === "other") {
expect(result).toMatchObject({
status: "failed",
reason: "registration_ownership",
detail: expect.stringContaining("foreign ownership"),
});
expect(await readFile(configPath, "utf8")).toBe(original);
await expect(readFile(`${configPath}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
} else {
expect(result).toMatchObject({ status: "configured" });
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
}
},
);