/** * `deploy/web-comment-only.mjs` 的判据。 * * ★ 这条判据本身必须能区分「仅注释」与「真代码」—— * 一条分不清的判据等于没有判据。所以下面每格都给**两侧**的对照: * 注释 ⇒ 放行,代码 ⇒ 拦截。变异验证见文件末尾注释。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { classifySourceChanges, isCodeChange } from '../../../deploy/web-comment-only.mjs'; /** 假 run:按 map 返回预置 stdout。 */ const fakeRun = (outputs) => (cmd) => { for (const [key, value] of Object.entries(outputs)) { if (cmd.join(' ').includes(key)) return { stdout: value, code: 0 }; } return { stdout: '', code: 0 }; }; // ── 行级:什么算「真改动」 ────────────────────────────── test('★ 行级判据:注释行不算改动,代码行算', () => { // 注释(这三类都放过) assert.equal(isCodeChange('+ * 自线索根整树 BFS 取一块'), false, '块注释中间行'); assert.equal(isCodeChange('- * dir=around 首屏'), false, '块注释行'); assert.equal(isCodeChange('+ // 说明一下'), false, '行注释(缩进)'); assert.equal(isCodeChange('+ */'), false, '块注释结尾'); assert.equal(isCodeChange('+'), false, '纯增行空行'); assert.equal(isCodeChange('-'), false, '纯删行空行'); // 文件头与元信息 assert.equal(isCodeChange('--- a/client/electron/src/api/client.ts'), false); assert.equal(isCodeChange('+++ b/client/electron/src/api/client.ts'), false); assert.equal(isCodeChange('@@ -1,3 +1,3 @@'), false); // 代码(这些必须拦住) assert.equal(isCodeChange('+const x = 1;'), true); assert.equal(isCodeChange('- const base = url;'), true); assert.equal(isCodeChange('+ const base = url; // 带行尾注释也算真改动'), true, '行尾注释不改变「这行有代码」的事实'); }); // ── 文件级:只有注释 ⇒ 放行 ───────────────────────────── test('★ 只有注释差异 ⇒ 放行(这正是 2026-10-02 拦了两次部署的那个形状)', () => { // 逐字取自当天实测的 git diff:client/electron/src/api/client.ts const diff = [ 'diff --git a/client/electron/src/api/client.ts b/client/electron/src/api/client.ts', 'index 1111111..2222222 100644', '--- a/client/electron/src/api/client.ts', '+++ b/client/electron/src/api/client.ts', '@@ -40,1 +40,1 @@', '- * dir=around 首屏(锚点 + 部分祖先 + 部分子孙),up/down 配 offset 增量加载。', '+ * 自线索根整树 BFS 取一块,offset 传上次返回的 next_offset 继续往后取。' ].join('\n'); const r = classifySourceChanges( ['client/electron/src/api/client.ts'], '/repo', fakeRun({ 'ls-files': '', 'diff': diff }) ); assert.equal(r.commentOnly, true, `应放行,实际 reason=${r.reason}`); assert.match(r.reason, /仅为注释/); }); // ── 文件级:有代码 ⇒ 拦截 ─────────────────────────────── test('★ 有非注释改动 ⇒ 拦截(否则旧界面会被嵌进二进制)', () => { const diff = [ '--- a/client/electron/src/api/client.ts', '+++ b/client/electron/src/api/client.ts', '@@ -10,1 +10,1 @@', '+ * 顺手加了句说明', '+const base = url.replace(/\\/+$/, "");' ].join('\n'); const r = classifySourceChanges( ['client/electron/src/api/client.ts'], '/repo', fakeRun({ 'ls-files': '', 'diff': diff }) ); assert.equal(r.commentOnly, false, `应拦截,实际 reason=${r.reason}`); assert.match(r.reason, /非注释改动/); }); // ★ 这一格是 2026-09-14 那个沉默失败的守门:改了请求路径却没重建 ⇒ // 接口 404(/api/v1/api/v1/…)而单测全绿。 test('★ 单行代码改动也必须拦截(只改一个字母那种)', () => { const diff = [ '--- a/client/electron/src/lib/appearance.ts', '+++ b/client/electron/src/lib/appearance.ts', '@@ -3,1 +3,1 @@', '-const API = "/api/v1";', '+const API = "/api/v1";' ].join('\n'); const r = classifySourceChanges( ['client/electron/src/lib/appearance.ts'], '/repo', fakeRun({ 'ls-files': '', 'diff': diff }) ); assert.equal(r.commentOnly, false, '单行改动也必须重建'); }); // ── 拿不准时一律拦 ───────────────────────────────────── test('★ 新增未跟踪文件 ⇒ 拦截(判不出是否注释就别放行)', () => { const r = classifySourceChanges( ['client/electron/src/new/Thing.ts'], '/repo', fakeRun({ 'ls-files': 'client/electron/src/new/Thing.ts' }) ); assert.equal(r.commentOnly, false); assert.match(r.reason, /未跟踪/); }); test('★ 没有文件更新 ⇒ 放行(空集不该拦)', () => { const r = classifySourceChanges([], '/repo', fakeRun({})); assert.equal(r.commentOnly, true); }); // ── 路径规范化:绝对路径要能转成仓库相对 ─────────────── test('绝对路径会被转成仓库相对(find 给的是绝对路径)', () => { const r = classifySourceChanges( ['/repo/client/electron/src/api/client.ts'], '/repo', fakeRun({ 'ls-files': '', 'diff': '+// 只有注释\n' }) ); assert.equal(r.commentOnly, true); assert.match(r.reason, /client\/electron\/src\/api\/client\.ts/); // 不能把绝对路径整条塞进 git diff -- 之后(那会查不到任何东西 ⇒ 静默「无改动」) assert.doesNotMatch(r.reason, /\/repo\//, 'reason 里不该出现仓库根前缀'); }); /* 变异验证(本判据必须转红,否则它分不清): 把 isCodeChange 里的注释分支去掉(所有行都算真改动) ⇒ 「只有注释 ⇒ 放行」那格必须红 —— 否则部署会被那一行注释继续拦。 把 isCodeChange 恒返回 false ⇒ 「有代码 ⇒ 拦截」「单行改动 ⇒ 拦截」两格必须红 —— 否则等于把 2026-09-14 那个静默失败放回来。 */