fix(evidence): preserve caller scope in secondary failures

This commit is contained in:
morluto committed 2026-11-03 03:28:55 +08:00
1 parent 68b1d91310
commit 588a4c81a0
2 files changed
+46 -11

No files matched your search

+22 -1
View File
@@ -33,7 +33,10 @@ import {
} from "./hopperStartupFailure.js";
import { ProviderAdapterError } from "./providerAdapterError.js";
import { UnknownRegistryError } from "./unknownRegistryError.js";
import { projectAnalysisError } from "./analysisErrorProjection.js";
import {
childFailureDiagnostics,
projectAnalysisError,
} from "./analysisErrorProjection.js";
import {
analysisErrorRemediationAction,
analysisErrorUserMessage,
@@ -1530,6 +1533,24 @@ describe("analysis error projection: composed failures for a caller lifetime", (
});
});
it("keeps caller lifetime for a secondary failure embedded in child diagnostics", () => {
const primary = new AnalysisInputError("read_bytes", undefined, []);
const secondary = new AnalysisTimeoutError("search_strings", 250, {
limit: analysisSettingLimit("regex.matching_deadline_ms", 250, "setting"),
continuingCallerGuidance: continuingOnly,
});
primary.retainSecondaryFailure(secondary);
const history = childFailureDiagnostics({
primary_error: { error: primary },
});
expect(history.diagnostics?.primary_error).toEqual(
projectAnalysisError(primary, "continuing"),
);
expect(history.invocationDiagnostics?.primary_error).toEqual(
projectAnalysisError(primary, "invocation"),
);
});
it("records one lifetime form when no child depends on the caller", () => {
const composed = analysisErrorWithCleanupFailure(
new AnalysisInputError("read_bytes", undefined, [
+24 -10
View File
@@ -301,8 +301,8 @@ export const childFailureDiagnostics = (
entries.map(([key, child]) => [key, project(child, "continuing")]),
),
};
const varying = entries.filter(([, { error }]) =>
projectionDependsOnCallerLifetime(error),
const varying = entries.filter(([, { error, omitRecovery }]) =>
projectionDependsOnCallerLifetime(error, omitRecovery !== true),
);
return varying.length === 0
? { diagnostics: continuing }
@@ -317,19 +317,33 @@ export const childFailureDiagnostics = (
};
};
/** Every lifetime-dependent input the projection reads lies on this chain. */
const projectionDependsOnCallerLifetime = (error: AnalysisError): boolean => {
/** Check the primary chain and the secondary failures included in its projection. */
const projectionDependsOnCallerLifetime = (
error: AnalysisError,
includeSecondary: boolean,
): boolean => {
const chainDepends = (first: AnalysisError): boolean => {
for (
let node: AnalysisError | undefined = first;
node !== undefined;
node = node.cleanupPrimary
)
if (
node.limit !== undefined ||
node.continuingCallerGuidance !== undefined ||
node.invocationDiagnostics !== undefined
)
return true;
return false;
};
if (chainDepends(error)) return true;
if (!includeSecondary) return false;
for (
let node: AnalysisError | undefined = error;
node !== undefined;
node = node.cleanupPrimary
)
if (
node.limit !== undefined ||
node.continuingCallerGuidance !== undefined ||
node.invocationDiagnostics !== undefined
)
return true;
if (node.secondaryFailures.some(chainDepends)) return true;
return false;
};