返回 CodeWhale
compiled-host-delivery.test.js
根目录 / scripts / release / compiled-host-delivery.test.js
1 const assert = require('node:assert/strict');
2 const fs = require('node:fs');
3 const os = require('node:os');
4 const path = require('node:path');
5 const { spawnSync } = require('node:child_process');
6 const test = require('node:test');
7 const hosts = require('../../npm/codewhale/scripts/compiled-hosts');
8 const artifacts = require('../../npm/codewhale/scripts/artifacts');
9 const { assemble, verifyAssetDirectory } = require('./assemble-release-assets');
10 const { fetchProof } = require('./fetch-compiled-host-proof');
11 const root = path.resolve(__dirname, '../..');
12 const source = 'b'.repeat(40), bundle = 'a'.repeat(64);
13 function catalog(target = 'linux-x64') {
14 const names = hosts.names(target), payloads = { [names.binary]: Buffer.from('tested-image'), [names.notices]: Buffer.from('notices-fixture'), [names.source]: Buffer.from('source-fixture') };
15 const value = { schema: 1, version: '0.10.1', source_sha: source, bundle_sha256: bundle, hosts: [{ target, asset: names.binary, sha256: hosts.sha256(payloads[names.binary]), notices_asset: names.notices, notices_sha256: hosts.sha256(payloads[names.notices]), source_asset: names.source, source_sha256: hosts.sha256(payloads[names.source]), runtime_version: '1.4.0', runtime_revision: 'c'.repeat(40), runtime_sha256: 'd'.repeat(64), webkit_revision: 'e'.repeat(40), bundle_sha256: bundle, source_commit: source, native_platform: hosts.TARGETS[target][0], native_arch: hosts.TARGETS[target][1], libc: target.startsWith('linux-') ? 'glibc' : 'none', passed: hosts.compiledMinimum(target), failed: 0, skipped: 0, test_log_sha256: 'f'.repeat(64), native_passed: hosts.nativeMinimum(target), native_failed: 0, native_skipped: 0, native_log_sha256: '1'.repeat(64), license_closure: 'complete', relink_source: 'complete' }] };
16 return { value, payloads };
17 }
18 function temp(t) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-delivery-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); return dir; }
19 function writeCatalog(dir, f) { fs.mkdirSync(dir, { recursive: true }); for (const [name, bytes] of Object.entries(f.payloads)) fs.writeFileSync(path.join(dir, name), bytes); fs.writeFileSync(path.join(dir, hosts.HOST_CATALOG), JSON.stringify(f.value)); }
20 function baselineInputs(input) {
21 const bundles = path.join(input, 'codewhale-bundles'); fs.mkdirSync(bundles, { recursive: true });
22 const rows = [];
23 for (const name of artifacts.allReleaseAssetNames()) {
24 if ([artifacts.CHECKSUM_MANIFEST, artifacts.BUNDLE_CHECKSUM_MANIFEST, 'codewhale.bat'].includes(name)) continue;
25 const dir = artifacts.BUNDLE_ASSET_NAMES.includes(name) ? bundles : path.join(input, name); fs.mkdirSync(dir, { recursive: true });
26 const bytes = Buffer.from('fixture-' + name); fs.writeFileSync(path.join(dir, name), bytes);
27 if (artifacts.BUNDLE_ASSET_NAMES.includes(name)) rows.push(`${hosts.sha256(bytes)} ${name}`);
28 }
29 fs.writeFileSync(path.join(bundles, artifacts.BUNDLE_CHECKSUM_MANIFEST), rows.sort().join('\n') + '\n');
30 }
31 test('qualified optional assembly extends exact inventory and verifies all payload digests', async t => {
32 const dir = temp(t), input = path.join(dir, 'input'), output = path.join(dir, 'output'), f = catalog();
33 baselineInputs(input); writeCatalog(path.join(input, 'codewhale-compiled-hosts'), f);
34 await assemble(input, output); await verifyAssetDirectory(output);
35 assert.equal(fs.readdirSync(output).length, 38);
36 assert.deepEqual(fs.readdirSync(output).sort(), artifacts.allReleaseAssetNames(f.value).sort());
37 fs.writeFileSync(path.join(output, f.value.hosts[0].notices_asset), 'changed');
38 await assert.rejects(verifyAssetDirectory(output), /checksum|SHA256/);
39 });
40 test('collector retains same tested bytes, rejects mixed source and has no implicit empty eligibility', t => {
41 const dir = temp(t), input = path.join(dir, 'input'), output = path.join(dir, 'output'), first = catalog(), second = catalog('macos-arm64');
42 writeCatalog(path.join(input, 'codewhale-compiled-host-linux-x64'), first);
43 writeCatalog(path.join(input, 'codewhale-compiled-host-macos-arm64'), second);
44 const invoke = () => spawnSync(process.execPath, [path.join(__dirname, 'stage-compiled-host.mjs'), '--collect', input, '--output-dir', output], { encoding: 'utf8' });
45 assert.equal(invoke().status, 0);
46 const combined = hosts.parseCatalog(fs.readFileSync(path.join(output, hosts.HOST_CATALOG)));
47 assert.equal(combined.hosts.length, 2); hosts.verifyDirectory(output, combined);
48 second.value.source_sha = '3'.repeat(40); second.value.hosts[0].source_commit = second.value.source_sha;
49 writeCatalog(path.join(input, 'codewhale-compiled-host-macos-arm64'), second);
50 assert.match(invoke().stderr, /mixed release source_sha/);
51 fs.rmSync(input, { recursive: true }); fs.mkdirSync(input);
52 assert.match(invoke().stderr, /no qualified/);
53 });
54 test('CI collector accepts only green exact official workflow and same contained image/log', t => {
55 const dir = temp(t), output = path.join(dir, 'proof'), image = Buffer.from('native-tested'), log = Buffer.from('actual-native-log');
56 let run = { workflow_id: 42, path: '.github/workflows/ci.yml', head_sha: source, status: 'completed', conclusion: 'success', repository: { full_name: 'codewhale-hq/CodeWhale' } };
57 let requestedArtifact, nativePassed = hosts.nativeMinimum('linux-x64');
58 const exec = (_binary, args) => {
59 if (args[0] === 'run') {
60 requestedArtifact = args[args.indexOf('--name') + 1];
61 fs.writeFileSync(path.join(output, 'codewhale-extension-host'), image);
62 fs.writeFileSync(path.join(output, 'native-containment.log'), log);
63 fs.writeFileSync(path.join(output, 'native-receipt.json'), JSON.stringify({ scope: 'native-compiled-host', source_sha: source, platform: 'linux', arch: 'x64', passed: nativePassed, failed: 0, skipped: 0, host_sha256: hosts.sha256(image), log: 'native-containment.log', log_sha256: hosts.sha256(log) }));
64 return '';
65 }
66 if (args[1].endsWith('/ci.yml')) return JSON.stringify({ id: 42 });
67 if (args[1].includes('/artifacts?')) return JSON.stringify([{ artifacts: [{ name: 'native-compiled-host-Linux-X64', expired: false }] }]);
68 return JSON.stringify(run);
69 };
70 const request = { repo: 'codewhale-hq/CodeWhale', runId: '123', sourceSha: source, target: 'linux-x64', output };
71 assert.equal(fetchProof(request, exec).image, path.join(output, 'codewhale-extension-host'));
72 assert.equal(requestedArtifact, 'native-compiled-host-Linux-X64');
73 fs.rmSync(output, { recursive: true }); nativePassed = 1;
74 assert.throws(() => fetchProof(request, exec), /qualify this exact native target/);
75 nativePassed = hosts.nativeMinimum('linux-x64');
76 fs.rmSync(output, { recursive: true }); run = { ...run, head_sha: '4'.repeat(40) };
77 assert.throws(() => fetchProof(request, exec), /exact source/);
78 run = { ...run, head_sha: source, conclusion: 'failure' }; assert.throws(() => fetchProof(request, exec), /green official/);
79 });
80 test('archive companion is opt-in and missing qualification refuses before command writes', t => {
81 const dir = temp(t), archive = path.join(dir, 'archive'), home = path.join(dir, 'home'); fs.mkdirSync(archive); fs.mkdirSync(home);
82 fs.copyFileSync(path.join(__dirname, 'install.sh'), path.join(archive, 'install.sh'));
83 for (const name of ['codewhale', 'codew']) fs.writeFileSync(path.join(archive, name), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
84 const attempt = spawnSync('bash', [path.join(archive, 'install.sh')], { env: { ...process.env, HOME: home, CODEWHALE_INSTALL_COMPILED_HOST: '1' }, encoding: 'utf8' });
85 assert.notEqual(attempt.status, 0); assert.match(attempt.stderr, /no complete qualified payload/);
86 assert.equal(fs.existsSync(path.join(home, '.local/bin/codewhale')), false);
87 });
88 test('whole platform archives contain only their own companion and Windows shared installer', t => {
89 const dir = temp(t), input = path.join(dir, 'input'), output = path.join(dir, 'output'), f = catalog();
90 baselineInputs(input); writeCatalog(path.join(input, 'codewhale-compiled-hosts'), f);
91 const result = spawnSync('bash', [path.join(__dirname, 'create-release-bundles.sh'), input, output], { cwd: root, env: { ...process.env, SOURCE_DATE_EPOCH: '1728000000' }, encoding: 'utf8' });
92 assert.equal(result.status, 0, result.stderr);
93 const list = name => spawnSync(name.endsWith('.zip') ? 'unzip' : 'tar', name.endsWith('.zip') ? ['-Z1', path.join(output, name)] : ['-tzf', path.join(output, name)], { encoding: 'utf8' }).stdout;
94 const linux = list('codewhale-linux-x64.tar.gz'); assert.match(linux, /codewhale-extension-host\.relink-source\.tar\.gz/); assert.match(linux, /codewhale-extension-host\.release\.json/);
95 assert.doesNotMatch(list('codewhale-android-arm64.tar.gz'), /codewhale-extension-host/);
96 assert.match(list('codewhale-windows-x64.zip'), /install\.ps1/);
97 });
98
99 test('runtime names and completeness flags alone cannot qualify missing notice texts', { skip: process.platform === 'win32' ? 'Unix executable fixture; actual Windows Native compiled-image proof is a separate required gate' : false }, t => {
100 const dir = temp(t), bun = path.join(dir, 'local-bun'), notices = path.join(dir, 'notices.txt'), sourceArchive = path.join(dir, 'source.tar.gz'), closure = path.join(dir, 'closure.json');
101 const revision = 'c'.repeat(40), version = '1.4.0';
102 fs.writeFileSync(bun, `#!/bin/sh\nif [ \"$1\" = \"--revision\" ]; then printf '${version}+${revision.slice(0, 9)}\\n'; else printf '${JSON.stringify({ platform: process.platform, arch: process.arch })}\\n'; fi\n`, { mode: 0o755 });
103 fs.writeFileSync(notices, 'MIT alone is not a complete Bun notice set'); fs.writeFileSync(sourceArchive, 'not a source closure');
104 const bundleFile = path.join(root, 'crates/tui/extension-host/dist/codewhale-extension-host.mjs');
105 const receipt = { schema: 1, runtime_version: version, runtime_revision: revision, runtime_sha256: hosts.sha256(fs.readFileSync(bun)), webkit_revision: 'e'.repeat(40), libc: process.platform === 'linux' ? 'glibc' : 'none', bundle_sha256: hosts.sha256(fs.readFileSync(bundleFile)), source_commit: source, notices: 'notices.txt', notices_sha256: hosts.sha256(fs.readFileSync(notices)), relink_source: 'source.tar.gz', source_sha256: hosts.sha256(fs.readFileSync(sourceArchive)), license_closure: 'complete', corresponding_source: 'complete', components: ['bun', 'JavaScriptCore', 'WebCore', 'tinycc', 'boringssl'] };
106 fs.writeFileSync(closure, JSON.stringify(receipt));
107 const target = Object.keys(hosts.TARGETS).find(name => hosts.TARGETS[name][0] === process.platform && hosts.TARGETS[name][1] === process.arch);
108 assert.ok(target, 'test must run on a supported native delivery platform');
109 const result = spawnSync(process.execPath, [path.join(__dirname, 'stage-compiled-host.mjs'), '--target', target, '--bun', bun, '--runtime-closure', closure, '--version', '0.10.1', '--source-sha', source, '--output-dir', path.join(dir, 'out')], { encoding: 'utf8' });
110 assert.notEqual(result.status, 0); assert.match(result.stderr, /incomplete runtime component closure/);
111 assert.equal(fs.existsSync(path.join(dir, 'out')), false);
112 });
113
114 test('executed Bun architecture cannot inherit an ARM runner label under emulation', { skip: process.platform === 'win32' ? 'Unix runtime executable fixture; Windows producer qualification remains required' : false }, t => {
115 const dir = temp(t), bun = path.join(dir, 'foreign-bun'), closure = path.join(dir, 'closure.json');
116 const revision = 'c'.repeat(40), version = '1.4.0';
117 const wrongArch = process.arch === 'arm64' ? 'x64' : 'arm64';
118 fs.writeFileSync(bun, `#!/bin/sh\nif [ "$1" = "--revision" ]; then printf '${version}+${revision.slice(0, 9)}\\n'; else printf '${JSON.stringify({ platform: process.platform, arch: wrongArch })}\\n'; fi\n`, { mode: 0o755 });
119 const bundleFile = path.join(root, 'crates/tui/extension-host/dist/codewhale-extension-host.mjs');
120 fs.writeFileSync(closure, JSON.stringify({ schema: 1, runtime_version: version, runtime_revision: revision, runtime_sha256: hosts.sha256(fs.readFileSync(bun)), bundle_sha256: hosts.sha256(fs.readFileSync(bundleFile)), source_commit: source }));
121 const target = Object.keys(hosts.TARGETS).find(name => hosts.TARGETS[name][0] === process.platform && hosts.TARGETS[name][1] === process.arch);
122 assert.ok(target);
123 const result = spawnSync(process.execPath, [path.join(__dirname, 'stage-compiled-host.mjs'), '--target', target, '--bun', bun, '--runtime-closure', closure, '--version', '0.10.1', '--source-sha', source, '--output-dir', path.join(dir, 'out')], { encoding: 'utf8' });
124 assert.notEqual(result.status, 0); assert.match(result.stderr, /selected Bun process identity.*emulation proof does not transfer/);
125 assert.equal(fs.existsSync(path.join(dir, 'out')), false);
126 });
127
127 lines JAVASCRIPT