Rearchitect click capture: strict click-time frames, off-main-process recorder, exact marker coordinates
Template tests / tests (push) Successful in 1m50s
Template tests / tests (push) Successful in 1m50s
Implements the architecture change from ai_prompts/prompt3.md: - New app/click-frames.js: shared timestamped frame ring + strict click-to-frame pairing (never a frame whose grab started after the click); legacy slack behavior kept behind capture.strictClickFrames=false. - New stream capture backend (app/stream-backend.js + hidden worker window): per-display desktop media streams sampled into ring buffers and PNG-encoded entirely off the main process, so click delivery is never starved by capture work. Auto-degrades to the legacy in-process frame loop when streams cannot start or the worker stops answering. - Clicks are paired with their frame at event time (eager pairing in enqueueClickCapture); only the storing is serialized, so slow encodes cannot skew later clicks in a fast burst. - Linux watcher: restored event-time root coordinates from xinput test-xi2 and merge raw/regular twin events structurally. - Replaced the 40ms time debounce with source-aware duplicate suppression: fast legitimate clicks are never dropped. - New app/coords.js: physical-to-DIP conversion with multi-monitor and scale-factor handling; Windows keeps screenToDipPoint. - STEPFORGE_CLICK_SELFTEST end-to-end hook: 3/3 clicks become steps via the stream backend with 0.00% marker offset on this host. - Tests rewritten/added: strict selection, coords, stream backend, Linux coordinate parsing, twin merge, burst clicking (126 passing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
FrameRing,
|
||||
frameUsableForClick,
|
||||
selectFrameForClick,
|
||||
} = require('../../app/click-frames');
|
||||
|
||||
function frame(name, { startedAt, capturedAt, mode = 'fullscreen', display = null } = {}) {
|
||||
return { name, mode, startedAt, capturedAt, display };
|
||||
}
|
||||
|
||||
// ---- FrameRing --------------------------------------------------------------
|
||||
|
||||
test('the ring keeps at most `limit` frames and drops the oldest first', () => {
|
||||
let now = 1000;
|
||||
const evicted = [];
|
||||
const ring = new FrameRing({ limit: 2, retentionMs: 60_000, now: () => now, onEvict: (f) => evicted.push(f.name) });
|
||||
ring.push(frame('a', { capturedAt: 1000 }));
|
||||
ring.push(frame('b', { capturedAt: 1100 }));
|
||||
now = 1200;
|
||||
ring.push(frame('c', { capturedAt: 1200 }));
|
||||
|
||||
assert.deepEqual(ring.frames().map((f) => f.name), ['b', 'c']);
|
||||
assert.deepEqual(evicted, ['a'], 'eviction must release the dropped frame');
|
||||
assert.equal(ring.latest().name, 'c');
|
||||
});
|
||||
|
||||
test('the ring evicts frames older than the retention window', () => {
|
||||
let now = 1000;
|
||||
const ring = new FrameRing({ limit: 10, retentionMs: 500, now: () => now });
|
||||
ring.push(frame('old', { capturedAt: 1000 }));
|
||||
now = 2000;
|
||||
ring.push(frame('new', { capturedAt: 2000 }));
|
||||
|
||||
assert.deepEqual(ring.frames().map((f) => f.name), ['new']);
|
||||
});
|
||||
|
||||
test('clear() releases every frame through onEvict', () => {
|
||||
const evicted = [];
|
||||
const ring = new FrameRing({ onEvict: (f) => evicted.push(f.name) });
|
||||
ring.push(frame('a', { capturedAt: Date.now() }));
|
||||
ring.push(frame('b', { capturedAt: Date.now() }));
|
||||
ring.clear();
|
||||
|
||||
assert.deepEqual(ring.frames(), []);
|
||||
assert.deepEqual(evicted.sort(), ['a', 'b']);
|
||||
});
|
||||
|
||||
// ---- strict selection -------------------------------------------------------
|
||||
|
||||
test('strict mode picks the newest frame completed at or before the click', () => {
|
||||
const clickAt = 10_000;
|
||||
const frames = [
|
||||
frame('older', { startedAt: 9300, capturedAt: 9400 }),
|
||||
frame('best', { startedAt: 9800, capturedAt: 9900 }),
|
||||
frame('post-click', { startedAt: 10_050, capturedAt: 10_150 }),
|
||||
];
|
||||
|
||||
const chosen = selectFrameForClick(frames, { clickAt, mode: 'fullscreen', strict: true });
|
||||
|
||||
assert.equal(chosen.name, 'best');
|
||||
});
|
||||
|
||||
test('strict mode never accepts a frame whose grab started after the click', () => {
|
||||
const clickAt = 10_000;
|
||||
// Even one millisecond after the click, and even via the in-flight path:
|
||||
// a post-click grab can already show the click's effects.
|
||||
const f = frame('late', { startedAt: 10_001, capturedAt: 10_200 });
|
||||
|
||||
assert.equal(frameUsableForClick(f, { clickAt, strict: true, allowInFlight: true }), false);
|
||||
assert.equal(selectFrameForClick([f], { clickAt, strict: true }), null);
|
||||
});
|
||||
|
||||
test('strict mode accepts an in-flight frame whose grab started before the click', () => {
|
||||
const clickAt = 10_000;
|
||||
const f = frame('in-flight', { startedAt: 9950, capturedAt: 10_300 });
|
||||
|
||||
assert.equal(frameUsableForClick(f, { clickAt, strict: true, allowInFlight: true }), true);
|
||||
assert.equal(frameUsableForClick(f, { clickAt, strict: true, allowInFlight: false }), false,
|
||||
'a not-yet-needed in-flight frame must not be selected from the buffer path');
|
||||
});
|
||||
|
||||
test('a frame older than maxAgeMs is too stale for the click', () => {
|
||||
const clickAt = 10_000;
|
||||
const f = frame('stale', { startedAt: 9000, capturedAt: 9100 });
|
||||
|
||||
assert.equal(frameUsableForClick(f, { clickAt, strict: true, maxAgeMs: 600 }), false);
|
||||
assert.equal(frameUsableForClick(f, { clickAt, strict: true, maxAgeMs: 2000 }), true);
|
||||
});
|
||||
|
||||
test('balanced mode accepts a grab started within the slack window after the click', () => {
|
||||
const clickAt = 10_000;
|
||||
const f = frame('slack', { startedAt: 10_100, capturedAt: 10_350 });
|
||||
|
||||
assert.equal(frameUsableForClick(f, {
|
||||
clickAt, strict: false, allowInFlight: true, startSlackMs: 300,
|
||||
}), true);
|
||||
assert.equal(frameUsableForClick(f, {
|
||||
clickAt, strict: true, allowInFlight: true, startSlackMs: 300,
|
||||
}), false, 'slack acceptance is balanced-mode only');
|
||||
});
|
||||
|
||||
test('frames from another display are rejected when the click position is known', () => {
|
||||
const clickAt = 10_000;
|
||||
const left = { bounds: { x: 0, y: 0, width: 1920, height: 1080 } };
|
||||
const right = { bounds: { x: 1920, y: 0, width: 1920, height: 1080 } };
|
||||
const f = frame('left-screen', { startedAt: 9900, capturedAt: 9950, display: left });
|
||||
|
||||
assert.equal(frameUsableForClick(f, { clickAt, clickPos: { x: 2500, y: 500 } }), false);
|
||||
assert.equal(frameUsableForClick(f, { clickAt, clickPos: { x: 500, y: 500 } }), true);
|
||||
const g = frame('right-screen', { startedAt: 9960, capturedAt: 9980, display: right });
|
||||
assert.equal(selectFrameForClick([f, g], { clickAt, clickPos: { x: 2500, y: 500 } }).name, 'right-screen');
|
||||
});
|
||||
|
||||
test('frames of the wrong capture mode are rejected', () => {
|
||||
const clickAt = 10_000;
|
||||
const f = frame('window-grab', { startedAt: 9900, capturedAt: 9950, mode: 'window' });
|
||||
|
||||
assert.equal(frameUsableForClick(f, { clickAt, mode: 'fullscreen' }), false);
|
||||
assert.equal(frameUsableForClick(f, { clickAt, mode: 'window' }), true);
|
||||
});
|
||||
|
||||
test('a frame without startedAt falls back to capturedAt for the strict check', () => {
|
||||
const clickAt = 10_000;
|
||||
const before = frame('legacy-before', { capturedAt: 9950 });
|
||||
const after = frame('legacy-after', { capturedAt: 10_050 });
|
||||
|
||||
assert.equal(frameUsableForClick(before, { clickAt, strict: true }), true);
|
||||
assert.equal(frameUsableForClick(after, { clickAt, strict: true, allowInFlight: true }), false);
|
||||
});
|
||||
Reference in New Issue
Block a user