Files
rea/src/application/javascript/JavaScriptRuntimeObservationService.test.ts
T
morluto 3b698645a0 fix(evidence): retain completed observations on late cancellation
Validate completed reports before cancellation projection, retain valid
Evidence and captured command diagnostics, and convert raw cleanup partials
at their application owner. Record partial Evidence consistently across MCP
handlers. Keep page inspection separate from unfinished bundle analysis.

Refs #4261
2026-10-16 06:46:25 +08:00

229 lines
6.7 KiB
TypeScript

import { expect, it } from "vitest";
import { BrowserObservationError } from "../../domain/browserObservationError.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js";
import { parseEvidence } from "../../domain/evidence.js";
import type { JavaScriptRuntimeTargetList } from "../../domain/javascript/javascriptRuntimeObservation.js";
import { javascriptRuntimeObservationSchema } from "../../domain/javascript/javascriptRuntimeObservation.js";
import type { JavaScriptRuntimeObservationPort } from "./JavaScriptRuntimeObservationPort.js";
import {
listJavaScriptRuntimeTargets,
observeJavaScriptRuntime,
} from "./JavaScriptRuntimeObservationService.js";
it("preserves failed Inspector cleanup when cancellation occurs during observation", async () => {
const controller = new AbortController();
const failure = new BrowserObservationError(
"observe_javascript_runtime",
"cleanup_failed",
{
cause: new AnalysisCancelledError("observe_javascript_runtime"),
},
);
const provider: JavaScriptRuntimeObservationPort = {
identity: () => ({
id: "test-inspector",
name: "Test Inspector",
version: "1",
}),
listTargets: async () => err(failure),
observe: async () => {
controller.abort();
return err(failure);
},
};
const result = await observeJavaScriptRuntime(
provider,
{
inspector_endpoint: "http://127.0.0.1:9229",
target_id: "target",
observation_ms: 0,
},
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed cleanup");
expect(result.error).toBe(failure);
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.cleanupResources).toEqual(["browser_transport"]);
expect(result.error.userCategory).toBe("cancelled");
});
it("does not publish targets when cancellation arrives during provider cleanup", async () => {
const controller = new AbortController();
const provider: JavaScriptRuntimeObservationPort = {
identity: () => ({
id: "test-inspector",
name: "Test Inspector",
version: "1",
}),
listTargets: async () => {
controller.abort();
return ok({
runtime: {
product: "node.js/v24",
protocol_version: "1.3",
v8_version: "13",
},
targets: [],
excluded: { unsupported_location: 0, unconnectable: 0 },
limitations: [],
});
},
observe: async () =>
err(new AnalysisCancelledError("observe_javascript_runtime")),
};
const result = await listJavaScriptRuntimeTargets(
provider,
{ inspector_endpoint: "http://127.0.0.1:9229" },
{ signal: controller.signal },
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "list_javascript_runtime_targets",
},
});
if (!result.ok) {
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "list_javascript_runtime_targets",
normalized_result: { targets: [] },
});
}
});
it("does not let late cancellation hide an invalid completed Inspector report", async () => {
const malformed = {
runtime: {
product: "Node.js",
protocol_version: "1.3",
v8_version: "12.0",
},
targets: [],
excluded: { unsupported_location: -1, unconnectable: 0 },
limitations: [],
} as unknown as JavaScriptRuntimeTargetList;
const output = await listJavaScriptRuntimeTargets(
{
identity: () => ({ id: "test", name: "Test", version: "1" }),
listTargets: async () => ok(malformed),
observe: async () => {
throw new Error("not used");
},
},
{ inspector_endpoint: "http://127.0.0.1:9229" },
);
expect(output).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
if (!output.ok) expect(output.error).toBeInstanceOf(AnalysisOutputError);
const controller = new AbortController();
const late = await listJavaScriptRuntimeTargets(
{
identity: () => ({ id: "test", name: "Test", version: "1" }),
listTargets: async () => {
controller.abort();
return ok(malformed);
},
observe: async () => {
throw new Error("not used");
},
},
{ inspector_endpoint: "http://127.0.0.1:9229" },
{ signal: controller.signal },
);
expect(late).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
if (!late.ok) expect(late.error).toBeInstanceOf(AnalysisOutputError);
});
it("converts a raw runtime observation retained by cleanup failure", async () => {
const observation = javascriptRuntimeObservationSchema.parse({
runtime: {
product: "node.js/v24",
protocol_version: "1.3",
v8_version: "13",
},
target: {
target_id: "target",
protocol_type: "node",
attached: false,
location: { kind: "file", file_path: "/fixture.js" },
runtime_kind: "node",
runtime_kind_authority: "caller-declared-unverified",
},
capture: {
observation_ms: 0,
events_observed: 0,
events_retained: 0,
events_dropped: 0,
metadata_bytes_retained: 0,
truncated: false,
truncation_reasons: [],
},
scripts: {
items: [],
observed_total: 0,
excluded: { unsupported_location: 0, invalid_protocol_value: 0 },
},
execution_contexts: [],
directly_observed: ["Inspector target attached and queried."],
unavailable_without_instrumentation: [],
unknowns: [],
limitations: [],
});
const failure = new BrowserObservationError(
"observe_javascript_runtime",
"cleanup_failed",
{
cleanup: {
reason: "Inspector close failed",
resources: ["browser_transport"],
},
partialObservation: observation,
},
);
const result = await observeJavaScriptRuntime(
{
identity: () => ({ id: "test", name: "Test", version: "1" }),
listTargets: async () =>
ok({
runtime: {
product: "node.js/v24",
protocol_version: "1.3",
v8_version: "13",
},
targets: [],
excluded: { unsupported_location: 0, unconnectable: 0 },
limitations: [],
}),
observe: async () => err(failure),
},
{
inspector_endpoint: "http://127.0.0.1:9229",
target_id: "target",
observation_ms: 0,
},
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBe(failure);
expect(result.error.cleanupIncomplete).toBe(true);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "observe_javascript_runtime",
normalized_result: observation,
});
}
});