Skip to content

Commit e426c03

Browse files
committed
test: pin hover contract errors, merge ordering and absolute endCursor across rotation
- hoverPoint/hoverRef: unadvertised legs are not bound; an advertised leg whose interactor lacks the member reports runtime-contract-invalid - merged snapshot stats keep p50 <= p95 <= max, including p95 === max; document that the weighted points equal the earlier reconstruction - endCursor stays absolute when the scan spans or stops in a rotated file Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_016qAu53rMak1bXMuR5U2gF4
1 parent 4994a14 commit e426c03

4 files changed

Lines changed: 79 additions & 1 deletion

File tree

‎packages/contracts/src/snapshot-diagnostics.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,29 @@ test('merge cost does not grow with a run capture count', () => {
214214
platform: 'ios',
215215
});
216216
});
217+
218+
test('merging summaries whose p95 equals max keeps p50 <= p95 <= max', () => {
219+
const slowTail = { count: 4, p50Ms: 200, p95Ms: 1_000, maxMs: 1_000, slowThresholdMs: 1_500 };
220+
const merged = mergeSnapshotDiagnostics([{ stats: slowTail }, { stats: VALID_STATS }])?.stats;
221+
expect(merged).toMatchObject({ count: 8, p50Ms: 200, p95Ms: 1_000, maxMs: 1_000 });
222+
expect(readSnapshotDiagnosticsSummary({ stats: merged })?.stats).toEqual(merged);
223+
});
224+
225+
test('every merge of valid summaries is itself a valid summary', () => {
226+
let seed = 7;
227+
const next = () => (seed = (seed * 1_103_515_245 + 12_345) & 0x7fffffff) / 0x7fffffff;
228+
for (let round = 0; round < 500; round += 1) {
229+
const runs = Array.from({ length: 1 + Math.floor(next() * 4) }, () => {
230+
const [p50Ms, p95Ms, maxMs] = [next(), next(), next()]
231+
.map((value) => Math.floor(value * 2_000))
232+
.sort((left, right) => left - right) as [number, number, number];
233+
const count = 1 + Math.floor(next() * 9);
234+
return {
235+
stats: { count, p50Ms, p95Ms: next() < 0.3 ? maxMs : p95Ms, maxMs, slowThresholdMs: 1_500 },
236+
};
237+
});
238+
const merged = mergeSnapshotDiagnostics(runs)?.stats;
239+
expect(merged && merged.p50Ms <= merged.p95Ms && merged.p95Ms <= merged.maxMs).toBe(true);
240+
expect(merged?.count).toBe(runs.reduce((sum, run) => sum + run.stats.count, 0));
241+
}
242+
});

‎packages/contracts/src/snapshot-diagnostics.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,9 @@ type WeightedTimingSample = { durationMs: number; weight: number; platform?: Pub
274274

275275
/**
276276
* One run's summary as at most three weighted points: its p50 stands for every capture below its
277-
* p95. The weights sum to `count`, so the merge costs O(runs) whatever each run's capture count.
277+
* p95. This is the same multiset the earlier per-sample reconstruction built (count - 2 copies of
278+
* p50, then p95 and max), so merged percentiles are unchanged; the weights sum to `count`, so the
279+
* merge costs O(runs) whatever each run's capture count.
278280
*/
279281
function weightedSamplesFromStats(stats: SnapshotTimingStats): WeightedTimingSample[] {
280282
const { count, platform } = stats;

‎packages/contracts/src/touch-runtime.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,43 @@ test('an admitted ref operation whose interactor lacks the member is a runtime c
352352
});
353353
}
354354
});
355+
356+
test('hover follows the touch rule: unadvertised is not bound, advertised without a member is a contract error', async () => {
357+
// Without facts for hover, neither leg is bound: the command layer refuses from the fact
358+
// (UNSUPPORTED_OPERATION with the longpress hint), and no interactor is consulted.
359+
const unadvertised = bindLocalTouchInteractor({
360+
device,
361+
signal: new AbortController().signal,
362+
resolveInteractor: async () => ({}) as unknown as Interactor,
363+
facts,
364+
pause: async () => {},
365+
});
366+
expect(unadvertised.hoverPoint).toBeUndefined();
367+
expect(unadvertised.hoverRef).toBeUndefined();
368+
369+
const advertised = bindLocalTouchInteractor({
370+
device,
371+
signal: new AbortController().signal,
372+
resolveInteractor: async () => ({}) as unknown as Interactor,
373+
facts: touchRuntimeOperationFacts({
374+
unsupported: unavailable,
375+
tap: available,
376+
tapRef: available,
377+
longPress: available,
378+
fill: available,
379+
fillRef: available,
380+
hover: available,
381+
hoverRef: available,
382+
}),
383+
pause: async () => {},
384+
});
385+
for (const run of [
386+
() => advertised.hoverPoint!({ point: { x: 1, y: 2 }, execution: { requestId: 'hover' } }),
387+
() => advertised.hoverRef!({ ref: '@e4', execution: { requestId: 'hover-ref' } }),
388+
]) {
389+
await expect(run()).rejects.toMatchObject({
390+
code: 'COMMAND_FAILED',
391+
details: { reason: 'runtime-contract-invalid' },
392+
});
393+
}
394+
});

‎packages/session-journal/src/__tests__/session-event-log-window.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ test('rotation caps events.ndjson while cursors stay absolute across the retaine
9696
['event 38'],
9797
);
9898
assert.equal(tail.nextCursor, '39');
99+
// endCursor is absolute like nextCursor, whichever retained file the scan stopped in.
100+
assert.equal(tail.endCursor, '39');
101+
assert.equal(
102+
readSessionEventLog(eventLogPath, { cursor: String(earliest), limit: 2 }).endCursor,
103+
String(earliest + 2),
104+
);
105+
assert.equal(
106+
readSessionEventLog(eventLogPath, { cursor: String(earliest), limit: 500 }).endCursor,
107+
'40',
108+
);
99109
assert.deepEqual(summaries(eventLogPath, tail.nextCursor, 1), ['event 39']);
100110

101111
const expired = readError(eventLogPath, '0');

0 commit comments

Comments
 (0)