test(perf): type-check the whole electron-backend-e2e project (#1869)

* test(e2e): fix type errors in four Electron E2E specs

- category-management: the poll's message and 15 s timeout were passed to
  toBe(), which takes one argument, so the poll ran with the default 10 s
  timeout and message. Pass them to expect.poll().
- settings: evaluate<void, HTMLVideoElement> set the argument type, not the
  element type; type the callback parameter instead.
- remote-control: keep the poll callback's writes visible to the return and
  fail with a clear error instead of casting null.
- downloads: read the dataset key through its index signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): fix type errors in the benchmark harness

None of these changes behaviour for a capture the harness produces today:

- cancellation report: a null playlist id on the request record now means
  unknown, like undefined, instead of matching only null DB records.
- main capture: type the worker transport map so its unavailable-reason
  literals stay narrow.
- worker request performance: a parsed capture carries non-null work
  timestamps; only the unavailable fallback reports null.
- Xtream iteration: hand the teardown closure the narrowed app and data
  directory.
- Xtream exact schema: return early when the identity is not an exact
  record instead of re-checking it three times.
- Xtream assembly: reject an unpainted UI explicitly; the null-to-0
  comparison already rejected it.
- Xtream scenario driver: drop the always-null cancellation observer from
  prepare; trigger() installs it.
- Electron process lifecycle: type the child process by the exit
  subscription it uses, so a test double need not return a ChildProcess.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): fix type errors in the benchmark harness specs

The dynamically imported helpers were checked with
assert.equal(typeof fn, 'function'), which does not narrow, so later calls
used fn?.(). assert.ok(typeof fn === 'function') narrows them. The rest
types fixtures by the module under test, keeps closure-assigned callbacks
visible after their callbacks run, and narrows nullable fixture values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): type-check the whole electron-backend-e2e project

Widen the spec config's include from the journey harness to src/**/*.ts
and the root Playwright configs, with no baseline or ignore list, and
update the validation map and the journey checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): build a complete renderer fixture in the request-performance spec

The fixture was cast through unknown, so the type check accepted an
incomplete RendererCaptureMetrics. Fill every required field with neutral
values; the summary reads only the probe arrays and heap fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 authored and GitHub committed 2026-10-10 14:33:15 +02:00
1 parent 6e18983400
commit 078b91bfdf
36 files changed
+229 -165

No files matched your search

