// 设计系统守护测试(父仓 DESIGN.md §11 禁止清单 · 宿主侧,2026-09-30)。 // 棘轮基线:现存豁免登记在下方;新增同类违规直接失败。收紧 = 修掉违规后删基线条目。 // 范围:src/**/*.css 与 src/**/*.tsx。@ui 组件库侧由 Agent-System-UI 的守护测试覆盖。 import { describe, it, expect } from 'vitest' import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs' import { join, relative, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const HERE = dirname(fileURLToPath(import.meta.url)) // test/ const FE_ROOT = join(HERE, '..') const SRC = join(FE_ROOT, 'src') const DEV_ROOT = join(FE_ROOT, '..', '..') // Dev 父仓根(DESIGN.md;单独检出本仓时不存在) function walk(dir: string, out: string[] = []): string[] { for (const name of readdirSync(dir)) { const p = join(dir, name) if (statSync(p).isDirectory()) walk(p, out) else out.push(p) } return out } const read = (p: string) => readFileSync(p, 'utf8') const stripCssComments = (t: string) => t.replace(/\/\*[\s\S]*?\*\//g, '') const collectHex = (t: string) => [...t.matchAll(/#[0-9a-fA-F]{3,8}\b/g)].map(m => m[0].toLowerCase()) // ---- 1. 宪法在位(组合检出时) ---- describe('DESIGN.md 宪法在位', () => { it('DESIGN.md 存在且 AGENTS.md 带强制阅读门禁', () => { const designPath = join(DEV_ROOT, 'DESIGN.md') if (!existsSync(designPath)) { console.warn('(单独检出 Frontend 仓:跳过宪法检查)'); return } expect(read(designPath)).toContain('Agent-System-Dev 设计系统') const agents = read(join(DEV_ROOT, 'AGENTS.md')) expect(agents).toContain('DESIGN.md') }) }) // ---- 2. 动效唯一所有者 ---- describe('动效唯一所有者', () => { it('宿主 CSS 无 @keyframes 或私有 transition(全部走 @ui/motion.css 的 mk-* 类)', () => { for (const path of walk(SRC).filter(p => p.endsWith('.css'))) { const css = stripCssComments(read(path)) expect(css, `${relative(SRC, path)} 不得定义 @keyframes`).not.toContain('@keyframes') const transitions = css.split('\n').filter(line => /transition\s*:/.test(line) && !/transition\s*:\s*none\b/.test(line)) if (path.endsWith('styles.css')) continue // 历史 transition 由现有棘轮逐步收紧;新分层 CSS 不再扩散。 expect(transitions, `${relative(SRC, path)} 动效必须消费 mk-*`).toEqual([]) } }) }) // ---- 3. 裸色值棘轮 ---- const HEX_BASELINE_STYLES = [ '#0e1116', '#101826', '#0b1f38', // 登录品牌面板固定深底渐变(面板自带深底,主题无关) '#dfe4f2', // 同上面板的固定浅字 ] as const describe('裸色值棘轮', () => { it('styles.css 不引入基线外裸色值', () => { const found = collectHex(read(join(SRC, 'styles.css'))) const allow = new Set(HEX_BASELINE_STYLES) const extra = found.filter(h => !allow.has(h)) expect(extra, `新增裸色值 ${[...new Set(extra)].join(', ')}——改用 tokens 令牌;确需豁免更新 HEX_BASELINE_STYLES 并写明理由`).toEqual([]) }) it('分层组件 CSS 只消费令牌,不派生私有颜色', () => { const componentCss = walk(SRC).filter(p => p.endsWith('.css') && !p.endsWith('styles.css')) const extra: string[] = [] for (const path of componentCss) { const css = stripCssComments(read(path)) for (const hex of collectHex(css)) extra.push(`${relative(SRC, path)}: ${hex}`) if (/color-mix\s*\(/.test(css)) extra.push(`${relative(SRC, path)}: color-mix`) } expect(extra, '分层组件 CSS 的颜色须由 @ui/tokens.css 提供').toEqual([]) }) it('组件 TSX 无裸色值', () => { const extra: string[] = [] for (const p of walk(SRC).filter(p => /\.(tsx|ts)$/.test(p) && !p.endsWith('.d.ts'))) { for (const h of collectHex(read(p))) extra.push(`${relative(SRC, p)}: ${h}`) } expect(extra, '组件里出现裸色值——走 tokens 令牌').toEqual([]) }) }) // ---- 4. 行内字号棘轮(--fs-ui-* 标尺) ---- const FONTSIZE_LITERAL_BASELINE = ['40'] as const // Conversation 空态装饰字形(⌘ 展示字号,非 UI 层级) describe('行内字号棘轮', () => { it('组件 TSX 无基线外 fontSize 数值字面量', () => { const allow = new Set(FONTSIZE_LITERAL_BASELINE) const extra: string[] = [] for (const p of walk(SRC).filter(p => p.endsWith('.tsx'))) { const lines = read(p).split('\n') lines.forEach((l: string, i: number) => { const m = l.match(/fontSize:\s*['"`]?([\d.]+)/) if (m && !allow.has(m[1])) extra.push(`${relative(SRC, p)}:${i + 1} (${m[1]})`) }) } expect(extra, '行内 fontSize 数值——改用 var(--fs-ui-*);确需豁免更新基线并写明理由').toEqual([]) }) it('组件 TSX 无
折叠(走 @ui Collapsible,DESIGN.md §11.7)', () => { const extra: string[] = [] for (const p of walk(SRC).filter(p => p.endsWith('.tsx'))) { const code = read(p).replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '') if (/]/.test(code)) extra.push(relative(SRC, p)) } expect(extra, '
折叠——改用 @ui Collapsible').toEqual([]) }) })