Files
rea/src/application/ClaudeDesktopConfigLocation.test.ts
T
morluto abb54ce374 test(setup): cover configuration identity regressions
Add direct controls for literal AppData roots, denied MSIX target rechecks,
and unrecognized Claude package deployments. Verify setup refuses mutation
and registration diagnostics remain invalid for unknown package readers.

Six new controls fail at 6d5281407 before the existing 9abdad335 fix; one
existing-behavior guard passes. All seven pass on current main. Keep native
Windows/app behavior distinct from macOS filesystem and platform-selected
adapter checks.

Validation: 82 tests across four configuration/CLI files, 42 header/skill/
lifecycle tests, 144 Ghidra/doctor consumer tests, check:fast, format:check,
and test-discovery verification passed. No provider/target execution.

Refs #4378, #4432, #4496, #4501, #4384, #4676
2026-10-18 07:16:10 +08:00

395 lines
12 KiB
TypeScript

import { basename, dirname, join, sep } from "node:path";
import { describe, expect, it } from "vitest";
import {
CLAUDE_DESKTOP_PACKAGE_FAMILY,
resolveWindowsClaudeDesktopLocation,
type ClaudeDesktopLocationFileSystem,
} from "./ClaudeDesktopConfigLocation.js";
import {
packagePrivateLauncherBlocker,
packagePrivateStorageFamily,
redirectedWriteBlocker,
type MsixPackageView,
windowsAppDataRoots,
} from "./MsixPackageStorage.js";
const home = join("/profile", "a");
const env = {
APPDATA: join(home, "AppData", "Roaming"),
LOCALAPPDATA: join(home, "AppData", "Local"),
};
const packages = join(env.LOCALAPPDATA, "Packages");
const appDataConfig = join(env.APPDATA, "Claude", "claude_desktop_config.json");
const privateConfig = join(
packages,
CLAUDE_DESKTOP_PACKAGE_FAMILY,
"LocalCache",
"Roaming",
"Claude",
"claude_desktop_config.json",
);
const failure = (code: string): Error =>
Object.assign(new Error(`${code}: injected`), { code });
const file = { isFile: () => true };
const privateDirectory = dirname(privateConfig);
const privatePreferences = join(privateDirectory, "Preferences");
const appDataDirectory = dirname(appDataConfig);
it.each([undefined, ""])(
"uses default AppData roots only for unset or empty values: %s",
(value) => {
expect(
windowsAppDataRoots(home, { APPDATA: value, LOCALAPPDATA: value }),
).toEqual({ roaming: env.APPDATA, local: env.LOCALAPPDATA });
},
);
it.each([" root with trailing space ", " "])(
"preserves a nonempty AppData root literally: %j",
(value) => {
expect(
windowsAppDataRoots(home, { APPDATA: value, LOCALAPPDATA: value }),
).toEqual({ roaming: value, local: value });
},
);
/** Package storage holding `privateFiles`; realpath decides the process view. */
const packagedFileSystem = (
realpath: ClaudeDesktopLocationFileSystem["realpath"],
privateFiles: readonly string[] = [privateConfig],
): ClaudeDesktopLocationFileSystem => ({
readdir: (path) => {
if (path === packages) return [CLAUDE_DESKTOP_PACKAGE_FAMILY];
if (path === privateDirectory)
return privateFiles.map((name) => basename(name));
throw failure("ENOENT");
},
lstat: (path) => {
if (!privateFiles.includes(path)) throw failure("ENOENT");
return file;
},
realpath,
});
describe("Claude Desktop MSIX process view", () => {
it("recognizes a process that sees the private copy at the AppData path", () => {
const location = resolveWindowsClaudeDesktopLocation(
home,
env,
packagedFileSystem((path) =>
path === appDataConfig ? privateConfig : path,
),
);
expect(location).toEqual({
ok: true,
value: expect.objectContaining({
configPath: privateConfig,
msixPackage: expect.objectContaining({ processView: "redirected" }),
}),
});
});
it("treats an absent AppData file as an unredirected process", () => {
const location = resolveWindowsClaudeDesktopLocation(
home,
env,
packagedFileSystem((path) => {
if (path === appDataConfig) throw failure("ENOENT");
return path;
}),
);
expect(location.ok && location.value.msixPackage?.processView).toBe(
"direct",
);
});
it("recognizes redirection through another private file before the configuration exists", () => {
const location = resolveWindowsClaudeDesktopLocation(
home,
env,
packagedFileSystem(
(path) =>
path === join(appDataDirectory, "Preferences")
? privatePreferences
: path,
[privatePreferences],
),
);
expect(location.ok && location.value.configPath).toBe(privateConfig);
expect(location.ok && location.value.msixPackage?.processView).toBe(
"redirected",
);
});
it("continues after a listed private file disappears", () => {
const fs = packagedFileSystem(
(path) =>
path === join(appDataDirectory, "Preferences")
? privatePreferences
: path,
[privatePreferences],
);
const location = resolveWindowsClaudeDesktopLocation(home, env, {
...fs,
readdir: (path) =>
path === privateDirectory
? ["vanished", "Preferences"]
: fs.readdir(path),
});
expect(location.ok && location.value.msixPackage?.processView).toBe(
"redirected",
);
});
it("keeps the view unknown when the package has no private files", () => {
const location = resolveWindowsClaudeDesktopLocation(
home,
env,
packagedFileSystem((path) => path, []),
);
expect(location.ok && location.value.msixPackage?.processView).toBe(
"unknown",
);
});
it("keeps the view unknown when the AppData path cannot be resolved", () => {
const location = resolveWindowsClaudeDesktopLocation(
home,
env,
packagedFileSystem((path) => {
if (path === appDataConfig) throw failure("EACCES");
return path;
}),
);
expect(location.ok && location.value.msixPackage?.processView).toBe(
"unknown",
);
});
});
describe("unresolved Claude Desktop package identity", () => {
it.each(["AnthropicPBC.Claude_fnn82j28hfe8t", "Claude_fnn82j28hfe8t"])(
"refuses an unrecognized package deployment %s instead of selecting AppData",
(name) => {
const location = resolveWindowsClaudeDesktopLocation(home, env, {
readdir: () => [name],
lstat: () => file,
realpath: (path) => path,
});
expect(location).toMatchObject({
ok: false,
error: {
path: join(packages, name),
message: expect.stringContaining("does not fall back"),
},
});
},
);
it.each([
"Notes.AnthropicPBC.Claude_fnn82j28hfe8t",
"AnthropicPBC.Claude_fnn82j28hfe8t.backup",
"AnthropicPBCxClaude_fnn82j28hfe8t",
])("does not treat %s as a Claude package family", (name) => {
const location = resolveWindowsClaudeDesktopLocation(home, env, {
readdir: () => [name],
lstat: () => file,
realpath: (path) => path,
});
expect(location).toEqual({
ok: true,
value: { configPath: appDataConfig, markerPath: appDataDirectory },
});
});
it("reports unreadable package storage instead of assuming AppData", () => {
const location = resolveWindowsClaudeDesktopLocation(home, env, {
readdir: () => {
throw failure("EACCES");
},
lstat: () => file,
realpath: (path) => path,
});
expect(location).toEqual({
ok: false,
error: {
path: packages,
message: expect.stringContaining("could not be inspected"),
},
});
});
it("reports an uninspectable private copy instead of choosing a file", () => {
const location = resolveWindowsClaudeDesktopLocation(home, env, {
readdir: () => [CLAUDE_DESKTOP_PACKAGE_FAMILY],
lstat: () => {
throw failure("EPERM");
},
realpath: (path) => path,
});
expect(location.ok).toBe(false);
expect(!location.ok && location.error.path).toBe(privateConfig);
});
it("matches the package family case-insensitively, as Windows does", () => {
const location = resolveWindowsClaudeDesktopLocation(home, env, {
readdir: () => [CLAUDE_DESKTOP_PACKAGE_FAMILY.toUpperCase()],
lstat: () => {
throw failure("ENOENT");
},
realpath: (path) => path,
});
expect(location.ok && location.value.msixPackage?.packageFamilyName).toBe(
CLAUDE_DESKTOP_PACKAGE_FAMILY.toUpperCase(),
);
});
});
describe("package-private REA launchers", () => {
const launcher = join(
env.APPDATA,
"npm",
"node_modules",
"rea-agents",
"scripts",
"rea.mjs",
);
const stored = join(
packages,
CLAUDE_DESKTOP_PACKAGE_FAMILY,
"LocalCache",
"Roaming",
"npm",
"node_modules",
"rea-agents",
"scripts",
"rea.mjs",
);
it("names the package that privately stores a path", () => {
expect(packagePrivateStorageFamily(stored, packages)).toBe(
CLAUDE_DESKTOP_PACKAGE_FAMILY,
);
expect(packagePrivateStorageFamily(launcher, packages)).toBeUndefined();
expect(
packagePrivateStorageFamily(
join(packages, CLAUDE_DESKTOP_PACKAGE_FAMILY, "LocalState", "x"),
packages,
),
).toBeUndefined();
});
it("blocks a launcher that only the package's processes can see", () => {
const blocker = packagePrivateLauncherBlocker(
[join("/Program Files", "nodejs", "node.exe"), launcher, "mcp"],
packages,
(path) => (path === launcher ? stored : path),
);
expect(blocker).toContain(stored);
expect(blocker).toContain(CLAUDE_DESKTOP_PACKAGE_FAMILY);
expect(blocker).toContain("npm install -g rea-agents");
});
it("compares the launcher with the canonical package storage path", () => {
const shortProfilePackages = join(
"/profile",
"A~1",
"AppData",
"Local",
"Packages",
);
const blocker = packagePrivateLauncherBlocker(
[launcher, "mcp"],
shortProfilePackages,
(path) =>
path === shortProfilePackages
? packages
: path === launcher
? stored
: path,
);
expect(blocker).toContain(CLAUDE_DESKTOP_PACKAGE_FAMILY);
});
it("allows ordinary and unresolvable launchers", () => {
expect(
packagePrivateLauncherBlocker(
["cmd.exe", "/d", "/c", "npx", "-y", "rea-agents@6.4.0", "mcp"],
packages,
() => {
throw new Error("package runners are not resolved");
},
),
).toBeUndefined();
expect(
packagePrivateLauncherBlocker([launcher, "mcp"], packages, () => {
throw failure("ENOENT");
}),
).toBeUndefined();
});
});
describe("writes from a redirected AppData view", () => {
const view: MsixPackageView = {
packageFamilyName: CLAUDE_DESKTOP_PACKAGE_FAMILY,
privateConfigPath: privateConfig,
appDataConfigPath: appDataConfig,
virtualizedRoots: [env.APPDATA, env.LOCALAPPDATA],
processView: "redirected",
};
const vscode = join(env.APPDATA, "Code", "User", "mcp.json");
const hermes = join(env.LOCALAPPDATA, "hermes", "config.yaml");
const codex = join(home, ".codex", "config.toml");
it("refuses only targets under the package's virtualized roots", () => {
const blocker = redirectedWriteBlocker(
view,
[vscode, codex, hermes],
"setup",
);
expect(blocker).toContain(`${vscode}, ${hermes}`);
expect(blocker).not.toContain(codex);
expect(blocker).toContain("Rerun setup from a terminal");
expect(redirectedWriteBlocker(view, [codex], "setup")).toBeUndefined();
});
it("compares aliases through the existing parent of a new destination", () => {
const alias = join(home, "skill-alias");
const target = join(alias, "skills", "reverse-engineer-anything");
const blocker = redirectedWriteBlocker(
view,
[target],
"uninstall",
(path) => {
if (path === alias) return join(env.APPDATA, "dsh");
if (path.startsWith(`${alias}${sep}`)) throw failure("ENOENT");
return path;
},
);
expect(blocker).toContain(target);
expect(blocker).toContain("Rerun uninstall");
});
it("keeps canonicalization failures visible instead of assuming a target is outside AppData", () => {
expect(
redirectedWriteBlocker(view, [codex], "setup", () => {
throw failure("EACCES");
}),
).toContain("Cannot establish the MSIX write location");
});
it("allows every target without positive evidence of redirection", () => {
for (const processView of ["direct", "unknown"] as const)
expect(
redirectedWriteBlocker({ ...view, processView }, [vscode], "uninstall"),
).toBeUndefined();
expect(
redirectedWriteBlocker(undefined, [vscode], "setup"),
).toBeUndefined();
});
});