From cf01c99fe6f255df688b7d11d73bf0e8010437fc Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 25 Aug 2026 10:13:34 +0000 Subject: [PATCH] =?UTF-8?q?=E7=B1=BB=E5=90=8D=E5=AE=88=E9=97=A8=E8=A1=A5?= =?UTF-8?q?=E4=B8=8A=E6=9D=A1=E4=BB=B6=E7=B1=BB=E4=B8=8E=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=20selector=20=E5=8C=B9=E9=85=8D=EF=BC=8C=E5=A0=B5=E6=8E=89?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=E4=BC=9A=E9=9D=99=E9=BB=98=E6=94=BE=E8=A1=8C?= =?UTF-8?q?=E7=9A=84=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这条守门是为「#193 把策划选择器整段误删」设的,但它自己有两处会放行: 一、条件类进不了清单。抽取时 `.replace(/\$\{[^}]*\}/g, ' ')` 把插值整段丢掉,而两个 组件里唯一的条件类恰好长在插值里:``plan-gdd-surface${showCard ? ' …--with-card' : ''}``。 清单 25 个类里没有 `plan-gdd-surface--with-card`。它不是装饰性的: `.game-workbench-chat .plan-gdd-surface--with-card` 的 `grid-template-rows: auto minmax(0, 1fr)` 是审批卡内滚的边界,策划面自己是 `display: grid` + `overflow: hidden` 的外壳,删掉这条,批准后的交付行连同路径和两个按钮会被切在壳外。实测把该类的两条 规则整段删掉,改前的测试照样绿。 改成只取插值里的字符串字面量——三元的条件和变量名不是类名,不能混进清单。清单 25 → 26,新进的正是 `plan-gdd-surface--with-card`,没有杂质。 二、`styles.includes('.' + name)` 是子串匹配,`.gdd-approval-card__header` 可以冒充 `.gdd-approval-card` 的存在证据。改成要求类名后面不再跟类名字符。当前 26 个类逐一 校验都有精确 selector,所以这一处堵的是潜在放行,不是正在掩盖的缺失。 另按评审要求补一条负向钉子:单独钉住上面那条承重的行模板。存在性检查只要 `--with-card` 还剩任意一条规则就绿,而两条里只有它是承重的。 三条负向都已验证非空转:删两条规则 → 红;只删承重那条留下分隔线 → 红;把 `.gdd-approval-card__details` 改名只留派生类 → 红(改前用 includes 会绿)。 评审同时建议做 computed-style/布局断言,这里没做:appSurface 跑 jsdom,没有布局引擎, 且这套 suite 不把 styles.css 挂进 DOM。要真断言得换真实浏览器 runner,是另一件事。 Co-Authored-By: Claude Opus 5 --- .../tests/appSurface/plan-gdd.suite.ts | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts index fd20d253e..cb3e568a0 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts @@ -546,7 +546,18 @@ export function registerPlanGddApprovalTests() { for (const match of componentSources.matchAll( /className=(?:"([^"]+)"|\{`([^`]+)`\})/g, )) { - const literal = (match[1] ?? match[2] ?? '').replace(/\$\{[^}]*\}/g, ' '); + // 条件类长在插值里:`plan-gdd-surface${showCard ? ' …--with-card' : ''}`。 + // 把 `${…}` 整段丢掉等于把它们排除在守门之外,而它们恰恰是最容易被顺手删干净 + // 的一档——`--with-card` 挂着审批卡的行模板,没有它卡片底部会被外壳的 + // `overflow: hidden` 切掉。只取插值里的字符串字面量:三元的条件、变量名都不是 + // 类名,不能混进清单。 + const literal = (match[1] ?? match[2] ?? '').replace( + /\$\{([^}]*)\}/g, + (_whole, expression: string) => + [...expression.matchAll(/'([^']*)'|"([^"]*)"/g)] + .map((piece) => piece[1] ?? piece[2] ?? '') + .join(' '), + ); for (const name of literal.split(/\s+/)) { if (name) { referencedClasses.add(name); @@ -559,9 +570,15 @@ export function registerPlanGddApprovalTests() { 'utf8', ); for (const name of referencedClasses) { + // 子串匹配会把 `.gdd-approval-card__header` 当成 `.gdd-approval-card` 的证据: + // 前缀类的规则被删光、只剩派生类时这里照样绿。要求类名后面不能再跟类名字符, + // 才是「存在这个类的精确 selector」。 + const exactSelector = new RegExp( + `\\.${name.replace(/[^\w-]/g, '\\$&')}(?![\\w-])`, + ); expect( - styles.includes(`.${name}`), - `styles.css 缺少 .${name} 的规则`, + exactSelector.test(styles), + `styles.css 缺少 .${name} 的精确 selector`, ).toBe(true); } // 正文弹层必须是浮层:backdrop 一旦丢掉 fixed 定位,整个 GDD 会内联平铺进 @@ -586,5 +603,12 @@ export function registerPlanGddApprovalTests() { expect(styles).toMatch( /\.game-workbench-chat\s+\.project-supervisor-conversation:has\([^)]*\.planning-lane-runtime-strip[^)]*\)\s*\{[^}]*display:\s*flex/s, ); + // `--with-card` 有两条规则,上面的存在性检查只要还剩一条就绿。承重的是这一条: + // 策划面是 `display: grid` + `overflow: hidden` 的外壳,审批卡的 `max-height` + // 和内滚要靠这个行模板才有边界。只删它、留下那条分隔线,表现是批准后的交付行 + // 连同路径和两个按钮被切在壳外。 + expect(styles).toMatch( + /\.game-workbench-chat\s+\.plan-gdd-surface--with-card\s*\{[^}]*grid-template-rows:\s*auto minmax\(0, 1fr\)/s, + ); }); }