import { execFileSync } from 'node:child_process'; import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { describe, expect, it } from 'vitest'; import { collectRuntimeDependencyLicenseEntries, createDeterministicZip, listFiles, writeReleaseFiles, } from '../scripts/package-release.mjs'; describe('release packaging', () => { it('creates byte-for-byte deterministic ZIP files', () => { const entries = [ { name: 'index.html', data: Buffer.from('

XSLT tools

') }, { name: 'toolbox-app.json', data: Buffer.from('{"schemaVersion":1}') }, ]; expect(createDeterministicZip(entries)).toEqual( createDeterministicZip(entries) ); }); it('creates the same archive in different host time zones', () => { const moduleUrl = pathToFileURL( join(process.cwd(), 'scripts/package-release.mjs') ).href; const program = ` import { createDeterministicZip } from ${JSON.stringify(moduleUrl)}; const archive = createDeterministicZip([ { name: 'index.html', data: Buffer.from('timezone invariant') } ]); process.stdout.write(archive.toString('base64')); `; const createInTimezone = (timezone: string) => execFileSync( process.execPath, ['--input-type=module', '--eval', program], { encoding: 'utf8', env: { ...process.env, TZ: timezone }, } ); expect(createInTimezone('UTC')).toBe(createInTimezone('Pacific/Honolulu')); }); it('includes legal files for production dependencies but not test tooling', () => { const names = collectRuntimeDependencyLicenseEntries().map( (entry) => entry.name ); expect( names.some((name) => /^LICENSES\/npm\/add-ideas--toolbox-contract-[^/]+\/LICENSE$/.test(name) ) ).toBe(true); expect( names.some((name) => /^LICENSES\/npm\/add-ideas--toolbox-shell-react-[^/]+\/LICENSE$/.test( name ) ) ).toBe(true); expect( names.some((name) => /^LICENSES\/npm\/codemirror--theme-one-dark-[^/]+\/LICENSE$/.test(name) ) ).toBe(true); expect(names.some((name) => /\/react-[^/]+\/LICENSE$/.test(name))).toBe( true ); expect(names.some((name) => name.includes('toolbox-testkit'))).toBe(false); }); it('refuses same-version overwrites unless force is explicit', () => { const directory = mkdtempSync(join(tmpdir(), 'xslt-release-test-')); const archiveTarget = join(directory, 'xslt-tools.zip'); const checksumTarget = join(directory, 'xslt-tools.sha256'); writeFileSync(archiveTarget, 'existing archive'); writeFileSync(checksumTarget, 'existing checksum'); try { expect(() => writeReleaseFiles({ archive: Buffer.from('new archive'), archiveTarget, checksum: 'new checksum', checksumTarget, }) ).toThrow(/Refusing to overwrite/); expect(readFileSync(archiveTarget, 'utf8')).toBe('existing archive'); expect(readFileSync(checksumTarget, 'utf8')).toBe('existing checksum'); writeReleaseFiles({ archive: Buffer.from('new archive'), archiveTarget, checksum: 'new checksum', checksumTarget, force: true, }); expect(readFileSync(archiveTarget, 'utf8')).toBe('new archive'); expect(readFileSync(checksumTarget, 'utf8')).toBe('new checksum'); } finally { rmSync(directory, { recursive: true, force: true }); } }); it('rejects symlinks instead of following them into an archive', () => { const directory = mkdtempSync(join(tmpdir(), 'xslt-release-tree-test-')); const target = join(directory, 'target.txt'); const link = join(directory, 'linked.txt'); writeFileSync(target, 'must not be followed through a link'); symlinkSync(target, link); try { expect(() => listFiles(directory)).toThrow(/symlink or special/i); } finally { rmSync(directory, { recursive: true, force: true }); } }); });