fix(browser): retain evidence across cleanup errors

This commit is contained in:
morluto committed 2026-10-27 12:49:05 +08:00
1 parent 178ab26290
commit 8f894546c8
2 files changed
+81 -17

No files matched your search

@@ -11,6 +11,7 @@ import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { resolvedAnalysisError } from "../domain/analysisErrorBase.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
expectDroppedPartial,
@@ -150,6 +151,49 @@ it("converts a raw page inspection retained after browser cleanup failure", asyn
}
});
it.each([true, false])(
"promotes cleanup primary facts once when wrapper retains them: %s",
async (wrapperRetainsFacts) => {
const inspection = passiveNetworkCapture();
const primary = new AnalysisCancelledError("inspect_web_page", {
partialObservation: inspection,
});
const failure = new BrowserObservationError(
"inspect_web_page",
"cleanup_failed",
{
cleanupPrimary: primary,
cleanup: {
reason: "target detach failed",
resources: ["browser target"],
},
...(wrapperRetainsFacts ? { partialObservation: inspection } : {}),
},
);
const browser = {
...provider(async () => ok(report)),
inspectPage: async () => err(failure),
} as unknown as BrowserObservationPort;
const result = await inspectWebPage(browser, pageInput());
expect(result.ok).toBe(false);
expect(parseEvidence(primary.partialObservation)).toMatchObject({
operation: "inspect_web_page",
normalized_result: inspection,
});
if (wrapperRetainsFacts)
expect(failure.partialObservation).toBe(primary.partialObservation);
failure.resolveCleanup();
expect(resolvedAnalysisError(failure)).toBe(primary);
expect(
parseEvidence(
projectAnalysisError(resolvedAnalysisError(failure)).details?.[
"partial_observation"
],
),
).toEqual(primary.partialObservation);
},
);
it("retains captured page Evidence when bundle analysis fails after capture", async () => {
const inspection = passiveNetworkCapture();
const failure = new BrowserObservationError(
+37 -17
View File
@@ -288,44 +288,71 @@ const retainBrowserObservation = (
provider: BrowserObservationPort,
errorOperation: "inspect_web_page" | "analyze_web_bundle",
): Result<Evidence, AnalysisError> => {
const partial = error.partialObservation;
const seen = new Set<AnalysisError>();
const promoted = new Map<object, Evidence>();
let current: AnalysisError | undefined = error;
while (current !== undefined && !seen.has(current)) {
seen.add(current);
const partial = current.partialObservation;
if (partial !== undefined) {
const evidence =
promoted.get(partial) ??
promoteBrowserObservation(partial, input, provider, errorOperation);
if (evidence !== undefined) {
promoted.set(partial, evidence);
current.partialObservation = evidence;
}
}
current = current.cleanupPrimary;
}
return err(
keepIncompleteObservation(
error,
errorOperation,
NON_JSON_BROWSER_OBSERVATION,
),
);
};
/** Each cleanup owner can become the selected error after cleanup succeeds. */
const promoteBrowserObservation = (
partial: NonNullable<AnalysisError["partialObservation"]>,
input: InspectWebPageInput | AnalyzeWebBundleInput,
provider: BrowserObservationPort,
errorOperation: "inspect_web_page" | "analyze_web_bundle",
): Evidence | undefined => {
if (
errorOperation === "analyze_web_bundle" &&
partial !== undefined &&
"kind" in partial &&
partial.kind === "web-bundle-acquisition"
) {
const acquisition =
webBundleAcquisitionPartialObservationSchema.safeParse(partial);
if (acquisition.success) {
error.partialObservation = createBrowserEvidence(
return createBrowserEvidence(
"analyze_web_bundle",
input,
acquisition.data,
provider.identity(),
);
return err(error);
}
}
if (
errorOperation === "analyze_web_bundle" &&
partial !== undefined &&
!("evidence_id" in partial) &&
"observations" in partial &&
"capture" in partial
) {
const bundle = webBundleAnalysisSchema.safeParse(partial);
if (bundle.success) {
error.partialObservation = createBrowserEvidence(
return createBrowserEvidence(
"analyze_web_bundle",
input,
bundle.data,
provider.identity(),
);
return err(error);
}
} else if (
partial !== undefined &&
!("evidence_id" in partial) &&
"target" in partial &&
"network" in partial &&
@@ -337,20 +364,13 @@ const retainBrowserObservation = (
"fetch_source_maps" in input
? (({ fetch_source_maps: _fetchSourceMaps, ...page }) => page)(input)
: input;
error.partialObservation = createBrowserEvidence(
return createBrowserEvidence(
"inspect_web_page",
pageInput,
inspection.data,
provider.identity(),
);
return err(error);
}
}
return err(
keepIncompleteObservation(
error,
errorOperation,
NON_JSON_BROWSER_OBSERVATION,
),
);
return undefined;
};