/** * daemon-version.test.mjs — 證明**產生端**吐的是裸號(inkstone/arcrun-rag#88) * * 🔴 為什麼這支測試存在,而不是改一個字串常數就算: * leo 2026-08-17 定「對外號就是三個數字,不要 v」,規約寫進了 * InkStoneCo `system-dev/wiki/ops-facts.md`——然後 leo 隔天問「剛剛不是說不要 v?」, * 因為 `install.arcrun.dev/api/latest` 照舊回 `daemon.version = "v0.18.28"`。 * ⇒ **規約只活在 wiki,產生號碼的那條路照舊帶 v。** * 這支測試釘住的就是那條路:不是「常數長什麼樣」,是「**下一次產生版本號時它是裸的**」。 * * 為什麼住在 `collector/` 而不是 `installer/scripts/`: * 被測的 `daemon-version.py` 是 collector 自己的版本產生器(D95 第一輪把它的三個輸入 * 全搬進 collector/,就是為了讓 collector/ 搬得成獨立 repo)。測試跟著被測物走, * 拆 repo 時不會留下一支指向別人家的孤兒測試。 * * 跑法:node --test collector/cmd/arcrun-app/daemon-version.test.mjs */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const SCRIPT = join(HERE, 'daemon-version.py'); /** * 造一個**只有版本產生器需要的三樣東西**的假 collector 樹,然後真的跑那支腳本。 * 刻意不是 git repo:`source_fingerprint()` 的 `git ls-files` 會失敗 → 回空字串 → 指紋閘不參與, * 這裡要量的是「版本號長什麼樣」,不是指紋機制(它有自己的病史,不要攪在一起)。 */ function runProducer({ changelog, line = '0.18', stamp = false }) { const root = mkdtempSync(join(tmpdir(), 'daemon-version-')); const appDir = join(root, 'cmd', 'arcrun-app'); mkdirSync(appDir, { recursive: true }); writeFileSync(join(root, 'CHANGELOG.md'), changelog); writeFileSync(join(root, 'DAEMON_LINE'), `${line}\n`); copyFileSync(SCRIPT, join(appDir, 'daemon-version.py')); try { const stdout = execFileSync('python3', [ join(appDir, 'daemon-version.py'), ...(stamp ? ['--stamp'] : []), ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return { version: stdout.trim(), changelogAfter: readFileSync(join(root, 'CHANGELOG.md'), 'utf8') }; } finally { rmSync(root, { recursive: true, force: true }); } } const UNRELEASED = '## 下一版(未發佈)\n\n- 修了一件事\n\n'; const OLD_V = '## v0.18.28(2026-08-16)\n\n- 舊的那一版,當初是帶 v 寫的\n'; const NEW_BARE = '## 0.18.29(2026-08-18)\n\n- 新的那一版,裸號\n'; test('① 升版:下一個版本號是裸的(leo 2026-08-17「不要 v」)', () => { const { version } = runProducer({ changelog: UNRELEASED + OLD_V }); assert.equal(version, '0.18.29'); assert.ok(!version.startsWith('v'), `產生端還在吐帶 v 的版本號:${version}`); }); test('② 升版並戳章:寫進 changelog 的那一行也是裸的(下一輪讀到的就是裸號)', () => { const { version, changelogAfter } = runProducer({ changelog: UNRELEASED + OLD_V, stamp: true }); assert.equal(version, '0.18.29'); assert.match(changelogAfter, /^## 0\.18\.29(\d{4}-\d{2}-\d{2})$/m); assert.ok(!/^## v0\.18\.29(/m.test(changelogAfter), 'changelog 被戳成帶 v 的標題'); }); test('③ 換版本線:新線第一版也是裸的,不是只有 patch 遞增那條路被修', () => { const { version } = runProducer({ changelog: UNRELEASED + OLD_V, line: '0.19' }); assert.equal(version, '0.19.0'); }); test('④ 重打同一版:沿用當初的寫法,**不回頭改已發出的版本**(紅線)', () => { // 病:若這裡吐 `0.18.28`,每一台裝著 v0.18.28 的機器都會看到一個內容一樣卻宣稱是新版的更新 // ——selfupdate.go 的 newerThanCurrent() 是字串不等於就提示。 const { version } = runProducer({ changelog: OLD_V }); assert.equal(version, 'v0.18.28'); }); test('⑤ 過渡會自己過期:頂上那段已是裸號時,重打也吐裸號(不必記例外)', () => { const { version } = runProducer({ changelog: NEW_BARE + OLD_V }); assert.equal(version, '0.18.29'); }); test('⑥ 讀得懂歷史:整份 changelog 都是舊的帶 v 寫法時,仍算得出下一版(且裸)', () => { const onlyOld = UNRELEASED + '## v0.18.28(2026-08-16)\n\n- a\n\n## v0.18.27(2026-08-15)\n\n- b\n'; const { version } = runProducer({ changelog: onlyOld }); assert.equal(version, '0.18.29'); });