aeb44c712b
總管複驗退回一格(`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。
104 lines
5.7 KiB
JavaScript
104 lines
5.7 KiB
JavaScript
/**
|
||
* 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.28(2026-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.47(2026-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 那三支閘的判斷式讀得回同一個版本');
|
||
});
|