import { describe, expect, it } from "vitest"; import { getAgentSettings, setAgentSettings } from "./agent-settings"; // --------------------------------------------------------------------------- // Helpers — drill into the written `[thinking]` / `[loop_control]` sections // --------------------------------------------------------------------------- function thinkingOf(raw: unknown): Record { const t = (raw as { thinking?: unknown })?.thinking; return t && typeof t === "object" && !Array.isArray(t) ? (t as Record) : {}; } function loopOf(raw: unknown): Record { const l = (raw as { loop_control?: unknown })?.loop_control; return l && typeof l === "object" && !Array.isArray(l) ? (l as Record) : {}; } // --------------------------------------------------------------------------- // Reading — legacy off values normalized to "off", "max" → "high" // --------------------------------------------------------------------------- describe("getAgentSettings — thinking.keep normalization", () => { it("normalizes a legacy boolean `false` to \"off\"", () => { expect(getAgentSettings({ thinking: { keep: false } }).thinking?.keep).toBe( "off" ); }); it("normalizes the other legacy off values (0, null, \"no\", \"none\") to \"off\"", () => { for (const v of [0, null, "no", "none"]) { expect( getAgentSettings({ thinking: { keep: v } }).thinking?.keep ).toBe("off"); } }); it("keeps \"all\" and \"off\" as-is on read", () => { expect(getAgentSettings({ thinking: { keep: "all" } }).thinking?.keep).toBe( "all" ); expect(getAgentSettings({ thinking: { keep: "off" } }).thinking?.keep).toBe( "off" ); }); it("keeps the default \"all\" when the key is absent", () => { expect(getAgentSettings({}).thinking?.keep).toBe("all"); }); }); describe("getAgentSettings — effort \"max\" read mapping", () => { it("normalizes a stored \"max\" to \"high\" on read", () => { expect(getAgentSettings({ thinking: { effort: "max" } }).thinking?.effort).toBe( "high" ); }); }); // --------------------------------------------------------------------------- // Writing — keep: "all" round-trips, "off" is written for legacy off values, // absent keep is not materialized // --------------------------------------------------------------------------- describe("setAgentSettings — keep serialization", () => { it("round-trips keep = \"all\"", () => { const raw = { thinking: { keep: "all" } }; const next = setAgentSettings(raw, { thinking: { keep: "all" } }); expect(thinkingOf(next).keep).toBe("all"); }); it("writes \"off\" (string) for a legacy keep = false, never a boolean", () => { const next = setAgentSettings({ thinking: { keep: false } }, {}); expect(thinkingOf(next).keep).toBe("off"); }); it("does not write the keep key when it was never set (absent in raw and patch)", () => { expect(thinkingOf(setAgentSettings({}, {}))).not.toHaveProperty("keep"); const next = setAgentSettings( { thinking: { enabled: true } }, { thinking: { effort: "high" } } ); expect(thinkingOf(next)).not.toHaveProperty("keep"); expect(thinkingOf(next).effort).toBe("high"); }); it("writes the key when an absent raw config's keep is explicitly toggled off", () => { const next = setAgentSettings({}, { thinking: { keep: "off" } }); expect(thinkingOf(next).keep).toBe("off"); }); }); // --------------------------------------------------------------------------- // loop_control — v2 default writes only max_attempts_per_step; legacyV1 opts // dual-writes; read fallback from the v1 key is preserved // --------------------------------------------------------------------------- describe("loop_control — v1/v2 key handling", () => { const raw = { loop_control: { max_attempts_per_step: 5, max_retries_per_step: 5 } }; it("default (v2): a save strips the legacy max_retries_per_step key", () => { const next = setAgentSettings(raw, { loop_control: { reserved_context_size: 60000 } }); expect(loopOf(next).max_attempts_per_step).toBe(5); expect(loopOf(next)).not.toHaveProperty("max_retries_per_step"); }); it("default (v2): a dual-write patch ends up with only the v2 key", () => { const next = setAgentSettings( raw, { loop_control: { max_attempts_per_step: 7, max_retries_per_step: 7 } } ); expect(loopOf(next).max_attempts_per_step).toBe(7); expect(loopOf(next)).not.toHaveProperty("max_retries_per_step"); }); it("legacyV1: both keys are written", () => { const next = setAgentSettings( raw, { loop_control: { max_attempts_per_step: 7, max_retries_per_step: 7 } }, { legacyV1: true } ); expect(loopOf(next).max_attempts_per_step).toBe(7); expect(loopOf(next).max_retries_per_step).toBe(7); }); it("read fallback keeps a max_retries_per_step-only config editable", () => { const s = getAgentSettings({ loop_control: { max_retries_per_step: 4 } }); expect(s.loop_control?.max_attempts_per_step).toBe(4); expect(s.loop_control?.max_retries_per_step).toBe(4); }); }); // --------------------------------------------------------------------------- // loop_control.compaction_max_attempts (kimi-code 0.43.0) — read via the loop // spread, written only when set, and 0/empty removes the key (upstream // default 5 applies) // --------------------------------------------------------------------------- describe("loop_control.compaction_max_attempts", () => { it("reads an existing value through the loop spread", () => { expect( getAgentSettings({ loop_control: { compaction_max_attempts: 7 } }) .loop_control?.compaction_max_attempts ).toBe(7); }); it("reads as undefined when the key is absent (UI falls back to 5)", () => { expect( getAgentSettings({ loop_control: {} }).loop_control?.compaction_max_attempts ).toBeUndefined(); }); it("writes the value when the patch sets it", () => { const next = setAgentSettings({}, { loop_control: { compaction_max_attempts: 8 }, }); expect(loopOf(next).compaction_max_attempts).toBe(8); }); it("drops the key for 0 and for a cleared (undefined) field", () => { const raw = { loop_control: { compaction_max_attempts: 6 } }; expect(loopOf(setAgentSettings(raw, { loop_control: { compaction_max_attempts: 0 }, }))).not.toHaveProperty("compaction_max_attempts"); expect(loopOf(setAgentSettings(raw, { loop_control: { compaction_max_attempts: undefined }, }))).not.toHaveProperty("compaction_max_attempts"); }); it("does not materialize the upstream default on an unrelated save", () => { // An absent key must stay absent — a plain save must not pin `5`. const next = setAgentSettings({ thinking: { effort: "high" } }, {}); expect(loopOf(next)).not.toHaveProperty("compaction_max_attempts"); // An existing value survives a save that does not touch it. const kept = setAgentSettings( { loop_control: { compaction_max_attempts: 4 } }, { thinking: { effort: "low" } } ); expect(loopOf(kept).compaction_max_attempts).toBe(4); }); }); // --------------------------------------------------------------------------- // [watch] enabled (kimi-code 2.0.1+) — top-level section, off by default since // 2.0.2; mirrors the [background] handling // --------------------------------------------------------------------------- function watchOf(raw: unknown): Record { const w = (raw as { watch?: unknown })?.watch; return w && typeof w === "object" && !Array.isArray(w) ? (w as Record) : {}; } describe("[watch] enabled", () => { it("reads as false when the section or key is absent (2.0.2 default)", () => { expect(getAgentSettings({}).watch?.enabled).toBe(false); expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(false); }); it("reads an explicit value", () => { expect(getAgentSettings({ watch: { enabled: true } }).watch?.enabled).toBe(true); expect(getAgentSettings({ watch: { enabled: false } }).watch?.enabled).toBe(false); }); it("writes the top-level [watch] section", () => { const next = setAgentSettings({}, { watch: { enabled: true } }) as { watch?: Record; }; expect(next.watch).toEqual({ enabled: true }); }); it("keeps the section on saves that do not touch it", () => { const next = setAgentSettings( { watch: { enabled: true } }, { thinking: { effort: "high" } } ) as { watch?: Record }; expect(next.watch).toEqual({ enabled: true }); }); it("does not disturb the other managed sections", () => { const next = setAgentSettings( { background: { max_running_tasks: 2 } }, { watch: { enabled: true } } ) as { background?: Record }; expect(next.background?.max_running_tasks).toBe(2); expect(watchOf(next).enabled).toBe(true); }); }); describe("permission.dangerous_command_guard", () => { it("reads as undefined when the key is absent (default on)", () => { expect( getAgentSettings({}).permission?.dangerous_command_guard ).toBeUndefined(); expect( getAgentSettings({ permission: { rules: [] } }).permission ?.dangerous_command_guard ).toBeUndefined(); }); it("reads an explicit true / false", () => { expect( getAgentSettings({ permission: { dangerous_command_guard: false } }) .permission?.dangerous_command_guard ).toBe(false); expect( getAgentSettings({ permission: { dangerous_command_guard: true } }) .permission?.dangerous_command_guard ).toBe(true); }); it("writes guard=false even with no rules (section kept)", () => { const next = setAgentSettings( {}, { permission: { rules: [], dangerous_command_guard: false } } ) as { permission?: Record }; expect(next.permission).toEqual({ dangerous_command_guard: false }); }); it("writes guard together with rules", () => { const rules = [{ decision: "allow" as const, pattern: "Read" }]; const next = setAgentSettings( {}, { permission: { rules, dangerous_command_guard: true } } ) as { permission?: Record }; expect(next.permission).toEqual({ rules, dangerous_command_guard: true, }); }); it("a rules-only update carries the existing guard through", () => { const raw = { permission: { dangerous_command_guard: false } }; const next = setAgentSettings(raw, { permission: { rules: [{ decision: "deny" as const, pattern: "Bash" }] }, }) as { permission?: Record }; expect(next.permission?.dangerous_command_guard).toBe(false); expect(next.permission?.rules).toHaveLength(1); }); it("omits an empty rules array; keeps an explicit guard on plain saves", () => { const raw = { permission: { dangerous_command_guard: true } }; const off = setAgentSettings(raw, { permission: { rules: [], dangerous_command_guard: false }, }) as { permission?: Record }; expect(off.permission).toEqual({ dangerous_command_guard: false }); expect(off.permission).not.toHaveProperty("rules"); // An explicit true is materialized on plain saves (harmless, and it // documents the state the UI toggle shows). const on = setAgentSettings(raw, {}) as { permission?: Record; }; expect(on.permission).toEqual({ dangerous_command_guard: true }); }); });