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["channels.googlechat.serviceAccount"]?.sensitive).toBe(true); expect(hints["gateway.auth.token"]?.sensitive).toBe(true); expect(hints["models.providers.*.headers.*"]?.sensitive).toBe(true); expect(hints["skills.entries.*.apiKey"]?.sensitive).toBe(true); }); });