Files
openclaw/src/config/schema.hints.test.ts

140 lines
5.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { z } from "zod";
import { __test__, isSensitiveConfigPath } from "./schema.hints.js";
import { OpenClawSchema } from "./zod-schema.js";
import { sensitive } from "./zod-schema.sensitive.js";
const { mapSensitivePaths } = __test__;
describe("isSensitiveConfigPath", () => {
it("matches whitelist suffixes case-insensitively", () => {
const whitelistedPaths = [
"maxTokens",
"maxOutputTokens",
"maxInputTokens",
"maxCompletionTokens",
"contextTokens",
"totalTokens",
"tokenCount",
"tokenLimit",
"tokenBudget",
"channels.irc.nickserv.passwordFile",
];
for (const path of whitelistedPaths) {
expect(isSensitiveConfigPath(path)).toBe(false);
expect(isSensitiveConfigPath(path.toUpperCase())).toBe(false);
}
});
it("keeps true sensitive keys redacted", () => {
expect(isSensitiveConfigPath("channels.slack.token")).toBe(true);
expect(isSensitiveConfigPath("models.providers.openai.apiKey")).toBe(true);
expect(isSensitiveConfigPath("channels.irc.nickserv.password")).toBe(true);
});
});
describe("mapSensitivePaths", () => {
it("should detect sensitive fields nested inside all structural Zod types", () => {
const GrandSchema = z.object({
simple: z.string().register(sensitive).optional(),
simpleReversed: z.string().optional().register(sensitive),
nested: z.object({
nested: z.string().register(sensitive),
}),
list: z.array(z.string().register(sensitive)),
listOfObjects: z.array(z.object({ nested: z.string().register(sensitive) })),
headers: z.record(z.string(), z.string().register(sensitive)),
headersNested: z.record(z.string(), z.object({ nested: z.string().register(sensitive) })),
auth: z.union([
z.object({ type: z.literal("none") }),
z.object({ type: z.literal("token"), value: z.string().register(sensitive) }),
]),
merged: z
.object({ id: z.string() })
.and(z.object({ nested: z.string().register(sensitive) })),
});
const result = mapSensitivePaths(GrandSchema, "", {});
expect(result["simple"]?.sensitive).toBe(true);
expect(result["simpleReversed"]?.sensitive).toBe(true);
expect(result["nested.nested"]?.sensitive).toBe(true);
expect(result["list[]"]?.sensitive).toBe(true);
expect(result["listOfObjects[].nested"]?.sensitive).toBe(true);
expect(result["headers.*"]?.sensitive).toBe(true);
expect(result["headersNested.*.nested"]?.sensitive).toBe(true);
expect(result["auth.value"]?.sensitive).toBe(true);
expect(result["merged.nested"]?.sensitive).toBe(true);
});
it("should not detect non-sensitive fields nested inside all structural Zod types", () => {
const GrandSchema = z.object({
simple: z.string().optional(),
simpleReversed: z.string().optional(),
nested: z.object({
nested: z.string(),
}),
list: z.array(z.string()),
listOfObjects: z.array(z.object({ nested: z.string() })),
headers: z.record(z.string(), z.string()),
headersNested: z.record(z.string(), z.object({ nested: z.string() })),
auth: z.union([
z.object({ type: z.literal("none") }),
z.object({ type: z.literal("token"), value: z.string() }),
]),
merged: z.object({ id: z.string() }).and(z.object({ nested: z.string() })),
});
const result = mapSensitivePaths(GrandSchema, "", {});
expect(result["simple"]?.sensitive).toBe(undefined);
expect(result["simpleReversed"]?.sensitive).toBe(undefined);
expect(result["nested.nested"]?.sensitive).toBe(undefined);
expect(result["list[]"]?.sensitive).toBe(undefined);
expect(result["listOfObjects[].nested"]?.sensitive).toBe(undefined);
expect(result["headers.*"]?.sensitive).toBe(undefined);
expect(result["headersNested.*.nested"]?.sensitive).toBe(undefined);
expect(result["auth.value"]?.sensitive).toBe(undefined);
expect(result["merged.nested"]?.sensitive).toBe(undefined);
});
it("maps sensitive fields nested under object catchall schemas", () => {
const schema = z.object({
custom: z.object({}).catchall(
z.object({
apiKey: z.string().register(sensitive),
label: z.string(),
}),
),
});
const result = mapSensitivePaths(schema, "", {});
expect(result["custom.*.apiKey"]?.sensitive).toBe(true);
expect(result["custom.*.label"]?.sensitive).toBe(undefined);
});
it("does not mark plain catchall values sensitive by default", () => {
const schema = z.object({
env: z.object({}).catchall(z.string()),
});
const result = mapSensitivePaths(schema, "", {});
expect(result["env.*"]?.sensitive).toBe(undefined);
});
it("main schema yields correct hints (samples)", () => {
const schema = OpenClawSchema.toJSONSchema({
target: "draft-07",
unrepresentable: "any",
});
schema.title = "OpenClawConfig";
const hints = mapSensitivePaths(OpenClawSchema, "", {});
expect(hints["agents.defaults.memorySearch.remote.apiKey"]?.sensitive).toBe(true);
expect(hints["agents.list[].memorySearch.remote.apiKey"]?.sensitive).toBe(true);
expect(hints["channels.discord.accounts.*.token"]?.sensitive).toBe(true);
expect(hints["gateway.auth.token"]?.sensitive).toBe(true);
expect(hints["skills.entries.*.apiKey"]?.sensitive).toBe(true);
});
});