mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 11:25:39 +00:00
* fix(uninstall): preflight layered OpenCode fallback * fix(uninstall): preserve inherited foreign registration facts * fix(uninstall): preflight legacy OpenCode layer removal
172 lines
5.6 KiB
TypeScript
172 lines
5.6 KiB
TypeScript
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
|
|
import {
|
|
runUninstall,
|
|
systemUninstallHost,
|
|
} from "../../../src/application/Uninstall.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { effectiveClientConfiguration } from "../../../src/application/ClientConfigurationFiles.js";
|
|
import { parseClientConfiguration } from "../../../src/application/ClientConfigurationDocument.js";
|
|
|
|
it.each(["native", "legacy"])(
|
|
"retains an owned %s lower layer when a foreign upper layer inherits its fields",
|
|
async (dialect) => {
|
|
const home = await createTestTempDirectory("rea-opencode-layered-foreign-");
|
|
const directory = join(home, ".config/opencode");
|
|
await mkdir(directory, { recursive: true });
|
|
const lower = join(directory, "opencode.json");
|
|
const upper = join(directory, "opencode.jsonc");
|
|
const owned = JSON.stringify(
|
|
dialect === "native"
|
|
? {
|
|
mcp: {
|
|
servers: {
|
|
rea: {
|
|
type: "local",
|
|
command: ["rea", "mcp"],
|
|
disabled: false,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
: {
|
|
mcp: {
|
|
servers: {},
|
|
rea: { type: "local", command: ["rea", "mcp"], enabled: true },
|
|
},
|
|
},
|
|
);
|
|
const partial = JSON.stringify({
|
|
mcp: { servers: { rea: { command: ["/usr/bin/false"] } } },
|
|
});
|
|
await writeFile(lower, owned);
|
|
await writeFile(upper, partial);
|
|
const effective = async () =>
|
|
effectiveClientConfiguration(
|
|
await Promise.all(
|
|
[lower, upper].map(async (sourcePath) => ({
|
|
...parseClientConfiguration(
|
|
await readFile(sourcePath, "utf8"),
|
|
"opencode",
|
|
),
|
|
sourcePath,
|
|
})),
|
|
),
|
|
)?.servers.rea;
|
|
const before = await effective();
|
|
expect(before).toMatchObject({
|
|
type: "local",
|
|
command: ["/usr/bin/false"],
|
|
disabled: false,
|
|
});
|
|
const result = await runUninstall(
|
|
false,
|
|
systemUninstallHost(home, undefined, {
|
|
XDG_CONFIG_HOME: join(home, ".config"),
|
|
}),
|
|
);
|
|
expect(result.status).toBe("failed");
|
|
expect(result.items).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "opencode",
|
|
status: "failed",
|
|
detail: expect.stringContaining("change an unowned"),
|
|
}),
|
|
);
|
|
expect(await readFile(lower, "utf8")).toBe(owned);
|
|
expect(await readFile(upper, "utf8")).toBe(partial);
|
|
expect(await effective()).toEqual(before);
|
|
},
|
|
);
|
|
|
|
it("preflights a foreign legacy registration across OpenCode layers before removal", async () => {
|
|
const home = await createTestTempDirectory("rea-opencode-layered-uninstall-");
|
|
const directory = join(home, ".config/opencode");
|
|
await mkdir(directory, { recursive: true });
|
|
const lower = join(directory, "opencode.json");
|
|
const upper = join(directory, "opencode.jsonc");
|
|
const legacy = JSON.stringify({
|
|
mcp: { rea: { type: "local", command: ["/usr/bin/false"], enabled: true } },
|
|
});
|
|
const native = JSON.stringify({
|
|
mcp: {
|
|
servers: {
|
|
rea: { type: "local", command: ["rea", "mcp"], disabled: false },
|
|
},
|
|
},
|
|
});
|
|
await writeFile(lower, legacy);
|
|
await writeFile(upper, native);
|
|
const environment = { XDG_CONFIG_HOME: join(home, ".config") };
|
|
const blocked = await runUninstall(
|
|
false,
|
|
systemUninstallHost(home, undefined, environment),
|
|
);
|
|
expect(blocked.status).toBe("failed");
|
|
expect(blocked.items).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "opencode",
|
|
status: "failed",
|
|
detail: expect.stringContaining("V1"),
|
|
}),
|
|
);
|
|
expect(await readFile(lower, "utf8")).toBe(legacy);
|
|
expect(await readFile(upper, "utf8")).toBe(native);
|
|
await expect(access(`${lower}.rea.backup`)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
await expect(access(`${upper}.rea.backup`)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
|
|
await writeFile(lower, "{}\n");
|
|
const removed = await runUninstall(
|
|
false,
|
|
systemUninstallHost(home, undefined, environment),
|
|
);
|
|
expect(removed.status).toBe("complete");
|
|
expect(removed.items).toContainEqual(
|
|
expect.objectContaining({ name: "opencode", status: "removed" }),
|
|
);
|
|
expect(await readFile(upper, "utf8")).not.toContain('"rea"');
|
|
|
|
// Removing a shadowed owned registration does not activate a registration
|
|
// that was already selected from a later layer.
|
|
await writeFile(lower, native);
|
|
const foreignNative = JSON.stringify({
|
|
mcp: {
|
|
servers: {
|
|
rea: { type: "local", command: ["/usr/bin/false"], disabled: false },
|
|
},
|
|
},
|
|
});
|
|
await writeFile(upper, foreignNative);
|
|
const alreadyForeign = await runUninstall(
|
|
false,
|
|
systemUninstallHost(home, undefined, environment),
|
|
);
|
|
expect(alreadyForeign.status).toBe("complete");
|
|
expect(await readFile(lower, "utf8")).not.toContain('"rea"');
|
|
expect(await readFile(upper, "utf8")).toBe(foreignNative);
|
|
|
|
// The same control applies when the shadowed owned row uses V1 syntax.
|
|
await writeFile(
|
|
lower,
|
|
JSON.stringify({
|
|
mcp: {
|
|
servers: {},
|
|
rea: { type: "local", command: ["rea", "mcp"], enabled: true },
|
|
},
|
|
}),
|
|
);
|
|
const legacyAlreadyForeign = await runUninstall(
|
|
false,
|
|
systemUninstallHost(home, undefined, environment),
|
|
);
|
|
expect(legacyAlreadyForeign.status).toBe("complete");
|
|
expect(await readFile(lower, "utf8")).not.toContain('"rea"');
|
|
expect(await readFile(upper, "utf8")).toBe(foreignNative);
|
|
});
|