Files
iptvnator/apps/electron-backend-e2e/src/performance/initial-import-diagnostic-artifacts.spec.ts
T
4grayandClaude Opus 5.5 078b91bfdf 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>
2026-10-10 14:33:15 +02:00

395 lines
13 KiB
TypeScript

/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { mkdtemp, rm, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
interface DiagnosticCapture {
readonly main: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly workers: readonly {
readonly kind: string;
readonly ordinal: number;
readonly profilePath: string | null;
readonly snapshotPath: string | null;
}[];
};
readonly renderer: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly tracePath: string | null;
};
}
interface DiagnosticArtifactsModule {
validateInitialImportDiagnosticArtifacts?: (
capture: DiagnosticCapture,
iterationDirectory: string
) => Promise<void>;
}
const diagnosticArtifactsModulePromise = import(
new URL('./initial-import-diagnostic-artifacts.ts', import.meta.url).href
)
.then((module) => module as DiagnosticArtifactsModule)
.catch(() => null);
const HEAP_SNAPSHOT = {
edges: [0, 0, 0],
nodes: [0, 0, 1, 0, 1, 0],
snapshot: {
meta: {
edge_fields: ['type', 'name_or_index', 'to_node'],
edge_types: [['context'], 'string_or_number', 'node'],
node_fields: [
'type',
'name',
'id',
'self_size',
'edge_count',
'detachedness',
],
node_types: [
['hidden'],
'string',
'number',
'number',
'number',
'number',
],
},
},
strings: ['', 'root'],
};
const RENDERER_TRACE = {
traceEvents: [{ name: 'initial-import', ph: 'X', ts: 1 }],
};
function capture(directory: string, databaseOrdinal = 4): DiagnosticCapture {
return {
main: {
cpuProfilePath: join(directory, 'main.cpuprofile'),
heapSnapshotPath: join(directory, 'main.heapsnapshot'),
workers: [
{
kind: 'database.worker',
ordinal: databaseOrdinal,
profilePath: join(
directory,
`database.worker-${databaseOrdinal}.cpuprofile`
),
snapshotPath: join(
directory,
`database.worker-${databaseOrdinal}.heapsnapshot`
),
},
],
},
renderer: {
cpuProfilePath: join(directory, 'renderer.cpuprofile'),
heapSnapshotPath: join(directory, 'renderer.heapsnapshot'),
tracePath: join(directory, 'renderer.trace.json'),
},
};
}
async function writeValidArtifacts(
directory: string,
databaseOrdinal = 4
): Promise<void> {
await Promise.all([
writeFile(
join(directory, 'main.cpuprofile'),
JSON.stringify(CPU_PROFILE)
),
writeFile(
join(directory, 'renderer.cpuprofile'),
JSON.stringify(CPU_PROFILE)
),
writeFile(
join(directory, `database.worker-${databaseOrdinal}.cpuprofile`),
JSON.stringify(CPU_PROFILE)
),
writeFile(
join(directory, `database.worker-${databaseOrdinal}.heapsnapshot`),
JSON.stringify(HEAP_SNAPSHOT)
),
writeFile(
join(directory, 'main.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
),
writeFile(
join(directory, 'renderer.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
),
writeFile(
join(directory, 'renderer.trace.json'),
JSON.stringify(RENDERER_TRACE)
),
]);
}
test('accepts the complete parseable initial-import diagnostic artifact set', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module, 'initial-import diagnostic validator must exist');
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-diagnostic-')
);
try {
await writeValidArtifacts(directory);
await assert.doesNotReject(() =>
validate(capture(directory), directory)
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('fails closed for malformed or missing required process artifacts', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-invalid-')
);
try {
await writeValidArtifacts(directory);
await writeFile(join(directory, 'renderer.trace.json'), '{');
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:renderer-trace/
);
await writeFile(
join(directory, 'renderer.trace.json'),
JSON.stringify(RENDERER_TRACE)
);
const missingMainProfile: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
cpuProfilePath: null,
},
};
await assert.rejects(
() => validate(missingMainProfile, directory),
/initial-import-diagnostic-artifact-missing:main-cpu-profile/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('rejects escaped paths and symlinks instead of reading outside the iteration directory', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-contained-')
);
const outsideDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-outside-')
);
try {
await writeValidArtifacts(directory);
const escapedCapture: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
cpuProfilePath: join(outsideDirectory, 'main.cpuprofile'),
},
};
await writeFile(
join(outsideDirectory, 'main.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate(escapedCapture, directory),
/initial-import-diagnostic-artifact-path-invalid:main-cpu-profile/
);
const rendererProfilePath = join(directory, 'renderer.cpuprofile');
await unlink(rendererProfilePath);
await symlink(
join(outsideDirectory, 'main.cpuprofile'),
rendererProfilePath
);
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:renderer-cpu-profile/
);
} finally {
await rm(directory, { force: true, recursive: true });
await rm(outsideDirectory, { force: true, recursive: true });
}
});
test('requires exactly one database profile and rejects every playlist-worker profile', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-workers-')
);
try {
await writeValidArtifacts(directory);
await writeFile(
join(directory, 'database.worker-9.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-database-profile-cardinality-invalid/
);
await unlink(join(directory, 'database.worker-9.cpuprofile'));
await writeFile(
join(directory, 'playlist-refresh.worker-2.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-playlist-profile-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('requires a structurally valid database worker heap snapshot', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-')
);
try {
await writeValidArtifacts(directory);
const missingSnapshot: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
workers: [
{
...capture(directory).main.workers[0],
snapshotPath: null,
},
],
},
};
await assert.rejects(
() => validate(missingSnapshot, directory),
/initial-import-diagnostic-artifact-missing:database-heap-snapshot/
);
await writeFile(
join(directory, 'database.worker-4.heapsnapshot'),
JSON.stringify({ snapshot: {}, nodes: [], edges: [] })
);
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:database-heap-snapshot/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('rejects escaped and symlinked database worker heap snapshots', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-contained-')
);
const outsideDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-outside-')
);
try {
await writeValidArtifacts(directory);
const outsideSnapshotPath = join(
outsideDirectory,
'database.worker-4.heapsnapshot'
);
await writeFile(outsideSnapshotPath, JSON.stringify(HEAP_SNAPSHOT));
const escapedSnapshot: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
workers: [
{
...capture(directory).main.workers[0],
snapshotPath: outsideSnapshotPath,
},
],
},
};
await assert.rejects(
() => validate(escapedSnapshot, directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
const snapshotPath = join(directory, 'database.worker-4.heapsnapshot');
await unlink(snapshotPath);
await symlink(outsideSnapshotPath, snapshotPath);
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
} finally {
await rm(directory, { force: true, recursive: true });
await rm(outsideDirectory, { force: true, recursive: true });
}
});
test('rejects extra database snapshots and every playlist-worker snapshot', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.ok(typeof validate === 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-count-')
);
try {
await writeValidArtifacts(directory);
await writeFile(
join(directory, 'database.worker-9.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-database-snapshot-cardinality-invalid/
);
await unlink(join(directory, 'database.worker-9.heapsnapshot'));
await writeFile(
join(directory, 'playlist-refresh.worker-2.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate(capture(directory), directory),
/initial-import-diagnostic-playlist-snapshot-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});