|
|
|
@@ -34,20 +34,13 @@ import {
|
|
|
|
|
type UninstallFileSystem,
|
|
|
|
|
} from "../../../src/application/Uninstall.js";
|
|
|
|
|
|
|
|
|
|
const roots: string[] = [];
|
|
|
|
|
const ownedSkillManifest = `---\nname: ${PRODUCT_IDENTITY.skillName}\n---\n# REA\n`;
|
|
|
|
|
beforeEach(clearClientLocationEnvironment);
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
vi.unstubAllEnvs();
|
|
|
|
|
await Promise.all(
|
|
|
|
|
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
|
|
|
|
|
|
describe("client configuration filesystem lifecycle", () => {
|
|
|
|
|
it("detects every supported client and skips absent clients", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-detect-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
for (const client of supportedClients(home))
|
|
|
|
|
if (client.markerPath !== undefined)
|
|
|
|
|
await mkdir(client.markerPath, { recursive: true });
|
|
|
|
@@ -84,13 +77,11 @@ describe("client configuration filesystem lifecycle", () => {
|
|
|
|
|
).toBe(join(home, ".claude.json"));
|
|
|
|
|
expect(detected.find(({ name }) => name === "devin")?.format).toBe("json");
|
|
|
|
|
const emptyHome = await createTestTempDirectory("rea-empty-");
|
|
|
|
|
roots.push(emptyHome);
|
|
|
|
|
expect(await detectClients(emptyHome)).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("preserves unrelated Codex TOML and supports an installed command", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-toml-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".codex/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -131,7 +122,6 @@ describe("client configuration filesystem lifecycle", () => {
|
|
|
|
|
|
|
|
|
|
it("does not rewrite an equivalent Codex registration with reordered environment keys", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-toml-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".codex/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -161,7 +151,6 @@ describe("client configuration filesystem lifecycle", () => {
|
|
|
|
|
describe("Codex MCP tool deadlines", () => {
|
|
|
|
|
it("derives the Codex tool deadline from an explicit Ghidra startup allowance", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-codex-long-timeout-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".codex/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
expect(
|
|
|
|
@@ -181,7 +170,6 @@ describe("Codex MCP tool deadlines", () => {
|
|
|
|
|
|
|
|
|
|
it("preserves positive caller-selected Codex startup and tool deadlines", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-codex-custom-timeout-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".codex/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -204,7 +192,6 @@ describe("Codex MCP tool deadlines", () => {
|
|
|
|
|
describe("client configuration filesystem lifecycle", () => {
|
|
|
|
|
it("updates a symlink target without replacing the TOML config symlink", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-toml-symlink-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".codex/config.toml");
|
|
|
|
|
const targetPath = join(home, "managed-config.toml");
|
|
|
|
|
const original = 'model = "gpt-5"\n';
|
|
|
|
@@ -226,7 +213,6 @@ describe("client configuration filesystem lifecycle", () => {
|
|
|
|
|
|
|
|
|
|
it("fails before mutation when a TOML config symlink is dangling", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-toml-symlink-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".codex/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await symlink(join(home, "missing-config.toml"), configPath);
|
|
|
|
@@ -248,7 +234,6 @@ describe("client configuration filesystem lifecycle", () => {
|
|
|
|
|
describe("Grok Build server table edits", () => {
|
|
|
|
|
it("edits only the Grok Build REA tables and removes them on uninstall", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original = [
|
|
|
|
@@ -368,7 +353,6 @@ describe("Grok Build server table edits", () => {
|
|
|
|
|
describe("Grok Build disabled server list", () => {
|
|
|
|
|
it("removes rea from disabled_mcp_servers and leaves other names", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-disabled-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -428,7 +412,6 @@ describe("Grok Build disabled server list", () => {
|
|
|
|
|
|
|
|
|
|
it("removes multiline rea entries from disabled_mcp_servers", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-disabled-multiline-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -474,7 +457,6 @@ describe("Grok Build disabled server list", () => {
|
|
|
|
|
describe("Grok Build text that resembles a server table", () => {
|
|
|
|
|
it("preserves Grok text that only looks like an REA table", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-preserve-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original = [
|
|
|
|
@@ -556,7 +538,6 @@ describe("Grok Build text that resembles a server table", () => {
|
|
|
|
|
|
|
|
|
|
it("replaces spaced and escaped Grok REA headers without duplicating them", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-alias-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -603,7 +584,6 @@ describe("Grok Build text that resembles a server table", () => {
|
|
|
|
|
describe("Grok Build multiline and escaped headers", () => {
|
|
|
|
|
it("preserves a multiline string opened on the line that closed the previous one", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-multiline-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -659,7 +639,6 @@ describe("Grok Build multiline and escaped headers", () => {
|
|
|
|
|
|
|
|
|
|
it("replaces a hex-escaped Grok REA header and keeps a hex-escaped foreign name", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-hex-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original = [
|
|
|
|
@@ -712,7 +691,6 @@ describe("Grok Build multiline and escaped headers", () => {
|
|
|
|
|
describe("Grok Build inline and dotted assignments", () => {
|
|
|
|
|
it("updates and removes inline and dotted Grok REA assignments", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-assign-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const inline = [
|
|
|
|
@@ -818,7 +796,6 @@ describe("Grok Build inline and dotted assignments", () => {
|
|
|
|
|
describe("Grok Build closed inline tables", () => {
|
|
|
|
|
it("splices REA into a closed mcp_servers inline table", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-inline-table-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original = [
|
|
|
|
@@ -898,7 +875,6 @@ describe("Grok Build closed inline tables", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps a quote that belongs to a multiline string closer", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-quotes-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -938,7 +914,6 @@ describe("Grok Build closed inline tables", () => {
|
|
|
|
|
describe("Grok Build scalar and nested values", () => {
|
|
|
|
|
it("preserves a basic string that uses the escape smol-toml accepts", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-escape-e-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -970,7 +945,6 @@ describe("Grok Build scalar and nested values", () => {
|
|
|
|
|
|
|
|
|
|
it("updates a closed inline table whose last value is a scalar", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-scalar-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -995,7 +969,6 @@ describe("Grok Build scalar and nested values", () => {
|
|
|
|
|
|
|
|
|
|
it("removes a nested array inside a Grok REA table", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-nested-array-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original = [
|
|
|
|
@@ -1043,7 +1016,6 @@ describe("Grok Build scalar and nested values", () => {
|
|
|
|
|
describe("Grok Build newlines and leading marks", () => {
|
|
|
|
|
it("edits a BOM-prefixed Grok document without losing the first table", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-bom-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -1081,7 +1053,6 @@ describe("Grok Build newlines and leading marks", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps CRLF string values in a Grok file that also uses LF", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-crlf-string-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original =
|
|
|
|
@@ -1112,7 +1083,6 @@ describe("Grok Build newlines and leading marks", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps LF string values in a Grok file that also uses CRLF", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-mixed-newline-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -1138,7 +1108,6 @@ describe("Grok Build newlines and leading marks", () => {
|
|
|
|
|
describe("Grok Build inline comments", () => {
|
|
|
|
|
it("keeps comments on dotted Grok fields without repeating a comma", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-dotted-comment-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original = [
|
|
|
|
@@ -1180,7 +1149,6 @@ describe("Grok Build inline comments", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps an inline comment that sits before the Grok REA pair", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-inline-before-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const original =
|
|
|
|
@@ -1216,7 +1184,6 @@ describe("Grok Build inline comments", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps inline-table comments and encodes a DEL in an inline value", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-inline-comment-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -1247,7 +1214,6 @@ describe("Grok Build inline comments", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps comments when uninstall removes the only inline Grok server", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grok-inline-empty-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".grok/config.toml");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -1267,7 +1233,6 @@ describe("Grok Build inline comments", () => {
|
|
|
|
|
describe("JSON client configuration comments", () => {
|
|
|
|
|
it("keeps a same-line JSONC comment when uninstall removes Cursor", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-cursor-jsonc-comment-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -1289,7 +1254,6 @@ describe("JSON client configuration comments", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps a JSONC comment when a block comment precedes the comma", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-cursor-jsonc-block-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
@@ -1314,7 +1278,6 @@ describe("JSON client configuration comments", () => {
|
|
|
|
|
|
|
|
|
|
it("keeps a JSONC comment without swallowing the next server or closers", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-cursor-jsonc-next-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
const files = [
|
|
|
|
@@ -1347,7 +1310,6 @@ describe("JSON client configuration comments", () => {
|
|
|
|
|
describe("Grok Bot registration", () => {
|
|
|
|
|
it("does not write or align a data-directory mcp.json", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-grokbot-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const directory = join(home, ".grokbot");
|
|
|
|
|
const configPath = join(directory, "mcp.json");
|
|
|
|
|
await mkdir(directory, { recursive: true });
|
|
|
|
@@ -1388,7 +1350,6 @@ describe("Grok Bot registration", () => {
|
|
|
|
|
describe("Codex configuration comments", () => {
|
|
|
|
|
it("keeps Codex comments and value spelling through setup and uninstall", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-toml-comments-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".codex/config.toml");
|
|
|
|
|
const original =
|
|
|
|
|
'# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\n';
|
|
|
|
@@ -1419,7 +1380,6 @@ describe("managed data purge root validation", () => {
|
|
|
|
|
"retains cache and state when the managed-root symlink is %s",
|
|
|
|
|
async (targetState) => {
|
|
|
|
|
const root = await createTestTempDirectory("rea-uninstall-data-root-");
|
|
|
|
|
roots.push(root);
|
|
|
|
|
const home = join(root, "home");
|
|
|
|
|
const outside = join(root, "unrelated-application");
|
|
|
|
|
await mkdir(home);
|
|
|
|
@@ -1458,7 +1418,6 @@ describe("managed data purge root validation", () => {
|
|
|
|
|
|
|
|
|
|
it("retains a managed data root that is not a directory", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-data-file-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const managedRoot = join(home, ".rea");
|
|
|
|
|
await writeFile(managedRoot, "unrelated file");
|
|
|
|
|
|
|
|
|
@@ -1477,7 +1436,6 @@ describe("managed data purge root validation", () => {
|
|
|
|
|
|
|
|
|
|
it("purges ordinary managed data through a legitimate home directory link", async () => {
|
|
|
|
|
const root = await createTestTempDirectory("rea-uninstall-home-link-");
|
|
|
|
|
roots.push(root);
|
|
|
|
|
const home = join(root, "actual-home");
|
|
|
|
|
const linkedHome = join(root, "linked-home");
|
|
|
|
|
const managedRoot = join(home, ".rea");
|
|
|
|
@@ -1514,7 +1472,6 @@ describe("managed data purge root validation", () => {
|
|
|
|
|
|
|
|
|
|
it("reports root inspection failure without removing managed data", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-data-denied-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const managedRoot = join(home, ".rea");
|
|
|
|
|
for (const name of ["cache", "state"]) {
|
|
|
|
|
await mkdir(join(managedRoot, name), { recursive: true });
|
|
|
|
@@ -1557,7 +1514,6 @@ describe("managed data purge root validation", () => {
|
|
|
|
|
describe("client configuration filesystem removal", () => {
|
|
|
|
|
it("uninstalls only owned entries and refuses purge symlinks", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const cursor = join(home, ".cursor/mcp.json");
|
|
|
|
|
const skill = join(
|
|
|
|
|
home,
|
|
|
|
@@ -1600,7 +1556,6 @@ describe("client configuration filesystem removal", () => {
|
|
|
|
|
|
|
|
|
|
it("removes emptied skill directories and keeps ones with user files", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-skill-dirs-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const shared = join(home, ".agents/skills/reverse-engineer-anything");
|
|
|
|
|
const claude = join(home, ".claude/skills/reverse-engineer-anything");
|
|
|
|
|
for (const root of [shared, claude]) {
|
|
|
|
@@ -1633,7 +1588,6 @@ describe("client configuration filesystem removal", () => {
|
|
|
|
|
"reports an emptied skill directory that cannot be removed",
|
|
|
|
|
async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-skill-perm-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const parent = join(home, ".agents/skills");
|
|
|
|
|
const root = join(parent, "reverse-engineer-anything");
|
|
|
|
|
await mkdir(join(root, "references"), { recursive: true });
|
|
|
|
@@ -1664,7 +1618,6 @@ describe("client configuration filesystem removal", () => {
|
|
|
|
|
describe("uninstall configuration symlink ownership", () => {
|
|
|
|
|
it("removes an owned entry through a symlink without replacing it", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-symlink-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".cursor/mcp.json");
|
|
|
|
|
const targetPath = join(home, "managed-mcp.json");
|
|
|
|
|
const original = JSON.stringify({
|
|
|
|
@@ -1695,7 +1648,6 @@ describe("uninstall configuration symlink ownership", () => {
|
|
|
|
|
|
|
|
|
|
it("fails before mutation when an uninstall config symlink is dangling", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-symlink-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const configPath = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
|
|
|
await symlink(join(home, "missing-mcp.json"), configPath);
|
|
|
|
@@ -1719,7 +1671,6 @@ describe("uninstall configuration symlink ownership", () => {
|
|
|
|
|
describe("uninstall registration ownership preflight", () => {
|
|
|
|
|
it("removes repeated owned registrations from the table the client reads", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-repeated-owned-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const config = join(home, ".claude.json");
|
|
|
|
|
await mkdir(dirname(config), { recursive: true });
|
|
|
|
|
const owned = {
|
|
|
|
@@ -1748,7 +1699,6 @@ describe("uninstall registration ownership preflight", () => {
|
|
|
|
|
const home = await createTestTempDirectory(
|
|
|
|
|
"rea-uninstall-repeated-foreign-",
|
|
|
|
|
);
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const claude = join(home, ".claude.json");
|
|
|
|
|
const cursor = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(claude), { recursive: true });
|
|
|
|
@@ -1785,7 +1735,6 @@ describe("uninstall registration ownership preflight", () => {
|
|
|
|
|
const home = await createTestTempDirectory(
|
|
|
|
|
"rea-uninstall-opencode-fallback-",
|
|
|
|
|
);
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const config = join(home, ".config/opencode/opencode.json");
|
|
|
|
|
await mkdir(dirname(config), { recursive: true });
|
|
|
|
|
const original = JSON.stringify({
|
|
|
|
@@ -1820,7 +1769,6 @@ describe("layered uninstall outcomes", () => {
|
|
|
|
|
const home = await createTestTempDirectory(
|
|
|
|
|
"rea-uninstall-opencode-layers-",
|
|
|
|
|
);
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const directory = join(home, ".config/opencode");
|
|
|
|
|
const json = join(directory, "opencode.json");
|
|
|
|
|
const jsonc = join(directory, "opencode.jsonc");
|
|
|
|
@@ -1897,7 +1845,6 @@ describe("layered uninstall outcomes", () => {
|
|
|
|
|
describe("uninstall configuration preflight", () => {
|
|
|
|
|
it("fails closed on malformed client configuration", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-bad-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const config = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(config), { recursive: true });
|
|
|
|
|
await writeFile(config, "not-json");
|
|
|
|
@@ -1922,7 +1869,6 @@ describe("uninstall configuration preflight", () => {
|
|
|
|
|
"names the structural problem in valid JSON %s",
|
|
|
|
|
async (content, problem) => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-shape-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const config = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(config), { recursive: true });
|
|
|
|
|
await writeFile(config, content);
|
|
|
|
@@ -1948,7 +1894,6 @@ describe("uninstall configuration preflight", () => {
|
|
|
|
|
|
|
|
|
|
it("stops before any removal when one client configuration is malformed", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-stop-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const codex = join(home, ".codex/config.toml");
|
|
|
|
|
const cursor = join(home, ".cursor/mcp.json");
|
|
|
|
|
const skillMarker = join(
|
|
|
|
@@ -2009,7 +1954,6 @@ describe("uninstall configuration preflight", () => {
|
|
|
|
|
describe("client configuration changed during uninstall", () => {
|
|
|
|
|
it("rechecks duplicate ownership after preflight and keeps the changed source", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-repeat-race-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const cursor = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(cursor), { recursive: true });
|
|
|
|
|
const owned = {
|
|
|
|
@@ -2048,7 +1992,6 @@ describe("client configuration changed during uninstall", () => {
|
|
|
|
|
|
|
|
|
|
it("stops the remaining removals when a configuration fails after preflight", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-race-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const codex = join(home, ".codex/config.toml");
|
|
|
|
|
const cursor = join(home, ".cursor/mcp.json");
|
|
|
|
|
const skill = join(
|
|
|
|
@@ -2203,7 +2146,6 @@ describe("client configuration filesystem failure reporting", () => {
|
|
|
|
|
|
|
|
|
|
it("reports a managed-path removal failure", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-remove-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const skillRoot = join(home, ".agents/skills/reverse-engineer-anything");
|
|
|
|
|
await mkdir(skillRoot, { recursive: true });
|
|
|
|
|
await writeFile(join(skillRoot, "SKILL.md"), ownedSkillManifest);
|
|
|
|
@@ -2228,7 +2170,6 @@ describe("client configuration filesystem failure reporting", () => {
|
|
|
|
|
|
|
|
|
|
it("uninstalls all managed skill roots, including a custom Claude Code directory", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-skills-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const customClaudeDirectory = join(home, "claude-config");
|
|
|
|
|
const rootsToRemove = [
|
|
|
|
|
join(home, ".agents/skills/reverse-engineer-anything"),
|
|
|
|
@@ -2263,7 +2204,6 @@ describe("client configuration filesystem failure reporting", () => {
|
|
|
|
|
describe("skill root client identifier labels", () => {
|
|
|
|
|
it("labels each retained or removed skill root by its client identifier", async () => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-skill-labels-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const customClaudeDirectory = join(home, "claude-config");
|
|
|
|
|
const sharedRoot = join(home, ".agents/skills/reverse-engineer-anything");
|
|
|
|
|
const claudeRoot = join(
|
|
|
|
@@ -2312,7 +2252,6 @@ const uninstallFixture = async (): Promise<{
|
|
|
|
|
readonly config: string;
|
|
|
|
|
}> => {
|
|
|
|
|
const home = await createTestTempDirectory("rea-uninstall-failure-");
|
|
|
|
|
roots.push(home);
|
|
|
|
|
const config = join(home, ".cursor/mcp.json");
|
|
|
|
|
await mkdir(dirname(config), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
|