mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
* 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>
395 lines
13 KiB
TypeScript
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 });
|
|
}
|
|
});
|