mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
186 lines
7.3 KiB
TypeScript
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");
|
|
});
|
|
});
|