Files
qqq-cli/scripts/update-packaging.test.mjs
james.maesandClaude Fable 5.1 4f41f41a05 fix(release): update-packaging refuses to write missing sums; real 1.0.0 sums
scripts/update-packaging.mjs could not be shown to write zeros: run against the real
cli-v1.0.0 SHA256SUMS from the all-zero placeholders at 1bc4ddd it fills correct values
(reproduced in a clean worktree), and branch head 8fc630d already carried them. The old
script was fragile, though: it read lines with split/reverse (a `*file` binary-mode name
or a stray column would silently mis-key), never checked that a sum was 64 hex, accepted
all-zero sums, did not check that any placeholder had actually matched, and wrote the
formula before it knew the manifest would succeed.

Now it parses `shasum -a 256` lines strictly, requires every target to be 64 lowercase
hex and not all zeros, requires at least one url/sha256 pair and no unfilled sha256 left in
the formula, computes both files first and writes only if everything passed, otherwise
exits 1 with "nothing was written". Tested in scripts/update-packaging.test.mjs (fixture
SHA256SUMS; run by `npm test` via `test:scripts`). Docs (packaging README, release runbook
step 7, tap README) say to compare the diff with SHA256SUMS and that tap/bucket CI catches
a wrong or zero sum only when its PR is opened and read.

Ran against /Volumes/HD-2/Git.Local/QRun-IO/.worktrees/qqq-cli-release/dist/SHA256SUMS;
the packaging files are unchanged from 8fc630d, and each sum was checked by hand against it:
  darwin-arm64  5017a32ac5031d9146d541381099a959e5c19bbf04c48f725045d89d6007b211
  darwin-x64    eb9b1d43d24b639118d94b175c343080a39ae9ba64c24645c1792ff3b16a37f1
  linux-arm64   25f513bdda13bbe40cf536844eebc1e3a7e21cf7804024f25ebe7c42793d7ebc
  linux-x64     804d06cb7c0012c2f185ae5d94c6bb8f9e2482a440bb3c165361f1a14c941e41
  windows-x64   cf418d7842a227cee412171878228004f9bc5098a272c64b89512d1e2d06a176
(linux musl sums are in SHA256SUMS but not used by the formula or manifest.)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 12:57:49 -06:00

74 lines
4.0 KiB
JavaScript

/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (see LICENSE).
*/
/** Tests for scripts/update-packaging.mjs. Run: node --test scripts/update-packaging.test.mjs */
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { cpSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test } from 'node:test'
import { parseSums, updatePackaging } from './update-packaging.mjs'
const ZERO = '0'.repeat(64)
const h = (c) => c.repeat(64)
const FILES = ['qqq-darwin-arm64', 'qqq-darwin-x64', 'qqq-linux-arm64', 'qqq-linux-x64']
const sumsText = (omit = []) =>
[...FILES, 'qqq-linux-x64-musl', 'qqq-windows-x64.exe'].filter((f) => !omit.includes(f)).map((f, i) => `${'abcdef0123456'[i].repeat(64)} ${f}`).join('\n') + '\n'
const formula = `class Qqq < Formula
version "0.0.0"
${FILES.map((f) => ` url "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v#{version}/${f}"\n sha256 "${ZERO}"`).join('\n')}
end
`
const manifest = JSON.stringify({ version: '0.0.0', architecture: { '64bit': { url: 'x', hash: ZERO } } }, null, 4) + '\n'
test('fills every sum from shasum output and never leaves zeros', () => {
const out = updatePackaging({ version: '1.0.0', sums: parseSums(sumsText()), formula, manifest })
assert.equal(out.formulaCount, 4)
assert.ok(!out.formula.includes(ZERO) && !out.manifest.includes(ZERO))
assert.match(out.formula, /version "1\.0\.0"/)
assert.ok(out.formula.includes(`qqq-darwin-arm64"\n sha256 "${h('a')}"`))
assert.equal(JSON.parse(out.manifest).architecture['64bit'].hash, h('f'))
})
test('a missing target throws', () => {
assert.throws(() => updatePackaging({ version: '1.0.0', sums: parseSums(sumsText(['qqq-linux-x64'])), formula, manifest }), /no qqq-linux-x64/)
assert.throws(() => updatePackaging({ version: '1.0.0', sums: parseSums(sumsText(['qqq-windows-x64.exe'])), formula, manifest }), /no qqq-windows-x64\.exe/)
})
test('malformed, short, uppercase-free or zero sums throw', () => {
for (const bad of ['abc', ZERO, 'g'.repeat(64)]) {
const sums = parseSums(sumsText().replace(h('a'), bad))
assert.throws(() => updatePackaging({ version: '1.0.0', sums, formula, manifest }), /malformed|all-zero/)
}
assert.throws(() => parseSums('only-one-column\n'), /unparseable/)
})
test('a formula with nothing to fill throws', () => {
assert.throws(() => updatePackaging({ version: '1.0.0', sums: parseSums(sumsText()), formula: 'class Qqq < Formula\nend\n', manifest }), /no url\/sha256/)
})
test('the CLI exits 1 and writes nothing when a sum is missing', () => {
const dir = mkdtempSync(join(tmpdir(), 'update-packaging-'))
mkdirSync(join(dir, 'scripts'))
cpSync(join(import.meta.dirname, 'update-packaging.mjs'), join(dir, 'scripts/update-packaging.mjs'))
mkdirSync(join(dir, 'packages/cli'), { recursive: true })
writeFileSync(join(dir, 'packages/cli/package.json'), '{"version":"1.0.0"}')
mkdirSync(join(dir, 'docs/packaging/homebrew-tap/Formula'), { recursive: true })
writeFileSync(join(dir, 'docs/packaging/homebrew-tap/Formula/qqq.rb'), formula)
writeFileSync(join(dir, 'docs/packaging/qqq.json'), manifest)
writeFileSync(join(dir, 'SHA256SUMS'), sumsText(['qqq-linux-arm64']))
const run = spawnSync(process.execPath, [join(dir, 'scripts/update-packaging.mjs'), join(dir, 'SHA256SUMS')], { encoding: 'utf8' })
assert.equal(run.status, 1)
assert.match(run.stderr, /nothing was written/)
assert.equal(readFileSync(join(dir, 'docs/packaging/homebrew-tap/Formula/qqq.rb'), 'utf8'), formula)
assert.equal(readFileSync(join(dir, 'docs/packaging/qqq.json'), 'utf8'), manifest)
writeFileSync(join(dir, 'SHA256SUMS'), sumsText())
const ok = spawnSync(process.execPath, [join(dir, 'scripts/update-packaging.mjs'), join(dir, 'SHA256SUMS')], { encoding: 'utf8' })
assert.equal(ok.status, 0, ok.stderr)
assert.ok(!readFileSync(join(dir, 'docs/packaging/qqq.json'), 'utf8').includes(ZERO))
})