diff --git a/apps/electron-backend-e2e/src/category-management.e2e.ts b/apps/electron-backend-e2e/src/category-management.e2e.ts index 7f4ae4d8e..1bbf31381 100644 --- a/apps/electron-backend-e2e/src/category-management.e2e.ts +++ b/apps/electron-backend-e2e/src/category-management.e2e.ts @@ -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(); - 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(); + 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!; } diff --git a/apps/electron-backend-e2e/src/downloads.e2e.ts b/apps/electron-backend-e2e/src/downloads.e2e.ts index 7247550bf..f76c3ea78 100644 --- a/apps/electron-backend-e2e/src/downloads.e2e.ts +++ b/apps/electron-backend-e2e/src/downloads.e2e.ts @@ -108,7 +108,7 @@ async function selectDownloadCoverSize( await expect .poll(() => page.evaluate( - () => document.documentElement.dataset.coverSize ?? null + () => document.documentElement.dataset['coverSize'] ?? null ) ) .toBe(size); diff --git a/apps/electron-backend-e2e/src/electron-process-lifecycle.ts b/apps/electron-backend-e2e/src/electron-process-lifecycle.ts index d0302668b..d8e4220fb 100644 --- a/apps/electron-backend-e2e/src/electron-process-lifecycle.ts +++ b/apps/electron-backend-e2e/src/electron-process-lifecycle.ts @@ -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; diff --git a/apps/electron-backend-e2e/src/performance/database-worker-peak-memory-validity.spec.ts b/apps/electron-backend-e2e/src/performance/database-worker-peak-memory-validity.spec.ts index ffe7d9fb2..89c4d88e3 100644 --- a/apps/electron-backend-e2e/src/performance/database-worker-peak-memory-validity.spec.ts +++ b/apps/electron-backend-e2e/src/performance/database-worker-peak-memory-validity.spec.ts @@ -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 -) { + databaseWorker: PeakMemoryWorker +): PeakMemoryIteration { return { kind, main: { workers: [databaseWorker] }, diff --git a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-cutoff.spec.ts b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-cutoff.spec.ts index 631081fd4..02efca2b9 100644 --- a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-cutoff.spec.ts +++ b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-cutoff.spec.ts @@ -31,7 +31,7 @@ async function restoreSerializableApi(): Promise { 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/); diff --git a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-finalization.spec.ts b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-finalization.spec.ts index 2223e48c1..da1a08843 100644 --- a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-finalization.spec.ts +++ b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-finalization.spec.ts @@ -57,7 +57,7 @@ async function restoreSerializableApi(): Promise { 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/); diff --git a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-probe.spec.ts b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-probe.spec.ts index 39802115d..126b32ed8 100644 --- a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-probe.spec.ts +++ b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-probe.spec.ts @@ -158,7 +158,7 @@ async function restoreSerializableApi(): Promise { 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); diff --git a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-selection.spec.ts b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-selection.spec.ts index 89559182f..837a2637b 100644 --- a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-selection.spec.ts +++ b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-selection.spec.ts @@ -59,7 +59,7 @@ async function restoreSerializableSelector(): Promise { 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/); diff --git a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-validity.spec.ts b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-validity.spec.ts index c26a4163d..76a14c74a 100644 --- a/apps/electron-backend-e2e/src/performance/database-worker-post-gc-validity.spec.ts +++ b/apps/electron-backend-e2e/src/performance/database-worker-post-gc-validity.spec.ts @@ -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( diff --git a/apps/electron-backend-e2e/src/performance/diagnostic-playlist-worker-capture.spec.ts b/apps/electron-backend-e2e/src/performance/diagnostic-playlist-worker-capture.spec.ts index f8fdc7434..a5a981c83 100644 --- a/apps/electron-backend-e2e/src/performance/diagnostic-playlist-worker-capture.spec.ts +++ b/apps/electron-backend-e2e/src/performance/diagnostic-playlist-worker-capture.spec.ts @@ -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( diff --git a/apps/electron-backend-e2e/src/performance/initial-import-diagnostic-artifacts.spec.ts b/apps/electron-backend-e2e/src/performance/initial-import-diagnostic-artifacts.spec.ts index 1270709c3..481a84559 100644 --- a/apps/electron-backend-e2e/src/performance/initial-import-diagnostic-artifacts.spec.ts +++ b/apps/electron-backend-e2e/src/performance/initial-import-diagnostic-artifacts.spec.ts @@ -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 { diff --git a/apps/electron-backend-e2e/src/performance/m3u-import-renderer-capture.spec.ts b/apps/electron-backend-e2e/src/performance/m3u-import-renderer-capture.spec.ts index 41feed196..3fc084e11 100644 --- a/apps/electron-backend-e2e/src/performance/m3u-import-renderer-capture.spec.ts +++ b/apps/electron-backend-e2e/src/performance/m3u-import-renderer-capture.spec.ts @@ -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, }), diff --git a/apps/electron-backend-e2e/src/performance/m3u-import-renderer-heartbeat-lifecycle.spec.ts b/apps/electron-backend-e2e/src/performance/m3u-import-renderer-heartbeat-lifecycle.spec.ts index fd4be42b1..4d49c5044 100644 --- a/apps/electron-backend-e2e/src/performance/m3u-import-renderer-heartbeat-lifecycle.spec.ts +++ b/apps/electron-backend-e2e/src/performance/m3u-import-renderer-heartbeat-lifecycle.spec.ts @@ -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((resolve) => { releaseHeartbeat = resolve; diff --git a/apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation-report.ts b/apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation-report.ts index 3a296d51e..6d08e2bc6 100644 --- a/apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation-report.ts +++ b/apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation-report.ts @@ -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', diff --git a/apps/electron-backend-e2e/src/performance/m3u-refresh-main-capture.ts b/apps/electron-backend-e2e/src/performance/m3u-refresh-main-capture.ts index e38defbd9..ef6490947 100644 --- a/apps/electron-backend-e2e/src/performance/m3u-refresh-main-capture.ts +++ b/apps/electron-backend-e2e/src/performance/m3u-refresh-main-capture.ts @@ -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, diff --git a/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts b/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts index 1bf9822be..e6bfbdabe 100644 --- a/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts +++ b/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts @@ -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', diff --git a/apps/electron-backend-e2e/src/performance/renderer-rss-validity.spec.ts b/apps/electron-backend-e2e/src/performance/renderer-rss-validity.spec.ts index 2e94cee01..7a3a0b9d5 100644 --- a/apps/electron-backend-e2e/src/performance/renderer-rss-validity.spec.ts +++ b/apps/electron-backend-e2e/src/performance/renderer-rss-validity.spec.ts @@ -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: [], diff --git a/apps/electron-backend-e2e/src/performance/renderer-window-rss-session.spec.ts b/apps/electron-backend-e2e/src/performance/renderer-window-rss-session.spec.ts index 3aa641215..7fd248b58 100644 --- a/apps/electron-backend-e2e/src/performance/renderer-window-rss-session.spec.ts +++ b/apps/electron-backend-e2e/src/performance/renderer-window-rss-session.spec.ts @@ -98,7 +98,7 @@ async function restoreSerializableFactory(): Promise; +> & { + 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([ diff --git a/apps/electron-backend-e2e/src/performance/worker-sample-capture-validity.spec.ts b/apps/electron-backend-e2e/src/performance/worker-sample-capture-validity.spec.ts index 95e502d7d..097296622 100644 --- a/apps/electron-backend-e2e/src/performance/worker-sample-capture-validity.spec.ts +++ b/apps/electron-backend-e2e/src/performance/worker-sample-capture-validity.spec.ts @@ -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/ ); }); diff --git a/apps/electron-backend-e2e/src/performance/worker-sample-deadline.spec.ts b/apps/electron-backend-e2e/src/performance/worker-sample-deadline.spec.ts index 4b826fa2b..c43ec6b04 100644 --- a/apps/electron-backend-e2e/src/performance/worker-sample-deadline.spec.ts +++ b/apps/electron-backend-e2e/src/performance/worker-sample-deadline.spec.ts @@ -84,7 +84,7 @@ async function restoreSerializableApi(): Promise { 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(); const sampleKey = {}; const applied: number[] = []; - let queuedTimeout: (() => void) | null = null; + let queuedTimeout = null as (() => void) | null; let timeoutCount = 0; const timers: WorkerSampleDeadlineTimers = { clearTimeout(): void { diff --git a/apps/electron-backend-e2e/src/performance/xtream-benchmark-iteration.ts b/apps/electron-backend-e2e/src/performance/xtream-benchmark-iteration.ts index 03868de83..0185e45cc 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-benchmark-iteration.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-benchmark-iteration.ts @@ -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 ); } diff --git a/apps/electron-backend-e2e/src/performance/xtream-exact-schema.ts b/apps/electron-backend-e2e/src/performance/xtream-exact-schema.ts index 1b139526c..59bd32812 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-exact-schema.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-exact-schema.ts @@ -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 && diff --git a/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler-background-ui.spec.ts b/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler-background-ui.spec.ts index e31ee1fe8..bbf45d406 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler-background-ui.spec.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler-background-ui.spec.ts @@ -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; +} diff --git a/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler.spec.ts b/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler.spec.ts index 02a73af95..8c45c504a 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler.spec.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler.spec.ts @@ -336,6 +336,7 @@ describe('Xtream raw iteration assembler', () => { ); const rendererTerminalEpochMs = input.rendererCapture.probe.uiPaintedEpochMs; + assert.ok(rendererTerminalEpochMs !== null); const result = assembleXtreamRawIteration({ ...input, diff --git a/apps/electron-backend-e2e/src/performance/xtream-iteration-assembly-scenarios.ts b/apps/electron-backend-e2e/src/performance/xtream-iteration-assembly-scenarios.ts index ed4b72541..472c7dea2 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-iteration-assembly-scenarios.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-iteration-assembly-scenarios.ts @@ -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(); diff --git a/apps/electron-backend-e2e/src/performance/xtream-legacy-ipc-clock-skew.spec.ts b/apps/electron-backend-e2e/src/performance/xtream-legacy-ipc-clock-skew.spec.ts index ad9a99ab8..f7bbabc14 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-legacy-ipc-clock-skew.spec.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-legacy-ipc-clock-skew.spec.ts @@ -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/ ); diff --git a/apps/electron-backend-e2e/src/performance/xtream-renderer-probe.spec.ts b/apps/electron-backend-e2e/src/performance/xtream-renderer-probe.spec.ts index 1af2bc63a..7d410d5e5 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-renderer-probe.spec.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-renderer-probe.spec.ts @@ -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; 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 { diff --git a/apps/electron-backend-e2e/src/performance/xtream-scenario-driver.ts b/apps/electron-backend-e2e/src/performance/xtream-scenario-driver.ts index 2f87c7c7b..eb1d8f2ac 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-scenario-driver.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-scenario-driver.ts @@ -97,7 +97,6 @@ export async function prepareXtreamScenario( options: PrepareXtreamScenarioOptions ): Promise { 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; } } diff --git a/apps/electron-backend-e2e/src/performance/xtream-terminal-settlement.spec.ts b/apps/electron-backend-e2e/src/performance/xtream-terminal-settlement.spec.ts index 632801dc2..0672fb690 100644 --- a/apps/electron-backend-e2e/src/performance/xtream-terminal-settlement.spec.ts +++ b/apps/electron-backend-e2e/src/performance/xtream-terminal-settlement.spec.ts @@ -99,7 +99,7 @@ describe('Xtream terminal settlement ordering', () => { }); await Promise.resolve(); - assert.deepEqual(calls, []); + assert.deepEqual(calls, []); rendererTerminal.resolve(); await waitFor(() => calls.includes('stop-main')); diff --git a/apps/electron-backend-e2e/src/remote-control.e2e.ts b/apps/electron-backend-e2e/src/remote-control.e2e.ts index e26056872..fdf683810 100644 --- a/apps/electron-backend-e2e/src/remote-control.e2e.ts +++ b/apps/electron-backend-e2e/src/remote-control.e2e.ts @@ -458,7 +458,9 @@ async function waitForRemoteStatus( port: number, predicate: (status: RemoteControlStatus) => boolean ): Promise { - 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( diff --git a/apps/electron-backend-e2e/src/settings.e2e.ts b/apps/electron-backend-e2e/src/settings.e2e.ts index a2d14bd82..19600e5ef 100644 --- a/apps/electron-backend-e2e/src/settings.e2e.ts +++ b/apps/electron-backend-e2e/src/settings.e2e.ts @@ -361,7 +361,7 @@ test.describe('Electron Settings', () => { 'app-html-video-player video' ); await expect(video).toBeAttached(); - await video.evaluate((video) => { + await video.evaluate((video: HTMLVideoElement) => { const ownerDocument = video.ownerDocument; let activePictureInPictureElement: Element | null = null; video.dataset['pictureInPictureRequestCount'] = '0'; diff --git a/apps/electron-backend-e2e/tsconfig.spec.json b/apps/electron-backend-e2e/tsconfig.spec.json index af86edd23..dfd9a01d2 100644 --- a/apps/electron-backend-e2e/tsconfig.spec.json +++ b/apps/electron-backend-e2e/tsconfig.spec.json @@ -7,5 +7,5 @@ "lib": ["es2022", "dom"], "types": ["node"] }, - "include": ["src/performance/*journey*.ts", "src/journeys/**/*.ts"] + "include": ["src/**/*.ts", "*.ts"] } diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index 776350400..d8e8d3192 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -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. diff --git a/docs/architecture/validation-map.md b/docs/architecture/validation-map.md index cf61ef3f3..3c49fd812 100644 --- a/docs/architecture/validation-map.md +++ b/docs/architecture/validation-map.md @@ -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`, `InstanceType` 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