Files
Michał Pierzchała 95f838f514 test: stop pinning settle capture counts against a wall-clock loop (#1307)
* test: model the UI in settle-observation fixtures instead of a capture count

settle-observation's "contention flakes" were a zero-margin comparison
meeting a 1ms clock skew, not generic load.

runStableCaptureLoop derives pollMs = min(300, max(25, quietMs)), so the
test's settleQuietMs: 25 made pollMs === quietMs, and the settle check
(one sleep(25) plus capture time, against >= 25) a 0ms margin. Node's
setTimeout(25) advances Date.now() by only 24ms in 0.13% of calls idle
and 0.63% under load, because libuv's timers and Date.now() read
different clocks. On a 24 the loop takes a third capture that the
transcript never scripted, and settle's best-effort catch reports the
resulting throw as settled: false.

The fixtures now model the surface rather than the runner's speed: a
quiet UI serves the settled tree to every capture, a busy one a fresh
tree per capture, via new transcript `repeat` entries and result
factories. The snapshot-floor economy guard survives as a bound (2-3
captures), and the follow-up's "no fresh capture" cost — previously
implied by the exact transcript — is now asserted directly.

Production is untouched: the same zero margin only costs a wasted extra
capture and poll there, filed separately as #1306.

* test: stop pinning the settle capture count in the iOS contract scenario too

A sweep for the same bug class found direct-ios-selector's settleObservation
scenario carrying the identical 0ms margin: settleQuietMs: 25 (so pollMs ===
quietMs) against a consume-once transcript scripting exactly two settle
captures, with no injected clock. It has not lost the coin flip in CI yet,
but it fails the same way when it does — a third capture finds no entry and
settle's best-effort catch reports settled: false.

Same fix: the fixture models a quiet UI (every settle capture sees the same
tree) instead of scripting how many captures fit in a wall-clock window.

The rest of the sweep was clean. The other contract scenarios and the
interaction runtime tests already use clamped mocks that repeat the last
snapshot, so any capture count is tolerated; the fake-clock tests are correct
to pin exact counts.

* style: oxfmt quietRunnerSnapshotEntry signature

* test: make one-shot-outranks-repeat a real transcript rule (P2 review)

The review is right: `one-shot entries still outrank a repeat entry` asserted
a guarantee the lookup did not provide. It passed only because the one-shot
happened to be declared first — unordered lookup took the first match, so a
repeat declared ahead of a matching one-shot shadowed it forever and left it
permanently unconsumed. Both failure modes reproduce; the reverse-order test
added here fails on the previous implementation.

Unordered lookup now searches matching one-shots before repeats, so
outranking holds whatever the declaration order. A repeat is documented as
its command's fallback.

Ordered transcripts now reject repeats at construction: ordered lookup only
ever reads the head, so a repeat there never advances and strands every entry
behind it. Refusing the combination beats failing later as a confusing
"Provider command mismatch".

Coverage added for both: reverse declaration order, and ordered + repeat.
2026-07-16 20:13:07 +02:00

172 lines
5.8 KiB
TypeScript

import assert from 'node:assert/strict';
import { isDeepStrictEqual } from 'node:util';
import type { Platform } from '../../../src/kernel/device.ts';
export interface ProviderScenarioProviderScope {
deviceId?: string;
platform?: Platform;
}
export interface ProviderScenarioProviderEntry<
TResult = unknown,
> extends ProviderScenarioProviderScope {
command: string;
request?: unknown;
/** A factory is invoked per call, so repeated calls can return fresh results. */
result?: TResult | (() => TResult);
/**
* Serves every matching call instead of being consumed by the first, and never
* counts as unconsumed. Use it where the call COUNT is not the contract — a
* quiet UI returns the same tree to every capture, a busy one returns a fresh
* tree per capture (pair with a `result` factory). Scripting an exact count
* there would assert the runner's speed rather than the behaviour.
*
* A repeat is the FALLBACK for its command: matching one-shot entries are
* always served first, whatever order the entries are declared in, so a
* repeat can never strand one. Unsupported in ordered transcripts, where a
* repeat would never advance the queue.
*/
repeat?: boolean;
error?: Error | string;
}
export interface ProviderScenarioProviderCall<
TResult = unknown,
> extends ProviderScenarioProviderScope {
command: string;
request?: unknown;
result?: TResult;
}
export interface ProviderScenarioTranscript {
readonly calls: readonly ProviderScenarioProviderCall[];
readonly remaining: readonly ProviderScenarioProviderEntry[];
next<TResult = unknown>(
command: string,
request?: unknown,
scope?: ProviderScenarioProviderScope,
): TResult;
assertComplete(): void;
}
export function createProviderTranscript(
entries: readonly ProviderScenarioProviderEntry[],
options: { ordered?: boolean } = {},
): ProviderScenarioTranscript {
if (options.ordered && entries.some((entry) => entry.repeat)) {
throw new Error(
'Ordered provider transcripts cannot use `repeat` entries: ordered lookup only reads the head, so a repeat never advances and strands every entry behind it.',
);
}
const pending = [...entries];
const calls: ProviderScenarioProviderCall[] = [];
return {
get calls() {
return [...calls];
},
get remaining() {
return [...pending];
},
next<TResult = unknown>(
command: string,
request?: unknown,
scope: ProviderScenarioProviderScope = {},
): TResult {
const entryIndex = options.ordered ? 0 : findEntryIndex(pending, command, request, scope);
const entry = entryIndex >= 0 ? pending[entryIndex] : undefined;
assert.ok(entry, `Unexpected provider call: ${formatCall(command, scope)}`);
if (!entry.repeat) pending.splice(entryIndex, 1);
assert.equal(command, entry.command, 'Provider command mismatch');
assertScope(scope, entry);
if (Object.hasOwn(entry, 'request')) {
assert.deepEqual(request, entry.request, 'Provider request mismatch');
}
const result = resolveEntryResult(entry) as TResult;
const call = {
command,
request,
deviceId: scope.deviceId,
platform: scope.platform,
result,
};
calls.push(call);
if (entry.error) {
throw entry.error instanceof Error ? entry.error : new Error(entry.error);
}
return result;
},
assertComplete() {
const outstanding = pending.filter((entry) => !entry.repeat);
assert.equal(
outstanding.length,
0,
`Unconsumed provider transcript entries: ${outstanding.map(formatEntry).join(', ')}`,
);
},
};
}
export function createOrderedProviderTranscript(
entries: readonly ProviderScenarioProviderEntry[],
): ProviderScenarioTranscript {
return createProviderTranscript(entries, { ordered: true });
}
/**
* One-shot entries are served before repeats regardless of declaration order: a
* repeat is its command's fallback, so taking the first match would let one
* shadow a one-shot forever and strand it as permanently unconsumed.
*/
function findEntryIndex(
pending: readonly ProviderScenarioProviderEntry[],
command: string,
request: unknown,
scope: ProviderScenarioProviderScope,
): number {
const matches = (candidate: ProviderScenarioProviderEntry): boolean =>
providerEntryMatches(candidate, command, request, scope);
const oneShotIndex = pending.findIndex((candidate) => !candidate.repeat && matches(candidate));
return oneShotIndex >= 0 ? oneShotIndex : pending.findIndex(matches);
}
function resolveEntryResult(entry: ProviderScenarioProviderEntry): unknown {
return typeof entry.result === 'function' ? (entry.result as () => unknown)() : entry.result;
}
function providerEntryMatches(
entry: ProviderScenarioProviderEntry,
command: string,
request: unknown,
scope: ProviderScenarioProviderScope,
): boolean {
if (entry.command !== command) return false;
if (entry.deviceId && entry.deviceId !== scope.deviceId) return false;
if (entry.platform && entry.platform !== scope.platform) return false;
return !Object.hasOwn(entry, 'request') || isDeepStrictEqual(request, entry.request);
}
function assertScope(
actual: ProviderScenarioProviderScope,
expected: ProviderScenarioProviderEntry,
): void {
if (expected.deviceId) {
assert.equal(actual.deviceId, expected.deviceId, 'Provider device id mismatch');
}
if (expected.platform) {
assert.equal(actual.platform, expected.platform, 'Provider platform mismatch');
}
}
function formatCall(command: string, scope: ProviderScenarioProviderScope): string {
return formatEntry({ command, ...scope });
}
function formatEntry(entry: { command: string; deviceId?: string; platform?: Platform }): string {
const scope = [entry.platform, entry.deviceId].filter(Boolean).join(':');
return scope ? `${scope}.${entry.command}` : entry.command;
}