mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
530 lines
16 KiB
TypeScript
530 lines
16 KiB
TypeScript
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { spawn } from "node:child_process";
|
|
import { rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { closeVerifierResources } from "../../../scripts/lib/verifier-cleanup.mjs";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { createTestCli, type CliResult } from "../../support/cli/cliFixture.js";
|
|
import { createTestProcessRegistry } from "../../support/process/processFixture.js";
|
|
|
|
import {
|
|
startFakeCdpBrowser,
|
|
type FakeCdpBrowser,
|
|
} from "../../fixtures/fakeCdpBrowser.js";
|
|
import { writeElectronBoundaryFixture } from "../../fixtures/electronBoundaryApplication.js";
|
|
import {
|
|
SOURCE,
|
|
applicationFixture,
|
|
analyzeFixture,
|
|
electronRuntimeEvidence,
|
|
} from "../../fixtures/javascriptRuntimeReconciliation.js";
|
|
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
|
|
|
const cliProcesses = createTestProcessRegistry();
|
|
const cli = createTestCli(cliProcesses);
|
|
const INTEGRATION_TEST_TIMEOUT_MS = 20_000;
|
|
const browsers: FakeCdpBrowser[] = [];
|
|
const temporary: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await closeVerifierResources([
|
|
() => cliProcesses.shutdown(),
|
|
...browsers.map((browser) => async () => {
|
|
await browser.close();
|
|
browsers.splice(browsers.indexOf(browser), 1);
|
|
}),
|
|
...temporary.map((path) => async () => {
|
|
await rm(path, { recursive: true, force: true });
|
|
temporary.splice(temporary.indexOf(path), 1);
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it(
|
|
"returns the same endpoint-scoped Electron Evidence contracts",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-electron-cli-");
|
|
temporary.push(root);
|
|
await writeFile(join(root, "index.html"), "<script src='app.js'></script>");
|
|
await writeFile(join(root, "app.js"), "export const app = true;");
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: `${pathToFileURL(join(root, "index.html")).href}?next=%2Fsettings#%5Csettings`,
|
|
commandEvents: ({ method }) =>
|
|
method === "Debugger.enable"
|
|
? [
|
|
{
|
|
method: "Debugger.scriptParsed",
|
|
sessionId: "session-1",
|
|
params: {
|
|
scriptId: "partial-script",
|
|
url: pathToFileURL(join(root, "app.js")).href,
|
|
executionContextId: 1,
|
|
},
|
|
},
|
|
]
|
|
: undefined,
|
|
});
|
|
browsers.push(browser);
|
|
const environment = { ...process.env };
|
|
const listed = parseEvidence(
|
|
await runCli(
|
|
["list-electron-targets", browser.endpoint, "--json"],
|
|
environment,
|
|
),
|
|
);
|
|
expect(listed).toMatchObject({
|
|
operation: "list_electron_targets",
|
|
environment: {
|
|
id: expect.any(String),
|
|
platform: "unknown",
|
|
architecture: "unknown",
|
|
isolation: "none",
|
|
},
|
|
limitations: expect.arrayContaining([
|
|
"Browser host platform and architecture were not observed.",
|
|
]),
|
|
normalized_result: {
|
|
targets: [{ target_id: "electron-page" }],
|
|
},
|
|
});
|
|
const inspected = await runCli(
|
|
[
|
|
"inspect-electron-page",
|
|
browser.endpoint,
|
|
"electron-page",
|
|
"--observation-ms",
|
|
"0",
|
|
"--json",
|
|
],
|
|
environment,
|
|
);
|
|
expect(inspected).toMatchObject({
|
|
operation: "inspect_electron_page",
|
|
environment: listed.environment,
|
|
provider: { id: "rea-cdp-electron" },
|
|
normalized_result: {
|
|
target: { file_path: join(root, "index.html") },
|
|
scripts: {
|
|
items: [{ cdp_hash: null, length: null, is_module: null }],
|
|
},
|
|
},
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
|
|
it(
|
|
"reports rejected Electron CLI arguments with structured issue details",
|
|
async () => {
|
|
const invalidTargetList = await runCliResult(
|
|
["list-electron-targets", "not a url", "--json"],
|
|
process.env,
|
|
);
|
|
expect(invalidTargetList.signal).toBeNull();
|
|
expect(invalidTargetList.exitCode).toBe(1);
|
|
expect(invalidTargetList.json).toMatchObject({
|
|
error: "Electron analysis failed",
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
details: {
|
|
operation: "list_electron_targets",
|
|
issues: [
|
|
{
|
|
path: ["cdp_endpoint"],
|
|
reason: "invalid_value",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const invalidPageInspect = await runCliResult(
|
|
["inspect-electron-page", "http://127.0.0.1:9222", "", "--json"],
|
|
process.env,
|
|
);
|
|
expect(invalidPageInspect.signal).toBeNull();
|
|
expect(invalidPageInspect.exitCode).toBe(1);
|
|
expect(invalidPageInspect.json).toMatchObject({
|
|
error: "Electron analysis failed",
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
details: {
|
|
operation: "inspect_electron_page",
|
|
issues: [
|
|
{
|
|
path: ["target_id"],
|
|
reason: "out_of_range",
|
|
minimum: 1,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
|
|
// On Windows child.kill("SIGINT") terminates the process without a console signal.
|
|
it.skipIf(process.platform === "win32")(
|
|
"forwards SIGINT through Electron page inspection and completes target cleanup",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-electron-cli-cancel-");
|
|
temporary.push(root);
|
|
const page = join(root, "index.html");
|
|
await writeFile(page, "<main>fixture</main>");
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(page).href,
|
|
});
|
|
browsers.push(browser);
|
|
|
|
const child = spawn(
|
|
process.execPath,
|
|
[
|
|
"scripts/rea.mjs",
|
|
"inspect-electron-page",
|
|
browser.endpoint,
|
|
"electron-page",
|
|
"--observation-ms",
|
|
"60000",
|
|
"--json",
|
|
],
|
|
{
|
|
cwd: process.cwd(),
|
|
env: { ...process.env },
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
);
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
const closed = new Promise<{
|
|
readonly code: number | null;
|
|
readonly signal: NodeJS.Signals | null;
|
|
}>((resolve, reject) => {
|
|
child.once("error", reject);
|
|
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
});
|
|
const hardStop = setTimeout(() => child.kill("SIGKILL"), 15_000);
|
|
try {
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(
|
|
browser.commands.some(({ method }) => method === "Runtime.enable"),
|
|
).toBe(true);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
child.kill("SIGINT");
|
|
|
|
const result = await closed;
|
|
expect(result, stderr).toEqual({ code: 130, signal: null });
|
|
expect(JSON.parse(stdout)).toMatchObject({
|
|
code: "cancelled",
|
|
category: "cancelled",
|
|
details: { operation: "inspect_electron_page" },
|
|
});
|
|
const methods = browser.commands.map(({ method }) => method);
|
|
expect(methods).toContain("Target.detachFromTarget");
|
|
expect(methods).not.toContain("Target.closeTarget");
|
|
expect(methods).not.toContain("Browser.close");
|
|
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
|
expect((await fetch(`${browser.endpoint}/json/version`)).ok).toBe(true);
|
|
} finally {
|
|
clearTimeout(hardStop);
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill("SIGKILL");
|
|
await closed;
|
|
}
|
|
}
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
|
|
it(
|
|
"returns the static JavaScript application Evidence contract",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-electron-static-cli-");
|
|
temporary.push(root);
|
|
await writeElectronBoundaryFixture(root);
|
|
const environment = {
|
|
...process.env,
|
|
};
|
|
|
|
const analyzed = await runCli(
|
|
["analyze-javascript-application", root, "--json"],
|
|
environment,
|
|
);
|
|
|
|
expect(analyzed).toMatchObject({
|
|
operation: "analyze_javascript_application",
|
|
provider: { id: "rea-javascript-application" },
|
|
normalized_result: {
|
|
input_path: root,
|
|
summary: {
|
|
browser_windows: 3,
|
|
context_bridge_apis: 2,
|
|
ipc: { paired_renderer_transmissions: 4 },
|
|
},
|
|
},
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
describe("Electron CLI artifact behavior", () => {
|
|
it(
|
|
"keeps artifact format separate from structured output format",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-electron-format-cli-");
|
|
temporary.push(root);
|
|
await writeElectronBoundaryFixture(root);
|
|
const environment = {
|
|
...process.env,
|
|
};
|
|
|
|
const analyzed = await runCli(
|
|
[
|
|
"--format",
|
|
"json",
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--artifact-format",
|
|
"directory",
|
|
],
|
|
environment,
|
|
);
|
|
|
|
expect(analyzed).toMatchObject({
|
|
operation: "analyze_javascript_application",
|
|
normalized_result: { input_path: root, format: "directory" },
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
|
|
it(
|
|
"routes generic directory analysis to static JavaScript",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-electron-routed-cli-");
|
|
temporary.push(root);
|
|
await writeElectronBoundaryFixture(root);
|
|
const environment = {
|
|
...process.env,
|
|
};
|
|
|
|
const analyzed = await runCli(["analyze", root, "--json"], environment);
|
|
|
|
expect(analyzed).toMatchObject({
|
|
operation: "analyze_javascript_application",
|
|
provider: { id: "rea-javascript-application" },
|
|
normalized_result: {
|
|
input_path: root,
|
|
format: "directory",
|
|
summary: { ipc: { paired_renderer_transmissions: 4 } },
|
|
},
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
});
|
|
|
|
describe("Electron CLI runtime reconciliation input", () => {
|
|
it(
|
|
"labels schema-invalid reconciliation input without naming Electron",
|
|
async () => {
|
|
const rejected = await runCli(
|
|
["reconcile-javascript-runtime", "{}", "--json"],
|
|
process.env,
|
|
1,
|
|
);
|
|
expect(rejected).toMatchObject({
|
|
error: "Application workflow failed",
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
details: { operation: "reconcile_javascript_runtime" },
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
});
|
|
|
|
describe("Electron CLI runtime reconciliation", () => {
|
|
it(
|
|
"reads a file and returns the derived static/runtime reconciliation contract",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-runtime-cli-");
|
|
const inputPath = join(root, "reconciliation.json");
|
|
temporary.push(root);
|
|
await writeFile(
|
|
inputPath,
|
|
JSON.stringify(JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE),
|
|
);
|
|
const reconciled = await runCli(
|
|
["reconcile-javascript-runtime", inputPath, "--json"],
|
|
process.env,
|
|
);
|
|
|
|
expect(reconciled).toMatchObject({
|
|
operation: "reconcile_javascript_runtime",
|
|
provider: { id: "rea-javascript-runtime-reconciliation" },
|
|
normalized_result: {
|
|
summary: { runtime_scripts: 1, matched: 1 },
|
|
source_map_authority: { used_for_primary_matching: false },
|
|
},
|
|
});
|
|
const missingPath = join(root, "missing.json");
|
|
const missing = await runCli(
|
|
["reconcile-javascript-runtime", missingPath, "--json"],
|
|
process.env,
|
|
1,
|
|
);
|
|
expect(missing).toMatchObject({
|
|
input_path: missingPath,
|
|
input_reason: "read-failed",
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
|
|
it(
|
|
"keeps ambiguous runtime candidates unknown while unrelated scoped assets remain absent",
|
|
async () => {
|
|
const application = await applicationFixture();
|
|
const assets = await createTestTempDirectory("rea-runtime-cli-assets-");
|
|
const runtime = await createTestTempDirectory("rea-runtime-cli-live-");
|
|
const inputPath = join(assets, "reconciliation.json");
|
|
temporary.push(application, assets, runtime);
|
|
await Promise.all([
|
|
writeFile(join(assets, "app.js"), SOURCE),
|
|
writeFile(join(assets, "absent.js"), "export const absent = true;\n"),
|
|
writeFile(join(runtime, "app.js"), SOURCE),
|
|
]);
|
|
const rootMapping = [{ kind: "file-root", root: runtime }] as const;
|
|
const input = {
|
|
static_layers: [
|
|
{
|
|
role: "application",
|
|
analysis: await analyzeFixture(application),
|
|
runtime_mappings: rootMapping,
|
|
},
|
|
{
|
|
role: "assets",
|
|
analysis: await analyzeFixture(assets),
|
|
runtime_mappings: rootMapping,
|
|
},
|
|
],
|
|
runtime_observations: [
|
|
electronRuntimeEvidence(runtime, SOURCE, { includeWorker: false }),
|
|
],
|
|
};
|
|
await writeFile(inputPath, JSON.stringify(input));
|
|
const reconciled = await runCli(
|
|
["reconcile-javascript-runtime", inputPath, "--json"],
|
|
process.env,
|
|
);
|
|
|
|
const normalized =
|
|
isRecord(reconciled) && isRecord(reconciled.normalized_result)
|
|
? reconciled.normalized_result
|
|
: undefined;
|
|
const reconciliations = normalized?.reconciliations;
|
|
const loadStates = normalized?.static_load_states;
|
|
if (!Array.isArray(reconciliations) || !Array.isArray(loadStates))
|
|
throw new Error("Missing runtime reconciliation output");
|
|
const ambiguous = reconciliations.find(
|
|
(item) =>
|
|
isRecord(item) &&
|
|
item.status === "ambiguous" &&
|
|
item.entity_kind === "script",
|
|
);
|
|
if (
|
|
!isRecord(ambiguous) ||
|
|
!Array.isArray(ambiguous.candidate_static_nodes)
|
|
)
|
|
throw new Error("Missing ambiguous runtime script");
|
|
const candidateIds = ambiguous.candidate_static_nodes.flatMap((item) =>
|
|
isRecord(item) && typeof item.static_node_id === "string"
|
|
? [item.static_node_id]
|
|
: [],
|
|
);
|
|
expect(candidateIds).toHaveLength(2);
|
|
const candidateStates = loadStates.filter(
|
|
(item) =>
|
|
isRecord(item) &&
|
|
typeof item.static_node_id === "string" &&
|
|
candidateIds.includes(item.static_node_id),
|
|
);
|
|
expect(candidateStates).toHaveLength(2);
|
|
expect(
|
|
candidateStates.every(
|
|
(item) =>
|
|
isRecord(item) &&
|
|
item.status === "unknown" &&
|
|
item.reason === "ambiguous-runtime-candidate",
|
|
),
|
|
).toBe(true);
|
|
expect(loadStates).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
status: "not-observed-in-capture",
|
|
reason: "not-observed-in-bounded-capture",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
const singleLayerPath = join(application, "single-reconciliation.json");
|
|
await writeFile(
|
|
singleLayerPath,
|
|
JSON.stringify({
|
|
static_layers: [input.static_layers[0]],
|
|
runtime_observations: input.runtime_observations,
|
|
}),
|
|
);
|
|
const uniquelyMatched = await runCli(
|
|
["reconcile-javascript-runtime", singleLayerPath, "--json"],
|
|
process.env,
|
|
);
|
|
expect(uniquelyMatched).toMatchObject({
|
|
normalized_result: {
|
|
static_load_states: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
status: "loaded",
|
|
reason: "runtime-script-correspondence",
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
},
|
|
INTEGRATION_TEST_TIMEOUT_MS,
|
|
);
|
|
});
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
|
|
const runCliResult = async (
|
|
arguments_: readonly string[],
|
|
environment: NodeJS.ProcessEnv,
|
|
): Promise<CliResult> =>
|
|
cli.run({ arguments: arguments_, cwd: process.cwd(), environment });
|
|
|
|
const runCli = async (
|
|
arguments_: readonly string[],
|
|
environment: NodeJS.ProcessEnv,
|
|
expectedExitCode: 0 | 1 = 0,
|
|
): Promise<unknown> => {
|
|
const result = await runCliResult(arguments_, environment);
|
|
expect(result.signal, result.stderr || result.stdout).toBeNull();
|
|
expect(result.exitCode, result.stderr || result.stdout).toBe(
|
|
expectedExitCode,
|
|
);
|
|
expect(result.json, result.stderr || result.stdout).toBeDefined();
|
|
return result.json;
|
|
};
|