Files
openclaw/src/agents/pi-embedded-error-observation.test.ts

186 lines
7.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import * as loggingConfigModule from "../logging/config.js";
import {
buildApiErrorObservationFields,
buildTextObservationFields,
sanitizeForConsole,
} from "./pi-embedded-error-observation.js";
const OBSERVATION_BEARER_TOKEN = "sk-redact-test-token";
const OBSERVATION_COOKIE_VALUE = "session-cookie-token";
afterEach(() => {
vi.restoreAllMocks();
});
describe("buildApiErrorObservationFields", () => {
it("redacts request ids and exposes stable hashes instead of raw payloads", () => {
const observed = buildApiErrorObservationFields(
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"},"request_id":"req_overload"}',
);
expect(observed).toMatchObject({
rawErrorPreview: expect.stringContaining('"request_id":"sha256:'),
rawErrorHash: expect.stringMatching(/^sha256:/),
rawErrorFingerprint: expect.stringMatching(/^sha256:/),
providerErrorType: "overloaded_error",
providerErrorMessagePreview: "Overloaded",
requestIdHash: expect.stringMatching(/^sha256:/),
});
expect(observed.rawErrorPreview).not.toContain("req_overload");
});
it("forces token redaction for observation previews", () => {
const observed = buildApiErrorObservationFields(
`Authorization: Bearer ${OBSERVATION_BEARER_TOKEN}`,
);
expect(observed.rawErrorPreview).not.toContain(OBSERVATION_BEARER_TOKEN);
expect(observed.rawErrorPreview).toContain(OBSERVATION_BEARER_TOKEN.slice(0, 6));
expect(observed.rawErrorHash).toMatch(/^sha256:/);
});
it("redacts observation-only header and cookie formats", () => {
const observed = buildApiErrorObservationFields(
`x-api-key: ${OBSERVATION_BEARER_TOKEN} Cookie: session=${OBSERVATION_COOKIE_VALUE}`,
);
expect(observed.rawErrorPreview).not.toContain(OBSERVATION_COOKIE_VALUE);
expect(observed.rawErrorPreview).toContain("x-api-key: ***");
expect(observed.rawErrorPreview).toContain("Cookie: session=");
});
it("does not let cookie redaction consume unrelated fields on the same line", () => {
const observed = buildApiErrorObservationFields(
`Cookie: session=${OBSERVATION_COOKIE_VALUE} status=503 request_id=req_cookie`,
);
expect(observed.rawErrorPreview).toContain("Cookie: session=");
expect(observed.rawErrorPreview).toContain("status=503");
expect(observed.rawErrorPreview).toContain("request_id=sha256:");
});
it("builds sanitized generic text observation fields", () => {
const observed = buildTextObservationFields(
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"},"request_id":"req_prev"}',
);
expect(observed).toMatchObject({
textPreview: expect.stringContaining('"request_id":"sha256:'),
textHash: expect.stringMatching(/^sha256:/),
textFingerprint: expect.stringMatching(/^sha256:/),
providerErrorType: "overloaded_error",
providerErrorMessagePreview: "Overloaded",
requestIdHash: expect.stringMatching(/^sha256:/),
});
expect(observed.textPreview).not.toContain("req_prev");
});
it("redacts request ids in formatted plain-text errors", () => {
const observed = buildApiErrorObservationFields(
"LLM error overloaded_error: Overloaded (request_id: req_plaintext_123)",
);
expect(observed).toMatchObject({
rawErrorPreview: expect.stringContaining("request_id: sha256:"),
rawErrorFingerprint: expect.stringMatching(/^sha256:/),
requestIdHash: expect.stringMatching(/^sha256:/),
});
expect(observed.rawErrorPreview).not.toContain("req_plaintext_123");
});
it("keeps fingerprints stable across request ids for equivalent errors", () => {
const first = buildApiErrorObservationFields(
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"},"request_id":"req_001"}',
);
const second = buildApiErrorObservationFields(
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"},"request_id":"req_002"}',
);
expect(first.rawErrorFingerprint).toBe(second.rawErrorFingerprint);
expect(first.rawErrorHash).not.toBe(second.rawErrorHash);
});
it("truncates oversized raw and provider previews", () => {
const longMessage = "X".repeat(260);
const observed = buildApiErrorObservationFields(
`{"type":"error","error":{"type":"server_error","message":"${longMessage}"},"request_id":"req_long"}`,
);
expect(observed.rawErrorPreview).toBeDefined();
expect(observed.providerErrorMessagePreview).toBeDefined();
expect(observed.rawErrorPreview?.length).toBeLessThanOrEqual(401);
expect(observed.providerErrorMessagePreview?.length).toBeLessThanOrEqual(201);
expect(observed.providerErrorMessagePreview?.endsWith("…")).toBe(true);
});
it("caps oversized raw inputs before hashing and fingerprinting", () => {
const oversized = "X".repeat(70_000);
const bounded = "X".repeat(64_000);
expect(buildApiErrorObservationFields(oversized)).toMatchObject({
rawErrorHash: buildApiErrorObservationFields(bounded).rawErrorHash,
rawErrorFingerprint: buildApiErrorObservationFields(bounded).rawErrorFingerprint,
});
});
it("returns empty observation fields for empty input", () => {
expect(buildApiErrorObservationFields(undefined)).toEqual({});
expect(buildApiErrorObservationFields("")).toEqual({});
expect(buildApiErrorObservationFields(" ")).toEqual({});
});
it("re-reads configured redact patterns on each call", () => {
const readLoggingConfig = vi.spyOn(loggingConfigModule, "readLoggingConfig");
readLoggingConfig.mockReturnValueOnce(undefined);
readLoggingConfig.mockReturnValueOnce({
redactPatterns: [String.raw`\bcustom-secret-[A-Za-z0-9]+\b`],
});
const first = buildApiErrorObservationFields("custom-secret-abc123");
const second = buildApiErrorObservationFields("custom-secret-abc123");
expect(first.rawErrorPreview).toContain("custom-secret-abc123");
expect(second.rawErrorPreview).not.toContain("custom-secret-abc123");
expect(second.rawErrorPreview).toContain("custom");
});
it("fails closed when observation sanitization throws", () => {
vi.spyOn(loggingConfigModule, "readLoggingConfig").mockImplementation(() => {
throw new Error("boom");
});
expect(buildApiErrorObservationFields("request_id=req_123")).toEqual({});
expect(buildTextObservationFields("request_id=req_123")).toEqual({
textPreview: undefined,
textHash: undefined,
textFingerprint: undefined,
httpCode: undefined,
providerErrorType: undefined,
providerErrorMessagePreview: undefined,
requestIdHash: undefined,
});
});
it("ignores non-string configured redact patterns", () => {
vi.spyOn(loggingConfigModule, "readLoggingConfig").mockReturnValue({
redactPatterns: [
123 as never,
{ bad: true } as never,
String.raw`\bcustom-secret-[A-Za-z0-9]+\b`,
],
});
const observed = buildApiErrorObservationFields("custom-secret-abc123");
expect(observed.rawErrorPreview).not.toContain("custom-secret-abc123");
expect(observed.rawErrorPreview).toContain("custom");
});
});
describe("sanitizeForConsole", () => {
it("strips control characters from console-facing values", () => {
expect(sanitizeForConsole("run-1\nprovider\tmodel\rtest")).toBe("run-1 provider model test");
});
});