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.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
/**
|
|
* keeper.test.ts
|
|
* --------------
|
|
* Loop-level tests for `runKeeper` driven by a fake `Device`. The loop runs
|
|
* forever in production, so the fake stops it by throwing a sentinel from
|
|
* `sleep` once a call budget is exhausted; the test then inspects the
|
|
* commands that were issued.
|
|
*
|
|
* These assert the two behaviors that matter: an idle cursor triggers a
|
|
* synthetic sweep, and a moving cursor never does.
|
|
*/
|
|
|
|
import { describe, expect, test } from "bun:test";
|
|
|
|
import { DEFAULT_CONFIG } from "../src/config.ts";
|
|
import type { Config } from "../src/config.ts";
|
|
import type { Device, Point } from "../src/device.ts";
|
|
import { runKeeper } from "../src/keeper.ts";
|
|
|
|
class StopError extends Error {}
|
|
|
|
/**
|
|
* Fake device that echoes the last commanded point (so a synthetic sweep
|
|
* completes cleanly) and aborts the loop after `budget` sleeps.
|
|
*
|
|
* `positions`, when provided, is consumed one entry per `getPosition` call
|
|
* to simulate real user movement; otherwise the cursor is reported as
|
|
* stationary at `initial`/the last commanded point (idle).
|
|
*/
|
|
class LoopDevice implements Device {
|
|
commanded: Point[] = [];
|
|
sleepCount = 0;
|
|
constructor(
|
|
public budget: number,
|
|
public initial: Point = { x: 100, y: 100 },
|
|
private positions: Point[] | null = null,
|
|
) {}
|
|
|
|
async getPosition(): Promise<Point> {
|
|
if (this.positions) return this.positions.shift() ?? this.initial;
|
|
return this.commanded.at(-1) ?? this.initial;
|
|
}
|
|
async setPosition(p: Point): Promise<void> {
|
|
this.commanded.push(p);
|
|
}
|
|
async width(): Promise<number> {
|
|
return 1920;
|
|
}
|
|
async height(): Promise<number> {
|
|
return 1080;
|
|
}
|
|
async sleep(): Promise<void> {
|
|
if (++this.sleepCount > this.budget) throw new StopError();
|
|
}
|
|
}
|
|
|
|
const quietConfig = (overrides: Partial<Config>): Config => ({
|
|
...DEFAULT_CONFIG,
|
|
verbose: false,
|
|
...overrides,
|
|
});
|
|
|
|
async function runUntilStop(config: Config, device: Device): Promise<void> {
|
|
try {
|
|
await runKeeper(config, device);
|
|
} catch (err) {
|
|
if (!(err instanceof StopError)) throw err;
|
|
}
|
|
}
|
|
|
|
describe("runKeeper", () => {
|
|
test("fires a synthetic sweep once the cursor has been idle long enough", async () => {
|
|
// moveInterval 0 => any elapsed time counts as "idle long enough",
|
|
// so the first idle check triggers a sweep deterministically.
|
|
const dev = new LoopDevice(50);
|
|
await runUntilStop(quietConfig({ moveInterval: 0, pattern: "line" }), dev);
|
|
// A sweep issued setPosition commands (the sweep is interrupted by the
|
|
// sleep budget before it finishes, but many steps land); an idle loop
|
|
// with no sweep would have issued none.
|
|
expect(dev.commanded.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
test("does not fire while the cursor keeps moving", async () => {
|
|
// Every poll reports a new position => always "real activity", so the
|
|
// idleness clock keeps resetting and no sweep ever fires.
|
|
const moving: Point[] = Array.from({ length: 40 }, (_, i) => ({ x: i, y: i }));
|
|
const dev = new LoopDevice(20, { x: 0, y: 0 }, moving);
|
|
await runUntilStop(quietConfig({ moveInterval: 0, pattern: "line" }), dev);
|
|
expect(dev.commanded.length).toBe(0);
|
|
});
|
|
});
|