返回 CodeWhale
compiled-hosts.test.js
根目录 / npm / codewhale / test / compiled-hosts.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 test = require('node:test');
6 const hosts = require('../scripts/compiled-hosts');
7 const { run, _internal } = require('../scripts/install');
8
9 function fixture(target = 'macos-arm64', marker = 'one') {
10 const names = hosts.names(target);
11 const payloads = { [names.binary]: Buffer.from(`image-${marker}`), [names.notices]: Buffer.from(`actual-notice-${marker}`), [names.source]: Buffer.from(`source-fixture-${marker}`) };
12 const digest = 'a'.repeat(64), source = 'b'.repeat(40);
13 const catalog = { schema: 1, version: '0.10.1', source_sha: source, bundle_sha256: digest, hosts: [{
14 target, asset: names.binary, sha256: hosts.sha256(payloads[names.binary]),
15 notices_asset: names.notices, notices_sha256: hosts.sha256(payloads[names.notices]),
16 source_asset: names.source, source_sha256: hosts.sha256(payloads[names.source]),
17 runtime_version: '1.4.0', runtime_revision: 'c'.repeat(40), runtime_sha256: 'd'.repeat(64), webkit_revision: 'e'.repeat(40),
18 bundle_sha256: digest, source_commit: source, native_platform: hosts.TARGETS[target][0], native_arch: hosts.TARGETS[target][1], libc: target.startsWith('linux-') ? 'glibc' : 'none',
19 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),
20 license_closure: 'complete', relink_source: 'complete',
21 }] };
22 const text = JSON.stringify(catalog);
23 payloads[hosts.HOST_CATALOG] = Buffer.from(text);
24 return { catalog, text, payloads, host: catalog.hosts[0] };
25 }
26 async function temporary(t) {
27 const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'cw-host-delivery-'));
28 t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
29 return dir;
30 }
31 function installOptions(releaseDir, f, extra = {}) {
32 const source = { baseUrl: 'https://fixture.invalid/', checksums: new Map(Object.entries(f.payloads).map(([name, bytes]) => [name, hosts.sha256(bytes)])) };
33 return { version: f.catalog.version, releaseDir, source, context: 'install', options: {
34 platform: hosts.TARGETS[f.host.target][0], arch: hosts.TARGETS[f.host.target][1],
35 fetchText: async () => f.text,
36 download: async (url, target) => fs.promises.writeFile(target, f.payloads[path.basename(new URL(url).pathname)]), ...extra,
37 } };
38 }
39 test('catalog refuses malformed, skipped, cross-native, duplicate and incomplete closure receipts', () => {
40 const good = fixture();
41 assert.equal(hosts.parseCatalog(Buffer.from(good.text), '0.10.1').hosts.length, 1);
42 for (const mutate of [c => c.hosts[0].native_skipped = 1, c => c.hosts[0].passed = 0, c => c.hosts[0].native_arch = 'x64', c => c.hosts[0].license_closure = 'pending', c => c.hosts[0].source_asset = '../source.tar.gz', c => c.hosts.push(c.hosts[0]), c => c.source_sha = 'bad']) {
43 const copy = structuredClone(good.catalog); mutate(copy); assert.throws(() => hosts.parseCatalog(copy));
44 }
45 assert.throws(() => hosts.parseCatalog(good.text, '0.10.2'), /differs/);
46 assert.throws(() => hosts.parseCatalog(' '.repeat(65537)), /64 KiB/);
47 assert.throws(() => hosts.selectedHost(good.catalog, 'android', 'arm64'), /Android/);
48 assert.throws(() => hosts.selectedHost(good.catalog, 'darwin', 'x64'), /no qualified image/);
49 assert.equal(hosts.requested({}), false);
50 });
51 test('payload directory rejects tampering and symbolic substitutions', async t => {
52 const dir = await temporary(t), f = fixture();
53 for (const [name, bytes] of Object.entries(f.payloads)) fs.writeFileSync(path.join(dir, name), bytes);
54 hosts.verifyDirectory(dir, f.catalog);
55 fs.writeFileSync(path.join(dir, f.host.source_asset), 'tampered');
56 assert.throws(() => hosts.verifyDirectory(dir, f.catalog), /SHA256/);
57 fs.unlinkSync(path.join(dir, f.host.source_asset));
58 fs.symlinkSync(path.join(dir, f.host.notices_asset), path.join(dir, f.host.source_asset));
59 assert.throws(() => hosts.verifyDirectory(dir, f.catalog), /regular payload/);
60 });
61 test('npm stages every companion before publication and keeps canonical file modes', async t => {
62 const dir = await temporary(t), f = fixture(), prepared = await _internal.prepareCompiledHost(installOptions(dir, f));
63 assert.equal(fs.existsSync(path.join(dir, hosts.HOST_NAME)), false);
64 try {
65 await prepared.publish();
66 assert.deepEqual(fs.readFileSync(path.join(dir, hosts.HOST_NAME)), f.payloads[f.host.asset]);
67 assert.equal(hosts.parseCatalog(fs.readFileSync(path.join(dir, hosts.HOST_NAME + '.release.json'))).version, '0.10.1');
68 if (process.platform !== 'win32') {
69 assert.equal(fs.statSync(path.join(dir, hosts.HOST_NAME)).mode & 0o777, 0o755);
70 assert.equal(fs.statSync(path.join(dir, hosts.HOST_NAME + '.LICENSES.txt')).mode & 0o777, 0o644);
71 }
72 } finally { await prepared.cleanup(); }
73 assert.equal(fs.readdirSync(dir).some(name => name.startsWith('.compiled-host-')), false);
74 });
75 test('npm rejects foreign companion ownership and destination changes without overwriting them', async t => {
76 const dir = await temporary(t), f = fixture();
77 fs.writeFileSync(path.join(dir, hosts.HOST_NAME), 'foreign');
78 await assert.rejects(_internal.prepareCompiledHost(installOptions(dir, f)), /unclaimed/);
79 assert.equal(fs.readFileSync(path.join(dir, hosts.HOST_NAME), 'utf8'), 'foreign');
80 fs.unlinkSync(path.join(dir, hosts.HOST_NAME));
81 const prepared = await _internal.prepareCompiledHost(installOptions(dir, f));
82 fs.writeFileSync(path.join(dir, hosts.HOST_NAME + '.LICENSES.txt'), 'concurrent');
83 try { await assert.rejects(prepared.publish(), /changed/); } finally { await prepared.cleanup(); }
84 assert.equal(fs.existsSync(path.join(dir, hosts.HOST_NAME)), false);
85 assert.equal(fs.readFileSync(path.join(dir, hosts.HOST_NAME + '.LICENSES.txt'), 'utf8'), 'concurrent');
86 });
87 test('npm updates an owned companion and rejects modified prior bytes', async t => {
88 const dir = await temporary(t), first = fixture(), second = fixture('macos-arm64', 'two');
89 await _internal.installCompiledHost(installOptions(dir, first));
90 await _internal.installCompiledHost(installOptions(dir, second));
91 assert.deepEqual(fs.readFileSync(path.join(dir, hosts.HOST_NAME)), second.payloads[second.host.asset]);
92 fs.writeFileSync(path.join(dir, hosts.HOST_NAME + '.LICENSES.txt'), 'changed');
93 await assert.rejects(_internal.installCompiledHost(installOptions(dir, first)), /modified/);
94 assert.deepEqual(fs.readFileSync(path.join(dir, hosts.HOST_NAME)), second.payloads[second.host.asset]);
95 });
96 test('explicit unavailable host fails before any CLI destination or download changes', async t => {
97 const dir = await temporary(t), f = fixture(), cli = path.join(dir, 'codewhale'), alias = path.join(dir, 'codew');
98 fs.writeFileSync(cli, 'old-cli'); fs.writeFileSync(alias, 'old-alias');
99 let downloads = 0;
100 const source = 'https://fixture.invalid/';
101 const checksums = new Map([['codewhale-macos-x64', 'a'.repeat(64)], ['codew-macos-x64', 'a'.repeat(64)], [hosts.HOST_CATALOG, hosts.sha256(Buffer.from(f.text))]]);
102 await assert.rejects(run({ releaseDir: dir, paths: { codewhale: { target: cli, asset: 'codewhale-macos-x64' }, codew: { target: alias, asset: 'codew-macos-x64' } }, platform: 'darwin', arch: 'x64', env: { CODEWHALE_VERSION: '0.10.1', CODEWHALE_RELEASE_BASE_URL: source, CODEWHALE_INSTALL_COMPILED_HOST: '1' }, fetchText: async url => url.endsWith(hosts.HOST_CATALOG) ? f.text : [...checksums].map(([name, hash]) => `${hash} ${name}`).join('\n'), download: async () => { downloads++; } }), /no qualified image/);
103 assert.equal(downloads, 0);
104 assert.equal(fs.readFileSync(cli, 'utf8'), 'old-cli'); assert.equal(fs.readFileSync(alias, 'utf8'), 'old-alias');
105 });
106 module.exports = { fixture };
107
108 test('Windows delivery requires containment and memory plus all seven LPAC cases', () => {
109 const f = fixture('windows-x64');
110 f.catalog.hosts[0].native_passed = 8;
111 assert.throws(() => hosts.parseCatalog(f.catalog), /Native compiled-image/);
112 f.catalog.hosts[0].native_passed = 9;
113 assert.equal(hosts.parseCatalog(f.catalog).hosts[0].passed, 5);
114 // Cross-source contract proof; actual PowerShell installation is separate.
115 const installer = fs.readFileSync(path.resolve(__dirname, '../../../scripts/release/install.ps1'), 'utf8');
116 for (const target of ['windows-x64', 'windows-arm64']) {
117 assert.match(installer, new RegExp(`\\$hostEntry\\.passed -ne ${hosts.compiledMinimum(target)}(?: |\\))`));
118 assert.match(installer, new RegExp(`\\$hostEntry\\.native_passed -lt ${hosts.nativeMinimum(target)}(?: |\\))`));
119 }
120 });
121
122 test('npm fresh publication never overwrites a file created after its identity check', async t => {
123 const dir = await temporary(t), f = fixture(), prepared = await _internal.prepareCompiledHost(installOptions(dir, f));
124 const original = fs.promises.link;
125 fs.promises.link = async (source, destination) => {
126 await fs.promises.writeFile(destination, 'another writer');
127 return original(source, destination);
128 };
129 try { await assert.rejects(prepared.publish(), /EEXIST/); }
130 finally { fs.promises.link = original; await prepared.cleanup(); }
131 assert.equal(fs.readFileSync(path.join(dir, hosts.HOST_NAME), 'utf8'), 'another writer');
132 });
133
134 // Fixture receipts verify contract refusal only; they are not Native OS proof.
135 test('every target requires its exact direct-image cases and separate Native memory proof without skips', () => {
136 for (const target of Object.keys(hosts.TARGETS)) {
137 const f = fixture(target), host = f.catalog.hosts[0];
138 const expectedDirect = target.startsWith('macos-') ? 6 : 5;
139 const expectedNative = target.startsWith('windows-') ? 9 : 2;
140 assert.equal(hosts.compiledMinimum(target), expectedDirect);
141 assert.equal(hosts.nativeMinimum(target), expectedNative);
142 assert.equal(hosts.parseCatalog(f.catalog).hosts[0].passed, expectedDirect);
143 for (const count of [expectedDirect - 1, expectedDirect + 1]) {
144 host.passed = count;
145 assert.throws(() => hosts.parseCatalog(f.catalog), /compiled-image qualification/);
146 }
147 host.passed = expectedDirect;
148 host.native_passed = expectedNative - 1;
149 assert.throws(() => hosts.parseCatalog(f.catalog), /Native compiled-image/);
150 host.native_passed = expectedNative;
151 host.skipped = 1;
152 assert.throws(() => hosts.parseCatalog(f.catalog), /compiled-image qualification/);
153 host.skipped = 0; host.native_skipped = 1;
154 assert.throws(() => hosts.parseCatalog(f.catalog), /Native compiled-image/);
155 }
156 });
157
157 lines JAVASCRIPT