Files
rea/tests/boundary/inspector/javascriptRuntimeObservation.test.ts
T
morluto e55ef72135 fix(runtime)!: preserve unknown debugger attachment state
Keep omitted discovery attachment state as null through Node, browser, Electron, and reconciliation evidence. Report the uncertainty while retaining Node observation unless discovery explicitly marks the target attached. Reject worker TargetInfo records missing the required boolean with partial coverage.

Validate omitted and explicit states through provider, CLI, and MCP boundaries and real Node 24 Inspector workflows. Consolidate Electron regression coverage into the existing list-and-inspect workflow.

BREAKING CHANGE: attached fields in JavaScript runtime, browser, and Electron target discovery and inspection results may be null when discovery does not report attachment state.
2026-10-12 12:37:00 +08:00

782 lines
26 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, test } from "vitest";
import { createJavaScriptRuntimeObservationEvidence } from "../../../src/application/javascript/JavaScriptRuntimeObservationEvidence.js";
import { createBrowserEvidence } from "../../../src/application/BrowserEvidence.js";
import {
listJavaScriptRuntimeTargets,
observeJavaScriptRuntime,
} from "../../../src/application/javascript/JavaScriptRuntimeObservationService.js";
import { reconcileJavaScriptRuntimeEvidence } from "../../../src/application/javascript/JavaScriptRuntimeReconciliationService.js";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../../../src/inspector/providerIdentity.js";
import { V8InspectorProvider } from "../../../src/inspector/V8InspectorProvider.js";
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
import type {
JavaScriptRuntimeObservation,
ObserveJavaScriptRuntimeInput,
} from "../../../src/domain/javascript/javascriptRuntimeObservation.js";
import { observeJavaScriptRuntimeInputSchema } from "../../../src/domain/javascript/javascriptRuntimeObservation.js";
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js";
import { startFakeV8Inspector } from "../../fixtures/inspector/fakeV8Inspector.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
test("reports static-layer Evidence digest failure at its caller path", () => {
const layer = JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers[0];
if (layer === undefined) throw new Error("Expected a static layer example");
const runtime = createJavaScriptRuntimeObservationEvidence(
"observe_javascript_runtime",
{
inspector_endpoint: "http://127.0.0.1:9229",
target_id: "example-v8-target",
runtime_kind: "electron-main",
observation_ms: 100,
},
runtimeObservation("/tmp/index.html", "/tmp/main.js"),
V8_INSPECTOR_PROVIDER_IDENTITY,
);
const result = reconcileJavaScriptRuntimeEvidence({
static_layers: [
{
...layer,
analysis: { ...layer.analysis, evidence_id: `ev_${"0".repeat(64)}` },
},
],
runtime_observations: [runtime],
});
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisInputError",
issues: [
{
path: ["static_layers", 0, "analysis", "evidence_id"],
reason: "invalid_value",
message: "Evidence semantic identifier does not match its record",
},
],
},
});
});
test("reports browser origin scope mismatch at the caller parameter path", async () => {
const browser = await startFakeCdpBrowser();
try {
const provider = new CdpBrowserProvider();
const input = inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
});
const observed = await provider.inspectPage(input);
if (!observed.ok) throw observed.error;
const evidence = createBrowserEvidence(
"inspect_web_page",
input,
observed.value,
provider.identity(),
);
const invalidEvidence = createEvidence(undefined, evidence.provider, {
predicateType: evidence.predicate_type,
operation: evidence.operation,
parameters: { ...evidence.parameters, allowed_origins: [] },
result: evidence.normalized_result,
confidence: evidence.confidence,
authority: evidence.authority,
environment: evidence.environment,
limitations: evidence.limitations,
});
const result = reconcileJavaScriptRuntimeEvidence({
static_layers: JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers,
runtime_observations: [invalidEvidence],
});
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisInputError",
issues: [
{
path: ["runtime_observations", 0, "parameters", "allowed_origins"],
reason: "invalid_value",
message:
"Browser Evidence target is outside its recorded origin scope",
},
],
},
});
} finally {
await browser.close();
}
});
describe("Inspector execution-context lifecycle metadata", () => {
test.each([
[
"Runtime.executionContextDestroyed",
{ executionContextId: 1 },
"destroyed",
],
["Runtime.executionContextsCleared", {}, "cleared"],
] as const)(
"preserves observed context metadata after %s",
async (method, params, state) => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
runtimeEvents: [
{
method: "Runtime.executionContextCreated",
params: {
context: {
id: 1,
origin: "https://context.example",
name: "worker#one",
},
},
},
{ method, params },
],
});
try {
const result = await new V8InspectorProvider().observe(
observeInput(fake.endpoint, fake.targetId, "node"),
);
if (!result.ok) throw result.error;
expect(result.value.execution_contexts).toEqual([
{
context_key: "1",
state,
name: "worker#one",
origin: "https://context.example",
},
]);
} finally {
await fake.close();
}
},
);
test("replaces context metadata only when a new context creation is observed", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
runtimeEvents: [
{
method: "Runtime.executionContextCreated",
params: {
context: {
id: 1,
origin: "https://old.example",
name: "old context",
},
},
},
{
method: "Runtime.executionContextDestroyed",
params: { executionContextId: 1 },
},
{
method: "Runtime.executionContextCreated",
params: { context: { id: 1, origin: "", name: "" } },
},
{
method: "Runtime.executionContextDestroyed",
params: { executionContextId: 2 },
},
],
});
try {
const result = await new V8InspectorProvider().observe(
observeInput(fake.endpoint, fake.targetId, "node"),
);
if (!result.ok) throw result.error;
expect(result.value.execution_contexts).toEqual([
{ context_key: "1", state: "created", name: "", origin: "" },
{ context_key: "2", state: "destroyed", name: null, origin: null },
]);
} finally {
await fake.close();
}
});
});
test("rejects unknown runtime observation fields", () => {
expect(
observeJavaScriptRuntimeInputSchema.safeParse({
inspector_endpoint: "http://127.0.0.1:9229",
target_id: "target-1",
unknown_field: true,
}).success,
).toBe(false);
});
test("returns every target exposed by the selected Inspector endpoint inline", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
additionalTargetCount: 205,
});
try {
const listed = await new V8InspectorProvider().listTargets({
inspector_endpoint: fake.endpoint,
});
if (!listed.ok) throw listed.error;
expect(listed.value.targets).toHaveLength(206);
expect(listed.value.targets[0]?.target_id).toBe(fake.targetId);
} finally {
await fake.close();
}
});
test.each([
{ label: "omitted", attached: undefined, expected: null },
{ label: "explicitly detached", attached: false, expected: false },
] as const)(
"preserves $label V8 attachment state through listing and observation",
async ({ attached, expected }) => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
...(attached === undefined ? {} : { attached }),
});
try {
const provider = new V8InspectorProvider();
const listed = await provider.listTargets({
inspector_endpoint: fake.endpoint,
});
if (!listed.ok) throw listed.error;
expect(listed.value.targets[0]?.attached).toBe(expected);
if (expected === null)
expect(
listed.value.limitations.some((item) =>
item.includes("attachment state"),
),
).toBe(true);
const observed = await provider.observe(
observeInput(fake.endpoint, fake.targetId, "node"),
);
if (!observed.ok) throw observed.error;
expect(observed.value.target.attached).toBe(expected);
if (expected === null)
expect(
observed.value.limitations.some((item) =>
item.includes("attachment state"),
),
).toBe(true);
} finally {
await fake.close();
}
},
);
test("rejects a V8 target explicitly marked attached", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
attached: true,
});
try {
const result = await new V8InspectorProvider().observe(
observeInput(fake.endpoint, fake.targetId, "node"),
);
expect(result).toMatchObject({
ok: false,
error: { reason: "target_not_allowed" },
});
expect(fake.commands).toEqual([]);
} finally {
await fake.close();
}
});
test("includes targets outside the old caller root filter", async () => {
const fixture = await runtimeFixture();
const outside = await temporaryFile("outside.js");
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
additionalTargetUrl: pathToFileURL(outside).href,
});
try {
const result = await new V8InspectorProvider().listTargets({
inspector_endpoint: fake.endpoint,
});
if (!result.ok) throw result.error;
expect(result.value.targets).toHaveLength(2);
expect(result.value.targets.map(({ location }) => location)).toContainEqual(
expect.objectContaining({ file_path: outside }),
);
} finally {
await fake.close();
}
});
test("captures complete metadata with two enable commands", async () => {
const fixture = await runtimeFixture();
const outside = await temporaryFile("secret.js");
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
scriptUrls: [
pathToFileURL(fixture.entry).href,
"node:fs",
pathToFileURL(outside).href,
],
});
try {
const result = await new V8InspectorProvider().observe(
observeInput(fake.endpoint, fake.targetId, "node"),
);
if (!result.ok) throw result.error;
expect(result.value.scripts.items).toHaveLength(3);
expect(result.value.scripts.excluded.unsupported_location).toBe(0);
expect(result.value.execution_contexts).toEqual([
{
context_key: "1",
state: "created",
name: "node[fixture]",
origin: "",
},
]);
expect(new Set(fake.commands.map(({ method }) => method))).toEqual(
new Set(["Runtime.enable", "Debugger.enable"]),
);
expect(JSON.stringify(result.value)).toContain(outside);
expect(result.value.unavailable_without_instrumentation).toContain(
"Electron IPC messages and handlers",
);
} finally {
await fake.close();
}
});
test("excludes an Electron main target that reports only bare file://", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: "file://",
scriptUrls: [pathToFileURL(fixture.entry).href],
});
try {
const provider = new V8InspectorProvider();
const listed = await provider.listTargets({
inspector_endpoint: fake.endpoint,
});
if (!listed.ok) throw listed.error;
expect(listed.value.targets).toEqual([]);
const observed = await provider.observe(
observeInput(fake.endpoint, fake.targetId, "electron-main"),
);
expect(observed.ok).toBe(false);
if (observed.ok) return;
expect(observed.error.message).toMatch(/target/u);
} finally {
await fake.close();
}
});
test.each([
["node", "node"],
["electron-main", "node"],
["electron-preload", "page"],
["electron-renderer", "page"],
] as const)(
"admits declared %s only on its protocol target family",
async (runtimeKind, targetType) => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
targetType,
});
try {
const result = await new V8InspectorProvider().observe(
observeInput(fake.endpoint, fake.targetId, runtimeKind),
);
expect(result.ok).toBe(true);
} finally {
await fake.close();
}
},
);
describe("complete Inspector script hashes", () => {
test("keeps more than one hundred long script hashes distinct", async () => {
const fixture = await runtimeFixture();
const url = pathToFileURL(fixture.entry).href;
const prefix = "h".repeat(512);
const scriptHashes = Array.from(
{ length: 101 },
(_, index) => `${prefix}${String(index)}`,
);
const fake = await startFakeV8Inspector({
targetUrl: url,
scriptUrls: scriptHashes.map(() => url),
scriptHashes,
});
try {
const result = await new V8InspectorProvider().observe({
...observeInput(fake.endpoint, fake.targetId, "node"),
observation_ms: 1_000,
});
if (!result.ok) throw result.error;
expect(result.value.scripts.items).toHaveLength(scriptHashes.length);
expect(
new Set(result.value.scripts.items.map(({ cdp_hash }) => cdp_hash)),
).toEqual(new Set(scriptHashes));
} finally {
await fake.close();
}
});
});
describe("undeclared V8 runtime role", () => {
test("observes a target without inventing a runtime role", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
targetType: "page",
});
try {
const input = observeInput(fake.endpoint, fake.targetId, "node");
delete input.runtime_kind;
const result = await new V8InspectorProvider().observe(input);
if (!result.ok) throw result.error;
expect(result.value.target).toMatchObject({
protocol_type: "page",
runtime_kind: "unknown",
runtime_kind_authority: "not-declared",
});
} finally {
await fake.close();
}
});
});
describe("V8 Inspector script retention", () => {
test("retains authorized script locations longer than the former byte ceiling", async () => {
const longUrl = `https://example.test/${"a".repeat(20_000)}`;
const fake = await startFakeV8Inspector({
targetUrl: longUrl,
scriptUrls: [longUrl],
});
try {
const input = {
...observeInput(fake.endpoint, fake.targetId, "node"),
};
const result = await new V8InspectorProvider().observe(input);
if (!result.ok) throw result.error;
expect(result.value.scripts.items).toHaveLength(1);
expect(result.value.scripts.items[0]?.location).toMatchObject({
kind: "url",
origin: "https://example.test",
sanitized_url: longUrl,
});
} finally {
await fake.close();
}
});
test("returns every authorized script beyond the former collection ceiling", async () => {
const fixture = await runtimeFixture();
const scriptUrls = [
pathToFileURL(fixture.entry).href,
...Array.from({ length: 2_004 }, () => pathToFileURL(fixture.entry).href),
];
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
scriptUrls,
});
try {
const input = {
...observeInput(fake.endpoint, fake.targetId, "node"),
observation_ms: 1_000,
};
const result = await new V8InspectorProvider().observe(input);
if (!result.ok) throw result.error;
expect(result.value.capture.truncated).toBe(false);
expect(result.value.capture.truncation_reasons).toEqual([]);
expect(result.value.capture.events_dropped).toBe(0);
expect(result.value.scripts.items).toHaveLength(scriptUrls.length);
} finally {
await fake.close();
}
});
});
describe("bounded V8 Inspector capture", () => {
test("bounds aggregate script and context metadata while retaining a useful partial capture", async () => {
const fixture = await runtimeFixture();
const url = pathToFileURL(fixture.entry).href;
const scriptHashes = Array.from(
{ length: 2_300 },
(_, index) => `${"h".repeat(4_096)}${String(index)}`,
);
const fake = await startFakeV8Inspector({
targetUrl: url,
scriptUrls: scriptHashes.map(() => url),
scriptHashes,
contextTransitionCount: 2_000,
});
try {
const result = await new V8InspectorProvider().observe({
...observeInput(fake.endpoint, fake.targetId, "node"),
observation_ms: 1_000,
});
if (!result.ok) throw result.error;
expect(result.value.capture.truncated).toBe(true);
expect(result.value.capture.truncation_reasons).toEqual([
"retained_metadata_budget_exceeded",
]);
expect(result.value.capture.events_dropped).toBeGreaterThan(0);
expect(result.value.capture.metadata_bytes_retained).toBeLessThanOrEqual(
8 * 1024 * 1024,
);
expect(result.value.capture.events_observed).toBeGreaterThan(
result.value.capture.events_retained,
);
expect(result.value.execution_contexts).toHaveLength(1);
} finally {
await fake.close();
}
});
test("rejects a CDP event larger than the configured transport payload budget", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
oversizedEventBytes: 16 * 1024 * 1024,
});
try {
const result = await new V8InspectorProvider().observe(
observeInput(fake.endpoint, fake.targetId, "node"),
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
reason: "payload_limit",
});
expect(result.error.userMessage).toContain("protocol budget");
} finally {
await fake.close();
}
});
});
describe("passive V8 Inspector evidence", () => {
test("produces deterministic Evidence through the service without grants", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: pathToFileURL(fixture.entry).href,
});
try {
const input = observeInput(fake.endpoint, fake.targetId, "node");
const first = await observeJavaScriptRuntime(
new V8InspectorProvider(),
input,
);
const second = await observeJavaScriptRuntime(
new V8InspectorProvider(),
input,
);
expect(first.ok).toBe(true);
expect(second.ok).toBe(true);
if (!first.ok || !second.ok) return;
expect(second.value.evidence_id).toBe(first.value.evidence_id);
const listed = await listJavaScriptRuntimeTargets(
new V8InspectorProvider(),
{
inspector_endpoint: fake.endpoint,
},
);
expect(listed.ok).toBe(true);
} finally {
await fake.close();
}
});
test("reconciles V8 script presence with static Application Graph Evidence", () => {
const root = "/Applications/Example.app/Contents/Resources/app";
const observation = runtimeObservation(
`${root}/index.html`,
`${root}/renderer.js`,
);
const runtimeEvidence = createJavaScriptRuntimeObservationEvidence(
"observe_javascript_runtime",
{
inspector_endpoint: "http://127.0.0.1:9229",
target_id: "example-v8-target",
runtime_kind: "electron-main",
observation_ms: 100,
},
observation,
V8_INSPECTOR_PROVIDER_IDENTITY,
);
const reconciled = reconcileJavaScriptRuntimeEvidence({
static_layers: JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers,
runtime_observations: [runtimeEvidence],
});
if (!reconciled.ok) throw reconciled.error;
const result = javascriptRuntimeReconciliationResultSchema.parse(
reconciled.value.normalized_result,
);
expect(result.runtime_captures[0]?.kind).toBe("v8-inspector");
expect(result.summary.runtime_scripts).toBe(1);
expect(result.summary.matched).toBeGreaterThan(0);
});
test("keeps unverified partial script URLs unresolved during reconciliation", () => {
const rawUrl = "file:///tmp/not-verified.js?caller-value=kept";
const resolvedObservation = runtimeObservation(
"/Applications/Example.app/Contents/Resources/app/index.html",
"/Applications/Example.app/Contents/Resources/app/renderer.js",
);
const resolvedScript = resolvedObservation.scripts.items[0];
if (resolvedScript === undefined)
throw new Error("Expected the runtime fixture to have one script");
const observation: JavaScriptRuntimeObservation = {
...resolvedObservation,
scripts: {
...resolvedObservation.scripts,
items: [
...resolvedObservation.scripts.items.map((script) => ({
...script,
location: {
kind: "unresolved" as const,
reported_url: rawUrl,
reason: "location-authorization-not-attempted" as const,
},
})),
{
...resolvedScript,
script_key: `v8_script_${"5".repeat(64)}`,
location: {
kind: "unresolved",
reported_url: "",
reason: "location-authorization-not-attempted",
},
cdp_hash: null,
length: 0,
},
],
},
};
const runtimeEvidence = createJavaScriptRuntimeObservationEvidence(
"observe_javascript_runtime",
{
inspector_endpoint: "http://127.0.0.1:9229",
target_id: "example-v8-target",
runtime_kind: "electron-main",
observation_ms: 100,
},
observation,
V8_INSPECTOR_PROVIDER_IDENTITY,
);
const reconciled = reconcileJavaScriptRuntimeEvidence({
static_layers: JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers,
runtime_observations: [runtimeEvidence],
});
if (!reconciled.ok) throw reconciled.error;
const result = javascriptRuntimeReconciliationResultSchema.parse(
reconciled.value.normalized_result,
);
expect(result.summary.runtime_scripts).toBe(2);
expect(result.reconciliations).toContainEqual(
expect.objectContaining({
entity_kind: "script",
status: "unknown",
reason: "runtime-location-unresolved",
}),
);
expect(JSON.stringify(result.graph)).toContain(rawUrl);
expect(JSON.stringify(result.graph)).toContain("v8-hash");
expect(JSON.stringify(result.graph)).toContain('"reported_url":""');
});
});
const runtimeFixture = async () => {
const root = await createTestTempDirectory("rea-v8-runtime-");
const entry = join(root, "entry.js");
await writeFile(entry, "export const value = 1;\n");
return { root, entry };
};
const temporaryFile = async (name: string): Promise<string> => {
const root = await createTestTempDirectory("rea-v8-outside-");
const path = join(root, name);
await writeFile(path, "export {};\n");
return path;
};
const observeInput = (
endpoint: string,
targetId: string,
runtimeKind: ObserveJavaScriptRuntimeInput["runtime_kind"],
): ObserveJavaScriptRuntimeInput => ({
inspector_endpoint: endpoint,
target_id: targetId,
runtime_kind: runtimeKind,
observation_ms: 10,
});
const runtimeObservation = (
targetPath: string,
scriptPath: string,
): JavaScriptRuntimeObservation => ({
runtime: {
product: "node.js/v24.4.1",
protocol_version: "1.3",
v8_version: "13.6",
},
target: {
target_id: "example-v8-target",
protocol_type: "node",
attached: false,
location: { kind: "file", file_path: targetPath },
runtime_kind: "electron-main",
runtime_kind_authority: "caller-declared-unverified",
},
capture: {
observation_ms: 100,
events_observed: 1,
events_retained: 1,
events_dropped: 0,
metadata_bytes_retained: 100,
truncated: false,
truncation_reasons: [],
},
scripts: {
items: [
{
script_key: `v8_script_${"4".repeat(64)}`,
location: { kind: "file", file_path: scriptPath },
execution_context_key: "1",
cdp_hash: "v8-hash",
length: 29,
is_module: true,
status: "observed-loaded",
},
],
observed_total: 1,
excluded: {
unsupported_location: 0,
invalid_protocol_value: 0,
},
},
execution_contexts: [],
directly_observed: ["Debugger.scriptParsed established script presence."],
unavailable_without_instrumentation: [
"require/import caller-to-callee edges",
],
unknowns: ["Capture is bounded."],
limitations: ["Passive Inspector metadata only."],
});