import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { prose } from './lib/read.mjs'; /* ★ 对齐参照物必须有**版本号**(pi 2026-09-14 §1)。 WebUI 的 `CalendarView.tsx` 可能同时在动(gui-lab 有未合入的改动)。照工作副本画完之后, 参照物一变,这一版就成了"照一份没人认领的草案对齐的" —— 而且**没人能判它对不对, 因为参照物没有版本号**。所以把参照物的 blob 哈希登记在 `docs/ALIGN-REFS.json`, **它一变就红**,逼一次"重新核对再更新登记"。 报错按 §14 写"正确修法 + 最常见的错误修法":正确修法是**重新核对对齐**(读差异、改骨架、再更新登记), **不是**把新哈希抄进去 —— 那是把这条判据降级成"记录当前状态",它就不再是闸门了。 */ const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..', '..'); test('★ 对齐参照物的版本必须与登记一致(变了就重新核对,不是抄新哈希)', () => { const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json'))); assert.ok(Array.isArray(reg.refs) && reg.refs.length > 0, '登记里至少要有一个参照物'); const drifted = []; for (const r of reg.refs) { const res = spawnSync('git', ['hash-object', r.path], { cwd: ROOT, encoding: 'utf8' }); assert.equal(res.status, 0, `要能算 ${r.path} 的 blob 哈希:${res.stderr}`); const now = res.stdout.trim(); if (now !== r.blob) drifted.push({ id: r.id, path: r.path, registered: r.blob.slice(0, 12), now: now.slice(0, 12), at: r.atCommit }); } assert.deepEqual(drifted, [], `对齐参照物变了,登记还是旧的:\n ${drifted.map(d => `${d.id}(${d.path}):登记 ${d.registered}(@${d.at})→ 现在 ${d.now}`).join('\n ')}\n` + `**正确修法**:读一遍参照物的差异 → 判断骨架要不要跟着改(并说明改了哪里)→ 再把登记的 blob/commit 更新。\n` + `**最常见的错误修法**:把新哈希抄进去就完事 —— 那是把这条判据降级成"记录当前状态",\n` + `它就不再是闸门,"照一份没人认领的草案对齐"这件事会静默发生。`); // 每一条都要有出处(哪个 commit)与它对齐的是什么,否则"版本号"只是个数字 for (const r of reg.refs) { assert.ok(r.atCommit && r.atCommitSubject, `参照物 ${r.id} 必须写明登记于哪个 commit(否则无人能判断对齐目标)`); assert.ok(r.note && r.note.trim(), `参照物 ${r.id} 必须写明"以哪次为准"`); } }); /* ★ 圆角也是"裸值"那一族:从 `index.css` 抄一个数字进 `.ets`,今天看起来一样, 等 WebUI 改一次半径,鸿蒙这边**没有任何东西会红**(与当初 14 处 Material 调色板同一形态, 量纲从颜色换成长度)。所以:**鸿蒙侧圆角必须来自 Theme.ets 令牌**, 且语义配对登记在 ALIGN-REFS.json 里(数值来源不同属于形态差异,另行登记)。 */ test('★ 圆角走令牌:鸿蒙侧必须有 radius 令牌,且每一条语义都在登记里配了对', () => { const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json'))); const theme = prose(join(ROOT, 'client', 'harmony', 'entry', 'src', 'main', 'ets', 'common', 'Theme.ets')); assert.match(theme, /radiusCard/, 'Theme.ets 必须有卡片圆角令牌(不许在页面里写裸数字)'); assert.match(theme, /radiusControl/, 'Theme.ets 必须有控件圆角令牌'); assert.ok(Array.isArray(reg.radius) && reg.radius.length >= 2, '圆角的语义配对要登记'); for (const r of reg.radius) { assert.ok(r.semantic && r.webui && r.harmony, `圆角条目必须写明语义、WebUI 侧与鸿蒙侧出处`); assert.match(r.webui, /--radius-/, `WebUI 侧要指向变量(不是抄来的数字):${r.semantic}`); assert.match(r.harmony, /Theme\.ets/, `鸿蒙侧要指向令牌出处:${r.semantic}`); assert.ok(r.judgement && r.judgement.includes('语义'), `${r.semantic} 必须写明按语义对齐还是按数值对齐(否则下一个人会去比数字)`); } }); /* ★ 包名必须与 AGC 下发的配置**完全一致**(pi 2026-09-14): 不一致时 Push Kit 推不到设备 —— 这是**功能性约束**,不是风格问题。 AGC 实测拒绝 `com.agentmail.harmony`(`harmony` 是包名保留字),用户定为 `com.jianf.agentmail`。 这条把"两处必须一致"变成可判的,免得改一处忘另一处(那种错只有在真机上表现为"收不到推送")。 */ test('★ bundleName 必须与 agconnect-services.json 的 package_name 一致(否则推送送不到)', () => { const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json'))); const app = prose(join(ROOT, 'client', 'harmony', 'AppScope', 'app.json5')); const m = /"bundleName"\s*:\s*"([^"]+)"/.exec(app); assert.ok(m, '要能从 AppScope/app.json5 取到 bundleName'); const bundle = m[1]; assert.ok(reg.agc && reg.agc.packageName, 'ALIGN-REFS.json 里要登记 AGC 的 package_name'); assert.equal(bundle, reg.agc.packageName, `bundleName(${bundle})与 AGC 的 package_name(${reg.agc.packageName})不一致 —— ` + `**正确修法**:改 AppScope/app.json5 让它与 AGC 一致(或按新包名在 AGC 重建应用)。` + `**最常见的错误修法**:只改这一处断言里的期望值 —— 那会让"收不到推送"变成一个测不出来的状态。`); assert.ok(!/harmony/i.test(bundle), `bundleName 里带 harmony 是 AGC 保留字(实测被拒):${bundle}`); });