fix(observation): freeze retained incomplete observations
A JSON partial stayed a live provider object, so a later mutation could make projection throw. The gate now keeps one frozen snapshot and still drops a non-JSON partial without copying it. Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
3 files changed
+45
-11
No files matched your search
@@ -136,3 +136,25 @@ it("clears a non-JSON partial from the cleanup primary projection returns after
|
||||
"partial_observation",
|
||||
);
|
||||
});
|
||||
|
||||
it("freezes a retained partial so a later mutation cannot break projection", () => {
|
||||
const partial = { steps: [] as unknown[] };
|
||||
const primary = new AnalysisCancelledError("capture_browser_scenario", {
|
||||
cause: { requested: "stop during capture" },
|
||||
partialObservation: partial as never,
|
||||
});
|
||||
keepIncompleteObservation(
|
||||
primary,
|
||||
"capture_browser_scenario",
|
||||
"unused because the partial is JSON",
|
||||
);
|
||||
partial.steps.push(undefined);
|
||||
|
||||
expect(primary.partialObservation).toEqual({ steps: [] });
|
||||
expect(Object.isFrozen(primary.partialObservation)).toBe(true);
|
||||
expect(primary.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(primary)).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: { partial_observation: { steps: [] } },
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,14 @@ import {
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { jsonValueValidationIssue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
freezeJsonSnapshot,
|
||||
isImmutableJsonSnapshot,
|
||||
} from "../domain/immutableJson.js";
|
||||
import {
|
||||
jsonValueSchema,
|
||||
jsonValueValidationIssue,
|
||||
} from "../domain/jsonValue.js";
|
||||
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
@@ -97,6 +104,8 @@ export const completeProviderEvidence = (
|
||||
|
||||
/**
|
||||
* Retain an incomplete observation when projection can serialize it.
|
||||
* A JSON partial is replaced by one frozen snapshot, so a later mutation
|
||||
* cannot make projection throw and projection does not copy it again.
|
||||
* A partial that is not JSON is cleared from the returned error and from
|
||||
* every cleanup primary projection can select later. Each cleared error
|
||||
* records an output failure, leaving cancellation or timeout primary.
|
||||
@@ -111,15 +120,18 @@ export const keepIncompleteObservation = (
|
||||
while (current !== undefined && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
const partial = current.partialObservation;
|
||||
// Validate without cloning. Projection serializes a kept partial later.
|
||||
if (
|
||||
partial !== undefined &&
|
||||
jsonValueValidationIssue(partial) !== undefined
|
||||
) {
|
||||
current.partialObservation = undefined;
|
||||
current.retainSecondaryFailure(
|
||||
new AnalysisOutputError(operation, reason),
|
||||
);
|
||||
if (partial !== undefined && !isImmutableJsonSnapshot(partial)) {
|
||||
// Validate without cloning. Copy only a partial that projection keeps.
|
||||
if (jsonValueValidationIssue(partial) === undefined) {
|
||||
current.partialObservation = freezeJsonSnapshot(
|
||||
jsonValueSchema.parse(partial),
|
||||
) as typeof partial;
|
||||
} else {
|
||||
current.partialObservation = undefined;
|
||||
current.retainSecondaryFailure(
|
||||
new AnalysisOutputError(operation, reason),
|
||||
);
|
||||
}
|
||||
}
|
||||
current = current.cleanupPrimary;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const expectKeptPartial = (
|
||||
partial: unknown,
|
||||
code: "cancelled" | "provider_timeout" = "cancelled",
|
||||
): void => {
|
||||
expect(error.partialObservation).toBe(partial);
|
||||
expect(error.partialObservation).toEqual(partial);
|
||||
expect(error.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(error)).toMatchObject({
|
||||
code,
|
||||
|
||||
Reference in new issue
Block a user