| 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 |