Files
rea/src/domain/explicitSensitiveFailure.test.ts
吴佳浩Alben d73ed6fce2 fix(domain): preserve retryable capability failures through redaction (#2494)
redactExplicitFailure rebuilds each typed failure field by field so explicit
literals are removed while the projected code stays the same. The rebuild for
AnalysisCapabilityUnavailableError carried providerId, operation, reason and
userMessage but not retryable, and the constructor defaults that field to false.
Redacting a transient capability failure therefore made it permanent and changed
the remediation shown to the caller.

Pass the field through with the same conditional-spread shape used for
userMessage. The constructor recomputes retryable from cleanupIncomplete, and
cleanup is already part of the reconstructed context, so a value that was true
before redaction stays true and a false value stays absent.

Verified with 66 error projection, access-denied, redaction and capture-failure
tests, check:fast and verify:test-discovery on macOS arm64. The regression test
failed with "expected false to be true" before the change.

Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
2026-10-13 14:43:10 +08:00

204 lines
7.0 KiB
TypeScript

import { AnalysisAccessDeniedError } from "./analysisErrorCore.js";
import { expect, it } from "vitest";
import {
AnalysisInputError,
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisTimeoutError,
AnalysisArtifactChangedError,
AnalysisOutputError,
AnalysisResourceConstraintError,
} from "./analysisErrorCore.js";
import { projectAnalysisError } from "./analysisErrorProjection.js";
import { ProviderCleanupError } from "./providerCleanupError.js";
import { ProviderAdapterError } from "./providerAdapterError.js";
import {
redactExplicitFailure,
redactExplicitPointer,
} from "./explicitSensitiveFailure.js";
const context = {
diagnostics: {
note: "explicit-secret warning",
nested: { "explicit-secret-key": 5, observed: 7 },
caller_path: "/tmp/password-fixture?token=public-value",
},
capturedOutput: {
stdout: "explicit-secret output",
stderr: "public stderr",
stdout_bytes: 22,
truncated: false,
},
};
it.each([
new AnalysisCancelledError("inspect", context),
new AnalysisTimeoutError("inspect", 250, context),
new AnalysisInputError("inspect", context),
new AnalysisAccessDeniedError("inspect", "/public", "EACCES", context),
new AnalysisArtifactChangedError("inspect", "/public", "changed", context),
new AnalysisOutputError("inspect", "invalid", context),
new AnalysisCapabilityUnavailableError(
"provider",
"inspect",
"unsupported",
context,
),
new AnalysisResourceConstraintError(
"inspect",
"memory",
"capacity",
null,
context,
),
new ProviderAdapterError("provider", "inspect", context),
])("preserves sanitized common context for $._tag", (error) => {
const sanitized = redactExplicitFailure(error, ["explicit-secret"]);
expect(sanitized._tag).toBe(error._tag);
const projected = projectAnalysisError(sanitized);
expect(projected.details).toMatchObject({
diagnostics: {
note: "[REDACTED] warning",
nested: { observed: 7 },
caller_path: context.diagnostics.caller_path,
},
captured_output: {
stdout: "[REDACTED] output",
stdout_bytes: 22,
stderr: "public stderr",
},
});
expect(JSON.stringify(projected)).not.toContain("explicit-secret");
});
it.each([
["/metadata/private~1~0/numeric", ["private/~"], "/metadata"],
["/metadata/private/child", ["private"], "/metadata"],
["/ordinary/path", [], "/ordinary/path"],
])(
"preserves a real parent for excluded pointer identities: %s",
(pointer, values, expected) => {
expect(redactExplicitPointer(pointer, values)).toBe(expected);
},
);
it("preserves cleanup failure authority and resources while excluding diagnostics", () => {
const error = new ProviderCleanupError(
"har",
["/root/private/capture", "pid:123"],
{
capture_path: "/root/private/capture",
nested: { "private-key": 5, ordinary: "private ordinary" },
exit_code: 2,
},
{ operation: "inspect_web_network_capture" },
);
const projected = projectAnalysisError(
redactExplicitFailure(error, ["private", "REDACTED"]),
);
expect(projected.code).toBe("cleanup_incomplete");
expect(JSON.stringify(projected)).not.toContain("private");
expect(JSON.stringify(projected)).not.toContain("REDACTED");
expect(JSON.stringify(projected)).toContain("pid:123");
expect(JSON.stringify(projected)).toContain('"exit_code":2');
});
it("keeps unsupported profile reasons distinct from malformed input", () => {
const error = new AnalysisCapabilityUnavailableError(
"mitmproxy",
"inspect_web_network_capture",
"Unsupported private profile",
{ userMessage: "Select private-compatible profile" },
);
const result = redactExplicitFailure(error, ["private"]);
expect(result).toBeInstanceOf(AnalysisCapabilityUnavailableError);
expect(projectAnalysisError(result).code).toBe("capability_unavailable");
expect(JSON.stringify(projectAnalysisError(result))).not.toContain("private");
});
it("preserves a retryable capability failure through explicit-value redaction", () => {
// Retryability is a semantic field of the error, not presentation: dropping
// it turns a transient admission failure into a permanent one and changes
// the remediation the caller is shown.
const error = new AnalysisCapabilityUnavailableError(
"mitmproxy",
"inspect_web_network_capture",
"Runtime admission for /targets/private/capture failed transiently",
{ retryable: true },
);
expect(projectAnalysisError(error).retryable).toBe(true);
const result = redactExplicitFailure(error, ["private"]);
expect(result).toBeInstanceOf(AnalysisCapabilityUnavailableError);
expect((result as AnalysisCapabilityUnavailableError).retryable).toBe(true);
expect(projectAnalysisError(result).retryable).toBe(true);
expect(JSON.stringify(projectAnalysisError(result))).not.toContain("private");
});
it("coarsens format issue pointers without creating replacement coordinates", () => {
const error = new AnalysisInputError(
"inspect_web_network_capture",
undefined,
[
{
path: ["capture_path", "/metadata/private~1~0/nested"],
reason: "invalid_format",
message: "Malformed private field",
},
],
);
const result = redactExplicitFailure(error, ["private/~", "private"]);
if (!(result instanceof AnalysisInputError))
throw new Error("Input error required");
expect(result.issues[0]?.path).toEqual(["capture_path", "/metadata"]);
});
it("excludes a marked permission-denied path without losing its host error code", () => {
const error = redactExplicitFailure(
new AnalysisAccessDeniedError(
"inspect_web_network_capture",
"/declared-path-value/capture.har",
"EACCES",
),
["declared-path-value"],
);
expect(error._tag).toBe("AnalysisAccessDeniedError");
const projected = projectAnalysisError(error);
expect(projected).toMatchObject({
code: "access_denied",
category: "unavailable",
details: { system_code: "EACCES" },
});
expect(JSON.stringify(projected)).not.toContain("declared-path-value");
});
it("coarsens ordinary sensitive issue-path segments to their real parent", () => {
const input = new AnalysisInputError(
"inspect_web_network_capture",
undefined,
[
{
path: ["input", "mysecret", "child"],
reason: "unknown_argument",
message: "Unrecognized mysecret",
},
],
);
const result = redactExplicitFailure(input, ["secret"]);
if (!(result instanceof AnalysisInputError))
throw new Error("Input error required");
expect(result.issues[0]?.path).toEqual(["input"]);
expect(JSON.stringify(projectAnalysisError(result))).not.toContain("secret");
});
it("does not reinterpret an unknown argument name as a JSON pointer", () => {
const input = new AnalysisInputError(
"inspect_web_network_capture",
undefined,
[{ path: ["/ordinary/secret"], reason: "unknown_argument" }],
);
const result = redactExplicitFailure(input, ["secret"]);
if (!(result instanceof AnalysisInputError))
throw new Error("Input error required");
expect(result.issues[0]?.path).toEqual([]);
});