mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 03:45:39 +00:00
Use one command owner for setup, client writers, Gemini policy and CLI defaults. Migrate recognized runner registrations with backups and remove runner prewarm. Retain unknown identities and exercise real offline npm, CLI and MCP workflows. Fixes #7757 BREAKING CHANGE: managed MCP registrations use the selected absolute runtime and REA entry script, including package-runner cache paths. Keep that installation available or restore it before setup; unknown registrations require confirmed manual repair. Setup no longer returns prewarm or emits its progress events. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
273 lines
9.5 KiB
TypeScript
273 lines
9.5 KiB
TypeScript
import {
|
|
access,
|
|
mkdir,
|
|
readFile,
|
|
realpath,
|
|
rm,
|
|
symlink,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
|
|
import { currentClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
|
|
import {
|
|
classifyClientRegistrationCommand,
|
|
registrationRuntimePath,
|
|
} from "../../../src/application/ClientRegistrationIdentity.js";
|
|
import { readClientRegistrationInspections } from "../../../src/application/ClientRegistrationStatus.js";
|
|
import { javaScriptRuntime } from "../../../src/domain/runtimeVersion.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"registers a same-file PATH launcher whose manager-owned alias survives a version switch",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-stable-launcher-alias-");
|
|
const installs = join(root, "installs");
|
|
const old = join(installs, "old");
|
|
const next = join(installs, "next");
|
|
const scriptWithin = (tag: string) =>
|
|
join("node_modules", ".mise", `rea-agents@${tag}`, "scripts", "rea.mjs");
|
|
for (const [version, tag] of [
|
|
[old, "old"],
|
|
[next, "next"],
|
|
] as const) {
|
|
await mkdir(join(version, "bin"), { recursive: true });
|
|
const script = join(version, scriptWithin(tag));
|
|
await mkdir(dirname(script), { recursive: true });
|
|
await writeFile(script, "#!/usr/bin/env node\n", { mode: 0o700 });
|
|
await symlink(script, join(version, "bin", "rea"));
|
|
}
|
|
const latest = join(installs, "latest");
|
|
await symlink(old, latest, "dir");
|
|
const unstable = join(latest, scriptWithin("old"));
|
|
const alias = join(latest, "bin", "rea");
|
|
const command = currentClientRegistrationCommand({
|
|
platform: "linux",
|
|
entrypointPath: unstable,
|
|
environment: { PATH: join(latest, "bin") },
|
|
});
|
|
expect(command).toEqual([process.execPath, alias, "mcp"]);
|
|
expect(classifyClientRegistrationCommand(command, unstable)).toBe("owned");
|
|
await rm(latest);
|
|
await symlink(next, latest, "dir");
|
|
await rm(old, { recursive: true });
|
|
await expect(access(unstable)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await access(command[1]!);
|
|
expect(await realpath(command[1]!)).toBe(join(next, scriptWithin("next")));
|
|
expect(
|
|
classifyClientRegistrationCommand(
|
|
command,
|
|
join(next, scriptWithin("next")),
|
|
),
|
|
).toBe("owned");
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32").each(["node", "bun"] as const)(
|
|
"preserves a same-file %s runtime alias across a manager version switch",
|
|
async (runtime) => {
|
|
const root = await createTestTempDirectory("rea-stable-runtime-alias-");
|
|
const old = join(root, "old");
|
|
const next = join(root, "next");
|
|
for (const [directory, contents] of [
|
|
[old, "old runtime"],
|
|
[next, "next runtime"],
|
|
] as const) {
|
|
await mkdir(directory);
|
|
await writeFile(join(directory, runtime), contents, { mode: 0o700 });
|
|
}
|
|
const latest = join(root, "latest");
|
|
await symlink(old, latest, "dir");
|
|
const executable = join(old, runtime);
|
|
const alias = join(latest, runtime);
|
|
const selected = registrationRuntimePath(
|
|
executable,
|
|
runtime,
|
|
{ PATH: latest },
|
|
"linux",
|
|
);
|
|
expect(selected).toBe(alias);
|
|
expect(await realpath(selected)).toBe(executable);
|
|
|
|
await rm(latest);
|
|
await symlink(next, latest, "dir");
|
|
await rm(old, { recursive: true });
|
|
await expect(access(executable)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await access(selected);
|
|
expect(await realpath(selected)).toBe(join(next, runtime));
|
|
expect(
|
|
registrationRuntimePath(
|
|
join(next, runtime),
|
|
runtime,
|
|
{ PATH: latest },
|
|
"linux",
|
|
),
|
|
).toBe(selected);
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"does not replace the selected runtime with a foreign executable or manager shim",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-runtime-alias-identity-");
|
|
const foreign = join(root, "foreign");
|
|
const missing = join(root, "missing");
|
|
const valid = join(root, "valid");
|
|
const runtime = javaScriptRuntime(process.versions).id;
|
|
const script = join(root, "rea.mjs");
|
|
await writeFile(script, "// selected REA script\n");
|
|
for (const directory of [foreign, missing, valid]) await mkdir(directory);
|
|
await writeFile(join(foreign, runtime), '#!/bin/sh\nexec node "$@"\n', {
|
|
mode: 0o700,
|
|
});
|
|
await symlink(join(root, "absent"), join(missing, runtime));
|
|
await symlink(process.execPath, join(valid, runtime));
|
|
expect(
|
|
registrationRuntimePath(
|
|
process.execPath,
|
|
runtime,
|
|
{ PATH: `${foreign}:${missing}:relative` },
|
|
"linux",
|
|
),
|
|
).toBe(process.execPath);
|
|
expect(
|
|
registrationRuntimePath(
|
|
process.execPath,
|
|
runtime,
|
|
{ PATH: `${foreign}:${valid}` },
|
|
"linux",
|
|
),
|
|
).toBe(join(valid, runtime));
|
|
expect(
|
|
currentClientRegistrationCommand({
|
|
platform: "linux",
|
|
entrypointPath: script,
|
|
environment: { PATH: valid },
|
|
}),
|
|
).toEqual([join(valid, runtime), script, "mcp"]);
|
|
expect(
|
|
registrationRuntimePath(
|
|
process.execPath,
|
|
runtime,
|
|
{ PATH: valid },
|
|
"win32",
|
|
),
|
|
).toBe(process.execPath);
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"checks the native PATH alias instead of collapsing a symlink parent segment",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-runtime-path-parent-");
|
|
const runtime = javaScriptRuntime(process.versions).id;
|
|
await mkdir(join(root, "selected"));
|
|
await mkdir(join(root, "other", "child"), { recursive: true });
|
|
await mkdir(join(root, "other", "selected"));
|
|
await symlink(join(root, "other", "child"), join(root, "link"));
|
|
await symlink(process.execPath, join(root, "selected", runtime));
|
|
await writeFile(
|
|
join(root, "other", "selected", runtime),
|
|
"foreign runtime\n",
|
|
{ mode: 0o700 },
|
|
);
|
|
expect(
|
|
registrationRuntimePath(
|
|
process.execPath,
|
|
runtime,
|
|
{
|
|
PATH: `${root}/link/../selected`,
|
|
},
|
|
process.platform,
|
|
),
|
|
).toBe(process.execPath);
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"retains unknown runtime identity after its executable disappears despite a known REA script",
|
|
async () => {
|
|
const home = await createTestTempDirectory("rea-removed-runtime-");
|
|
const executable = join(home, "node");
|
|
const script = join(home, "rea.mjs");
|
|
const config = join(home, ".cursor", "mcp.json");
|
|
await symlink(process.execPath, executable);
|
|
await writeFile(script, "// selected REA script\n");
|
|
await mkdir(join(home, ".cursor"));
|
|
const command = [executable, script, "mcp"];
|
|
const original = JSON.stringify({
|
|
mcpServers: {
|
|
rea: { command: executable, args: [script, "mcp"] },
|
|
},
|
|
});
|
|
await writeFile(config, original);
|
|
const inspect = () =>
|
|
readClientRegistrationInspections(home, script, {
|
|
environment: {},
|
|
selectedClientIds: ["cursor"],
|
|
});
|
|
expect(await inspect()).toContainEqual(
|
|
expect.objectContaining({
|
|
commandIdentity: "owned",
|
|
status: expect.objectContaining({ client: "cursor", state: "aligned" }),
|
|
}),
|
|
);
|
|
await rm(executable);
|
|
expect(classifyClientRegistrationCommand(command, script)).toBe("unknown");
|
|
expect(await inspect()).toContainEqual(
|
|
expect.objectContaining({
|
|
commandIdentity: "unknown",
|
|
status: expect.objectContaining({
|
|
client: "cursor",
|
|
state: "unknown",
|
|
command,
|
|
remediation: expect.any(String),
|
|
}),
|
|
}),
|
|
);
|
|
expect(await readFile(config, "utf8")).toBe(original);
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"keeps selected entrypoint when PATH aliases are foreign, unresolved, unexecutable or relative",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-unproven-launcher-alias-");
|
|
const entrypoint = join(root, "selected.mjs");
|
|
await writeFile(entrypoint, "selected", { mode: 0o700 });
|
|
const foreign = join(root, "foreign");
|
|
const missing = join(root, "missing");
|
|
const unexecutable = join(root, "unexecutable");
|
|
for (const path of [foreign, missing, unexecutable]) await mkdir(path);
|
|
await writeFile(join(foreign, "rea"), "different program", { mode: 0o700 });
|
|
await symlink(join(root, "absent"), join(missing, "rea"));
|
|
// Same identity cannot be reached via an executable launcher when the selected
|
|
// file itself has no executable bit; no shell wrapper is inferred or run.
|
|
const selected = join(root, "unexecutable.mjs");
|
|
await writeFile(selected, "selected", { mode: 0o600 });
|
|
await symlink(selected, join(unexecutable, "rea"));
|
|
expect(
|
|
currentClientRegistrationCommand({
|
|
platform: "linux",
|
|
entrypointPath: entrypoint,
|
|
environment: { PATH: `${foreign}:${missing}:relative` },
|
|
}),
|
|
).toEqual([process.execPath, entrypoint, "mcp"]);
|
|
expect(
|
|
currentClientRegistrationCommand({
|
|
platform: "linux",
|
|
entrypointPath: selected,
|
|
environment: { PATH: unexecutable },
|
|
}),
|
|
).toEqual([process.execPath, selected, "mcp"]);
|
|
expect(
|
|
currentClientRegistrationCommand({
|
|
platform: "win32",
|
|
entrypointPath: entrypoint,
|
|
environment: { PATH: foreign },
|
|
}),
|
|
).toEqual([process.execPath, entrypoint, "mcp"]);
|
|
},
|
|
);
|