Files
iptvnator/tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging

* docs: plan Linux frame-copy packaging

* feat(packaging): define Linux frame-copy profiles

* fix(packaging): reject inherited profile names

* feat(embedded-mpv): validate staged Linux runtime

* fix(embedded-mpv): require Linux source packages

* fix(embedded-mpv): harden Linux runtime staging

* feat(embedded-mpv): build LGPL Linux runtime

* fix(embedded-mpv): pin Linux runtime inputs

* feat(embedded-mpv): build relocatable Linux helper

* fix(embedded-mpv): require bundled Linux runtime

* fix(embedded-mpv): make Linux runtime portable

* feat(packaging): ship Linux frame-copy artifacts

* fix(embedded-mpv): verify Linux helper linkage

* fix(packaging): enforce Linux frame-copy isolation

* fix(embedded-mpv): pin Linux display data

* docs(embedded-mpv): document Linux frame-copy packaging

* feat(embedded-mpv): probe Linux frame-copy runtime

* test(embedded-mpv): smoke packaged Linux frame-copy

* docs(embedded-mpv): clarify Linux system runtime baseline

* fix(embedded-mpv): harden Linux runtime capability gate

* ci: verify Linux frame-copy packages

* test(embedded-mpv): harden packaged Linux smoke

* test(embedded-mpv): preserve packaged GL mode

* test(packaging): harden Linux package probes

* fix(embedded-mpv): enable private Snap shared memory

* fix(embedded-mpv): sanitize Linux helper environment

* fix(packaging): enforce private Snap memory semantics

* fix(packaging): reject ambiguous Snap memory metadata

* fix(embedded-mpv): prioritize trusted Snap GL

* fix(packaging): reject advanced Snap YAML semantics

* fix(packaging): reject arbitrary Snap YAML aliases

* feat(packaging): ship Linux runtime license notices

* docs(embedded-mpv): document Linux runtime distribution

* fix(packaging): parse Snap trailing comments safely

* fix(release): gate Snap publish on public source release

* fix(packaging): strip VCS metadata from source bundle

* docs(packaging): clarify Linux source release gate

* test(embedded-mpv): smoke missing bundled libmpv

* style(embedded-mpv): format final validation inputs

* fix(e2e): satisfy fixture index signature typing

* fix(ci): declare fontconfig gperf generator

* fix(embedded-mpv): hash runtime cache identities

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): tighten runtime delivery gates

* fix(ci): decouple Linux runtime matrix

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): validate Linux frame-copy runtimes

* fix(packaging): scope Snap Electron library checks

* feat(packaging): ship Linux frame-copy runtimes

* fix(packaging): improve Linux runtime smoke diagnostics

* fix(packaging): expose bounded helper probe details

* test(packaging): trace Snap EGL probe failures

* fix(packaging): prefer core22 ABI in Snap helper

* fix(packaging): bound helper probe capture

* fix(packaging): harden Linux frame-copy releases

* fix(packaging): canonicalize libplacebo submodule identity

* fix(packaging): make source archive inspection portable

* fix(packaging): harden Snap release verification
2026-07-18 17:28:22 +02:00

777 lines
26 KiB
JavaScript

