Files
Move/tests/config.test.ts
T
nokeo08 ec33648e74 Remove stepCount/stepSize; patterns own their geometry
The stepCount and stepSize knobs were two controls for one quantity users
actually care about (reach), and the number of steps is an implementation
detail nobody meaningfully tunes. Each pattern has a natural size and
resolution — a jitter is inherently small, an arc a broad curve — so those
now live as constants in each strategy rather than as global config.

- strategies.ts: each pattern defines its own step count and size; MoveContext
  drops `config` down to pure geometry (start/width/height/rng), and the
  module no longer imports Config at all (dissolving the type-only-import
  cycle workaround). line stays byte-for-byte: 250 one-pixel steps.
- executor.ts: executePath takes `config` for pacing (stepDelay); the path
  itself needs nothing from it.
- config.ts / cli.ts / move.ts / config.default.json: drop stepCount and
  stepSize from the type, seed, validation, resolver, CLI flags (-n, -s),
  and help. stepDelay stays as the one pacing lever.
- configFile.ts: tolerate the removed keys instead of rejecting them — every
  pre-1.3.0 install seeded stepCount, so a hard "unknown key" failure on
  upgrade is avoided. They're ignored with a one-line stderr notice; genuine
  unknown keys still error.

The -n/--step-count CLI flag (shipped since 1.0.0) is now an unknown option;
config files degrade gracefully, command lines don't. Stays in the unpushed
1.3.0 release. 64 tests pass.
2026-08-14 12:56:22 -05:00

129 lines
4.6 KiB
TypeScript

/**
* 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,
pattern: 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 passes through untouched (no unit conversion)", () => {
const cli: ConfigOverrides = { ...NONE, stepDelay: 75 };
const cfg = resolveConfig(null, cli);
expect(cfg.stepDelay).toBe(75);
});
test("pattern: CLI wins over file, file wins over default", () => {
expect(resolveConfig({ ...NONE, pattern: "arc" }, { ...NONE, pattern: "walk" }).pattern).toBe("walk");
expect(resolveConfig({ ...NONE, pattern: "arc" }, NONE).pattern).toBe("arc");
expect(resolveConfig(null, NONE).pattern).toBe(DEFAULT_CONFIG.pattern);
});
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);
});
});