release-macos-manifest.test.mjs
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠ breaking
24 days ago
| 1 | import assert from 'node:assert/strict'; |
| 2 | import fs from 'node:fs'; |
| 3 | import os from 'node:os'; |
| 4 | import path from 'node:path'; |
| 5 | import { spawnSync } from 'node:child_process'; |
| 6 | import test from 'node:test'; |
| 7 | |
| 8 | function makeApp(root) { |
| 9 | const resources = path.join(root, 'Knowtation.app/Contents/Resources'); |
| 10 | fs.mkdirSync(path.join(resources, 'runtime'), { recursive: true }); |
| 11 | fs.writeFileSync(path.join(resources, 'runtime/data.txt'), 'deterministic payload'); |
| 12 | fs.writeFileSync(path.join(resources, 'runtime/tool'), '#!/bin/sh\nexit 0\n', { mode: 0o755 }); |
| 13 | return path.dirname(path.dirname(resources)); |
| 14 | } |
| 15 | |
| 16 | function generate(app, output) { |
| 17 | return spawnSync(process.execPath, [ |
| 18 | 'scripts/release/macos/generate-manifest.mjs', |
| 19 | '--stage', 'unsigned', |
| 20 | '--app', app, |
| 21 | '--output', output, |
| 22 | ], { |
| 23 | encoding: 'utf8', |
| 24 | env: { ...process.env, KNOWTATION_ALLOW_UNCOMMITTED_BUILD: '1' }, |
| 25 | }); |
| 26 | } |
| 27 | |
| 28 | test('unsigned runtime manifest is deterministic and records mode, size, and digest', () => { |
| 29 | const one = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-manifest-one-')); |
| 30 | const two = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-manifest-two-')); |
| 31 | try { |
| 32 | const appOne = makeApp(one); |
| 33 | const appTwo = makeApp(two); |
| 34 | const outputOne = path.join(appOne, 'Contents/Resources/runtime-manifest.json'); |
| 35 | const outputTwo = path.join(appTwo, 'Contents/Resources/runtime-manifest.json'); |
| 36 | assert.equal(generate(appOne, outputOne).status, 0); |
| 37 | assert.equal(generate(appTwo, outputTwo).status, 0); |
| 38 | assert.equal(fs.readFileSync(outputOne, 'utf8'), fs.readFileSync(outputTwo, 'utf8')); |
| 39 | const manifest = JSON.parse(fs.readFileSync(outputOne, 'utf8')); |
| 40 | assert.equal(manifest.files.length, 2); |
| 41 | assert.equal(manifest.sourceRevision.status, 'uncommitted'); |
| 42 | assert.equal(manifest.files.find((entry) => entry.path === 'runtime/tool').executable, true); |
| 43 | assert.match(manifest.files[0].sha256, /^[0-9a-f]{64}$/); |
| 44 | } finally { |
| 45 | fs.rmSync(one, { recursive: true, force: true }); |
| 46 | fs.rmSync(two, { recursive: true, force: true }); |
| 47 | } |
| 48 | }); |
| 49 | |
| 50 | test('release manifest generation rejects an uncommitted source identity', () => { |
| 51 | const root = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-final-manifest-')); |
| 52 | try { |
| 53 | const pkg = path.join(root, 'Knowtation-0.1.0-100-macos-arm64.pkg'); |
| 54 | fs.writeFileSync(pkg, 'fixture'); |
| 55 | fs.writeFileSync(path.join(root, 'Knowtation-0.1.0-100-macos-arm64.zip'), 'zip fixture'); |
| 56 | fs.writeFileSync(path.join(root, 'sbom.spdx.json'), '{}\n'); |
| 57 | fs.writeFileSync(path.join(root, 'LICENSE'), 'fixture license\n'); |
| 58 | fs.writeFileSync(path.join(root, 'THIRD-PARTY-NOTICES.txt'), 'fixture\n'); |
| 59 | const result = spawnSync(process.execPath, [ |
| 60 | 'scripts/release/macos/generate-manifest.mjs', |
| 61 | '--stage', 'final', |
| 62 | '--dist', root, |
| 63 | '--output', path.join(root, 'release-manifest.json'), |
| 64 | ], { |
| 65 | encoding: 'utf8', |
| 66 | env: { |
| 67 | ...process.env, |
| 68 | KNOWTATION_ALLOW_UNCOMMITTED_BUILD: '1', |
| 69 | KNOWTATION_UPDATE_ISSUED_AT: '1000', |
| 70 | KNOWTATION_UPDATE_EXPIRES_AT: '2000', |
| 71 | KNOWTATION_UPDATE_KEY_ID: 'test-release-key', |
| 72 | KNOWTATION_UPDATE_SOURCE_MIN_BUILD: '0', |
| 73 | KNOWTATION_UPDATE_SOURCE_MAX_BUILD: '99', |
| 74 | KNOWTATION_SOURCE_MUSE_REVISION: '', |
| 75 | KNOWTATION_SOURCE_GIT_REVISION: '', |
| 76 | }, |
| 77 | }); |
| 78 | assert.notEqual(result.status, 0); |
| 79 | assert.match(result.stderr, /exact KNOWTATION_SOURCE_MUSE_REVISION/); |
| 80 | } finally { |
| 81 | fs.rmSync(root, { recursive: true, force: true }); |
| 82 | } |
| 83 | }); |
| 84 | |
| 85 | test('manifest generation rejects symbolic links', () => { |
| 86 | const root = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-manifest-link-')); |
| 87 | try { |
| 88 | const app = makeApp(root); |
| 89 | fs.symlinkSync('/etc/hosts', path.join(app, 'Contents/Resources/runtime/substitution')); |
| 90 | const result = generate(app, path.join(app, 'Contents/Resources/runtime-manifest.json')); |
| 91 | assert.notEqual(result.status, 0); |
| 92 | assert.match(result.stderr, /symbolic links are forbidden/); |
| 93 | } finally { |
| 94 | fs.rmSync(root, { recursive: true, force: true }); |
| 95 | } |
| 96 | }); |
| 97 | |
| 98 | test('runtime verifier accepts the exact generated resource tree', () => { |
| 99 | const root = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-manifest-exact-')); |
| 100 | try { |
| 101 | const app = makeApp(root); |
| 102 | const resources = path.join(app, 'Contents/Resources'); |
| 103 | fs.copyFileSync('release/macos/release-lock.json', path.join(resources, 'release-lock.json')); |
| 104 | const output = path.join(resources, 'runtime-manifest.json'); |
| 105 | assert.equal(generate(app, output).status, 0); |
| 106 | const result = spawnSync(process.execPath, [ |
| 107 | 'scripts/release/macos/verify-runtime-manifest.mjs', '--app', app, |
| 108 | ], { encoding: 'utf8' }); |
| 109 | assert.equal(result.status, 0, result.stderr); |
| 110 | } finally { |
| 111 | fs.rmSync(root, { recursive: true, force: true }); |
| 112 | } |
| 113 | }); |
| 114 | |
| 115 | test('runtime verifier rejects an added file after manifest generation', () => { |
| 116 | const root = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-manifest-extra-')); |
| 117 | try { |
| 118 | const app = makeApp(root); |
| 119 | const resources = path.join(app, 'Contents/Resources'); |
| 120 | fs.copyFileSync('release/macos/release-lock.json', path.join(resources, 'release-lock.json')); |
| 121 | const output = path.join(resources, 'runtime-manifest.json'); |
| 122 | assert.equal(generate(app, output).status, 0); |
| 123 | fs.writeFileSync(path.join(resources, 'substitution.txt'), 'unexpected'); |
| 124 | const result = spawnSync(process.execPath, [ |
| 125 | 'scripts/release/macos/verify-runtime-manifest.mjs', '--app', app, |
| 126 | ], { encoding: 'utf8' }); |
| 127 | assert.notEqual(result.status, 0); |
| 128 | assert.match(result.stderr, /runtime manifest file closure mismatch/); |
| 129 | } finally { |
| 130 | fs.rmSync(root, { recursive: true, force: true }); |
| 131 | } |
| 132 | }); |
File History
1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠
24 days ago