Files
arcrun-collector/cmd/arcrun-app/daemon-version.test.mjs
T
Leo aeb44c712b fix(88): 撤回「產生端吐裸號」的宣稱——那個 v 是版本線判別器,不能單獨拿掉
總管複驗退回一格(`inkstone/arcrun-rag#88#issuecomment-3082`),退得對。
我照他要的兩條路去查,結論是**選 B,而且原本那個改動本身有 bug**。

## 兩件事要分開講

**① 總管的事實宣稱有一處錯,但他看到的現象是真的**
「這條分支一行都沒動 daemon-version.py」— 不對,上一個 commit 動了 37 行,
含 `version = "v%d.%d.%d"` → `"%d.%d.%d"`。他跑 `daemon-version.py` 得到 `v0.18.29`
也是真的:當時的過渡規則讓「重打已發佈的版本」沿用原本寫法。
⇒ 他跑對了指令、看到真東西,只是把原因歸成「沒改」。

**② 但我的改動確實有 bug,而且比他指出的更嚴重**
`^## v` 這條判斷式有**四份各自為政的拷貝**(daemon-freshness/daemon-in-bundle-gate/
ship.mjs daemon-sync/daemon-version.py),三份只認帶 `v` 的。
產生端一旦戳出 `## 0.18.30(…)`,那三份**不會報錯**,而是往下比對到更舊的
`## v0.18.29(…)` ⇒ **打包出 0.18.30 的執行檔,manifest 卻宣稱是 v0.18.29。**
版本號說謊,靜默,21 站全綠。上一個 commit 就埋著這顆。

## 為什麼不是把三份放寬成 `v?`(我試了,當場被測試擋下)

🔴 **那個 `v` 目前身兼「哪一條版本線」的判別器。**
放寬後 daemon 的閘開始把雲端那條 `## 1.4.47(…)` 當成 daemon 版本撈走
⇒ `daemon-in-bundle-gate.test.mjs` ①⑮⑯ 轉紅,而它們釘的正是 D95 第二輪剛修好的
「出貨線指到錯的 changelog 卻安靜放行」。`daemon-notes.changelogRelFor()` 也用 `/^v/`。

⇒ 「不要 v」要落到產生端,得**先給那些閘一個替代判別器**
  (建議拿 `DAEMON_LINE` 比對,例 `^## v?0\.18\.\d+(`,同時擋「錯的檔」與「錯的線」)。
  那要動 `topReleasedVersion()` 等對外簽章、跨 installer/ 與 collector/,
  **與正在進行的 collector 拆 repo 相衝** ⇒ 不在這條分支做。

## 這個 commit 做了什麼

- 產生端**恢復原行為**(一律吐 `v`):不留「產生端裸號+閘只認 v」那顆靜默炸彈
- 四份拷貝**收成一份**(`daemon-notes.DAEMON_RELEASED_RE`,維持嚴格版)
  ⇒ **零行為變更**,但哪天要改格式只需改一個地方,而不是記得改四個
- 把「為什麼還不能改裸號」釘成一支會說話的測試(`daemon-version.test.mjs` ⑤⑥)
  ——不是註解,是紅燈
- 兩處註解寫清楚接手要先做什麼

**上一個 commit 的另外兩格(release 掛成品、內外對應)未受影響,總管已複驗通過。**

測試:installer 252/252、collector 6/6。
2026-08-18 15:25:22 +08:00

