From 5d9e7c942cf4b64839cb7f66235bf33d98e94636 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 21:43:14 +0000 Subject: [PATCH] test: consolidate agent command and config scenarios --- src/config/redact-snapshot.test.ts | 647 ++++++++++++++--------------- 1 file changed, 308 insertions(+), 339 deletions(-) diff --git a/src/config/redact-snapshot.test.ts b/src/config/redact-snapshot.test.ts index e8cf2644625..a82976d0b97 100644 --- a/src/config/redact-snapshot.test.ts +++ b/src/config/redact-snapshot.test.ts @@ -47,81 +47,48 @@ function restoreRedactedValues( } describe("redactConfigSnapshot", () => { - it("redacts top-level token fields", () => { + it("redacts common secret field patterns across config sections", () => { const snapshot = makeSnapshot({ - gateway: { auth: { token: "my-super-secret-gateway-token-value" } }, - }); - const result = redactConfigSnapshot(snapshot); - expect(result.config).toEqual({ - gateway: { auth: { token: REDACTED_SENTINEL } }, - }); - }); - - it("redacts botToken in channel configs", () => { - const snapshot = makeSnapshot({ - channels: { - telegram: { botToken: "123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef" }, - slack: { botToken: "fake-slack-bot-token-placeholder-value" }, + gateway: { + auth: { + token: "my-super-secret-gateway-token-value", + password: "super-secret-password-value-here", + }, + }, + channels: { + telegram: { + botToken: "123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef", + webhookSecret: "telegram-webhook-secret-value-1234", + }, + slack: { + botToken: "fake-slack-bot-token-placeholder-value", + signingSecret: "slack-signing-secret-value-1234", + token: "secret-slack-token-value-here", + }, + feishu: { appSecret: "feishu-app-secret-value-here-1234" }, }, - }); - const result = redactConfigSnapshot(snapshot); - const channels = result.config.channels as Record>; - expect(channels.telegram.botToken).toBe(REDACTED_SENTINEL); - expect(channels.slack.botToken).toBe(REDACTED_SENTINEL); - }); - - it("redacts apiKey in model providers", () => { - const snapshot = makeSnapshot({ models: { providers: { openai: { apiKey: "sk-proj-abcdef1234567890ghij", baseUrl: "https://api.openai.com" }, }, }, + shortSecret: { token: "short" }, }); - const result = redactConfigSnapshot(snapshot); - const models = result.config.models as Record>>; - expect(models.providers.openai.apiKey).toBe(REDACTED_SENTINEL); - expect(models.providers.openai.baseUrl).toBe("https://api.openai.com"); - }); - it("redacts password fields", () => { - const snapshot = makeSnapshot({ - gateway: { auth: { password: "super-secret-password-value-here" } }, - }); const result = redactConfigSnapshot(snapshot); - const gw = result.config.gateway as Record>; - expect(gw.auth.password).toBe(REDACTED_SENTINEL); - }); + const cfg = result.config as typeof snapshot.config; - it("redacts appSecret fields", () => { - const snapshot = makeSnapshot({ - channels: { - feishu: { appSecret: "feishu-app-secret-value-here-1234" }, - }, - }); - const result = redactConfigSnapshot(snapshot); - const channels = result.config.channels as Record>; - expect(channels.feishu.appSecret).toBe(REDACTED_SENTINEL); - }); - - it("redacts signingSecret fields", () => { - const snapshot = makeSnapshot({ - channels: { - slack: { signingSecret: "slack-signing-secret-value-1234" }, - }, - }); - const result = redactConfigSnapshot(snapshot); - const channels = result.config.channels as Record>; - expect(channels.slack.signingSecret).toBe(REDACTED_SENTINEL); - }); - - it("redacts short secrets with same sentinel", () => { - const snapshot = makeSnapshot({ - gateway: { auth: { token: "short" } }, - }); - const result = redactConfigSnapshot(snapshot); - const gw = result.config.gateway as Record>; - expect(gw.auth.token).toBe(REDACTED_SENTINEL); + expect(cfg.gateway.auth.token).toBe(REDACTED_SENTINEL); + expect(cfg.gateway.auth.password).toBe(REDACTED_SENTINEL); + expect(cfg.channels.telegram.botToken).toBe(REDACTED_SENTINEL); + expect(cfg.channels.telegram.webhookSecret).toBe(REDACTED_SENTINEL); + expect(cfg.channels.slack.botToken).toBe(REDACTED_SENTINEL); + expect(cfg.channels.slack.signingSecret).toBe(REDACTED_SENTINEL); + expect(cfg.channels.slack.token).toBe(REDACTED_SENTINEL); + expect(cfg.channels.feishu.appSecret).toBe(REDACTED_SENTINEL); + expect(cfg.models.providers.openai.apiKey).toBe(REDACTED_SENTINEL); + expect(cfg.models.providers.openai.baseUrl).toBe("https://api.openai.com"); + expect(cfg.shortSecret.token).toBe(REDACTED_SENTINEL); }); it("preserves non-sensitive fields", () => { @@ -226,23 +193,15 @@ describe("redactConfigSnapshot", () => { expect(result.raw).toContain(REDACTED_SENTINEL); }); - it("redacts parsed object as well", () => { - const config = { + it("redacts parsed and resolved objects", () => { + const snapshot = makeSnapshot({ channels: { discord: { token: "MTIzNDU2Nzg5MDEyMzQ1Njc4.GaBcDe.FgH" } }, - }; - const snapshot = makeSnapshot(config); + gateway: { auth: { token: "supersecrettoken123456" } }, + }); const result = redactConfigSnapshot(snapshot); const parsed = result.parsed as Record>>; - expect(parsed.channels.discord.token).toBe(REDACTED_SENTINEL); - }); - - it("redacts resolved object as well", () => { - const config = { - gateway: { auth: { token: "supersecrettoken123456" } }, - }; - const snapshot = makeSnapshot(config); - const result = redactConfigSnapshot(snapshot); const resolved = result.resolved as Record>>; + expect(parsed.channels.discord.token).toBe(REDACTED_SENTINEL); expect(resolved.gateway.auth.token).toBe(REDACTED_SENTINEL); }); @@ -303,17 +262,6 @@ describe("redactConfigSnapshot", () => { expect(channels.slack.accounts.workspace2.appToken).toBe(REDACTED_SENTINEL); }); - it("handles webhookSecret field", () => { - const snapshot = makeSnapshot({ - channels: { - telegram: { webhookSecret: "telegram-webhook-secret-value-1234" }, - }, - }); - const result = redactConfigSnapshot(snapshot); - const channels = result.config.channels as Record>; - expect(channels.telegram.webhookSecret).toBe(REDACTED_SENTINEL); - }); - it("redacts env vars that look like secrets", () => { const snapshot = makeSnapshot({ env: { @@ -330,41 +278,45 @@ describe("redactConfigSnapshot", () => { expect(env.vars.OPENAI_API_KEY).toBe(REDACTED_SENTINEL); }); - it("does NOT redact numeric 'tokens' fields (token regex fix)", () => { - const snapshot = makeSnapshot({ - memory: { tokens: 8192 }, - }); - const result = redactConfigSnapshot(snapshot); - const memory = result.config.memory as Record; - expect(memory.tokens).toBe(8192); - }); + it("respects token-name redaction boundaries", () => { + const cases = [ + { + name: "does not redact numeric tokens field", + snapshot: makeSnapshot({ memory: { tokens: 8192 } }), + assert: (config: Record) => { + expect((config.memory as Record).tokens).toBe(8192); + }, + }, + { + name: "does not redact softThresholdTokens", + snapshot: makeSnapshot({ compaction: { softThresholdTokens: 50000 } }), + assert: (config: Record) => { + expect((config.compaction as Record).softThresholdTokens).toBe(50000); + }, + }, + { + name: "does not redact string tokens field", + snapshot: makeSnapshot({ memory: { tokens: "should-not-be-redacted" } }), + assert: (config: Record) => { + expect((config.memory as Record).tokens).toBe("should-not-be-redacted"); + }, + }, + { + name: "still redacts singular token field", + snapshot: makeSnapshot({ + channels: { slack: { token: "secret-slack-token-value-here" } }, + }), + assert: (config: Record) => { + const channels = config.channels as Record>; + expect(channels.slack.token).toBe(REDACTED_SENTINEL); + }, + }, + ] as const; - it("does NOT redact 'softThresholdTokens' (token regex fix)", () => { - const snapshot = makeSnapshot({ - compaction: { softThresholdTokens: 50000 }, - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - const compaction = config.compaction as Record; - expect(compaction.softThresholdTokens).toBe(50000); - }); - - it("does NOT redact string 'tokens' field either", () => { - const snapshot = makeSnapshot({ - memory: { tokens: "should-not-be-redacted" }, - }); - const result = redactConfigSnapshot(snapshot); - const memory = result.config.memory as Record; - expect(memory.tokens).toBe("should-not-be-redacted"); - }); - - it("still redacts 'token' (singular) fields", () => { - const snapshot = makeSnapshot({ - channels: { slack: { token: "secret-slack-token-value-here" } }, - }); - const result = redactConfigSnapshot(snapshot); - const channels = result.config.channels as Record>; - expect(channels.slack.token).toBe(REDACTED_SENTINEL); + for (const testCase of cases) { + const result = redactConfigSnapshot(testCase.snapshot); + testCase.assert(result.config as Record); + } }); it("uses uiHints to determine sensitivity", () => { @@ -439,234 +391,251 @@ describe("redactConfigSnapshot", () => { expect(config.plugins.entries["voice-call"].config.apiToken).toBe("not-secret-on-purpose"); }); - it("handles nested values properly (roundtrip)", () => { - const snapshot = makeSnapshot({ - custom1: { anykey: { mySecret: "this-is-a-custom-secret-value" } }, - custom2: [{ mySecret: "this-is-a-custom-secret-value" }], - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.custom1.anykey.mySecret).toBe(REDACTED_SENTINEL); - expect(config.custom2[0].mySecret).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.custom1.anykey.mySecret).toBe("this-is-a-custom-secret-value"); - expect(restored.custom2[0].mySecret).toBe("this-is-a-custom-secret-value"); - }); - - it("handles nested values properly with hints (roundtrip)", () => { - const hints: ConfigUiHints = { - "custom1.*.mySecret": { sensitive: true }, - "custom2[].mySecret": { sensitive: true }, - }; - const snapshot = makeSnapshot({ - custom1: { anykey: { mySecret: "this-is-a-custom-secret-value" } }, - custom2: [{ mySecret: "this-is-a-custom-secret-value" }], - }); - const result = redactConfigSnapshot(snapshot, hints); - const config = result.config as typeof snapshot.config; - expect(config.custom1.anykey.mySecret).toBe(REDACTED_SENTINEL); - expect(config.custom2[0].mySecret).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.custom1.anykey.mySecret).toBe("this-is-a-custom-secret-value"); - expect(restored.custom2[0].mySecret).toBe("this-is-a-custom-secret-value"); - }); - - it("handles records that are directly sensitive (roundtrip)", () => { - const snapshot = makeSnapshot({ - custom: { token: "this-is-a-custom-secret-value", mySecret: "this-is-a-custom-secret-value" }, - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.custom.token).toBe(REDACTED_SENTINEL); - expect(config.custom.mySecret).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.custom.token).toBe("this-is-a-custom-secret-value"); - expect(restored.custom.mySecret).toBe("this-is-a-custom-secret-value"); - }); - - it("handles records that are directly sensitive with hints (roundtrip)", () => { - const hints: ConfigUiHints = { - "custom.*": { sensitive: true }, - }; - const snapshot = makeSnapshot({ - custom: { - anykey: "this-is-a-custom-secret-value", - mySecret: "this-is-a-custom-secret-value", + it("round-trips nested and array sensitivity cases", () => { + const cases: Array<{ + name: string; + snapshot: TestSnapshot>; + hints?: ConfigUiHints; + assert: (params: { + redacted: Record; + restored: Record; + }) => void; + }> = [ + { + name: "nested values (schema)", + snapshot: makeSnapshot({ + custom1: { anykey: { mySecret: "this-is-a-custom-secret-value" } }, + custom2: [{ mySecret: "this-is-a-custom-secret-value" }], + }), + assert: ({ redacted, restored }) => { + const cfg = redacted as Record>; + const cfgCustom2 = cfg.custom2 as unknown[]; + expect(cfgCustom2.length).toBeGreaterThan(0); + expect((cfg.custom1.anykey as Record).mySecret).toBe(REDACTED_SENTINEL); + expect((cfgCustom2[0] as Record).mySecret).toBe(REDACTED_SENTINEL); + const out = restored as Record>; + const outCustom2 = out.custom2 as unknown[]; + expect(outCustom2.length).toBeGreaterThan(0); + expect((out.custom1.anykey as Record).mySecret).toBe( + "this-is-a-custom-secret-value", + ); + expect((outCustom2[0] as Record).mySecret).toBe( + "this-is-a-custom-secret-value", + ); + }, }, - }); - const result = redactConfigSnapshot(snapshot, hints); - const config = result.config as typeof snapshot.config; - expect(config.custom.anykey).toBe(REDACTED_SENTINEL); - expect(config.custom.mySecret).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.custom.anykey).toBe("this-is-a-custom-secret-value"); - expect(restored.custom.mySecret).toBe("this-is-a-custom-secret-value"); - }); - - it("handles arrays that are directly sensitive (roundtrip)", () => { - const snapshot = makeSnapshot({ - token: ["this-is-a-custom-secret-value", "this-is-a-custom-secret-value"], - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.token[0]).toBe(REDACTED_SENTINEL); - expect(config.token[1]).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.token[0]).toBe("this-is-a-custom-secret-value"); - expect(restored.token[1]).toBe("this-is-a-custom-secret-value"); - }); - - it("handles arrays that are directly sensitive with hints (roundtrip)", () => { - const hints: ConfigUiHints = { - "custom[]": { sensitive: true }, - }; - const snapshot = makeSnapshot({ - custom: ["this-is-a-custom-secret-value", "this-is-a-custom-secret-value"], - }); - const result = redactConfigSnapshot(snapshot, hints); - const config = result.config as typeof snapshot.config; - expect(config.custom[0]).toBe(REDACTED_SENTINEL); - expect(config.custom[1]).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.custom[0]).toBe("this-is-a-custom-secret-value"); - expect(restored.custom[1]).toBe("this-is-a-custom-secret-value"); - }); - - it("handles arrays that are not sensitive (roundtrip)", () => { - const snapshot = makeSnapshot({ - harmless: ["this-is-a-custom-harmless-value", "this-is-a-custom-secret-looking-value"], - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.harmless[0]).toBe("this-is-a-custom-harmless-value"); - expect(config.harmless[1]).toBe("this-is-a-custom-secret-looking-value"); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.harmless[0]).toBe("this-is-a-custom-harmless-value"); - expect(restored.harmless[1]).toBe("this-is-a-custom-secret-looking-value"); - }); - - it("handles arrays that are not sensitive with hints (roundtrip)", () => { - const hints: ConfigUiHints = { - "custom[]": { sensitive: false }, - }; - const snapshot = makeSnapshot({ - custom: ["this-is-a-custom-harmless-value", "this-is-a-custom-secret-value"], - }); - const result = redactConfigSnapshot(snapshot, hints); - const config = result.config as typeof snapshot.config; - expect(config.custom[0]).toBe("this-is-a-custom-harmless-value"); - expect(config.custom[1]).toBe("this-is-a-custom-secret-value"); - const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.custom[0]).toBe("this-is-a-custom-harmless-value"); - expect(restored.custom[1]).toBe("this-is-a-custom-secret-value"); - }); - - it("handles deep arrays that are directly sensitive (roundtrip)", () => { - const snapshot = makeSnapshot({ - nested: { - level: { + { + name: "nested values (uiHints)", + hints: { + "custom1.*.mySecret": { sensitive: true }, + "custom2[].mySecret": { sensitive: true }, + }, + snapshot: makeSnapshot({ + custom1: { anykey: { mySecret: "this-is-a-custom-secret-value" } }, + custom2: [{ mySecret: "this-is-a-custom-secret-value" }], + }), + assert: ({ redacted, restored }) => { + const cfg = redacted as Record>; + const cfgCustom2 = cfg.custom2 as unknown[]; + expect(cfgCustom2.length).toBeGreaterThan(0); + expect((cfg.custom1.anykey as Record).mySecret).toBe(REDACTED_SENTINEL); + expect((cfgCustom2[0] as Record).mySecret).toBe(REDACTED_SENTINEL); + const out = restored as Record>; + const outCustom2 = out.custom2 as unknown[]; + expect(outCustom2.length).toBeGreaterThan(0); + expect((out.custom1.anykey as Record).mySecret).toBe( + "this-is-a-custom-secret-value", + ); + expect((outCustom2[0] as Record).mySecret).toBe( + "this-is-a-custom-secret-value", + ); + }, + }, + { + name: "directly sensitive records and arrays", + snapshot: makeSnapshot({ + custom: { + token: "this-is-a-custom-secret-value", + mySecret: "this-is-a-custom-secret-value", + }, token: ["this-is-a-custom-secret-value", "this-is-a-custom-secret-value"], - }, - }, - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.nested.level.token[0]).toBe(REDACTED_SENTINEL); - expect(config.nested.level.token[1]).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.nested.level.token[0]).toBe("this-is-a-custom-secret-value"); - expect(restored.nested.level.token[1]).toBe("this-is-a-custom-secret-value"); - }); + }), + assert: ({ redacted, restored }) => { + const cfg = redacted; + const custom = cfg.custom as Record; + expect(custom.token).toBe(REDACTED_SENTINEL); + expect(custom.mySecret).toBe(REDACTED_SENTINEL); + expect((cfg.token as unknown[])[0]).toBe(REDACTED_SENTINEL); + expect((cfg.token as unknown[])[1]).toBe(REDACTED_SENTINEL); - it("handles deep arrays that are directly sensitive with hints (roundtrip)", () => { - const hints: ConfigUiHints = { - "nested.level.custom[]": { sensitive: true }, - }; - const snapshot = makeSnapshot({ - nested: { - level: { - custom: ["this-is-a-custom-secret-value", "this-is-a-custom-secret-value"], + const out = restored; + const restoredCustom = out.custom as Record; + expect(restoredCustom.token).toBe("this-is-a-custom-secret-value"); + expect(restoredCustom.mySecret).toBe("this-is-a-custom-secret-value"); + expect((out.token as unknown[])[0]).toBe("this-is-a-custom-secret-value"); + expect((out.token as unknown[])[1]).toBe("this-is-a-custom-secret-value"); }, }, - }); - const result = redactConfigSnapshot(snapshot, hints); - const config = result.config as typeof snapshot.config; - expect(config.nested.level.custom[0]).toBe(REDACTED_SENTINEL); - expect(config.nested.level.custom[1]).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.nested.level.custom[0]).toBe("this-is-a-custom-secret-value"); - expect(restored.nested.level.custom[1]).toBe("this-is-a-custom-secret-value"); - }); + { + name: "directly sensitive records and arrays (uiHints)", + hints: { + "custom.*": { sensitive: true }, + "customArray[]": { sensitive: true }, + }, + snapshot: makeSnapshot({ + custom: { + anykey: "this-is-a-custom-secret-value", + mySecret: "this-is-a-custom-secret-value", + }, + customArray: ["this-is-a-custom-secret-value", "this-is-a-custom-secret-value"], + }), + assert: ({ redacted, restored }) => { + const cfg = redacted; + const custom = cfg.custom as Record; + expect(custom.anykey).toBe(REDACTED_SENTINEL); + expect(custom.mySecret).toBe(REDACTED_SENTINEL); + expect((cfg.customArray as unknown[])[0]).toBe(REDACTED_SENTINEL); + expect((cfg.customArray as unknown[])[1]).toBe(REDACTED_SENTINEL); - it("handles deep non-string arrays that are directly sensitive (roundtrip)", () => { - const snapshot = makeSnapshot({ - nested: { - level: { - token: [42, 815], + const out = restored; + const restoredCustom = out.custom as Record; + expect(restoredCustom.anykey).toBe("this-is-a-custom-secret-value"); + expect(restoredCustom.mySecret).toBe("this-is-a-custom-secret-value"); + expect((out.customArray as unknown[])[0]).toBe("this-is-a-custom-secret-value"); + expect((out.customArray as unknown[])[1]).toBe("this-is-a-custom-secret-value"); }, }, - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.nested.level.token[0]).toBe(42); - expect(config.nested.level.token[1]).toBe(815); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.nested.level.token[0]).toBe(42); - expect(restored.nested.level.token[1]).toBe(815); - }); + { + name: "non-sensitive arrays remain unchanged", + hints: { + "custom[]": { sensitive: false }, + }, + snapshot: makeSnapshot({ + harmless: ["this-is-a-custom-harmless-value", "this-is-a-custom-secret-looking-value"], + custom: ["this-is-a-custom-harmless-value", "this-is-a-custom-secret-value"], + }), + assert: ({ redacted, restored }) => { + const cfg = redacted; + expect((cfg.harmless as unknown[])[0]).toBe("this-is-a-custom-harmless-value"); + expect((cfg.harmless as unknown[])[1]).toBe("this-is-a-custom-secret-looking-value"); + expect((cfg.custom as unknown[])[0]).toBe("this-is-a-custom-harmless-value"); + expect((cfg.custom as unknown[])[1]).toBe("this-is-a-custom-secret-value"); - it("handles deep non-string arrays that are directly sensitive with hints (roundtrip)", () => { - const hints: ConfigUiHints = { - "nested.level.custom[]": { sensitive: true }, - }; - const snapshot = makeSnapshot({ - nested: { - level: { - custom: [42, 815], + const out = restored; + expect((out.harmless as unknown[])[0]).toBe("this-is-a-custom-harmless-value"); + expect((out.harmless as unknown[])[1]).toBe("this-is-a-custom-secret-looking-value"); + expect((out.custom as unknown[])[0]).toBe("this-is-a-custom-harmless-value"); + expect((out.custom as unknown[])[1]).toBe("this-is-a-custom-secret-value"); }, }, - }); - const result = redactConfigSnapshot(snapshot, hints); - const config = result.config as typeof snapshot.config; - expect(config.nested.level.custom[0]).toBe(42); - expect(config.nested.level.custom[1]).toBe(815); - const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.nested.level.custom[0]).toBe(42); - expect(restored.nested.level.custom[1]).toBe(815); - }); + { + name: "deep schema-sensitive arrays and upstream-sensitive paths", + snapshot: makeSnapshot({ + nested: { + level: { + token: ["this-is-a-custom-secret-value", "this-is-a-custom-secret-value"], + harmless: ["value", "value"], + }, + password: { + harmless: ["value", "value"], + }, + }, + }), + assert: ({ redacted, restored }) => { + const cfg = redacted as Record>>; + expect((cfg.nested.level.token as unknown[])[0]).toBe(REDACTED_SENTINEL); + expect((cfg.nested.level.token as unknown[])[1]).toBe(REDACTED_SENTINEL); + expect((cfg.nested.level.harmless as unknown[])[0]).toBe("value"); + expect((cfg.nested.level.harmless as unknown[])[1]).toBe("value"); + expect((cfg.nested.password.harmless as unknown[])[0]).toBe(REDACTED_SENTINEL); + expect((cfg.nested.password.harmless as unknown[])[1]).toBe(REDACTED_SENTINEL); - it("handles deep arrays that are upstream sensitive (roundtrip)", () => { - const snapshot = makeSnapshot({ - nested: { - password: { - harmless: ["value", "value"], + const out = restored as Record>>; + expect((out.nested.level.token as unknown[])[0]).toBe("this-is-a-custom-secret-value"); + expect((out.nested.level.token as unknown[])[1]).toBe("this-is-a-custom-secret-value"); + expect((out.nested.level.harmless as unknown[])[0]).toBe("value"); + expect((out.nested.level.harmless as unknown[])[1]).toBe("value"); + expect((out.nested.password.harmless as unknown[])[0]).toBe("value"); + expect((out.nested.password.harmless as unknown[])[1]).toBe("value"); }, }, - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.nested.password.harmless[0]).toBe(REDACTED_SENTINEL); - expect(config.nested.password.harmless[1]).toBe(REDACTED_SENTINEL); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.nested.password.harmless[0]).toBe("value"); - expect(restored.nested.password.harmless[1]).toBe("value"); - }); + { + name: "deep non-string arrays on schema-sensitive paths remain unchanged", + snapshot: makeSnapshot({ + nested: { + level: { + token: [42, 815], + }, + }, + }), + assert: ({ redacted, restored }) => { + const cfg = redacted as Record>>; + expect((cfg.nested.level.token as unknown[])[0]).toBe(42); + expect((cfg.nested.level.token as unknown[])[1]).toBe(815); - it("handles deep arrays that are not sensitive (roundtrip)", () => { - const snapshot = makeSnapshot({ - nested: { - level: { - harmless: ["value", "value"], + const out = restored as Record>>; + expect((out.nested.level.token as unknown[])[0]).toBe(42); + expect((out.nested.level.token as unknown[])[1]).toBe(815); }, }, - }); - const result = redactConfigSnapshot(snapshot); - const config = result.config as typeof snapshot.config; - expect(config.nested.level.harmless[0]).toBe("value"); - expect(config.nested.level.harmless[1]).toBe("value"); - const restored = restoreRedactedValues(result.config, snapshot.config); - expect(restored.nested.level.harmless[0]).toBe("value"); - expect(restored.nested.level.harmless[1]).toBe("value"); + { + name: "deep arrays respect uiHints sensitivity", + hints: { + "nested.level.custom[]": { sensitive: true }, + }, + snapshot: makeSnapshot({ + nested: { + level: { + custom: ["this-is-a-custom-secret-value", "this-is-a-custom-secret-value"], + }, + }, + }), + assert: ({ redacted, restored }) => { + const cfg = redacted as Record>>; + expect((cfg.nested.level.custom as unknown[])[0]).toBe(REDACTED_SENTINEL); + expect((cfg.nested.level.custom as unknown[])[1]).toBe(REDACTED_SENTINEL); + + const out = restored as Record>>; + expect((out.nested.level.custom as unknown[])[0]).toBe("this-is-a-custom-secret-value"); + expect((out.nested.level.custom as unknown[])[1]).toBe("this-is-a-custom-secret-value"); + }, + }, + { + name: "deep non-string arrays respect uiHints sensitivity", + hints: { + "nested.level.custom[]": { sensitive: true }, + }, + snapshot: makeSnapshot({ + nested: { + level: { + custom: [42, 815], + }, + }, + }), + assert: ({ redacted, restored }) => { + const cfg = redacted as Record>>; + expect((cfg.nested.level.custom as unknown[])[0]).toBe(42); + expect((cfg.nested.level.custom as unknown[])[1]).toBe(815); + + const out = restored as Record>>; + expect((out.nested.level.custom as unknown[])[0]).toBe(42); + expect((out.nested.level.custom as unknown[])[1]).toBe(815); + }, + }, + ]; + + for (const testCase of cases) { + const redacted = redactConfigSnapshot(testCase.snapshot, testCase.hints); + const restored = restoreRedactedValues( + redacted.config, + testCase.snapshot.config, + testCase.hints, + ); + testCase.assert({ + redacted: redacted.config as Record, + restored: restored as Record, + }); + } }); it("respects sensitive:false in uiHints even for regex-matching paths", () => { @@ -793,12 +762,12 @@ describe("restoreRedactedValues", () => { expect(restoreRedactedValues_orig(incoming, original).ok).toBe(false); }); - it("handles null and undefined inputs", () => { - expect(restoreRedactedValues_orig(null, { token: "x" }).ok).toBe(false); - expect(restoreRedactedValues_orig(undefined, { token: "x" }).ok).toBe(false); - }); - - it("rejects non-object inputs", () => { + it("rejects invalid restore inputs", () => { + const invalidInputs = [null, undefined, "token-value"] as const; + for (const input of invalidInputs) { + const result = restoreRedactedValues_orig(input, { token: "x" }); + expect(result.ok).toBe(false); + } expect(restoreRedactedValues_orig("token-value", { token: "x" })).toEqual({ ok: false, error: "input not an object",