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
This commit is contained in:
morluto committed 2026-10-16 06:46:25 +08:00
1 parent 131fd5ddcd
commit 3b698645a0
39 files changed
+1897 -418

No files matched your search

@@ -4,7 +4,6 @@ import {
type Evidence,
type EvidenceObservation,
} from "../../domain/evidence.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import type {
ElectronPageInspection,
ElectronTargetList,
@@ -28,7 +27,7 @@ export const createElectronEvidence = (
: "rea.electron-page-inspection",
operation,
parameters: parameters(input),
result: jsonValueSchema.parse(result),
result,
confidence: "observed",
authority: "external-service",
environment:
@@ -0,0 +1,150 @@
import { expect, it } from "vitest";
import { classifyBrowserCompleteness } from "../../domain/browserCompleteness.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { parseEvidence } from "../../domain/evidence.js";
import {
electronPageInspectionSchema,
electronTargetListSchema,
inspectElectronPageInputSchema,
} from "../../domain/javascript/electronObservation.js";
import { err } from "../../domain/result.js";
import type { ElectronObservationPort } from "./ElectronObservationPort.js";
import {
inspectElectronPage,
listElectronTargets,
} from "./ElectronObservationService.js";
const identity = { id: "electron-fixture", name: "Fixture", version: "1" };
const input = inspectElectronPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
target_id: "electron-page",
observation_ms: 0,
});
const inspection = electronPageInspectionSchema.parse({
browser: {
product: "Electron",
protocol_version: "1.3",
revision: "fixture",
user_agent: "Electron fixture",
js_version: "V8 fixture",
},
target: {
target_id: "electron-page",
type: "page",
title: "Fixture",
file_path: "/app/index.html",
attached: false,
},
capture_window: {
started_at: "2026-01-01T00:00:00.000Z",
ended_at: "2026-01-01T00:00:00.000Z",
observation_ms: 0,
},
completeness: classifyBrowserCompleteness({
policyFilteredSections: new Set(),
attachLimitedSections: new Set(),
truncatedSections: new Set(),
unavailableSections: new Set(),
excluded: [],
droppedEvents: {
scripts: 0,
network_requests: 0,
console_events: 0,
websocket_connections: 0,
websocket_frames: 0,
webmcp_tools: 0,
timeline_events: 0,
},
}),
frames: [],
dom: { total_nodes: 0, nodes: [] },
scripts: { total: 0, items: [] },
resources: [],
workers: [],
limitations: [],
});
it("converts a retained raw Electron inspection into partial Evidence", async () => {
const failure = new AnalysisCancelledError("inspect_electron_page", {
partialObservation: inspection,
});
const provider: ElectronObservationPort = {
identity: () => identity,
listTargets: async () =>
err(new AnalysisCancelledError("list_electron_targets")),
inspectPage: async () => err(failure),
};
const result = await inspectElectronPage(provider, input);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBe(failure);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_electron_page",
normalized_result: inspection,
});
}
});
it("converts a raw inspection retained by provider cleanup failure", async () => {
const failure = new ProviderCleanupError(
"rea-cdp-electron",
["electron-page"],
{ reason: "target detach failed" },
{ operation: "inspect_electron_page", partialObservation: inspection },
);
const provider: ElectronObservationPort = {
identity: () => identity,
listTargets: async () =>
err(new AnalysisCancelledError("list_electron_targets")),
inspectPage: async () => err(failure),
};
const result = await inspectElectronPage(provider, input);
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: "inspect_electron_page",
normalized_result: inspection,
});
}
});
it("returns a typed output error for malformed Electron target output after abort", async () => {
const controller = new AbortController();
const malformed = {
browser: {
product: "Electron",
protocol_version: "1.3",
revision: "fixture",
user_agent: "Electron fixture",
js_version: "V8 fixture",
},
targets: [],
excluded: { unsupported_url: 0, non_page: 0 },
limitations: [() => undefined],
} as unknown as ReturnType<typeof electronTargetListSchema.parse>;
const provider: ElectronObservationPort = {
identity: () => identity,
listTargets: async () => {
controller.abort();
return { ok: true, value: malformed };
},
inspectPage: async () =>
err(new AnalysisCancelledError("inspect_electron_page")),
};
const result = await listElectronTargets(
provider,
{
cdp_endpoint: "http://127.0.0.1:9222",
},
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
@@ -5,10 +5,19 @@ import type {
InspectElectronPageInput,
ListElectronTargetsInput,
} from "../../domain/javascript/electronObservation.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import { createElectronEvidence } from "./ElectronEvidence.js";
import {
electronPageInspectionSchema,
electronTargetListSchema,
} from "../../domain/javascript/electronObservation.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
/** List local file pages exposed by the explicitly selected endpoint. */
export const listElectronTargets = async (
@@ -18,17 +27,31 @@ export const listElectronTargets = async (
): Promise<Result<Evidence, AnalysisError>> => {
const ready = requireProvider(provider, "list_electron_targets");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("list_electron_targets"));
const result = await ready.value.listTargets(input, options);
return result.ok
? ok(
createElectronEvidence(
"list_electron_targets",
input,
result.value,
ready.value.identity(),
),
)
: result;
if (!result.ok) return result;
const report = electronTargetListSchema.safeParse(result.value);
if (!report.success)
return err(
new AnalysisOutputError(
"list_electron_targets",
"Electron provider returned a malformed target list.",
{ cause: report.error },
),
);
return completeProviderEvidence(
ok(
createElectronEvidence(
"list_electron_targets",
input,
report.data,
ready.value.identity(),
),
),
"list_electron_targets",
options,
);
};
/** Inspect one local file page from the explicitly selected endpoint. */
@@ -39,17 +62,61 @@ export const inspectElectronPage = async (
): Promise<Result<Evidence, AnalysisError>> => {
const ready = requireProvider(provider, "inspect_electron_page");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("inspect_electron_page"));
const result = await ready.value.inspectPage(input, options);
return result.ok
? ok(
createElectronEvidence(
"inspect_electron_page",
input,
result.value,
ready.value.identity(),
),
)
: result;
if (!result.ok) {
if (
result.error.partialObservation !== undefined &&
!("evidence_id" in result.error.partialObservation) &&
"target" in result.error.partialObservation &&
"dom" in result.error.partialObservation &&
"capture_window" in result.error.partialObservation
) {
const report = electronPageInspectionSchema.safeParse(
result.error.partialObservation,
);
if (!report.success) {
if (result.error instanceof AnalysisCancelledError)
return err(
new AnalysisOutputError(
"inspect_electron_page",
"Electron provider retained a malformed completed page inspection.",
{ cause: report.error },
),
);
return result;
}
result.error.partialObservation = createElectronEvidence(
"inspect_electron_page",
input,
report.data,
ready.value.identity(),
);
}
return result;
}
const report = electronPageInspectionSchema.safeParse(result.value);
if (!report.success)
return err(
new AnalysisOutputError(
"inspect_electron_page",
"Electron provider returned a malformed page inspection.",
{ cause: report.error },
),
);
return completeProviderEvidence(
ok(
createElectronEvidence(
"inspect_electron_page",
input,
report.data,
ready.value.identity(),
),
),
"inspect_electron_page",
options,
);
};
const requireProvider = (
@@ -4,7 +4,6 @@ import {
type Evidence,
type EvidenceObservation,
} from "../../domain/evidence.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import type {
JavaScriptRuntimeObservation,
JavaScriptRuntimeTargetList,
@@ -30,7 +29,7 @@ export const createJavaScriptRuntimeObservationEvidence = (
: "rea.javascript-runtime-observation",
operation,
parameters: parameters(input),
result: jsonValueSchema.parse(result),
result,
rawResult: null,
confidence: "observed",
authority: "external-service",
@@ -1,8 +1,14 @@
import { expect, it } from "vitest";
import { BrowserObservationError } from "../../domain/browserObservationError.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.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,
@@ -85,4 +91,138 @@ it("does not publish targets when cancellation arrives during provider cleanup",
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,
});
}
});
@@ -5,6 +5,7 @@ import type { Evidence } from "../../domain/evidence.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
@@ -12,6 +13,11 @@ import type {
ObserveJavaScriptRuntimeInput,
} from "../../domain/javascript/javascriptRuntimeObservation.js";
import { err, ok, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import {
javascriptRuntimeObservationSchema,
javascriptRuntimeTargetListSchema,
} from "../../domain/javascript/javascriptRuntimeObservation.js";
/** List targets exposed by the explicitly selected loopback Inspector. */
export const listJavaScriptRuntimeTargets = async (
@@ -21,19 +27,26 @@ export const listJavaScriptRuntimeTargets = async (
): Promise<Result<Evidence, AnalysisError>> => {
const ready = requireProvider(provider, "list_javascript_runtime_targets");
if (!ready.ok) return ready;
const result = await ready.value.listTargets(input, options);
if (result.ok && options.signal?.aborted === true)
if (options.signal?.aborted)
return err(new AnalysisCancelledError("list_javascript_runtime_targets"));
return result.ok
? ok(
createJavaScriptRuntimeObservationEvidence(
"list_javascript_runtime_targets",
input,
result.value,
ready.value.identity(),
),
)
: result;
const result = await ready.value.listTargets(input, options);
if (!result.ok) return result;
const report = javascriptRuntimeTargetListSchema.safeParse(result.value);
if (!report.success)
return err(
new AnalysisOutputError(
"list_javascript_runtime_targets",
"Inspector returned a malformed target list.",
{ cause: report.error },
),
);
return runtimeEvidence(
"list_javascript_runtime_targets",
input,
report.data,
ready.value.identity(),
options,
);
};
/** Observe one attach-only Inspector target selected by the request. */
@@ -44,17 +57,51 @@ export const observeJavaScriptRuntime = async (
): Promise<Result<Evidence, AnalysisError>> => {
const ready = requireProvider(provider, "observe_javascript_runtime");
if (!ready.ok) return ready;
const result = await ready.value.observe(input, options);
if (!result.ok) return result;
if (options.signal?.aborted === true)
if (options.signal?.aborted)
return err(new AnalysisCancelledError("observe_javascript_runtime"));
return ok(
createJavaScriptRuntimeObservationEvidence(
"observe_javascript_runtime",
input,
result.value,
ready.value.identity(),
),
const result = await ready.value.observe(input, options);
if (!result.ok) {
const partial = result.error.partialObservation;
if (
partial !== undefined &&
!("evidence_id" in partial) &&
!("kind" in partial)
) {
const report = javascriptRuntimeObservationSchema.safeParse(partial);
if (!report.success && result.error instanceof AnalysisCancelledError)
return err(
new AnalysisOutputError(
"observe_javascript_runtime",
"Inspector retained a malformed completed runtime observation.",
{ cause: report.error },
),
);
if (report.success)
result.error.partialObservation =
createJavaScriptRuntimeObservationEvidence(
"observe_javascript_runtime",
input,
report.data,
ready.value.identity(),
);
}
return result;
}
const report = javascriptRuntimeObservationSchema.safeParse(result.value);
if (!report.success)
return err(
new AnalysisOutputError(
"observe_javascript_runtime",
"Inspector returned a malformed runtime observation.",
{ cause: report.error },
),
);
return runtimeEvidence(
"observe_javascript_runtime",
input,
report.data,
ready.value.identity(),
options,
);
};
@@ -71,3 +118,24 @@ const requireProvider = (
),
)
: ok(provider);
const runtimeEvidence = (
operation: Parameters<typeof createJavaScriptRuntimeObservationEvidence>[0],
input: Parameters<typeof createJavaScriptRuntimeObservationEvidence>[1],
result: Parameters<typeof createJavaScriptRuntimeObservationEvidence>[2],
provider: Parameters<typeof createJavaScriptRuntimeObservationEvidence>[3],
options: ExecutionOptions,
): Result<Evidence, AnalysisError> => {
return completeProviderEvidence(
ok(
createJavaScriptRuntimeObservationEvidence(
operation,
input,
result,
provider,
),
),
operation,
options,
);
};