104 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* daemon-version.test.mjs — 證明**產生端**吐的是裸號(inkstone/arcrun-rag#88
*
* 🔴 **這支測試現在釘的是「還沒做到」,不是「做到了」**(誠實優先於好看)。
*
* leo 2026-08-17 定「對外號就是三個數字,不要 v」;隔天他問「剛剛不是說不要 v?」,
* 因為 `install.arcrun.dev/api/latest` 照舊回 `daemon.version = "v0.18.28"`。
* 那個 `v` 的產地就是本目錄的 `daemon-version.py`。
*
* 2026-08-18 實際改成裸號,當場撞到:**那個 `v` 目前身兼「哪一條版本線」的判別器**
* (三支閘+`changelogRelFor()` 靠它認出「這份 changelog 是不是 daemon 的」)。
* 一改裸號,daemon 的閘就把雲端那條 `## 1.4.47(…)` 當成 daemon 版本撈走
* ⇒ `installer/scripts/daemon-in-bundle-gate.test.mjs` 的 ①⑮⑯ 轉紅,
* 而它們釘的正是 D95 第二輪剛修好的「指到錯的 changelog 卻安靜放行」。
*
* ⇒ 所以本檔的測試**斷言的是現況(一律吐 `v`)**,並把「為什麼還不能改」釘成
* 一支會說話的測試。等替代判別器就位(見 `daemon-version.py` 那段註解與
* `inkstone/arcrun-rag#88`),把 ①②③ 的期望值改成裸號,這支就變成驗收。
*
* 為什麼住在 `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.282026-08-16\n\n- 舊的那一版\n';
test('① 升版:**目前**仍吐帶 v 的版本號(leo 要的裸號尚未落到產生端)', () => {
const { version } = runProducer({ changelog: UNRELEASED + OLD_V });
assert.equal(version, 'v0.18.29');
// 🔴 這一行是給未來的人看的:改成裸號之前,先讀 daemon-version.py 裡
// `version = "v%d.%d.%d" % nxt` 上方那段——單獨改這裡會讓 manifest 說謊。
});
test('② 升版並戳章:寫進 changelog 的標題也帶 v(兩邊一致,不會自我矛盾)', () => {
const { version, changelogAfter } = runProducer({ changelog: UNRELEASED + OLD_V, stamp: true });
assert.equal(version, 'v0.18.29');
assert.match(changelogAfter, /^## v0\.18\.29\d{4}-\d{2}-\d{2}$/m);
});
test('③ 換版本線:新線第一版(同樣帶 v)', () => {
const { version } = runProducer({ changelog: UNRELEASED + OLD_V, line: '0.19' });
assert.equal(version, 'v0.19.0');
});
test('④ 重打同一版:冪等,不虛增(原有行為,未動)', () => {
const { version } = runProducer({ changelog: OLD_V });
assert.equal(version, 'v0.18.28');
});
test('⑤ 🔴 判別器還在:daemon 的判斷式**不可以**撈到雲端那條線的版號', () => {
// 這支就是「為什麼還不能改裸號」的本體。`v` 目前是兩條版本線唯一的區分方式:
// installer/scripts/daemon-notes.mjs 的 DAEMON_RELEASED_RE 只認 `^## vX.Y.Z`。
// 放寬成 v? 之後,`## 1.4.47(…)` 會被 daemon 的閘當成 daemon 版本。
const CLOUD = '## 1.4.472026-08-15\n\n- 雲端那條線\n';
const strict = /^## (v\d+\.\d+\.\d+)/m;
const loose = /^## (v?\d+\.\d+\.\d+)/m;
assert.equal(CLOUD.match(strict), null, '嚴格版正確地認不出雲端版號');
assert.equal(CLOUD.match(loose)[1], '1.4.47', '放寬版會撈到雲端版號 ⇒ 這就是那三支閘轉紅的原因');
});
test('⑥ 產生端與那條判斷式對得上(兩份實作靠測試對齊,不靠註解)', () => {
const { version } = runProducer({ changelog: UNRELEASED + OLD_V, stamp: true });
const { changelogAfter } = runProducer({ changelog: UNRELEASED + OLD_V, stamp: true });
const strict = /^## (v\d+\.\d+\.\d+)/m;
assert.equal(changelogAfter.match(strict)[1], version,
'產生端戳出來的標題,必須被 installer 那三支閘的判斷式讀得回同一個版本');
});