import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import test from 'node:test';
import { fileURLToPath, pathToFileURL } from 'node:url';
const require = createRequire(import.meta.url);
const {
canonicalizeGitSubmoduleStatus,
} = require('../embedded-mpv/build-linux-runtime.cjs');
const helperPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'prepare-linux-runtime-source-snapshot.cjs'
);
async function loadHelper() {
if (!fs.existsSync(helperPath)) {
return null;
}
return import(pathToFileURL(helperPath).href);
}
function runGit(cwd, args, env = {}) {
const result = spawnSync('git', args, {
cwd,
encoding: 'utf8',
env: {
...process.env,
...env,
},
});
assert.equal(
result.status,
0,
`git ${args.join(' ')} failed:\n${result.stderr}`
);
return result.stdout.trim();
}
function createEquivalentCheckouts(root) {
const origin = path.join(root, 'origin');
fs.mkdirSync(origin);
runGit(origin, ['init', '--quiet']);
fs.mkdirSync(path.join(origin, 'src'));
fs.writeFileSync(path.join(origin, 'src', 'renderer.c'), 'int main() {}\n');
fs.writeFileSync(path.join(origin, 'LICENSE'), 'license\n');
fs.symlinkSync('../LICENSE', path.join(origin, 'src', 'license-link'));
runGit(origin, ['add', '.']);
runGit(origin, ['commit', '--quiet', '-m', 'source fixture'], {
GIT_AUTHOR_NAME: 'Fixture',
GIT_AUTHOR_EMAIL: 'fixture@example.invalid',
GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
GIT_COMMITTER_NAME: 'Fixture',
GIT_COMMITTER_EMAIL: 'fixture@example.invalid',
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
});
const commit = runGit(origin, ['rev-parse', 'HEAD']);
const first = path.join(root, 'first');
const second = path.join(root, 'second');
runGit(root, ['clone', '--quiet', origin, first]);
runGit(root, ['clone', '--quiet', origin, second]);
fs.appendFileSync(
path.join(first, '.git', 'config'),
'\n[fixture]\n\tvalue = first\n'
);
fs.appendFileSync(
path.join(second, '.git', 'config'),
'\n[fixture]\n\tvalue = second\n'
);
fs.writeFileSync(path.join(first, '.git', 'fixture-cache'), 'one');
fs.writeFileSync(path.join(second, '.git', 'fixture-cache'), 'two');
const differentTime = new Date('2030-01-01T00:00:00Z');
fs.utimesSync(
path.join(second, '.git', 'index'),
differentTime,
differentTime
);
return { commit, first, second };
}
function createCheckoutWithSubmodule(root) {
const submoduleOrigin = path.join(root, 'submodule-origin');
fs.mkdirSync(submoduleOrigin);
runGit(submoduleOrigin, ['init', '--quiet']);
fs.writeFileSync(
path.join(submoduleOrigin, 'submodule-source.c'),
'int submodule_source;\n'
);
runGit(submoduleOrigin, ['add', '.']);
runGit(submoduleOrigin, ['commit', '--quiet', '-m', 'submodule source'], {
GIT_AUTHOR_NAME: 'Fixture',
GIT_AUTHOR_EMAIL: 'fixture@example.invalid',
GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
GIT_COMMITTER_NAME: 'Fixture',
GIT_COMMITTER_EMAIL: 'fixture@example.invalid',
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
});
const checkout = path.join(root, 'checkout-with-submodule');
fs.mkdirSync(checkout);
runGit(checkout, ['init', '--quiet']);
fs.writeFileSync(path.join(checkout, 'README'), 'source tree\n');
runGit(checkout, ['add', 'README']);
runGit(checkout, [
'-c',
'protocol.file.allow=always',
'submodule',
'add',
'--quiet',
submoduleOrigin,
'3rdparty/example',
]);
runGit(checkout, ['commit', '--quiet', '-m', 'source with submodule'], {
GIT_AUTHOR_NAME: 'Fixture',
GIT_AUTHOR_EMAIL: 'fixture@example.invalid',
GIT_AUTHOR_DATE: '2000-01-02T00:00:00Z',
GIT_COMMITTER_NAME: 'Fixture',
GIT_COMMITTER_EMAIL: 'fixture@example.invalid',
GIT_COMMITTER_DATE: '2000-01-02T00:00:00Z',
});
return {
checkout,
expected: {
sourceGitCommit: runGit(checkout, ['rev-parse', 'HEAD']),
sourceSubmodules: canonicalizeGitSubmoduleStatus(
runGit(checkout, ['submodule', 'status', '--recursive'])
),
},
};
}
function snapshotRecords(root) {
const records = [];
function visit(directory, relativeDirectory = '') {
for (const entry of fs
.readdirSync(directory, { withFileTypes: true })
.sort(({ name: left }, { name: right }) =>
left.localeCompare(right)
)) {
const relativePath = path.posix.join(relativeDirectory, entry.name);
const absolutePath = path.join(directory, entry.name);
const stat = fs.lstatSync(absolutePath);
if (entry.isDirectory()) {
records.push({
path: `${relativePath}/`,
mode: stat.mode & 0o777,
});
visit(absolutePath, relativePath);
} else if (entry.isSymbolicLink()) {
records.push({
path: relativePath,
mode: stat.mode & 0o777,
symlink: fs.readlinkSync(absolutePath),
});
} else {
records.push({
path: relativePath,
mode: stat.mode & 0o777,
sha256: crypto
.createHash('sha256')
.update(fs.readFileSync(absolutePath))
.digest('hex'),
});
}
}
}
visit(root);
return records;
}
const FIXTURE_SOURCE_SNAPSHOT = Object.freeze({
schemaVersion: 1,
sha256: '445a78c08a661d68f8ab15a790ba9c3462766d23e31b00eeca7429544c21a497',
entryCount: 4,
totalBytes: 22,
entries: [
{
path: 'LICENSE',
type: 'file',
size: 8,
executable: false,
sha256: 'c0c56958ef8be5c1979366896b7e0c7206949a5aa2b23f51429c7f56b10990d3',
},
{
path: 'src',
type: 'directory',
},
{
path: 'src/license-link',
type: 'symlink',
target: '../LICENSE',
},
{
path: 'src/renderer.c',
type: 'file',
size: 14,
executable: false,
sha256: 'bc8bb8e433bf65214540115414c821c904b2a30d60a3ac0424bf9b77a00024b7',
},
],
});
function cloneFixtureSourceSnapshot() {
return JSON.parse(JSON.stringify(FIXTURE_SOURCE_SNAPSHOT));
}
test('purely validates the exact source snapshot contract independent of object key order', async () => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const shuffled = {
entries: FIXTURE_SOURCE_SNAPSHOT.entries.map((entry) => {
if (entry.type === 'file') {
return {
sha256: entry.sha256,
executable: entry.executable,
size: entry.size,
type: entry.type,
path: entry.path,
};
}
if (entry.type === 'symlink') {
return {
target: entry.target,
type: entry.type,
path: entry.path,
};
}
return {
type: entry.type,
path: entry.path,
};
}),
totalBytes: FIXTURE_SOURCE_SNAPSHOT.totalBytes,
entryCount: FIXTURE_SOURCE_SNAPSHOT.entryCount,
sha256: FIXTURE_SOURCE_SNAPSHOT.sha256,
schemaVersion: FIXTURE_SOURCE_SNAPSHOT.schemaVersion,
};
assert.deepEqual(
helper.validateLinuxRuntimeSourceSnapshot(shuffled),
FIXTURE_SOURCE_SNAPSHOT
);
assert.deepEqual(
helper.validateLinuxRuntimeSourceSnapshot(shuffled, {
expectedSha256: FIXTURE_SOURCE_SNAPSHOT.sha256,
}),
FIXTURE_SOURCE_SNAPSHOT
);
assert.deepEqual(shuffled.entries[0], {
sha256: FIXTURE_SOURCE_SNAPSHOT.entries[0].sha256,
executable: false,
size: 8,
type: 'file',
path: 'LICENSE',
});
});
test('rejects non-exact source snapshot fields and malformed entry shapes', async () => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const invalidSnapshots = [];
const extraTopLevelField = cloneFixtureSourceSnapshot();
extraTopLevelField.extra = true;
invalidSnapshots.push(extraTopLevelField);
const missingTopLevelField = cloneFixtureSourceSnapshot();
delete missingTopLevelField.totalBytes;
invalidSnapshots.push(missingTopLevelField);
const extraEntryField = cloneFixtureSourceSnapshot();
extraEntryField.entries[0].mode = 0o644;
invalidSnapshots.push(extraEntryField);
const missingEntryField = cloneFixtureSourceSnapshot();
delete missingEntryField.entries[0].executable;
invalidSnapshots.push(missingEntryField);
const invalidFileSize = cloneFixtureSourceSnapshot();
invalidFileSize.entries[0].size = -1;
invalidSnapshots.push(invalidFileSize);
const invalidExecutable = cloneFixtureSourceSnapshot();
invalidExecutable.entries[0].executable = 1;
invalidSnapshots.push(invalidExecutable);
const invalidFileHash = cloneFixtureSourceSnapshot();
invalidFileHash.entries[0].sha256 = 'A'.repeat(64);
invalidSnapshots.push(invalidFileHash);
const invalidType = cloneFixtureSourceSnapshot();
invalidType.entries[1].type = 'socket';
invalidSnapshots.push(invalidType);
for (const snapshot of invalidSnapshots) {
assert.throws(
() => helper.validateLinuxRuntimeSourceSnapshot(snapshot),
/invalid source snapshot/i
);
}
});
test('rejects unsafe or unsorted paths, bad links, aggregates, and digests', async () => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const unsorted = cloneFixtureSourceSnapshot();
[unsorted.entries[0], unsorted.entries[1]] = [
unsorted.entries[1],
unsorted.entries[0],
];
assert.throws(
() => helper.validateLinuxRuntimeSourceSnapshot(unsorted),
/sorted and unique/i
);
const duplicate = cloneFixtureSourceSnapshot();
duplicate.entries[1].path = duplicate.entries[0].path;
assert.throws(
() => helper.validateLinuxRuntimeSourceSnapshot(duplicate),
/sorted and unique/i
);
const unsafePath = cloneFixtureSourceSnapshot();
unsafePath.entries[0].path = '../LICENSE';
assert.throws(
() => helper.validateLinuxRuntimeSourceSnapshot(unsafePath),
/unsafe source snapshot path/i
);
const unsafeLink = cloneFixtureSourceSnapshot();
unsafeLink.entries[2].target = '../../outside';
assert.throws(
() => helper.validateLinuxRuntimeSourceSnapshot(unsafeLink),
/unsafe source snapshot symlink/i
);
const missingParentDirectory = cloneFixtureSourceSnapshot();
missingParentDirectory.entries.splice(1, 1);
missingParentDirectory.entryCount -= 1;
assert.throws(
() => helper.validateLinuxRuntimeSourceSnapshot(missingParentDirectory),
/parent.*directory/i
);
for (const [field, value] of [
['entryCount', FIXTURE_SOURCE_SNAPSHOT.entryCount + 1],
['totalBytes', FIXTURE_SOURCE_SNAPSHOT.totalBytes + 1],
['sha256', '0'.repeat(64)],
]) {
const invalidAggregate = cloneFixtureSourceSnapshot();
invalidAggregate[field] = value;
assert.throws(
() => helper.validateLinuxRuntimeSourceSnapshot(invalidAggregate),
/invalid source snapshot/i
);
}
assert.throws(
() =>
helper.validateLinuxRuntimeSourceSnapshot(FIXTURE_SOURCE_SNAPSHOT, {
expectedSha256: '0'.repeat(64),
}),
/source snapshot digest mismatch/i
);
});
test('prepares identical VCS-free snapshots from equivalent clean checkouts', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-snapshot-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { commit, first, second } = createEquivalentCheckouts(root);
const firstOutput = path.join(root, 'snapshot-first');
const secondOutput = path.join(root, 'snapshot-second');
const expected = {
sourceGitCommit: commit,
sourceSubmodules: [],
};
const firstRecord = helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: firstOutput,
expected,
});
const secondRecord = helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: second,
outputPath: secondOutput,
expected,
});
assert.deepEqual(firstRecord, {
...expected,
sourceSnapshot: FIXTURE_SOURCE_SNAPSHOT,
});
assert.deepEqual(secondRecord, {
...expected,
sourceSnapshot: FIXTURE_SOURCE_SNAPSHOT,
});
assert.deepEqual(
helper.LINUX_RUNTIME_SOURCE_SNAPSHOT_CONTRACT,
Object.freeze({
schemaVersion: 1,
hashAlgorithm: 'sha256',
canonicalEncoding: 'utf8-json-line-v1',
})
);
assert.deepEqual(
helper.inventoryLinuxRuntimeSourceSnapshot(firstOutput),
FIXTURE_SOURCE_SNAPSHOT
);
assert.deepEqual(
snapshotRecords(firstOutput),
snapshotRecords(secondOutput)
);
assert.equal(
snapshotRecords(firstOutput).some(({ path: recordPath }) =>
recordPath.split('/').includes('.git')
),
false
);
assert.equal(
fs.readlinkSync(path.join(firstOutput, 'src', 'license-link')),
'../LICENSE'
);
});
test('normalizes regular-file executable permissions in the canonical inventory', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-modes-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const first = path.join(root, 'first');
const second = path.join(root, 'second');
fs.mkdirSync(path.join(first, 'bin'), { recursive: true, mode: 0o700 });
fs.mkdirSync(path.join(second, 'bin'), { recursive: true, mode: 0o755 });
fs.writeFileSync(path.join(first, 'bin', 'tool'), '#!/bin/sh\n', {
mode: 0o700,
});
fs.writeFileSync(path.join(second, 'bin', 'tool'), '#!/bin/sh\n', {
mode: 0o755,
});
fs.writeFileSync(path.join(first, 'README'), 'same\n', { mode: 0o600 });
fs.writeFileSync(path.join(second, 'README'), 'same\n', { mode: 0o644 });
const firstInventory = helper.inventoryLinuxRuntimeSourceSnapshot(first);
const secondInventory = helper.inventoryLinuxRuntimeSourceSnapshot(second);
assert.deepEqual(firstInventory, secondInventory);
assert.deepEqual(
firstInventory.entries.map((entry) => ({
path: entry.path,
executable: entry.executable,
})),
[
{ path: 'README', executable: false },
{ path: 'bin', executable: undefined },
{ path: 'bin/tool', executable: true },
]
);
});
test('globally sorts prefix-colliding sibling paths before hashing', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-global-order-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'c'));
fs.mkdirSync(path.join(root, 'c++'));
fs.writeFileSync(path.join(root, 'c', 'tool'), 'c\n');
fs.writeFileSync(path.join(root, 'c++', 'tool'), 'c++\n');
const inventory = helper.inventoryLinuxRuntimeSourceSnapshot(root);
assert.deepEqual(
inventory.entries.map(({ path: entryPath }) => entryPath),
['c', 'c++', 'c++/tool', 'c/tool']
);
assert.doesNotThrow(() =>
helper.validateLinuxRuntimeSourceSnapshot(inventory)
);
});
test('fails closed on VCS entries, unsafe symlinks, and special files', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-unsafe-entry-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const vcsRoot = path.join(root, 'vcs');
fs.mkdirSync(vcsRoot);
fs.writeFileSync(path.join(vcsRoot, '.git'), 'gitdir: elsewhere\n');
assert.throws(
() => helper.inventoryLinuxRuntimeSourceSnapshot(vcsRoot),
/must not contain VCS metadata.*\.git/i
);
const escapingLinkRoot = path.join(root, 'escaping-link');
fs.mkdirSync(path.join(escapingLinkRoot, 'nested'), { recursive: true });
fs.symlinkSync(
'../../outside',
path.join(escapingLinkRoot, 'nested', 'link')
);
assert.throws(
() => helper.inventoryLinuxRuntimeSourceSnapshot(escapingLinkRoot),
/unsafe source snapshot symlink.*nested\/link.*\.\.\/\.\.\/outside/i
);
const absoluteLinkRoot = path.join(root, 'absolute-link');
fs.mkdirSync(absoluteLinkRoot);
fs.symlinkSync('/outside', path.join(absoluteLinkRoot, 'link'));
assert.throws(
() => helper.inventoryLinuxRuntimeSourceSnapshot(absoluteLinkRoot),
/unsafe source snapshot symlink.*\/outside/i
);
if (process.platform !== 'win32') {
const specialRoot = path.join(root, 'special');
fs.mkdirSync(specialRoot);
const fifoPath = path.join(specialRoot, 'pipe');
const mkfifo = spawnSync('mkfifo', [fifoPath], { encoding: 'utf8' });
assert.equal(mkfifo.status, 0, mkfifo.stderr);
assert.throws(
() => helper.inventoryLinuxRuntimeSourceSnapshot(specialRoot),
/unsupported source snapshot entry.*pipe/i
);
}
});
test('checks an optional expected digest atomically for direct callers', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-expected-digest-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { commit, first } = createEquivalentCheckouts(root);
const expected = {
sourceGitCommit: commit,
sourceSubmodules: [],
};
const matchingOutput = path.join(root, 'matching');
assert.deepEqual(
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: matchingOutput,
expected,
expectedSourceSnapshotSha256: FIXTURE_SOURCE_SNAPSHOT.sha256,
}).sourceSnapshot,
FIXTURE_SOURCE_SNAPSHOT
);
const mismatchingOutput = path.join(root, 'mismatching');
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: mismatchingOutput,
expected,
expectedSourceSnapshotSha256: '0'.repeat(64),
}),
/source snapshot digest mismatch.*expected 000000.*received 445a78/i
);
assert.equal(fs.existsSync(mismatchingOutput), false);
});
test('production CLI always enforces the trusted pinned libplacebo digest', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
assert.match(
helper.EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SNAPSHOT_SHA256,
/^[a-f0-9]{64}$/
);
assert.equal(
helper.EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SNAPSHOT_SHA256,
'0db67c1523411255244186af437e9fbfe7ccac04a5ac1b3dc9275dd0806f6f0c'
);
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-cli-digest-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { commit, first } = createEquivalentCheckouts(root);
const runtimeManifestPath = path.join(root, 'runtime-manifest.json');
fs.writeFileSync(
runtimeManifestPath,
JSON.stringify({
packages: {
libplacebo: {
sourceGitCommit: commit,
sourceSubmodules: [],
},
},
})
);
const outputPath = path.join(root, 'output');
const recordOutputPath = path.join(root, 'record.json');
const result = spawnSync(
process.execPath,
[
helperPath,
'prepare',
'--runtime-manifest',
runtimeManifestPath,
'--checkout',
first,
'--output',
outputPath,
'--record-output',
recordOutputPath,
],
{ encoding: 'utf8' }
);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /source snapshot digest mismatch/i);
assert.equal(fs.existsSync(outputPath), false);
assert.equal(fs.existsSync(recordOutputPath), false);
});
test('validates checkout cleanliness and exact commit/submodule identities before copying', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-identity-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { commit, first, second } = createEquivalentCheckouts(root);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: path.join(root, 'wrong-commit'),
expected: {
sourceGitCommit: '0'.repeat(40),
sourceSubmodules: [],
},
}),
/commit does not match/
);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: path.join(root, 'wrong-submodules'),
expected: {
sourceGitCommit: commit,
sourceSubmodules: [`${'1'.repeat(40)} 3rdparty/example`],
},
}),
/submodules do not match/
);
fs.writeFileSync(path.join(second, 'untracked-build-output'), 'dirty');
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: second,
outputPath: path.join(root, 'dirty'),
expected: {
sourceGitCommit: commit,
sourceSubmodules: [],
},
}),
/dirty or untracked files/
);
const existingOutput = path.join(root, 'existing-output');
fs.mkdirSync(existingOutput);
fs.writeFileSync(
path.join(existingOutput, 'owned-by-someone-else'),
'keep'
);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: existingOutput,
expected: {
sourceGitCommit: commit,
sourceSubmodules: [],
},
}),
/output.*must not already exist/i
);
assert.equal(
fs.readFileSync(
path.join(existingOutput, 'owned-by-someone-else'),
'utf8'
),
'keep'
);
const existingSymlink = path.join(root, 'existing-symlink');
fs.symlinkSync('missing-target', existingSymlink);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: existingSymlink,
expected: {
sourceGitCommit: commit,
sourceSubmodules: [],
},
}),
/output.*must not already exist/i
);
assert.equal(fs.readlinkSync(existingSymlink), 'missing-target');
});
test('preserves recursive submodule sources while stripping every nested .git link', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-submodule-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { checkout, expected } = createCheckoutWithSubmodule(root);
const output = path.join(root, 'submodule-snapshot');
const record = helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: checkout,
outputPath: output,
expected,
});
assert.deepEqual(
{
sourceGitCommit: record.sourceGitCommit,
sourceSubmodules: record.sourceSubmodules,
},
expected
);
assert.deepEqual(
record.sourceSnapshot,
helper.inventoryLinuxRuntimeSourceSnapshot(output)
);
assert.equal(
fs.readFileSync(
path.join(output, '3rdparty', 'example', 'submodule-source.c'),
'utf8'
),
'int submodule_source;\n'
);
assert.doesNotThrow(() => helper.assertNoGitMetadata(output));
});
test('fails closed when a prepared snapshot contains any nested .git entry', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-vcs-entry-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'nested', '.git'), { recursive: true });
fs.writeFileSync(path.join(root, 'nested', '.git', 'index'), 'metadata');
assert.throws(
() => helper.assertNoGitMetadata(root),
/must not contain VCS metadata.*nested\/\.git/i
);
});