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
+24 -1
View File
@@ -10,7 +10,9 @@
* moveInterval number seconds, positive
* checkInterval number seconds, positive
* stepDelay number milliseconds, positive
* stepCount number pixels, positive
* stepCount number count, positive
* stepSize number pixels, positive
* pattern string a registered strategy name
* verbose boolean
*
* Unknown keys, wrong types, and non-positive numerics are rejected with a
@@ -29,12 +31,15 @@ import { existsSync, readFileSync, statSync } from "node:fs";
import { defaultConfigPath, type ConfigOverrides } from "./config.ts";
import { CliError } from "./errors.ts";
import { PATTERN_NAMES, resolvePatternName } from "./strategies.ts";
const ALLOWED_KEYS: ReadonlySet<string> = new Set<string>([
"moveInterval",
"checkInterval",
"stepDelay",
"stepCount",
"stepSize",
"pattern",
"verbose",
]);
@@ -60,6 +65,16 @@ function requireBoolean(name: string, raw: unknown, path: string): boolean {
return raw;
}
function requirePatternName(name: string, raw: unknown, path: string): string {
const canonical: string | null = typeof raw === "string" ? resolvePatternName(raw) : null;
if (canonical === null) {
throw new CliError(
`invalid value for '${name}' in ${path}: ${JSON.stringify(raw)} (valid: ${PATTERN_NAMES.join(", ")})`,
);
}
return canonical;
}
/**
* Load and validate the config file. See module docstring for return
* semantics.
@@ -130,6 +145,14 @@ export function loadConfigFile(explicitPath: string | undefined): ConfigOverride
"stepCount" in parsed
? requirePositiveNumber("stepCount", parsed.stepCount, path)
: undefined,
stepSize:
"stepSize" in parsed
? requirePositiveNumber("stepSize", parsed.stepSize, path)
: undefined,
pattern:
"pattern" in parsed
? requirePatternName("pattern", parsed.pattern, path)
: undefined,
verbose:
"verbose" in parsed
? requireBoolean("verbose", parsed.verbose, path)