/** * config.test.ts * -------------- * Unit tests for the layered config resolver and the default-path helper. * Run via `bun test` (or `bun run test`). */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { DEFAULT_CONFIG, defaultConfigPath, resolveConfig } from "../src/config.ts"; import type { ConfigOverrides } from "../src/config.ts"; import { CliError } from "../src/errors.ts"; const NONE: ConfigOverrides = { moveInterval: undefined, checkInterval: undefined, stepDelay: undefined, stepCount: undefined, verbose: undefined, }; describe("resolveConfig", () => { test("returns DEFAULT_CONFIG when neither layer supplies a value", () => { expect(resolveConfig(null, NONE)).toEqual(DEFAULT_CONFIG); }); test("CLI value wins over file value", () => { const file: ConfigOverrides = { ...NONE, moveInterval: 60 }; const cli: ConfigOverrides = { ...NONE, moveInterval: 30 }; const cfg = resolveConfig(file, cli); expect(cfg.moveInterval).toBe(30 * 1000); // CLI 30s -> 30000ms }); test("file value wins over default when CLI is undefined", () => { const file: ConfigOverrides = { ...NONE, moveInterval: 60 }; const cfg = resolveConfig(file, NONE); expect(cfg.moveInterval).toBe(60 * 1000); // file 60s -> 60000ms }); test("seconds-to-ms conversion at the boundary for time-valued fields", () => { const cli: ConfigOverrides = { ...NONE, moveInterval: 5, checkInterval: 2 }; const cfg = resolveConfig(null, cli); expect(cfg.moveInterval).toBe(5000); expect(cfg.checkInterval).toBe(2000); }); test("stepDelay and stepCount pass through untouched (no unit conversion)", () => { const cli: ConfigOverrides = { ...NONE, stepDelay: 75, stepCount: 100 }; const cfg = resolveConfig(null, cli); expect(cfg.stepDelay).toBe(75); expect(cfg.stepCount).toBe(100); }); test("verbose: CLI true wins over file false", () => { const cfg = resolveConfig( { ...NONE, verbose: false }, { ...NONE, verbose: true }, ); expect(cfg.verbose).toBe(true); }); test("verbose: file true wins over default (no CLI)", () => { const cfg = resolveConfig({ ...NONE, verbose: true }, NONE); expect(cfg.verbose).toBe(true); }); test("verbose: file false wins over default (no CLI)", () => { const cfg = resolveConfig({ ...NONE, verbose: false }, NONE); expect(cfg.verbose).toBe(false); }); test("verbose: falls back to DEFAULT_CONFIG.verbose when neither set", () => { const cfg = resolveConfig(null, NONE); expect(cfg.verbose).toBe(DEFAULT_CONFIG.verbose); }); }); describe("defaultConfigPath", () => { let savedXdg: string | undefined; let savedHome: string | undefined; beforeEach(() => { savedXdg = process.env.XDG_CONFIG_HOME; savedHome = process.env.HOME; }); afterEach(() => { if (savedXdg === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = savedXdg; if (savedHome === undefined) delete process.env.HOME; else process.env.HOME = savedHome; }); test("honors XDG_CONFIG_HOME when set", () => { process.env.XDG_CONFIG_HOME = "/custom/xdg"; process.env.HOME = "/should/not/be/used"; expect(defaultConfigPath()).toBe("/custom/xdg/move/config.json"); }); test("falls back to $HOME/.config when XDG_CONFIG_HOME is unset", () => { delete process.env.XDG_CONFIG_HOME; process.env.HOME = "/u/test"; expect(defaultConfigPath()).toBe("/u/test/.config/move/config.json"); }); test("treats empty XDG_CONFIG_HOME as unset (per XDG spec)", () => { process.env.XDG_CONFIG_HOME = ""; process.env.HOME = "/u/test"; expect(defaultConfigPath()).toBe("/u/test/.config/move/config.json"); }); test("throws CliError when both XDG_CONFIG_HOME and HOME are unset", () => { delete process.env.XDG_CONFIG_HOME; delete process.env.HOME; expect(() => defaultConfigPath()).toThrow(CliError); }); test("throws CliError when both XDG_CONFIG_HOME and HOME are empty", () => { process.env.XDG_CONFIG_HOME = ""; process.env.HOME = ""; expect(() => defaultConfigPath()).toThrow(CliError); }); });