mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
9c25dc4ed2
* fix(comparisons)!: preserve unknowns across partial observations Share absence and digest semantics, keep unobserved managed bodies unknown, and match unique managed type/name/signatures before structural fallback. BREAKING CHANGE: Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback. * fix(javascript): preserve projected result binding semantics Share syntax-aware property reads and assigned-result resolution. Retain destructuring paths and downgrade downstream whole-result relationships instead of resolving nested or wrapped calls as aliases. * fix(native)!: admit complete parser values and preserve typed failures Decode Mach headers by named columns, validate complete slice keys and lipo alignment, and preserve codesign runtime flags and tagged parser errors. Rename the Ghidra test dossier fixture and connect fake-backed suites to real verification lanes. BREAKING CHANGE: Native load-command results no longer contain the unused, always-null file_offset field. * fix(process): preserve host identity and actionable boundary failures Thread selected platform and environment through process and Doctor seams, stamp observed producer identity in Evidence, and retain PTY, filesystem, lease and lazy-provider failure causes. Drain cancelled lazy loads and omit exited descendants from verified lineage. * fix(browser): preserve cleanup and document-transition evidence Make V8 and Playwright teardown cancellation-aware, expose primary failures alongside incomplete cleanup, share frame-tree and navigation rules, and clear stale WebMCP registrations without pruning unrelated child frames. * fix(adapters): align managed targets and typed CLI outcomes Inspect managed PE/CLI targets independently of native provider selection, reuse successful managed selections, and keep evidence bound to the actual inspected target. Classify canonical CLI error envelopes and exhaustive setup/update/uninstall outcomes, audit registered CLI coverage, and assert platform-specific runtime results. * docs: refresh boundary contracts and shipped roadmap behavior
489 lines
15 KiB
TypeScript
489 lines
15 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
|
|
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
|
|
import { runDoctor } from "./Doctor.js";
|
|
|
|
describe("doctor", () => {
|
|
it("returns exact recovery for every failed diagnostic", async () => {
|
|
const result = await runDoctor(
|
|
"/missing/app",
|
|
host({
|
|
nodeVersion: "20.0.0",
|
|
macosVersion: () => Promise.resolve("11.7"),
|
|
executable: () => Promise.resolve(false),
|
|
validTarget: () => Promise.resolve(false),
|
|
}),
|
|
);
|
|
expect(
|
|
Object.fromEntries(
|
|
result.checks.map(({ name, remediation }) => [name, remediation]),
|
|
),
|
|
).toEqual({
|
|
node: "Install Node.js 22.19+ or 24.11+.",
|
|
host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.",
|
|
hopper: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
|
|
target: "Supply a readable local app or program path.",
|
|
});
|
|
});
|
|
|
|
it.each(["11.7", undefined])(
|
|
"rejects unsupported macOS version %s",
|
|
async (version) => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({ macosVersion: () => Promise.resolve(version) }),
|
|
);
|
|
expect(result.checks).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "host",
|
|
ok: false,
|
|
classification: "unsupported_host",
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
it("admits only the experimental Windows x64 host boundary", async () => {
|
|
const windows = await runDoctor(
|
|
undefined,
|
|
host({
|
|
platform: "win32",
|
|
architecture: "x64",
|
|
macosVersion: () => Promise.resolve(undefined),
|
|
executable: () => Promise.resolve(false),
|
|
}),
|
|
);
|
|
const arm = await runDoctor(
|
|
undefined,
|
|
host({
|
|
platform: "win32",
|
|
architecture: "arm64",
|
|
macosVersion: () => Promise.resolve(undefined),
|
|
executable: () => Promise.resolve(false),
|
|
}),
|
|
);
|
|
|
|
expect(windows.checks.find(({ name }) => name === "host")).toMatchObject({
|
|
ok: true,
|
|
detail: "win32 x64",
|
|
});
|
|
expect(arm.checks.find(({ name }) => name === "host")).toMatchObject({
|
|
ok: false,
|
|
detail: "win32 arm64",
|
|
});
|
|
});
|
|
it("detects a manual configured Hopper before Homebrew", async () => {
|
|
let brewProbes = 0;
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
configuredHopperPath: "/manual/Hopper",
|
|
executable: (path) => Promise.resolve(path === "/manual/Hopper"),
|
|
brewHopperPath: () => {
|
|
brewProbes += 1;
|
|
return Promise.resolve(undefined);
|
|
},
|
|
}),
|
|
);
|
|
expect(result.hopperPath).toBe("/manual/Hopper");
|
|
expect(result.healthy).toBe(true);
|
|
expect(brewProbes).toBe(0);
|
|
});
|
|
|
|
it("reports optional BYO ilspycmd diagnostics only when configured", async () => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
configuredIlspyCmdPath: "/tools/ilspycmd",
|
|
ilspyCmdVersion: (path) =>
|
|
Promise.resolve(
|
|
path === "/tools/ilspycmd" ? "ilspycmd: 9.1.0.7988" : undefined,
|
|
),
|
|
}),
|
|
);
|
|
expect(result.healthy).toBe(true);
|
|
expect(result.checks).toContainEqual({
|
|
name: "ilspycmd",
|
|
ok: true,
|
|
classification: "healthy",
|
|
detail: "/tools/ilspycmd (ilspycmd: 9.1.0.7988)",
|
|
});
|
|
});
|
|
|
|
it("reports configured ilspycmd drift without installing it", async () => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
configuredIlspyCmdPath: "/missing/ilspycmd",
|
|
ilspyCmdVersion: () => Promise.resolve(undefined),
|
|
}),
|
|
);
|
|
expect(result.healthy).toBe(false);
|
|
expect(result.checks).toContainEqual({
|
|
name: "ilspycmd",
|
|
ok: false,
|
|
classification: "config_drift",
|
|
detail: "/missing/ilspycmd",
|
|
remediation:
|
|
"Unset REA_ILSPY_CMD_PATH or point it at a runnable ilspycmd executable.",
|
|
});
|
|
});
|
|
|
|
it("does not fall back when the configured Hopper launcher is invalid", async () => {
|
|
const configuredPath = "/invalid/custom/hopper";
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({ configuredHopperPath: configuredPath }),
|
|
);
|
|
|
|
expect(result.hopperPath).toBeUndefined();
|
|
expect(result.healthy).toBe(false);
|
|
expect(result.checks.find(({ name }) => name === "hopper")).toEqual({
|
|
name: "hopper",
|
|
ok: false,
|
|
classification: "config_drift",
|
|
detail: configuredPath,
|
|
remediation:
|
|
"Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("doctor installation identity", () => {
|
|
it("distinguishes stale skills, path shadowing, and unobservable live state", async () => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
installationPaths: () =>
|
|
Promise.resolve(["/usr/local/bin/rea", "/home/user/bin/rea"]),
|
|
installedSkillIdentity: () =>
|
|
Promise.resolve({
|
|
version: "10",
|
|
toolCount: null,
|
|
catalogDigest: null,
|
|
}),
|
|
}),
|
|
);
|
|
|
|
expect(result.identity).toMatchObject({
|
|
live_server: { state: "unknown" },
|
|
installations: { state: "multiple" },
|
|
skill: {
|
|
installed_version: "10",
|
|
state: "stale",
|
|
remediation: "Run rea setup to update the installed REA skill.",
|
|
},
|
|
});
|
|
});
|
|
it("reports a missing installed skill as unhealthy", async () => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({ installedSkillIdentity: () => Promise.resolve(undefined) }),
|
|
);
|
|
|
|
expect(result.healthy).toBe(false);
|
|
expect(result.identity?.skill).toMatchObject({
|
|
state: "missing",
|
|
remediation: "Run rea setup to update the installed REA skill.",
|
|
});
|
|
expect(result.checks).toContainEqual({
|
|
name: "skill:identity",
|
|
ok: false,
|
|
classification: "config_drift",
|
|
detail: "Installed REA skill identity is missing.",
|
|
remediation: "Run rea setup to update the installed REA skill.",
|
|
});
|
|
});
|
|
it("reports stale client registration paths without claiming live state", async () => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
clientRegistrations: () =>
|
|
Promise.resolve([
|
|
{
|
|
client: "codex",
|
|
config_path: "/home/user/.codex/config.toml",
|
|
command: ["/old/rea", "mcp"],
|
|
state: "stale",
|
|
remediation:
|
|
"Run rea setup to refresh this registration, then restart the client.",
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
|
|
expect(result.healthy).toBe(false);
|
|
expect(result.identity).toMatchObject({
|
|
live_server: { state: "unknown" },
|
|
registrations: [{ client: "codex", state: "stale" }],
|
|
});
|
|
expect(result.checks).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "registration:codex",
|
|
classification: "config_drift",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("doctor scoped readiness", () => {
|
|
it("scopes effective readiness to selected clients without hiding drift", async () => {
|
|
const registrations: readonly ClientRegistrationStatus[] = [
|
|
{
|
|
client: "codex",
|
|
config_path: "/home/user/.codex/config.toml",
|
|
command: ["/current/rea", "mcp"],
|
|
state: "aligned",
|
|
remediation: null,
|
|
},
|
|
{
|
|
client: "cursor",
|
|
config_path: "/home/user/.cursor/mcp.json",
|
|
command: ["/old/rea", "mcp"],
|
|
state: "stale",
|
|
remediation: "Run rea setup to refresh this registration.",
|
|
},
|
|
];
|
|
const scoped = await runDoctor(
|
|
undefined,
|
|
host({ clientRegistrations: () => Promise.resolve(registrations) }),
|
|
{ clients: ["codex"] },
|
|
);
|
|
|
|
expect(scoped).toMatchObject({
|
|
healthy: true,
|
|
environment_healthy: false,
|
|
scope: { mode: "explicit", clients: ["codex"] },
|
|
});
|
|
expect(scoped.scope_checks).toContainEqual(
|
|
expect.objectContaining({ name: "registration:codex", ok: true }),
|
|
);
|
|
expect(scoped.informational_checks).toContainEqual(
|
|
expect.objectContaining({ name: "registration:cursor", ok: false }),
|
|
);
|
|
|
|
const staleSelected = await runDoctor(
|
|
undefined,
|
|
host({ clientRegistrations: () => Promise.resolve(registrations) }),
|
|
{ clients: ["cursor"] },
|
|
);
|
|
expect(staleSelected.healthy).toBe(false);
|
|
});
|
|
|
|
it("keeps agent readiness independent of Hopper host requirements", async () => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
platform: "linux",
|
|
linuxDistribution: () =>
|
|
Promise.resolve({
|
|
id: "debian",
|
|
versionId: "13",
|
|
packageFamily: "deb",
|
|
supported: false,
|
|
}),
|
|
clientRegistrations: () =>
|
|
Promise.resolve([
|
|
{
|
|
client: "codex",
|
|
config_path: "/home/user/.codex/config.toml",
|
|
command: ["rea", "mcp"],
|
|
state: "aligned",
|
|
remediation: null,
|
|
},
|
|
]),
|
|
}),
|
|
{ clients: ["codex"] },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
healthy: true,
|
|
environment_healthy: false,
|
|
scope: { mode: "explicit", clients: ["codex"], providers: [] },
|
|
});
|
|
expect(result.scope_checks.map(({ name }) => name)).toEqual([
|
|
"node",
|
|
"registration:codex",
|
|
]);
|
|
expect(result.informational_checks).toContainEqual(
|
|
expect.objectContaining({ name: "host", ok: false }),
|
|
);
|
|
});
|
|
|
|
it("requires explicitly selected providers instead of any available provider", async () => {
|
|
const ghidra = {
|
|
id: "ghidra",
|
|
configured: true,
|
|
available: true,
|
|
providerVersion: "12.1.4",
|
|
registrationEnvironment: { GHIDRA_INSTALL_DIR: "/tools/ghidra" },
|
|
checks: [
|
|
{
|
|
name: "configuration",
|
|
ok: true,
|
|
code: null,
|
|
detail: "/tools/ghidra",
|
|
remediation: null,
|
|
classification: "config_drift" as const,
|
|
},
|
|
],
|
|
};
|
|
const noHopper = host({
|
|
executable: () => Promise.resolve(false),
|
|
providerInspections: () => Promise.resolve([ghidra]),
|
|
});
|
|
|
|
const hopper = await runDoctor(undefined, noHopper, {
|
|
providers: ["hopper"],
|
|
});
|
|
expect(hopper).toMatchObject({
|
|
healthy: false,
|
|
environment_healthy: true,
|
|
});
|
|
|
|
const selectedGhidra = await runDoctor(undefined, noHopper, {
|
|
providers: ["ghidra"],
|
|
});
|
|
expect(selectedGhidra.healthy).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("doctor Linux and Hopper discovery", () => {
|
|
it("accepts an officially supported Linux distribution", async () => {
|
|
const path = "/home/user/.local/share/rea/hopper/bin/Hopper";
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
platform: "linux",
|
|
macosVersion: () => Promise.resolve(undefined),
|
|
linuxDistribution: () =>
|
|
Promise.resolve({
|
|
id: "ubuntu",
|
|
versionId: "24.04",
|
|
packageFamily: "deb",
|
|
supported: true,
|
|
}),
|
|
configuredHopperPath: path,
|
|
executable: (candidate) => Promise.resolve(candidate === path),
|
|
}),
|
|
);
|
|
expect(result.healthy).toBe(true);
|
|
expect(result.hopperPath).toBe(path);
|
|
});
|
|
it("reports missing Linux demo-session dependencies", async () => {
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
platform: "linux",
|
|
macosVersion: () => Promise.resolve(undefined),
|
|
linuxDistribution: () =>
|
|
Promise.resolve({
|
|
id: "ubuntu",
|
|
versionId: "24.04",
|
|
packageFamily: "deb",
|
|
supported: true,
|
|
}),
|
|
linuxDemoRuntimeCheck: () =>
|
|
Promise.resolve({
|
|
name: "hopper-demo-runtime",
|
|
ok: false,
|
|
classification: "missing_dependency",
|
|
remediation: "Install Xvfb, xauth, Python 3, libX11, and libXtst.",
|
|
}),
|
|
}),
|
|
);
|
|
expect(result.healthy).toBe(false);
|
|
expect(
|
|
result.checks.find(({ name }) => name === "hopper-demo-runtime"),
|
|
).toMatchObject({
|
|
ok: false,
|
|
classification: "missing_dependency",
|
|
remediation: expect.stringContaining("xauth"),
|
|
});
|
|
});
|
|
it("explains how to recover from an unsupported configured launcher", async () => {
|
|
const path = "/custom/Hopper";
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
platform: "linux",
|
|
configuredHopperPath: path,
|
|
linuxDistribution: () =>
|
|
Promise.resolve({
|
|
id: "ubuntu",
|
|
versionId: "24.04",
|
|
packageFamily: "deb",
|
|
supported: true,
|
|
}),
|
|
executable: (candidate) => Promise.resolve(candidate === path),
|
|
supportedLinuxHopper: () => Promise.resolve(false),
|
|
}),
|
|
);
|
|
expect(
|
|
result.checks.find(({ name }) => name === "hopper-version")?.remediation,
|
|
).toContain("Unset or update HOPPER_LAUNCHER_PATH");
|
|
});
|
|
it("detects a Homebrew cask installed outside /Applications", async () => {
|
|
const path =
|
|
"/opt/homebrew/Caskroom/hopper/Hopper Disassembler.app/Contents/MacOS/hopper";
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
executable: (candidate) => Promise.resolve(candidate === path),
|
|
brewHopperPath: () => Promise.resolve(path),
|
|
}),
|
|
);
|
|
expect(result.hopperPath).toBe(path);
|
|
});
|
|
|
|
it("detects a manually installed Hopper application", async () => {
|
|
const path = "/Applications/Hopper v6.app/Contents/MacOS/hopper";
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
executable: (candidate) => Promise.resolve(candidate === path),
|
|
manualHopperPaths: () => Promise.resolve([path]),
|
|
}),
|
|
);
|
|
expect(result.hopperPath).toBe(path);
|
|
});
|
|
|
|
it("uses shared app target validation when a target is supplied", async () => {
|
|
const result = await runDoctor(
|
|
"/Applications/Notes.app",
|
|
host({
|
|
validTarget: (path) =>
|
|
Promise.resolve(path === "/Applications/Notes.app"),
|
|
}),
|
|
);
|
|
expect(result.checks).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "target",
|
|
ok: true,
|
|
classification: "healthy",
|
|
detail: "/Applications/Notes.app",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects a readable Hopper launcher without execute permission", async () => {
|
|
const path = "/manual/Hopper";
|
|
const result = await runDoctor(
|
|
undefined,
|
|
host({
|
|
configuredHopperPath: path,
|
|
validTarget: () => Promise.resolve(true),
|
|
executable: () => Promise.resolve(false),
|
|
}),
|
|
);
|
|
expect(result.hopperPath).toBeUndefined();
|
|
expect(result.checks).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "hopper",
|
|
ok: false,
|
|
classification: "config_drift",
|
|
}),
|
|
);
|
|
});
|
|
});
|