Add pluggable movement strategies (v1.3.0)

Turn the hardcoded straight-line sweep into a strategy system behind three
seams so new patterns are easy to add and, for the first time, testable
without nut.js or a real screen:

- src/device.ts:     injectable Device seam over nut.js (autoDelayMs lives
                     here now); the only module that touches the native lib.
- src/strategies.ts: pure per-pattern path generators + registry + lenient
                     name resolution. Ships line, diagonal, jitter, walk,
                     arc, figureEight.
- src/executor.ts:   single executePath driver owning bounds policy
                     (abort/clamp/reflect), pacing, interrupt detection, and
                     restore-on-clean.

keeper.ts's simulateActivity now selects a strategy and delegates to the
executor; the default `line` pattern is byte-for-byte the previous behavior.

New config surface, layered CLI > file > default with strict validation:
- -p/--pattern <name>   movement strategy (names matched case/-/_-insensitive)
- -s/--step-size <px>   pixels per step; stepCount is now a step *count*

Robustness for the new edge-seeking patterns: interrupt detection compares
against the last commanded (rounded) point with a 2px tolerance, and
clamp/reflect stay a couple pixels off the screen edge, so sub-pixel cursor
placement on scaled/multi-monitor displays isn't misread as user activity.
jitter's radius scales with sweep length so it moves at the default stepSize.

Tests: new suites for strategies, the executor (all bounds policies,
rounding, interrupt, tolerance, pacing), and the keeper loop; config and
configFile suites extended for pattern/stepSize. editor.test.ts moved to
tests/ for consistency. 64 pass.
This commit is contained in:
2026-08-13 15:36:22 -05:00
parent 7777b16540
commit db3310c247
18 changed files with 1319 additions and 153 deletions
+11 -2
View File
@@ -16,6 +16,8 @@ const NONE: ConfigOverrides = {
checkInterval: undefined,
stepDelay: undefined,
stepCount: undefined,
stepSize: undefined,
pattern: undefined,
verbose: undefined,
};
@@ -44,11 +46,18 @@ describe("resolveConfig", () => {
expect(cfg.checkInterval).toBe(2000);
});
test("stepDelay and stepCount pass through untouched (no unit conversion)", () => {
const cli: ConfigOverrides = { ...NONE, stepDelay: 75, stepCount: 100 };
test("stepDelay, stepCount, stepSize pass through untouched (no unit conversion)", () => {
const cli: ConfigOverrides = { ...NONE, stepDelay: 75, stepCount: 100, stepSize: 4 };
const cfg = resolveConfig(null, cli);
expect(cfg.stepDelay).toBe(75);
expect(cfg.stepCount).toBe(100);
expect(cfg.stepSize).toBe(4);
});
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", () => {