| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- /**
- * 票 18(批次 5:装饰/纹理替换)专项断言面。
- *
- * 立场同 batch2-token-check.mjs:零依赖 Node 静态断言,不运行构建/dev/浏览器。
- * 覆盖四件可断言事实:
- * [A] 资源化装饰:manifest 条目存在、文件可达、hash 一致、status=formal、有注册表与消费点
- * [B] 无死装饰资源:运行时图标目录不再有未登记的装饰候选文件
- * [C] 代码化装饰无散落主题色:四角切角 data-URI 不再内联色值、第二底色走 --bg-1
- * [D] 消费方式合法:样式层不出现物理资产路径(经语义 key 注入),装饰不碰结构属性
- */
- import assert from 'node:assert/strict'
- import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
- import { createHash } from 'node:crypto'
- import { fileURLToPath } from 'node:url'
- import { dirname, join, relative } from 'node:path'
- const here = dirname(fileURLToPath(import.meta.url))
- const root = join(here, '..', '..', '..')
- const cockpitDir = join(root, 'src', 'modules', 'cockpit')
- const read = (p) => readFileSync(join(root, p), 'utf8')
- const problems = []
- function check(name, fn) {
- try {
- fn()
- console.log(` PASS ${name}`)
- } catch (error) {
- problems.push(`${name}: ${error.message}`)
- console.log(` FAIL ${name}\n ${error.message}`)
- }
- }
- const manifest = JSON.parse(read('public/cockpit/assets/manifest.json'))
- const shell = read('src/modules/cockpit/CockpitShell.vue')
- const panel = read('src/modules/cockpit/components/CockpitPanel.vue')
- const mapCanvas = read('src/modules/cockpit/components/MapCanvas.vue')
- const icons = read('src/modules/cockpit/domain/icons.js')
- const tokens = read('src/modules/cockpit/styles/tokens.css')
- console.log('\n[A] 资源化装饰(光池)manifest 与消费链')
- check('A1 icon.brand.pool 条目存在且 formal', () => {
- const e = manifest.entries.find((x) => x.key === 'icon.brand.pool')
- assert.ok(e, 'manifest 缺 icon.brand.pool 条目')
- assert.equal(e.status, 'formal')
- assert.equal(e.format, 'png')
- assert.equal(e.renderMode, 'image')
- assert.equal(e.dimensions.width, 489)
- assert.equal(e.dimensions.height, 128)
- })
- check('A2 光池文件真实存在且 hash 一致(含 HTML 兜底拒绝)', () => {
- const e = manifest.entries.find((x) => x.key === 'icon.brand.pool')
- const abs = join(root, 'public/cockpit', e.path)
- assert.ok(existsSync(abs), `文件不存在: ${e.path}`)
- const buf = readFileSync(abs)
- assert.ok(!(buf[0] === 0x3c && buf.subarray(0, 15).toString().includes('!DOCTYPE')), '取到 HTML 兜底页')
- assert.equal(`sha256:${createHash('sha256').update(buf).digest('hex')}`, e.hash)
- })
- check('A3 注册表与语义 key 一致(icons.js brandPool -> icon.brand.pool)', () => {
- assert.match(icons, /brandPool:\s*'icon\.brand\.pool'/)
- })
- check('A4 消费点存在:Shell 经 --brand-pool-url 语义注入,样式无物理路径', () => {
- assert.match(shell, /iconFor\('brandPool'\)/, 'navDecorStyle 未解析 brandPool')
- assert.match(shell, /--brand-pool-url'/, '未注入 --brand-pool-url')
- assert.match(shell, /--brand-pool-url, none\)/, 'CSS 未消费 --brand-pool-url(缺失退化)')
- assert.doesNotMatch(shell, /assets\/icons\/brand-pool\.png/, '样式层出现物理路径(违反共识 §3)')
- })
- check('A5 光池按设计 frame 布局:489/128 比例、不参与拾取、缺失自动不画', () => {
- assert.match(shell, /aspect-ratio:\s*489\s*\/\s*128/)
- assert.match(shell, /pointer-events:\s*none/, '装饰必须 pointer-events:none')
- })
- console.log('\n[B] 无死装饰资源(共识 §1:没有消费点的图不进运行时)')
- check('B1 装饰候选不在运行时目录(已挪归档或改名)', () => {
- const iconDir = join(root, 'public/cockpit/assets/icons')
- const files = readdirSync(iconDir, { recursive: true }).map(String)
- const gone = [
- 'brand-glow-1.png', 'brand-glow-3.png', 'panel-subtitle-decoration.png', 'panel-subtitle-lead.png',
- 'select-bg.png', 'select-bg-base.png', 'topbar-line.png',
- 'panel-corner-tl.png', 'panel-corner-tr.png', 'panel-corner-bl.png', 'panel-corner-br.png',
- ]
- const left = gone.filter((f) => files.includes(f))
- assert.deepEqual(left, [], `装饰候选仍在运行时目录: ${left.join(', ')}`)
- assert.ok(files.includes('brand-pool.png'), 'brand-pool.png 应在运行时目录(icon.brand.pool 消费中)')
- })
- check('B2 归档留痕:unconsumed-decor/ 含 11 个候选 + LEDGER 索引', () => {
- const archDir = join(root, '.scratch/lanhu-cockpit-asset-replacement/design-archive-v2/unconsumed-decor')
- assert.ok(existsSync(archDir), '归档目录不存在')
- const files = readdirSync(archDir).filter((f) => f.endsWith('.png'))
- assert.equal(files.length, 11, `归档应为 11 个候选,实际 ${files.length}`)
- assert.ok(existsSync(join(archDir, 'LEDGER.md')), 'LEDGER.md 索引缺失')
- })
- console.log('\n[C] 代码化装饰无散落主题色(规格 03:色值单一来源)')
- check('C1 面板四角切角 data-URI 不再内联主题色(%2301abea 清零,几何白母版)', () => {
- assert.doesNotMatch(panel, /%2301abea/, '四角 data-URI 仍内联 #01abea')
- assert.match(panel, /%23fff/, '四角 data-URI 应为白色几何母版')
- assert.match(panel, /background-color:\s*var\(--panel-line-hi/, '四角应经 mask 染 --panel-line-hi')
- assert.match(panel, /-webkit-mask:[\s\S]*--corner-tl/, '应改用 mask 简写消费四角(mask-image 不收层语法)')
- })
- check('C2 第二底色 --bg-1 已建槽位且有消费点(氛围底 + 地图画布)', () => {
- assert.match(tokens, /--bg-1:\s*#02091b/, 'tokens.css 缺 --bg-1')
- assert.match(shell, /var\(--bg-1,\s*#02091b\)/, 'Shell 氛围底未消费 --bg-1')
- assert.match(mapCanvas, /var\(--bg-1,\s*#02091b\)/, 'MapCanvas 画布底未消费 --bg-1')
- assert.doesNotMatch(shell, /linear-gradient\(180deg,\s*#010514/, 'Shell 氛围底仍有裸 hex 渐变')
- assert.doesNotMatch(mapCanvas, /background:\s*#02091b/, 'MapCanvas 仍有裸 hex 画布底')
- })
- console.log('\n[D] 结构保护(差异判断表:装饰不碰结构)')
- check('D1 装饰改动未触及面板结构属性(panelId/收放/滚动边界选择器仍在)', () => {
- assert.match(panel, /data-panel-id="panelId"/)
- assert.match(panel, /is-collapsed/)
- assert.match(panel, /overflow-y:\s*auto/)
- assert.match(panel, /overflow-x:\s*hidden/)
- })
- check('D2 批次 2 既有 token 断言面未破坏(语义槽未被装饰批改名)', () => {
- for (const name of ['--cyan', '--cyan-hi', '--cyan-d', '--bg-0', '--panel-line-hi']) {
- assert.match(tokens, new RegExp(`${name}:`), `${name} 缺失`)
- }
- assert.doesNotMatch(tokens, /--fs-metric/)
- })
- if (problems.length) {
- console.log(`\n${problems.length} 项失败`)
- process.exit(1)
- }
- console.log('\n批次 5 装饰断言面全部通过。')
|