// tools/desktop-release.ts and tools/desktop-version.ts: the pure parts of the desktop release pipeline. import { describe, expect, test } from 'bun:test'; import { cpSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { artifactName, developerIdIdentity, mergeManifest, pathFreeEnv, releaseUrl, remoteSessionReason, sliceProblems, sumsText, uploadable, archProblems, commitProblems, releaseTogetherProblems } from '../tools/desktop-release'; import { COPIES, setVersion, versions } from '../tools/desktop-version'; describe('desktop version authority', () => { test('the repository has one version: the CLI, the plugin manifests and the desktop app', () => { const { version, drift } = versions(); expect(version).toMatch(/^\d+\.\d+\.\d+$/); expect(drift).toEqual([]); const pkg = JSON.parse(readFileSync(join(import.meta.dir, '../package.json'), 'utf8')); expect(version).toBe(pkg.version); expect(pkg.desktopVersion).toBeUndefined(); expect(COPIES.map(c => c.file)).toContain('apps/desktop/src-tauri/tauri.conf.json'); expect(COPIES[0]!.file).toBe('package.json'); }); test('drift is found and --set fixes every copy', () => { const root = mkdtempSync(join(tmpdir(), 'sb-ver-')); for (const c of COPIES) { mkdirSync(join(root, c.file, '..'), { recursive: true }); cpSync(join(import.meta.dir, '..', c.file), join(root, c.file)); } const conf = join(root, 'apps/desktop/src-tauri/tauri.conf.json'); writeFileSync(conf, readFileSync(conf, 'utf8').replace(/"version": "[^"]+"/, '"version": "9.9.9"')); expect(versions(root).drift.map(d => d.file)).toEqual(['apps/desktop/src-tauri/tauri.conf.json']); setVersion('0.2.0-beta.1', root); expect(versions(root)).toEqual({ version: '0.2.0-beta.1', drift: [] }); expect(JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version).toBe('0.2.0-beta.1'); expect(JSON.parse(readFileSync(join(root, '.claude-plugin/marketplace.json'), 'utf8')).plugins[0].version).toBe('0.2.0-beta.1'); // only the package's own version line of Cargo.toml / Cargo.lock changes expect(readFileSync(join(root, 'apps/desktop/src-tauri/Cargo.toml'), 'utf8')).toContain('tauri = { version = "2.12"'); expect(() => setVersion('latest', root)).toThrow(); }); }); describe('desktop release', () => { test('the app and the npm CLI are published with one version', () => { expect(releaseTogetherProblems('0.8.0', '0.8.0')).toEqual([]); expect(releaseTogetherProblems('0.1.0', '0.8.0')[0]).toMatch(/one version/); expect(releaseTogetherProblems('0.8.0', null)).toHaveLength(1); }); test('signing refuses SSH and non-GUI sessions', () => { expect(remoteSessionReason({}, 'Aqua')).toBeNull(); expect(remoteSessionReason({ SSH_CONNECTION: '1 2 3 4' }, 'Aqua')).toMatch(/SSH/); expect(remoteSessionReason({ SSH_TTY: '/dev/ttys001' }, 'Aqua')).toMatch(/SSH/); expect(remoteSessionReason({}, 'Background')).toMatch(/GUI session/); }); test('only a Developer ID Application identity signs a release', () => { const out = ` 1) ${'A'.repeat(40)} "Apple Distribution: Example Studio (TEAM000001)" 2) ${'B'.repeat(40)} "Apple Development: Example Person (TEAM000002)" 2 valid identities found`; expect(developerIdIdentity(out)).toBeNull(); expect(developerIdIdentity(`${out}\n 3) ${'C'.repeat(40)} "Developer ID Application: Example Studio (TEAM000001)"`)).toBe('Developer ID Application: Example Studio (TEAM000001)'); }); test('artifact names say when a build is unsigned', () => { expect(artifactName('0.1.0', 'dmg', false)).toBe('storebound_0.1.0_universal-unsigned.dmg'); expect(artifactName('0.1.0', 'updater', true)).toBe('storebound_0.1.0_universal.app.tar.gz'); expect(artifactName('0.1.0', 'nsis', false)).toBe('storebound_0.1.0_x64-setup-unsigned.exe'); expect(artifactName('0.1.0', 'appimage', false)).toBe('storebound_0.1.0_amd64.AppImage'); expect(releaseUrl('0.1.0', 'a.dmg')).toBe('https://storebound.dev/desktop/dl/0.1.0/a.dmg'); }); test('path-free build: home, checkout, cargo, rustup and target dir are remapped, general first', () => { const env = pathFreeEnv('/Users/you/src/storebound', { CARGO_TARGET_DIR: '/tmp/t', RUSTFLAGS: '-Cforce-frame-pointers=yes', CFLAGS: '-O2' }, '/Users/you'); const flags = env.CARGO_ENCODED_RUSTFLAGS!.split('\x1f'); expect(flags).toEqual([ '-Cforce-frame-pointers=yes', '--remap-path-prefix=/Users/you=~', '--remap-path-prefix=/Users/you/src/storebound=/src', '--remap-path-prefix=/tmp/t=/target', '--remap-path-prefix=/Users/you/.cargo=/cargo', '--remap-path-prefix=/Users/you/.rustup=/rustup', ]); expect(env.CFLAGS).toStartWith('-O2 -ffile-prefix-map=/Users/you=~'); expect(env.CXXFLAGS).toContain('-ffile-prefix-map=/Users/you/.rustup=/rustup'); const w = pathFreeEnv('C:\\src\\storebound', { CARGO_TARGET_DIR: 'C:\\t' }, 'C:\\Users\\you', 'win32'); const wf = w.CARGO_ENCODED_RUSTFLAGS!.split('\x1f'); expect(wf).toContain('--remap-path-prefix=C:\\Users\\you=~'); expect(wf).toContain('--remap-path-prefix=\\\\?\\C:\\Users\\you=~'); expect(wf).toContain('-Clink-arg=/PDBALTPATH:%_PDB%'); expect(w.CL).toContain('"/d1trimfile:C:\\Users\\you\\.rustup/"'); expect(w.LINK).toBe('/PDBALTPATH:%_PDB%'); expect(w.CFLAGS).toBeUndefined(); }); test('latest.json keeps the other platforms of the same version, a new version starts fresh', () => { const mac = { platforms: { 'darwin-aarch64': { signature: 's', url: 'https://storebound.dev/desktop/dl/0.1.0/m.app.tar.gz' } } }; const win = { platforms: { 'windows-x86_64': { signature: 'w', url: 'https://storebound.dev/desktop/dl/0.1.0/w.exe' } } }; const one = mergeManifest(null, '0.1.0', 'first', '2026-10-11T00:00:00Z', mac); const two = mergeManifest(one, '0.1.0', 'other notes', '2026-10-12T00:00:00Z', win); expect(Object.keys(two.platforms)).toEqual(['darwin-aarch64', 'windows-x86_64']); expect(two.notes).toBe('first'); expect(two.pub_date).toBe('2026-10-12T00:00:00Z'); expect(Object.keys(mergeManifest(two, '0.1.1', 'n', 'x', win).platforms)).toEqual(['windows-x86_64']); }); test('SHA256SUMS and what is uploaded', () => { expect(sumsText([{ file: 'b', sha256: '2' }, { file: 'a', sha256: '1' }])).toBe('1 a\n2 b\n'); expect(uploadable('storebound_0.1.0_universal-unsigned.dmg')).toBe(true); expect(uploadable('latest.json')).toBe(true); for (const n of ['evidence-macos-0.1.0.json', 'evidence-linux-0.1.0.json', '.DS_Store', 'latest.json.part', 'a b.dmg']) expect(uploadable(n)).toBe(false); }); test('add: every artifact of a version comes from one commit', () => { const a = 'a'.repeat(40), b = 'b'.repeat(40); expect(commitProblems(a, { 'evidence-macos-0.1.0.json': a, 'evidence-linux-0.1.0.json': undefined })).toEqual([]); expect(commitProblems(undefined, { 'evidence-macos-0.1.0.json': a })).toEqual([]); expect(commitProblems(b, { 'evidence-macos-0.1.0.json': a })[0]).toContain('evidence-macos-0.1.0.json says commit aaaaaaaaaa'); expect(commitProblems('25258a8', {})[0]).toContain('40-character'); }); }); describe('universal slices (never run under Rosetta)', () => { test('every executable must carry arm64 and x86_64', () => { expect(sliceProblems([{ file: 'Contents/MacOS/storebound-desktop', archs: ['x86_64', 'arm64'] }, { file: 'Contents/MacOS/storebound', archs: ['arm64', 'x86_64'] }])).toEqual([]); expect(sliceProblems([{ file: 'a', archs: ['x86_64'] }, { file: 'b', archs: ['arm64'] }, { file: 'c', archs: [] }])).toEqual(['a (x86_64)', 'b (arm64)', 'c (no slices)']); }); test('the release tools never launch an x86_64 slice (Rosetta warns the owner about the app)', () => { for (const f of ['tools/desktop-release.ts', 'tools/desktop-boot-check.ts', 'tools/build-sidecars.ts']) { const src = readFileSync(join(import.meta.dir, '..', f), 'utf8'); expect(src).not.toMatch(/['"]arch['"]\s*,|arch -x86_64|`-\$\{arch\}`/); } }); }); describe('architecture guard (no Rosetta notice for users)', () => { const ok = [{ file: 'Contents/MacOS/storebound-desktop', lipo: 'x86_64 arm64\n' }, { file: 'Contents/MacOS/storebound', lipo: 'x86_64 arm64' }, { file: 'Contents/Frameworks/libx.dylib', lipo: 'arm64' }]; test('every Mach-O with arm64 and a neutral Info.plist pass', () => { expect(archProblems(ok, { CFBundleIdentifier: 'dev.example' })).toEqual([]); expect(archProblems(ok, { LSRequiresNativeExecution: true, LSArchitecturePriority: ['arm64', 'x86_64'] })).toEqual([]); }); test('an Intel-only helper, a file without slices or one without arm64 fail', () => { expect(archProblems([...ok, { file: 'Contents/Resources/helper', lipo: 'x86_64' }], {})).toEqual(['Contents/Resources/helper: no arm64 slice (x86_64, Intel-only: needs Rosetta)']); expect(archProblems([{ file: 'a', lipo: '' }], {})).toEqual(['a: no arm64 slice (no slices)']); expect(archProblems([{ file: 'b', lipo: 'x86_64h i386' }], {})).toEqual(['b: no arm64 slice (x86_64h i386, Intel-only: needs Rosetta)']); }); test('an Info.plist that forces Intel fails', () => { expect(archProblems(ok, { LSRequiresNativeExecution: false })).toEqual(['Info.plist: LSRequiresNativeExecution is false']); expect(archProblems(ok, { LSArchitecturePriority: ['x86_64', 'arm64'] })).toEqual(['Info.plist: LSArchitecturePriority puts x86_64 first']); }); test('the release runs the guard before signing (tauri build --no-sign, then tauri bundle)', () => { const src = readFileSync(join(import.meta.dir, '..', 'tools/desktop-release.ts'), 'utf8'); const build = src.indexOf("'--no-sign'"), guard = src.indexOf("archGuard(join(targetDir"), bundle = src.indexOf("['tauri', 'bundle'"); expect(build).toBeGreaterThan(0); expect(guard).toBeGreaterThan(build); expect(bundle).toBeGreaterThan(guard); }); });