mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 22:06:10 +00:00
* test: strengthen suite guards and regression oracles * test: share JSON client registration contract * test: reuse fixtures and reduce repeated setup * fix(process): preserve ambiguous Darwin token ownership * test(process): bound child readiness waits
261 lines
9.5 KiB
TypeScript
261 lines
9.5 KiB
TypeScript
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { parse } from "yaml";
|
|
import { expect, onTestFinished } from "vitest";
|
|
|
|
import { currentClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
|
|
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
|
|
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
|
|
import { supportedClients } from "../../../src/application/SupportedClients.js";
|
|
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
|
|
|
|
const cliLifecycle = (cli: TestCli, home: string, profile: string) => {
|
|
const client = supportedClients(home, process.platform, {
|
|
HERMES_HOME: profile,
|
|
}).find(({ name }) => name === "hermes");
|
|
if (client?.configPath !== join(profile, "config.yaml"))
|
|
throw new Error("Hermes profile did not resolve to the test config");
|
|
const run = (arguments_: readonly string[]) =>
|
|
cli.run({
|
|
arguments: arguments_,
|
|
cwd: home,
|
|
environment: { HOME: home, USERPROFILE: home, HERMES_HOME: profile },
|
|
timeoutMs: 20_000,
|
|
});
|
|
const setup = [
|
|
"setup",
|
|
"--client",
|
|
"hermes",
|
|
"--skill=false",
|
|
"--yes",
|
|
"--json",
|
|
];
|
|
const remove = async () => {
|
|
const result = await run(["uninstall", "--json"]);
|
|
expect(result.exitCode, JSON.stringify(result)).toBe(0);
|
|
};
|
|
return {
|
|
configure: async () => {
|
|
const result = await run(setup);
|
|
expect(result.exitCode, JSON.stringify(result)).toBe(0);
|
|
expect(result.json).toMatchObject({ status: "ready" });
|
|
},
|
|
verifyAndRepeat: async () => {
|
|
const doctor = await run(["doctor", "--client", "hermes", "--json"]);
|
|
expect(doctor.exitCode, JSON.stringify(doctor)).toBe(0);
|
|
expect(doctor.json).toMatchObject({
|
|
healthy: true,
|
|
identity: { registrations: [{ client: "hermes", state: "aligned" }] },
|
|
});
|
|
const repeated = await run(setup);
|
|
expect(repeated.exitCode, JSON.stringify(repeated)).toBe(0);
|
|
expect(repeated.json).toMatchObject({
|
|
plannedActions: [],
|
|
appliedActions: [],
|
|
});
|
|
},
|
|
remove,
|
|
removeAgain: remove,
|
|
};
|
|
};
|
|
|
|
const configurationLifecycle = (
|
|
home: string,
|
|
profile: string,
|
|
config: string,
|
|
command: readonly string[],
|
|
) => {
|
|
const environment = { HERMES_HOME: profile };
|
|
const client = supportedClients(home, process.platform, environment).find(
|
|
({ name }) => name === "hermes",
|
|
);
|
|
if (client === undefined || client.configPath !== config)
|
|
throw new Error("Hermes profile did not resolve to the test config");
|
|
const uninstallHost = systemUninstallHost(
|
|
home,
|
|
undefined,
|
|
environment,
|
|
process.platform,
|
|
);
|
|
onTestFinished(async () => {
|
|
expect(await uninstallHost.close?.()).toBeUndefined();
|
|
});
|
|
return {
|
|
configure: async () => {
|
|
expect(
|
|
await configureClientConfiguration(client, {}, command),
|
|
).toMatchObject({ status: "configured" });
|
|
},
|
|
verifyAndRepeat: async () => {
|
|
expect(
|
|
(
|
|
await readClientRegistrationStatuses(home, undefined, {
|
|
platform: process.platform,
|
|
environment,
|
|
selectedClientIds: ["hermes"],
|
|
})
|
|
).find(({ client: name }) => name === "hermes"),
|
|
).toMatchObject({ state: "aligned", config_path: config });
|
|
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
|
status: "unchanged",
|
|
});
|
|
},
|
|
remove: async () => {
|
|
expect((await uninstallHost.removeClient(client)).status).toBe("removed");
|
|
},
|
|
removeAgain: async () => {
|
|
expect((await uninstallHost.removeClient(client)).status).toBe("skipped");
|
|
},
|
|
};
|
|
};
|
|
|
|
type AnchorStyle = "entry" | "child" | "shadowed" | "key" | "external";
|
|
|
|
const anchoredConfiguration = (
|
|
version: "1.1" | "1.2",
|
|
style: AnchorStyle,
|
|
server: {
|
|
command: string;
|
|
args: string[];
|
|
env: { KEEP: string };
|
|
enabled: boolean;
|
|
},
|
|
) => {
|
|
const directive = version === "1.2" ? "%YAML 1.2\n---\n" : "";
|
|
const hasDefaults = style === "shadowed" || style === "external";
|
|
const entry =
|
|
` command: ${JSON.stringify(server.command)}\n` +
|
|
` args: ${JSON.stringify(server.args)}\n` +
|
|
" enabled: false\n" +
|
|
(hasDefaults
|
|
? " env: *settings # Keep nested reference comment\n"
|
|
: ` env: ${style === "child" ? "&settings " : ""}{KEEP: 'off'}\n`);
|
|
const original =
|
|
directive +
|
|
"# Keep this user setting\nmodel: custom\n" +
|
|
(hasDefaults ? "defaults: &settings {KEEP: 'off'}\n" : "") +
|
|
`mcp_servers:\n ${style === "key" ? "&server_name " : ""}rea:${style === "child" || style === "key" ? "" : " &server"}${style === "entry" ? " # Keep anchor definition comment" : ""}\n` +
|
|
entry +
|
|
(style === "child"
|
|
? " other:\n command: other-server\n env: *settings # Shared environment\n"
|
|
: style === "key"
|
|
? " other: {command: other-server}\nobserver: *server_name # Registration name\n"
|
|
: " other: *server # Independent server\n") +
|
|
(style === "shadowed"
|
|
? "later: &settings {KEEP: later}\nobserver: *server\n"
|
|
: "");
|
|
const other =
|
|
style === "child"
|
|
? { command: "other-server", env: { KEEP: "off" } }
|
|
: style === "key"
|
|
? { command: "other-server" }
|
|
: server;
|
|
return { original, other, hasDefaults };
|
|
};
|
|
|
|
for (const version of ["1.1", "1.2"] as const) {
|
|
for (const style of [
|
|
"entry",
|
|
"child",
|
|
"shadowed",
|
|
"key",
|
|
"external",
|
|
] as const) {
|
|
const publicCli = style === "entry" || style === "shadowed";
|
|
cliTest(
|
|
`Hermes ${version} preserves ${style} anchor consumers through ${publicCli ? "CLI" : "configuration boundary"} setup and uninstall`,
|
|
async ({ cli }) => {
|
|
// Exercise both YAML versions through the CLI, including the shadowed
|
|
// anchor case; the other styles retain the full registration lifecycle
|
|
// at the production configuration and uninstall boundary.
|
|
const home = await createTestTempDirectory(
|
|
"rea-hermes-anchored-entry-",
|
|
);
|
|
const profile = join(home, "hermes");
|
|
const config = join(profile, "config.yaml");
|
|
const command = publicCli
|
|
? [process.execPath, resolve("scripts/rea.mjs"), "mcp"]
|
|
: currentClientRegistrationCommand({ platform: process.platform });
|
|
if (command[0] === undefined) throw new Error("Missing REA entrypoint");
|
|
const server = {
|
|
command: command[0],
|
|
args: command.slice(1),
|
|
env: { KEEP: "off" },
|
|
enabled: false,
|
|
};
|
|
const { original, other, hasDefaults } = anchoredConfiguration(
|
|
version,
|
|
style,
|
|
server,
|
|
);
|
|
await mkdir(profile);
|
|
await writeFile(config, original);
|
|
const lifecycle = publicCli
|
|
? cliLifecycle(cli, home, profile)
|
|
: configurationLifecycle(home, profile, config, command);
|
|
await lifecycle.configure();
|
|
const configured = await readFile(config, "utf8");
|
|
const configuredValues = parse(configured, { version });
|
|
expect(configuredValues).toMatchObject({
|
|
model: "custom",
|
|
mcp_servers: { rea: { enabled: true, env: { KEEP: "off" } }, other },
|
|
...(style === "shadowed"
|
|
? { observer: server, later: { KEEP: "later" } }
|
|
: {}),
|
|
...(style === "key" ? { observer: "rea" } : {}),
|
|
});
|
|
if (hasDefaults)
|
|
expect(configuredValues.mcp_servers.other.env).toBe(
|
|
configuredValues.defaults,
|
|
);
|
|
expect(configured).toContain("# Keep this user setting");
|
|
if (style === "entry")
|
|
expect(configured).toContain("# Keep anchor definition comment");
|
|
expect(configured).toContain(
|
|
style === "child"
|
|
? "# Shared environment"
|
|
: style === "key"
|
|
? "# Registration name"
|
|
: "# Independent server",
|
|
);
|
|
if (hasDefaults)
|
|
expect(configured).toContain("# Keep nested reference comment");
|
|
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
|
|
await lifecycle.verifyAndRepeat();
|
|
expect(await readFile(config, "utf8")).toBe(configured);
|
|
await lifecycle.remove();
|
|
const removedValues = parse(await readFile(config, "utf8"), {
|
|
version,
|
|
});
|
|
expect(removedValues).toEqual({
|
|
model: "custom",
|
|
mcp_servers: { other },
|
|
...(hasDefaults ? { defaults: { KEEP: "off" } } : {}),
|
|
...(style === "shadowed"
|
|
? {
|
|
later: { KEEP: "later" },
|
|
observer: server,
|
|
}
|
|
: {}),
|
|
...(style === "key" ? { observer: "rea" } : {}),
|
|
});
|
|
if (hasDefaults)
|
|
expect(removedValues.mcp_servers.other.env).toBe(
|
|
removedValues.defaults,
|
|
);
|
|
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
|
|
const remaining = await readFile(config, "utf8");
|
|
if (style === "entry")
|
|
expect(remaining).toContain("# Keep anchor definition comment");
|
|
if (hasDefaults)
|
|
expect(remaining).toContain("# Keep nested reference comment");
|
|
await lifecycle.removeAgain();
|
|
expect(await readFile(config, "utf8")).toBe(remaining);
|
|
},
|
|
);
|
|
}
|
|
}
|