@@ -619,38 +619,43 @@ async function pickSidebarCategory(
} | null = null;
await expect
.poll(async () => {
const candidates = (
await readVisibleSidebarCategories(page)
).filter(
(candidate) =>
candidate.id && candidate.name && candidate.itemCount > 0
);
if (candidates.length === 0) {
preferredCandidate = null;
return false;
}
const nameCounts = new Map<string, number>();
for (const candidate of candidates) {
nameCounts.set(
candidate.name,
(nameCounts.get(candidate.name) ?? 0) + 1
.poll(
async () => {
const candidates = (
await readVisibleSidebarCategories(page)
).filter(
(candidate) =>
candidate.id &&
candidate.name &&
candidate.itemCount > 0
);
if (candidates.length === 0) {
preferredCandidate = null;
return false;
}
const nameCounts = new Map<string, number>();
for (const candidate of candidates) {
nameCounts.set(
candidate.name,
(nameCounts.get(candidate.name) ?? 0) + 1
);
}
preferredCandidate =
candidates.find(
(candidate) => nameCounts.get(candidate.name) === 1
) ?? candidates[0];
return preferredCandidate !== null;
},
{
message: 'No visible Xtream category with content was found.',
timeout: 15000,
}
preferredCandidate =
candidates.find(
(candidate) => nameCounts.get(candidate.name) === 1
) ?? candidates[0];
return preferredCandidate !== null;
})
.toBe(true, {
message: 'No visible Xtream category with content was found.',
timeout: 15000,
});
)
.toBe(true);
return preferredCandidate!;
}
@@ -108,7 +108,7 @@ async function selectDownloadCoverSize(
await expect
.poll(() =>
page.evaluate(
() => document.documentElement.dataset.coverSize ?? null
() => document.documentElement.dataset['coverSize'] ?? null
)
)
.toBe(size);
@@ -1,10 +1,15 @@
import type { ChildProcess } from 'node:child_process';
import { terminateElectronProcess } from './electron-process-termination';
type ElectronChildProcess = Pick<
// Only the exit subscription is used; picking ChildProcess's own once and
// removeListener would demand that every listener host returns a ChildProcess.
interface ElectronChildProcess extends Pick<
ChildProcess,
'exitCode' | 'kill' | 'once' | 'pid' | 'removeListener' | 'signalCode'
>;
'exitCode' | 'kill' | 'pid' | 'signalCode'
> {
once(event: 'exit', listener: () => void): unknown;
removeListener(event: 'exit', listener: () => void): unknown;
}
export interface ClosableElectronApplication {
close(): Promise<void>;
@@ -6,6 +6,11 @@ import {
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON,
} from './database-worker-peak-memory-validity';
type PeakMemoryIteration = Parameters<
typeof assessDatabaseWorkerPeakMemoryValidity
>[0][number];
type PeakMemoryWorker = PeakMemoryIteration['main']['workers'][number];
describe('database worker peak-memory validity', () => {
it('requires successful heap and external-memory samples in every measured run', () => {
const validity = assessDatabaseWorkerPeakMemoryValidity([
@@ -96,8 +101,8 @@ describe('database worker peak-memory validity', () => {
function iteration(
runId: string,
kind: string,
databaseWorker: Record<string, unknown>
) {
databaseWorker: PeakMemoryWorker
): PeakMemoryIteration {
return {
kind,
main: { workers: [databaseWorker] },
@@ -31,7 +31,7 @@ async function restoreSerializableApi(): Promise<CutoffApi> {
const module = await cutoffModulePromise;
assert.ok(module, 'database worker post-GC cutoff module must exist');
const factory = module.createDatabaseWorkerPostGcCutoffApi;
assert.equal(typeof factory, 'function');
assert.ok(typeof factory === 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
@@ -57,7 +57,7 @@ async function restoreSerializableApi(): Promise<FinalizationApi> {
const module = await finalizationModulePromise;
assert.ok(module, 'database worker post-GC finalization module must exist');
const factory = module.createDatabaseWorkerPostGcFinalizationApi;
assert.equal(typeof factory, 'function');
assert.ok(typeof factory === 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
@@ -158,7 +158,7 @@ async function restoreSerializableApi(): Promise<ProbeApi> {
const module = await probeModulePromise;
assert.ok(module, 'database worker post-GC probe module must exist');
const factory = module.createDatabaseWorkerPostGcProbeApi;
assert.equal(typeof factory, 'function');
assert.ok(typeof factory === 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
@@ -351,18 +351,20 @@ test('times out once with an injectable deadline and ignores every later termina
const api = await restoreSerializableApi();
const harness = createProbeHarness();
let terminateCalls = 0;
// A real worker also exposes terminate(); the probe must never call it.
const worker = {
postMessage(): void {
// Keep the one-shot probe pending until its bounded deadline.
},
terminate(): void {
terminateCalls += 1;
},
};
const resultPromise = api.probe({
createMessageChannel: harness.createMessageChannel,
timeoutMs: 37,
timers: harness.timers,
worker: {
postMessage(): void {
// Keep the one-shot probe pending until its bounded deadline.
},
terminate(): void {
terminateCalls += 1;
},
},
worker,
});
const timer = harness.timers.scheduled[0];
assert.ok(timer);
@@ -59,7 +59,7 @@ async function restoreSerializableSelector(): Promise<Selector> {
const module = await selectionModulePromise;
assert.ok(module, 'database worker post-GC selector module must exist');
const factory = module.createDatabaseWorkerPostGcSelectionApi;
assert.equal(typeof factory, 'function');
assert.ok(typeof factory === 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
@@ -86,7 +86,7 @@ test('validates database post-GC heap per measured iteration without losing raw
const module = await validityModulePromise;
assert.ok(module, 'database worker post-GC validity helper must exist');
const assess = module.assessDatabaseWorkerPostGcValidity;
assert.equal(typeof assess, 'function');
assert.ok(typeof assess === 'function');
const valid = assess([
iteration('warmup-broken', 'warmup', []),
@@ -183,7 +183,7 @@ test('marks parsing cancellation before persistence as DB N/A without accepting
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessDatabaseWorkerPostGcValidity;
assert.equal(typeof assess, 'function');
assert.ok(typeof assess === 'function');
const noDatabasePhase = assess([
iteration(
@@ -52,7 +52,7 @@ test('accepts one parseable diagnostic playlist-worker profile inside the iterat
'diagnostic playlist worker capture validator must exist'
);
const validate = module.validateDiagnosticPlaylistWorkerCapture;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-diagnostic-worker-')
);
@@ -67,7 +67,7 @@ test('accepts one parseable diagnostic playlist-worker profile inside the iterat
JSON.stringify({ nodes: [{ id: 1 }], samples: [1] })
);
await assert.doesNotReject(() =>
validate?.(mainCapture({ profilePath }), directory)
validate(mainCapture({ profilePath }), directory)
);
} finally {
await rm(directory, { force: true, recursive: true });
@@ -78,19 +78,19 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
const module = await diagnosticCaptureModulePromise;
assert.ok(module);
const validate = module.validateDiagnosticPlaylistWorkerCapture;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-diagnostic-worker-invalid-')
);
try {
await assert.rejects(
() => validate?.(mainCapture({ profilePath: null }), directory),
() => validate(mainCapture({ profilePath: null }), directory),
/diagnostic-playlist-worker-profile-missing/
);
await assert.rejects(
() =>
validate?.(
validate(
mainCapture({
profilePath: join(
directory,
@@ -109,7 +109,7 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
await writeFile(malformedPath, '{"nodes":[]}');
await assert.rejects(
() =>
validate?.(
validate(
mainCapture({ profilePath: malformedPath }),
directory
),
@@ -117,7 +117,7 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
);
await assert.rejects(
() =>
validate?.(
validate(
mainCapture({
profilePath: malformedPath,
timelineType:
@@ -129,7 +129,7 @@ test('fails closed for missing, escaped, malformed, or contaminated diagnostic a
);
await assert.rejects(
() =>
validate?.(
validate(
mainCapture({
profilePath: malformedPath,
snapshotPath: join(
@@ -137,7 +137,7 @@ test('accepts the complete parseable initial-import diagnostic artifact set', as
const module = await diagnosticArtifactsModulePromise;
assert.ok(module, 'initial-import diagnostic validator must exist');
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-diagnostic-')
);
@@ -145,7 +145,7 @@ test('accepts the complete parseable initial-import diagnostic artifact set', as
try {
await writeValidArtifacts(directory);
await assert.doesNotReject(() =>
validate?.(capture(directory), directory)
validate(capture(directory), directory)
);
} finally {
await rm(directory, { force: true, recursive: true });
@@ -156,7 +156,7 @@ test('fails closed for malformed or missing required process artifacts', async (
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-invalid-')
);
@@ -165,7 +165,7 @@ test('fails closed for malformed or missing required process artifacts', async (
await writeValidArtifacts(directory);
await writeFile(join(directory, 'renderer.trace.json'), '{');
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:renderer-trace/
);
@@ -181,7 +181,7 @@ test('fails closed for malformed or missing required process artifacts', async (
},
};
await assert.rejects(
() => validate?.(missingMainProfile, directory),
() => validate(missingMainProfile, directory),
/initial-import-diagnostic-artifact-missing:main-cpu-profile/
);
} finally {
@@ -193,7 +193,7 @@ test('rejects escaped paths and symlinks instead of reading outside the iteratio
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-contained-')
);
@@ -215,7 +215,7 @@ test('rejects escaped paths and symlinks instead of reading outside the iteratio
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(escapedCapture, directory),
() => validate(escapedCapture, directory),
/initial-import-diagnostic-artifact-path-invalid:main-cpu-profile/
);
@@ -226,7 +226,7 @@ test('rejects escaped paths and symlinks instead of reading outside the iteratio
rendererProfilePath
);
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:renderer-cpu-profile/
);
} finally {
@@ -239,7 +239,7 @@ test('requires exactly one database profile and rejects every playlist-worker pr
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-workers-')
);
@@ -251,7 +251,7 @@ test('requires exactly one database profile and rejects every playlist-worker pr
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-database-profile-cardinality-invalid/
);
@@ -261,7 +261,7 @@ test('requires exactly one database profile and rejects every playlist-worker pr
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-playlist-profile-unexpected/
);
} finally {
@@ -273,7 +273,7 @@ test('requires a structurally valid database worker heap snapshot', async () =>
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-')
);
@@ -293,7 +293,7 @@ test('requires a structurally valid database worker heap snapshot', async () =>
},
};
await assert.rejects(
() => validate?.(missingSnapshot, directory),
() => validate(missingSnapshot, directory),
/initial-import-diagnostic-artifact-missing:database-heap-snapshot/
);
@@ -302,7 +302,7 @@ test('requires a structurally valid database worker heap snapshot', async () =>
JSON.stringify({ snapshot: {}, nodes: [], edges: [] })
);
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:database-heap-snapshot/
);
} finally {
@@ -314,7 +314,7 @@ test('rejects escaped and symlinked database worker heap snapshots', async () =>
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-contained-')
);
@@ -342,7 +342,7 @@ test('rejects escaped and symlinked database worker heap snapshots', async () =>
},
};
await assert.rejects(
() => validate?.(escapedSnapshot, directory),
() => validate(escapedSnapshot, directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
@@ -350,7 +350,7 @@ test('rejects escaped and symlinked database worker heap snapshots', async () =>
await unlink(snapshotPath);
await symlink(outsideSnapshotPath, snapshotPath);
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
} finally {
@@ -363,7 +363,7 @@ test('rejects extra database snapshots and every playlist-worker snapshot', asyn
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-count-')
);
@@ -375,7 +375,7 @@ test('rejects extra database snapshots and every playlist-worker snapshot', asyn
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-database-snapshot-cardinality-invalid/
);
@@ -385,7 +385,7 @@ test('rejects extra database snapshots and every playlist-worker snapshot', asyn
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
() => validate(capture(directory), directory),
/initial-import-diagnostic-playlist-snapshot-unexpected/
);
} finally {
@@ -335,9 +335,9 @@ test('fails closed when any terminal proof or the probe itself is missing', asyn
const module = await captureModulePromise;
assert.ok(module);
const assertComplete = module.assertCompleteM3uImportRendererProbe;
assert.equal(typeof assertComplete, 'function');
assert.ok(typeof assertComplete === 'function');
assert.doesNotThrow(() => assertComplete?.(completeProbe()));
assert.doesNotThrow(() => assertComplete(completeProbe()));
for (const field of [
'playlistId',
'routeReadyEpochMs',
@@ -347,13 +347,13 @@ test('fails closed when any terminal proof or the probe itself is missing', asyn
] as const) {
const incomplete = { ...completeProbe(), [field]: null };
assert.throws(
() => assertComplete?.(incomplete),
() => assertComplete(incomplete),
/m3u-import-renderer-probe-incomplete/
);
}
assert.throws(
() =>
assertComplete?.({
assertComplete({
...completeProbe(),
operationStartEpochMs: 0,
}),
@@ -23,7 +23,7 @@ test('waits for an in-flight heartbeat and flushes its next deadline once', asyn
'document',
'HTMLButtonElement',
]);
let scheduledHeartbeat: (() => void) | null = null;
let scheduledHeartbeat = null as (() => void) | null;
let releaseHeartbeat!: () => void;
const heartbeatGate = new Promise<void>((resolve) => {
releaseHeartbeat = resolve;
@@ -329,7 +329,7 @@ function derivePhases(
);
const response = findTimeline(main.timeline, 'playlist-response');
const playlistRequest = findTimeline(main.timeline, 'playlist-request');
const playlistId = playlistRequest?.playlistId;
const playlistId = playlistRequest?.playlistId ?? undefined;
const dbGetRequest = findPlaylistDbRecord(
main.timeline,
'db-request',
@@ -205,6 +205,8 @@ export interface XtreamMainCaptureResult {
}
type MainCaptureStopTransport = XtreamMainCaptureTransport;
type MainCaptureWorkerTransport =
MainCaptureGenerationTransport['workers'][number];
export async function installMainCapture(
electronApp: ElectronApplication
@@ -1897,7 +1899,7 @@ export async function installMainCapture(
record.kind === 'database.worker' ||
record.kind === 'playlist-refresh.worker'
)
.map((record) => ({
.map((record): MainCaptureWorkerTransport => ({
captureGeneration: record.captureGeneration,
metrics: {
cancelPostedEpochMs: record.cancelPostedEpochMs,
@@ -43,7 +43,7 @@ function readProject(relativePath: string): ProjectConfiguration {
}
function readResolvedWebBuildTask(): NxGraphTask | undefined {
const environment = {
const environment: NodeJS.ProcessEnv = {
...process.env,
FORCE_COLOR: '0',
NX_DAEMON: 'false',
@@ -77,7 +77,7 @@ test('validates only measured renderer RSS runs and preserves the exact summary
const module = await validityModulePromise;
assert.ok(module, 'generic renderer RSS validity helper must exist');
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
assert.ok(typeof assess === 'function');
const result = assess([
iteration('warmup-invalid', 'warmup', { identity: null }),
@@ -102,7 +102,7 @@ test('fails every incomplete measured capture without losing its raw reason and
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
assert.ok(typeof assess === 'function');
const result = assess([
iteration('run-no-identity', 'measured', { identity: null }),
@@ -147,7 +147,7 @@ test('requires at least one measured run', async () => {
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
assert.ok(typeof assess === 'function');
assert.deepEqual(assess([iteration('warmup-01', 'warmup')]), {
invalidMeasuredRuns: [],
@@ -98,7 +98,7 @@ async function restoreSerializableFactory(): Promise<RendererWindowRssSessionFac
const module = await sessionModulePromise;
assert.ok(module, 'renderer window RSS session module must exist');
const factory = module.createRendererWindowRssSessionApi;
assert.equal(typeof factory, 'function');
assert.ok(typeof factory === 'function');
const source = factory.toString();
const restoredFactory = Function(
@@ -6,6 +6,7 @@ import test from 'node:test';
import {
type CancellationBenchmarkManifest,
type CancellationIterationResult,
type NumericDistribution,
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
type WorkerCaptureMetrics,
@@ -120,6 +121,16 @@ function playlistWorker(
};
}
const EMPTY_DISTRIBUTION: NumericDistribution = {
count: 0,
max: null,
mean: null,
median: null,
min: null,
p95: null,
p99: null,
};
function measuredIteration(
workers: readonly WorkerCaptureMetrics[]
): CancellationIterationResult {
@@ -157,14 +168,28 @@ function measuredIteration(
} as CancellationIterationResult['main'],
phases: {} as CancellationIterationResult['phases'],
renderer: {
cpuProfilePath: null,
frameGap: EMPTY_DISTRIBUTION,
heapSnapshotPath: null,
heartbeatDelay: EMPTY_DISTRIBUTION,
longTask: EMPTY_DISTRIBUTION,
peakHeapUsedBytes: 0,
postGcHeapUsedBytes: null,
probe: {
cancelButtonFound: true,
cancelClickEpochMs: null,
events: [],
frameGapsMs: [],
heartbeatDelaysMs: [],
longTasksMs: [],
operationStartEpochMs: 0,
terminalEpochMs: null,
uiPaintedEpochMs: null,
uiPhaseEpochMs: {},
uiSettledEpochMs: null,
},
} as CancellationIterationResult['renderer'],
tracePath: null,
},
runId: 'measured-1',
};
}
@@ -34,6 +34,8 @@ export interface MainCaptureGenerationTransport {
}[];
}
// A parsed capture always carries its four work timestamps; only the
// unavailable fallback reports them as null.
type ParsedWorkerPerformanceCapture = Pick<
WorkerRequestPerformanceMetrics,
| 'eventLoopDelay'
@@ -43,14 +45,15 @@ type ParsedWorkerPerformanceCapture = Pick<
| 'histogramFlushedEpochMs'
| 'invalidReason'
| 'phaseEvents'
| 'requestReceivedEpochMs'
| 'responsePostedEpochMs'
| 'threadCpuSystemMicros'
| 'threadCpuUnavailableReason'
| 'threadCpuUserMicros'
| 'workEndedEpochMs'
| 'workStartedEpochMs'
>;
> & {
readonly requestReceivedEpochMs: number;
readonly responsePostedEpochMs: number;
readonly workEndedEpochMs: number;
readonly workStartedEpochMs: number;
};
const INVALID_REASONS = new Set(['overlapping-database-worker-requests']);
const EVENT_LOOP_DELAY_REASONS = new Set([
@@ -16,11 +16,11 @@ const modulePromise = import(
test('rejects a timed-out worker sample before returning plain capture metrics', async () => {
const module = await modulePromise;
const assertCaptureValid = module.assertWorkerSampleCaptureValid;
assert.equal(typeof assertCaptureValid, 'function');
assert.ok(typeof assertCaptureValid === 'function');
assert.doesNotThrow(() => assertCaptureValid?.([]));
assert.doesNotThrow(() => assertCaptureValid([]));
assert.throws(
() => assertCaptureValid?.(['worker-sample-timeout']),
() => assertCaptureValid(['worker-sample-timeout']),
/main-capture-worker-sample-timeout/
);
});
@@ -84,7 +84,7 @@ async function restoreSerializableApi(): Promise<WorkerSampleDeadlineApi> {
const module = await modulePromise;
assert.ok(module, 'worker sample deadline module must exist');
const factory = module.createWorkerSampleDeadlineApi;
assert.equal(typeof factory, 'function');
assert.ok(typeof factory === 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
@@ -127,7 +127,7 @@ test('ignores an already queued deadline callback after successful completion',
const operation = deferred<number>();
const sampleKey = {};
const applied: number[] = [];
let queuedTimeout: (() => void) | null = null;
let queuedTimeout = null as (() => void) | null;
let timeoutCount = 0;
const timers: WorkerSampleDeadlineTimers = {
clearTimeout(): void {
@@ -369,8 +369,14 @@ export async function runXtreamBenchmarkIteration(
await prepared?.dispose().catch(() => undefined);
await renderer?.dispose().catch(() => undefined);
if (app && dataDirectory) {
const launchedApp = app;
const launchedDataDirectory = dataDirectory;
await runXtreamFinalTeardown(
() => disposeXtreamBenchmarkApp(app, dataDirectory),
() =>
disposeXtreamBenchmarkApp(
launchedApp,
launchedDataDirectory
),
propagatedFailure
);
}
@@ -321,16 +321,14 @@ export function isExactXtreamProcessIdentity(value: unknown): boolean {
'startupAttemptCount',
'startupRetryReasons',
] as const;
if (!isExactRecord(value, keys)) return false;
const startupAttemptCount = isPositiveSafeInteger(
isExactRecord(value, keys) ? value['startupAttemptCount'] : null
value['startupAttemptCount']
)
? Number(value['startupAttemptCount'])
: 0;
const startupRetryReasons = isExactRecord(value, keys)
? value['startupRetryReasons']
: null;
const startupRetryReasons = value['startupRetryReasons'];
return (
isExactRecord(value, keys) &&
isPositiveSafeInteger(value['captureGeneration']) &&
isPositiveSafeInteger(value['electronPid']) &&
value['freshProcess'] === true &&
@@ -4,7 +4,10 @@ import { describe, it } from 'node:test';
import { XTREAM_SCENARIO_ID } from './xtream-benchmark-contract';
import { createXtreamAssemblerFixture } from './xtream-iteration-assembler.fixture';
import { assembleXtreamRawIteration } from './xtream-iteration-assembler';
import { summarizeXtreamUiActionSamples } from './xtream-ui-action-probe';
import {
summarizeXtreamUiActionSamples,
type XtreamUiActionProbeResult,
} from './xtream-ui-action-probe';
describe('Xtream raw iteration assembler background UI evidence', () => {
it('binds samples to the full refresh lifecycle after the delete prelude', () => {
@@ -23,9 +26,10 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
assert.ok(background);
assert.ok(operation);
assert.ok(storeTerminalEpochMs);
const deleteStartedEpochMs = request.workStartedEpochMs;
const step = (storeTerminalEpochMs - request.workEndedEpochMs) / 12;
assert.ok(step > 0);
const samples = background.result.samples.map((sample, index) => {
const samples = uiResult(background).samples.map((sample, index) => {
const startEpochMs =
Number(request.workEndedEpochMs) + step * (index * 2 + 1);
const endEpochMs = startEpochMs + step;
@@ -51,7 +55,7 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
dashboardPaintVisibility: 'visible-without-overlay',
samples,
},
startedEpochMs: request.workStartedEpochMs,
startedEpochMs: deleteStartedEpochMs,
},
operation: {
...operation,
@@ -78,7 +82,7 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
const step =
(request.workEndedEpochMs - background.startedEpochMs) / 12;
assert.ok(step > 0);
const samples = background.result.samples.map((sample, index) => {
const samples = uiResult(background).samples.map((sample, index) => {
const startEpochMs =
background.startedEpochMs + step * (index * 2 + 1);
const endEpochMs = startEpochMs + step;
@@ -123,17 +127,19 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
);
const background = input.terminal.backgroundProbe;
assert.ok(background);
const samples = background.result.samples.map((sample, index, all) => {
if (index !== 1) return sample;
const previous = all[0];
assert.ok(previous);
const startEpochMs = previous.endEpochMs - 0.1;
return {
...sample,
latencyMs: sample.endEpochMs - startEpochMs,
startEpochMs,
};
});
const samples = uiResult(background).samples.map(
(sample, index, all) => {
if (index !== 1) return sample;
const previous = all[0];
assert.ok(previous);
const startEpochMs = previous.endEpochMs - 0.1;
return {
...sample,
latencyMs: sample.endEpochMs - startEpochMs,
startEpochMs,
};
}
);
const summary = summarizeXtreamUiActionSamples(samples);
assert.throws(
@@ -166,15 +172,17 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
input.rendererCapture.probe.storeTerminalEpochMs;
assert.ok(background);
assert.ok(storeTerminalEpochMs);
const samples = background.result.samples.map((sample, index, all) => {
if (index !== all.length - 1) return sample;
const endEpochMs = storeTerminalEpochMs + 0.1;
return {
...sample,
endEpochMs,
latencyMs: endEpochMs - sample.startEpochMs,
};
});
const samples = uiResult(background).samples.map(
(sample, index, all) => {
if (index !== all.length - 1) return sample;
const endEpochMs = storeTerminalEpochMs + 0.1;
return {
...sample,
endEpochMs,
latencyMs: endEpochMs - sample.startEpochMs,
};
}
);
const summary = summarizeXtreamUiActionSamples(samples);
assert.throws(
@@ -199,3 +207,11 @@ describe('Xtream raw iteration assembler background UI evidence', () => {
);
});
});
// The background-UI fixture builds its probe result from the UI action probe;
// the terminal evidence type erases it to unknown.
function uiResult(background: {
readonly result: unknown;
}): XtreamUiActionProbeResult {
return background.result as XtreamUiActionProbeResult;
}
@@ -336,6 +336,7 @@ describe('Xtream raw iteration assembler', () => {
);
const rendererTerminalEpochMs =
input.rendererCapture.probe.uiPaintedEpochMs;
assert.ok(rendererTerminalEpochMs !== null);
const result = assembleXtreamRawIteration({
...input,
@@ -107,6 +107,7 @@ function backgroundMetrics(
operation.startedEpochMs > background.startedEpochMs ||
operation.terminalEpochMs > input.terminal.terminalEpochMs ||
background.completedEpochMs > input.terminal.terminalEpochMs ||
input.renderer.probe.uiPaintedEpochMs === null ||
background.completedEpochMs >= input.renderer.probe.uiPaintedEpochMs
) {
invalid();
@@ -13,7 +13,8 @@ describe('Xtream legacy IPC clock skew', () => {
const request = input.mainCapture.requests.find(
({ operation }) => operation === 'DB_UPSERT_APP_PLAYLIST'
);
assert.equal(typeof request?.responseEpochMs, 'number');
assert.ok(request);
assert.equal(typeof request.responseEpochMs, 'number');
const updateSuccessEpoch = (sourceEpochMs: number) => ({
...input,
mainCapture: {
@@ -22,7 +23,7 @@ describe('Xtream legacy IPC clock skew', () => {
...input.mainCapture.capture,
timeline: input.mainCapture.capture.timeline.map((record) =>
record.type === 'preload-performance-success' &&
record.ipcCallId === request?.ipcCallId &&
record.ipcCallId === request.ipcCallId &&
record.operation === request.operation
? { ...record, sourceEpochMs }
: record
@@ -32,7 +33,7 @@ describe('Xtream legacy IPC clock skew', () => {
});
const normalized = assembleXtreamRawIteration(
updateSuccessEpoch(Number(request?.responseEpochMs) - 0.25)
updateSuccessEpoch(Number(request.responseEpochMs) - 0.25)
);
const response = normalized.phaseCapture.ipcSpans.find(
({ boundary, method }) =>
@@ -45,7 +46,7 @@ describe('Xtream legacy IPC clock skew', () => {
assert.throws(
() =>
assembleXtreamRawIteration(
updateSuccessEpoch(Number(request?.responseEpochMs) - 1)
updateSuccessEpoch(Number(request.responseEpochMs) - 1)
),
/xtream-iteration-assembly-invalid/
);
@@ -155,8 +155,9 @@ test('rejects incomplete and internally inconsistent terminal evidence', async (
test('keeps a completed delete inside the identified refresh lifecycle until store terminal', async () => {
const module = await modulePromise;
assert.ok(module?.assertXtreamBackgroundRefreshActive);
assert.ok(module.XTREAM_RENDERER_STATE_KEY);
const assertRefreshActive = module?.assertXtreamBackgroundRefreshActive;
assert.ok(assertRefreshActive);
assert.ok(module?.XTREAM_RENDERER_STATE_KEY);
const stateKey = module.XTREAM_RENDERER_STATE_KEY;
const target = globalThis as unknown as Record<string, unknown>;
const identity = {
@@ -171,17 +172,13 @@ test('keeps a completed delete inside the identified refresh lifecycle until sto
};
target[stateKey] = backgroundRefreshState(identity);
try {
await assert.doesNotReject(() =>
module.assertXtreamBackgroundRefreshActive?.(page, identity)
);
await assert.doesNotReject(() => assertRefreshActive(page, identity));
target[stateKey] = {
...backgroundRefreshState(identity),
storeTerminalEpochMs: 200,
};
await assert.rejects(
() =>
module.assertXtreamBackgroundRefreshActive?.(page, identity) ??
Promise.resolve(),
() => assertRefreshActive(page, identity),
/xtream-background-refresh-not-active/
);
target[stateKey] = backgroundRefreshState({
@@ -189,9 +186,7 @@ test('keeps a completed delete inside the identified refresh lifecycle until sto
operationId: 'different-operation',
});
await assert.rejects(
() =>
module.assertXtreamBackgroundRefreshActive?.(page, identity) ??
Promise.resolve(),
() => assertRefreshActive(page, identity),
/xtream-background-refresh-not-active/
);
} finally {
@@ -97,7 +97,6 @@ export async function prepareXtreamScenario(
options: PrepareXtreamScenarioOptions
): Promise<PreparedXtreamScenario> {
validateOptions(options);
const cancellationObserver: XtreamCancellationObserver | null = null;
let backgroundOperation: XtreamOperationWindow | null = null;
try {
if (options.scenarioId === XTREAM_SCENARIO_ID.BACKGROUND_UI) {
@@ -106,16 +105,13 @@ export async function prepareXtreamScenario(
const trigger = await prepareFinalTrigger(page, options);
return createPreparedScenario({
backgroundOperation,
cancellationObserver,
cancellationObserver: null,
options,
page,
trigger,
});
} catch (error) {
await Promise.allSettled([
cancellationObserver?.dispose(),
backgroundOperation?.dispose(),
]);
await Promise.allSettled([backgroundOperation?.dispose()]);
throw error;
}
}
@@ -99,7 +99,7 @@ describe('Xtream terminal settlement ordering', () => {
});
await Promise.resolve();
assert.deepEqual(calls, []);
assert.deepEqual<string[]>(calls, []);
rendererTerminal.resolve();
await waitFor(() => calls.includes('stop-main'));
@@ -458,7 +458,9 @@ async function waitForRemoteStatus(
port: number,
predicate: (status: RemoteControlStatus) => boolean
): Promise<RemoteControlStatus> {
let latestStatus: RemoteControlStatus | null = null;
// Typed through an assertion so the poll callback's writes stay visible
// to the return below instead of narrowing to the initial null.
let latestStatus = null as RemoteControlStatus | null;
try {
await expect
@@ -479,7 +481,10 @@ async function waitForRemoteStatus(
);
}
return latestStatus as RemoteControlStatus;
if (!latestStatus) {
throw new Error('Remote status poll passed without a status payload');
}
return latestStatus;
}
async function getRemoteStatus(
@@ -361,7 +361,7 @@ test.describe('Electron Settings', () => {
'app-html-video-player video'
);
await expect(video).toBeAttached();
await video.evaluate<void, HTMLVideoElement>((video) => {
await video.evaluate((video: HTMLVideoElement) => {
const ownerDocument = video.ownerDocument;
let activePictureInPictureElement: Element | null = null;
video.dataset['pictureInPictureRequestCount'] = '0';
+1 -1
View File
@@ -7,5 +7,5 @@
"lib": ["es2022", "dom"],
"types": ["node"]
},
"include": ["src/performance/*journey*.ts", "src/journeys/**/*.ts"]
"include": ["src/**/*.ts", "*.ts"]
}
+2 -3
View File
@@ -1507,9 +1507,8 @@ detail 187 to 143 MB.
`node:test` (`pnpm nx run electron-backend-e2e:test-performance-harness`).
`tsx` runs those specs without type checking, so also run
`pnpm run typecheck:spec electron-backend-e2e`. Its
`apps/electron-backend-e2e/tsconfig.spec.json` includes `src/journeys/**`
and `src/performance/*journey*.ts`; keep `journey` in the name of the
journey's files and specs under `src/performance/`.
`apps/electron-backend-e2e/tsconfig.spec.json` includes every TypeScript
file in the project, so a new journey file needs no `include` change.
6. Validate a counter before it becomes a guardrail: one PR must show that
lowering it moved wall-clock in the same journey.
+4 -5
View File
@@ -63,10 +63,9 @@ whose types drifted from the code it exercises. CI runs it in the
the harness's `import.meta`, BigInt literals and ES2022 library names. It
also sets `types: ["node"]`: the specs import `describe` and `it` from
`node:test`, so a Jest or Mocha global fails the check. Its `include` covers
the journey harness (`src/performance/*journey*.ts` and `src/journeys/**`);
the older benchmark files and the Electron E2E specs still have type errors,
so widen the `include` as they are fixed instead of adding a baseline or an
ignore list.
the whole project (`src/**/*.ts` and the root Playwright configs): the
journey harness, the benchmark harness and its specs, and the Electron E2E
specs, with no baseline or ignore list.
- Type test doubles instead of casting to `any`: `jest.Mocked<T>`,
`InstanceType<typeof SomeStore>` for signal stores, and
`Object.defineProperty` or a writable mapped type for read-only capability
@@ -331,7 +330,7 @@ written to the same summary file; its probe specs run with
`pnpm nx run electron-backend-e2e:test-performance-harness`, which CI runs in
the `Unit Tests and Typechecks` job of `ci.yml` on every run. `tsx` runs them
without type checking; `pnpm run typecheck:spec electron-backend-e2e` checks
the journey harness (see Unit And Type Checks). The
the whole project, those specs included (see Unit And Type Checks). The
`electron-backend-e2e` command targets call `tsx` and `playwright` directly,
not through `pnpm exec`: under `pnpm nx`, a nested `pnpm exec` can run from the
workspace root instead of the target `cwd` and miss cwd-relative specs, globs