From 9a03aa5e4afa02b6431776531d198257f615a90a Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 8 Sep 2026 16:34:29 +0000 Subject: [PATCH] test: touchfiles, tiers, shim E2E, real-engine fixture Touchfiles: the catalog, contract, detector bin, checklist resolver, review- army resolver, and DESIGN.md lib join the dep lists of review-design-lite, design-review-fix, the design-consultation cases, and plan-design-review-no- ui-scope, so editing any of them re-selects the tests that read their output. Three new E2E keys: design-review-detector-shim (gate; source mode on a feature-branch diff), design-review-detector-shim-dom (gate; DOM mode: the slop fixture served on loopback, dumped through the browse binary with lib/dom-dump.js, persisted under a GSTACK_HOME-scoped REPORT_DIR, scanned once; self-skips when browse/dist/browse is absent), and design-html-slop-gate (periodic; one fix pass, at most two scans, remaining findings accepted with reason). Every case reaches the engine through test/fixtures/fake-impeccable.ts via IMPECCABLE_BIN from outside the temp repo, reads extracted skill sections (never a whole SKILL.md) with the installed bin path rewritten to this checkout, and asserts the probe ran, the right scan verb ran, `npx impeccable` never did, and the output carries FINDING rows tagged [ai-color-palette] and [low-contrast]. review-design-lite gets the fake engine and an eighth tally signal for a detector row; its 4-hit threshold is unchanged. test/gstack-design-detect.test.ts evaluates design-review's REPORT_DIR expression with GSTACK_HOME set and proves a dump under it is accepted by the wrapper's allow-list. The sample fixtures were real captures from commit 1 (engine 0.1.3), so there is nothing hand-written left to swap. Co-Authored-By: Claude Fable 5.1 --- test/design-detect-contract.test.ts | 2 +- test/frontend-scope.test.ts | 4 +- test/gstack-design-detect.test.ts | 26 ++++ test/helpers/touchfiles-data.ts | 17 +- test/skill-e2e-design.test.ts | 230 +++++++++++++++++++++++++++- test/skill-e2e-review.test.ts | 21 ++- 6 files changed, 289 insertions(+), 11 deletions(-) diff --git a/test/design-detect-contract.test.ts b/test/design-detect-contract.test.ts index 58304dddc..a36b5fdd1 100644 --- a/test/design-detect-contract.test.ts +++ b/test/design-detect-contract.test.ts @@ -19,7 +19,7 @@ const ROOT = path.join(import.meta.dir, '..'); const TOKEN = /\b(IMPECCABLE_[A-Z_]+|DETECT_[A-Z_]+|DESIGN_MD_[A-Z_]+|DOM_DUMP_[A-Z_]+|DESIGN_DETECTOR_[A-Z_]+|DESIGN_DETECT_[A-Z_]+)\b/g; // Things that look like sentinels but are env vars / flags the prose legitimately names. // Env vars, flags, and resolver placeholder names the prose legitimately names. -const NOT_SENTINELS = new Set(['IMPECCABLE_BIN', 'IMPECCABLE_HOME', 'IMPECCABLE_HOOK_DISABLED', 'DESIGN_DETECT_TIMEOUT_MS', 'DESIGN_MD_CHECK', 'DESIGN_DETECTOR']); +const NOT_SENTINELS = new Set(['IMPECCABLE_BIN', 'IMPECCABLE_HOME', 'IMPECCABLE_HOOK_DISABLED', 'DESIGN_DETECT_TIMEOUT_MS', 'DESIGN_MD_CHECK', 'DESIGN_DETECTOR', 'IMPECCABLE_INTEROP' /* docs/designs/IMPECCABLE_INTEROP.md */]); function* agentReadableFiles(): Generator { const skip = new Set(['node_modules', '.git', 'dist', 'build', 'test', 'docs', '.context', '.claude', '.agents', '.factory', '.cursor', '.kiro', '.opencode', '.openclaw', '.hermes', '.slate', '.gstack', '.gbrain', '.conductor']); diff --git a/test/frontend-scope.test.ts b/test/frontend-scope.test.ts index fe4eff179..edac93460 100644 --- a/test/frontend-scope.test.ts +++ b/test/frontend-scope.test.ts @@ -57,7 +57,7 @@ describe.skipIf(!POSIX)('parity with bin/gstack-diff-scope', () => { test('SCOPE_FRONTEND agrees with isFrontendPath for every sample, one file per diff', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-scope-parity-')); const git = (...a: string[]) => { - const r = spawnSync('git', a, { cwd: dir, encoding: 'utf-8' }); + const r = spawnSync('git', a, { cwd: dir, encoding: 'utf-8', timeout: 30_000 }); if (r.status !== 0) throw new Error(r.stderr); }; try { @@ -73,7 +73,7 @@ describe.skipIf(!POSIX)('parity with bin/gstack-diff-scope', () => { fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, '/* x */\n'); git('add', '-A'); git('commit', '-q', '-m', rel); - const r = spawnSync('bash', [path.join(ROOT, 'bin', 'gstack-diff-scope'), 'main'], { cwd: dir, encoding: 'utf-8' }); + const r = spawnSync('bash', [path.join(ROOT, 'bin', 'gstack-diff-scope'), 'main'], { cwd: dir, encoding: 'utf-8', timeout: 30_000 }); const bashSays = /SCOPE_FRONTEND=true/.test(r.stdout); if (bashSays !== expected) mismatches.push(`${rel}: bash=${bashSays} ts=${expected}`); git('checkout', '-q', 'main'); git('branch', '-q', '-D', 'probe'); diff --git a/test/gstack-design-detect.test.ts b/test/gstack-design-detect.test.ts index cd28e7117..ba8bf1372 100644 --- a/test/gstack-design-detect.test.ts +++ b/test/gstack-design-detect.test.ts @@ -453,6 +453,32 @@ describe('scan', () => { }); }); +describe('design-review REPORT_DIR agrees with the allow-list', () => { + test.skipIf(!POSIX)('the template expression, evaluated with GSTACK_HOME set, lands under /projects//designs/ and a dump there is scanned', () => { + const tmpl = fs.readFileSync(path.join(ROOT, 'design-review', 'SKILL.md.tmpl'), 'utf-8'); + const m = tmpl.match(/^REPORT_DIR="(.+)"$/m); + expect(m).not.toBeNull(); + const expr = m![1]; + expect(expr.startsWith('${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/designs/')).toBe(true); + const r = spawnSync('bash', ['-c', `SLUG=my-repo; echo "${expr}"`], { encoding: 'utf-8', timeout: 30_000, env: { PATH: process.env.PATH!, HOME: path.join(SANDBOX, 'fake-home'), GSTACK_HOME } }); + const reportDir = r.stdout.trim(); + expect(reportDir.startsWith(path.join(GSTACK_HOME, 'projects', 'my-repo', 'designs', 'design-audit-'))).toBe(true); + const dom = path.join(reportDir, 'dom', '120000-1'); + fs.mkdirSync(dom, { recursive: true }); + fs.writeFileSync(path.join(dom, 'home.dom.html'), ''); + const log = path.join(SANDBOX, 'argv-report.log'); + fs.rmSync(log, { force: true }); + try { + const s = run(['scan', '--format', 'gstack', dom + '/home.dom.html'], { env: { IMPECCABLE_BIN: FAKE, FAKE_IMPECCABLE_LOG: log } }); + expect(s.err).not.toContain(SENTINEL.DETECT_REFUSED); + const argv = JSON.parse(fs.readFileSync(log, 'utf-8').trim().split('\n')[0]).argv as string[]; + expect(argv.slice(2)).toEqual([fs.realpathSync(path.join(dom, 'home.dom.html'))]); + } finally { + fs.rmSync(path.join(GSTACK_HOME, 'projects'), { recursive: true, force: true }); + } + }); +}); + describe('rules', () => { test('prints every mapped id with kind/impact/tier/handoff and the tested engine versions', () => { const r = run(['rules']); diff --git a/test/helpers/touchfiles-data.ts b/test/helpers/touchfiles-data.ts index 33c507fb3..500696d36 100644 --- a/test/helpers/touchfiles-data.ts +++ b/test/helpers/touchfiles-data.ts @@ -66,7 +66,7 @@ export const E2E_TOUCHFILES: Record = { 'review-sql-injection': ['review/**', 'test/fixtures/review-eval-vuln.rb', 'test/skill-e2e-review.test.ts'], 'review-enum-completeness': ['review/**', 'test/fixtures/review-eval-enum*.rb', 'test/skill-e2e-review.test.ts'], 'review-base-branch': ['review/**', 'test/skill-e2e-review-attribution.test.ts'], - 'review-design-lite': ['review/**', 'test/fixtures/review-eval-design-slop.*', 'test/skill-e2e-review.test.ts'], + 'review-design-lite': ['review/**', 'test/fixtures/review-eval-design-slop.*', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'lib/design-catalog.ts', 'lib/design-detect-contract.ts', 'bin/gstack-design-detect.ts', 'scripts/resolvers/design-checklist.ts', 'scripts/resolvers/review-army.ts', 'test/skill-e2e-review.test.ts'], // Review Army (specialist dispatch) 'review-army-migration-safety': ['review/**', 'scripts/resolvers/review-army.ts', 'bin/gstack-diff-scope', 'test/skill-e2e-review-army.test.ts'], @@ -305,12 +305,16 @@ export const E2E_TOUCHFILES: Record = { ], // Design - 'design-consultation-core': ['design-consultation/**', 'scripts/gen-skill-docs.ts', 'test/helpers/llm-judge.ts', 'test/skill-e2e-design.test.ts'], - 'design-consultation-existing': ['design-consultation/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], + 'design-consultation-core': ['design-consultation/**', 'lib/design-catalog.ts', 'lib/design-md.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/llm-judge.ts', 'test/skill-e2e-design.test.ts'], + 'design-consultation-existing': ['design-consultation/**', 'lib/design-md.ts', 'bin/gstack-design-md.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], 'design-consultation-research': ['design-consultation/**', 'scripts/resolvers/aside.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], 'design-consultation-preview': ['design-consultation/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], - 'plan-design-review-no-ui-scope': ['plan-design-review/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], - 'design-review-fix': ['design-review/**', 'scripts/resolvers/aside.ts', 'scripts/resolvers/design.ts', 'browse/src/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], + 'plan-design-review-no-ui-scope': ['plan-design-review/**', 'lib/design-catalog.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], + 'design-review-fix': ['design-review/**', 'scripts/resolvers/aside.ts', 'scripts/resolvers/design.ts', 'lib/design-catalog.ts', 'browse/src/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], + // Design detector (user-installed impeccable engine) through the fake engine shim: source mode on a diff and DOM mode on a served page. + 'design-review-detector-shim': ['design-review/**', 'scripts/resolvers/design.ts', 'lib/design-catalog.ts', 'lib/design-detect-contract.ts', 'lib/dom-dump-script.ts', 'lib/dom-dump.js', 'bin/gstack-design-detect.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/fixtures/review-eval-design-slop.*', 'test/skill-e2e-design.test.ts'], + 'design-review-detector-shim-dom': ['design-review/**', 'scripts/resolvers/design.ts', 'lib/design-detect-contract.ts', 'lib/dom-dump-script.ts', 'lib/dom-dump.js', 'bin/gstack-design-detect.ts', 'browse/src/**', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/fixtures/review-eval-design-slop.*', 'test/skill-e2e-design.test.ts'], + 'design-html-slop-gate': ['design-html/**', 'scripts/resolvers/design.ts', 'lib/design-detect-contract.ts', 'bin/gstack-design-detect.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/skill-e2e-design.test.ts'], // /diagram (diagram-render bundle consumers). Triplet = deterministic // functional (gate); authoring quality = LLM-judged benchmark (periodic). @@ -738,6 +742,9 @@ export const E2E_TIERS: Record = { 'design-consultation-preview': 'periodic', // D2a demotion 2026-08 ($0.89/481s) 'plan-design-review-no-ui-scope': 'gate', 'design-review-fix': 'periodic', + 'design-review-detector-shim': 'gate', // deterministic sentinels from the fake engine (source mode on a diff) + 'design-review-detector-shim-dom': 'gate', // same shim, DOM mode through the browse binary's dump; self-skips when the binary is absent + 'design-html-slop-gate': 'periodic', // one-pass gate behavior is a judgment call on a fake engine's fixed output // /diagram — triplet is deterministic functional (gstack-render falls back // to the browse daemon, so CI runs it); judge is a quality benchmark diff --git a/test/skill-e2e-design.test.ts b/test/skill-e2e-design.test.ts index a6b196c15..e9a653e95 100644 --- a/test/skill-e2e-design.test.ts +++ b/test/skill-e2e-design.test.ts @@ -6,7 +6,7 @@ import { ROOT, runId, evalsEnabled, selectedTests, describeIfSelected, testConcurrentIfSelected, copyDirSync, logCost, recordE2E, - createEvalCollector, finalizeEvalCollector, + createEvalCollector, finalizeEvalCollector, browseBin, } from './helpers/e2e-helpers'; import { asideAvailable } from './helpers/aside-available'; import { spawnSync } from 'child_process'; @@ -701,3 +701,231 @@ Review the site at ${serverUrl}. Use --quick mode. Skip any AskUserQuestion call afterAll(async () => { await finalizeEvalCollector(evalCollector); }); + +// --- Design detector (impeccable engine shim) E2E --- +// +// The user-installed impeccable engine is stood in for by test/fixtures/ +// fake-impeccable.ts (prints the captured detect --json sample, exit 2), +// reached through IMPECCABLE_BIN from OUTSIDE the temp repo (the wrapper +// ignores an in-repo IMPECCABLE_BIN by design). The skill text the agent reads +// is the extracted Setup detector block + Phase 0 (+ the Phase 3 DOM-dump +// section for the DOM case), never the 1,500-line SKILL.md, with the installed +// bin path pointed at THIS checkout so the test does not depend on ~/.claude. + +const FAKE_ENGINE_SRC = path.join(ROOT, 'test', 'fixtures', 'fake-impeccable.ts'); +const DETECT_SAMPLE = path.join(ROOT, 'test', 'fixtures', 'impeccable-detect-sample.json'); + +function sliceBetween(text: string, start: string, end: string): string { + const i = text.indexOf(start); + if (i < 0) throw new Error(`marker not found: ${start}`); + const j = text.indexOf(end, i + start.length); + return text.slice(i, j > i ? j : undefined); +} + +/** design-review's detector prose with the installed bin/lib paths rewritten to this checkout. */ +function detectorSkillText(sections: Array<[string, string]>): string { + const full = fs.readFileSync(path.join(ROOT, 'design-review', 'SKILL.md'), 'utf-8'); + return sections.map(([a, b]) => sliceBetween(full, a, b)).join('\n\n---\n\n') + .replaceAll('$HOME/.claude/skills/gstack', ROOT) + .replaceAll('~/.claude/skills/gstack', ROOT); +} + +function makeFakeEngine(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-fake-impeccable-')); + fs.copyFileSync(FAKE_ENGINE_SRC, path.join(dir, 'impeccable')); + fs.chmodSync(path.join(dir, 'impeccable'), 0o755); + return dir; +} + +describeIfSelected('Design review detector shim E2E', ['design-review-detector-shim', 'design-review-detector-shim-dom'], () => { + let repoDir: string; + let engineDir: string; + let server: ReturnType | null = null; + + beforeAll(() => { + repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-detector-shim-')); + const run = (cmd: string, args: string[]) => spawnSync(cmd, args, { cwd: repoDir, stdio: 'pipe', timeout: 5000 }); + run('git', ['init', '-b', 'main']); + run('git', ['config', 'user.email', 'test@test.com']); + run('git', ['config', 'user.name', 'Test']); + fs.writeFileSync(path.join(repoDir, 'index.html'), '

Clean

\n'); + fs.writeFileSync(path.join(repoDir, 'styles.css'), 'body { font-size: 16px; }\n'); + run('git', ['add', '.']); + run('git', ['commit', '-m', 'initial']); + run('git', ['checkout', '-b', 'feature/landing']); + fs.writeFileSync(path.join(repoDir, 'index.html'), fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'review-eval-design-slop.html'), 'utf-8')); + fs.writeFileSync(path.join(repoDir, 'styles.css'), fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'review-eval-design-slop.css'), 'utf-8')); + run('git', ['add', '.']); + run('git', ['commit', '-m', 'add landing page']); + engineDir = makeFakeEngine(); + fs.writeFileSync( + path.join(repoDir, 'design-review-detector.md'), + detectorSkillText([ + ['**Design detector (optional, deterministic):**', '**Create output directories:**'], + ['**Phase 0: mechanical scan**', '## Phases 1-6'], + ]), + ); + fs.writeFileSync( + path.join(repoDir, 'design-review-dom-dump.md'), + detectorSkillText([['### DOM dump (DOM mode only', '### Auth Detection']]), + ); + }); + + afterAll(() => { + server?.stop(true); + try { fs.rmSync(repoDir, { recursive: true, force: true }); } catch {} + try { fs.rmSync(engineDir, { recursive: true, force: true }); } catch {} + }); + + testConcurrentIfSelected('design-review-detector-shim', async () => { + const result = await runSkillTest({ + prompt: `You are in a git repo on branch feature/landing with changes against main (the base branch). +Read design-review-detector.md: it is the Setup "Design detector" block and "Phase 0: mechanical scan" from /design-review. +This is a diff-aware run with no URL, so it is SOURCE mode. Run the probe, then the Phase 0 source-mode scan with base main, exactly as written (use --host claude). +Do not run any browser step, do not fix anything, do not run npx. +Then write ${repoDir}/detector-output.md: one FINDING-NNN row per rule in the DETECT_TOP block, each tagged with its [rule-id] and the printed impact, plus the first line the probe printed.`, + workingDirectory: repoDir, + maxTurns: 15, + timeout: CAPTURE_MS, + testName: 'design-review-detector-shim', + runId, + env: { IMPECCABLE_BIN: path.join(engineDir, 'impeccable'), FAKE_IMPECCABLE_OUTPUT: DETECT_SAMPLE }, + }); + + logCost('/design-review detector shim (source)', result); + recordE2E(evalCollector, '/design-review detector shim', 'Design review detector shim E2E (source mode)', result); + expect(result.exitReason).toBe('success'); + + const bash = result.toolCalls.filter(c => c.tool === 'Bash').map(c => String(c.input?.command ?? '')); + expect(bash.some(c => c.includes('gstack-design-detect.ts probe'))).toBe(true); + expect(bash.some(c => /gstack-design-detect\.ts scan --changed main/.test(c))).toBe(true); + expect(bash.some(c => c.includes('npx impeccable'))).toBe(false); + expect(result.output).toContain('IMPECCABLE_READY'); + + const outPath = path.join(repoDir, 'detector-output.md'); + expect(fs.existsSync(outPath)).toBe(true); + const out = fs.readFileSync(outPath, 'utf-8'); + expect(out).toContain('FINDING-001'); + expect(out).toContain('[ai-color-palette]'); + expect(out).toContain('[low-contrast]'); + }, CAPTURE_MS); + + // DOM mode needs a browser engine for the dump: gstack's own browse binary + // (CI builds it with build:gates). Self-skips when it is absent, like the + // other render gates. + testConcurrentIfSelected( + 'design-review-detector-shim-dom', + async () => { + if (!fs.existsSync(browseBin)) { + console.log('design-review-detector-shim (dom mode): browse binary absent, skipping (build it with bun run build:gates)'); + return; + } + const site = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-detector-site-')); + fs.copyFileSync(path.join(ROOT, 'test', 'fixtures', 'review-eval-design-slop.html'), path.join(site, 'index.html')); + fs.copyFileSync(path.join(ROOT, 'test', 'fixtures', 'review-eval-design-slop.css'), path.join(site, 'styles.css')); + server = Bun.serve({ + hostname: '127.0.0.1', port: 0, + fetch(req) { + const p = new URL(req.url).pathname.replace(/^\//, '') || 'index.html'; + const f = path.join(site, p); + return fs.existsSync(f) ? new Response(Bun.file(f)) : new Response('not found', { status: 404 }); + }, + }); + const url = `http://127.0.0.1:${server.port}/index.html`; + const reportDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-detector-report-')); + const gstackHome = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-detector-home-')); + // REPORT_DIR must sit under /projects//designs/ for the wrapper's allow-list. + const allowed = path.join(gstackHome, 'projects', 'shim', 'designs', 'design-audit-20260908'); + fs.mkdirSync(path.join(allowed, 'dom', 'run1'), { recursive: true }); + try { + const result = await runSkillTest({ + prompt: `Read design-review-detector.md (the /design-review detector block + Phase 0) and design-review-dom-dump.md (the Phase 3 DOM dump section). +The target is the URL ${url}, so this is DOM mode: never scan source files. +Aside is NOT available; use the fallback browser engine: $B is ${browseBin}. Run "$B goto ${url}" first, then follow the fallback-engine DOM dump steps exactly as written, with {page} = home, REPORT_DIR=${allowed}, RUN_ID=run1, and --host claude. Then run the single scan over ${allowed}/dom/run1 and write ${allowed}/detector-output.md with one FINDING-NNN row per rule in the DETECT_TOP block, each tagged [rule-id], and the line "static scan of the rendered DOM; cross-origin CSS not resolved". +Do not run npx. Do not fix anything.`, + workingDirectory: repoDir, + maxTurns: 25, + timeout: CAPTURE_LONG_MS, + testName: 'design-review-detector-shim-dom', + runId, + env: { IMPECCABLE_BIN: path.join(engineDir, 'impeccable'), FAKE_IMPECCABLE_OUTPUT: DETECT_SAMPLE, GSTACK_HOME: gstackHome }, + }); + logCost('/design-review detector shim (dom)', result); + recordE2E(evalCollector, '/design-review detector shim (dom)', 'Design review detector shim E2E (DOM mode)', result); + expect(result.exitReason).toBe('success'); + const bash = result.toolCalls.filter(c => c.tool === 'Bash').map(c => String(c.input?.command ?? '')); + expect(bash.some(c => c.includes('dom-dump.js') && c.includes('--out') && c.includes('--raw'))).toBe(true); + expect(bash.some(c => /gstack-design-detect\.ts scan /.test(c) && c.includes('dom/run1'))).toBe(true); + expect(bash.some(c => /gstack-design-detect\.ts scan --changed/.test(c))).toBe(false); + const dumps = fs.readdirSync(path.join(allowed, 'dom', 'run1')).filter(f => f.endsWith('.dom.html')); + expect(dumps.length).toBeGreaterThan(0); + expect(fs.readFileSync(path.join(allowed, 'dom', 'run1', dumps[0]), 'utf-8')).toContain('data-gstack-dom-css'); + const out = fs.readFileSync(path.join(allowed, 'detector-output.md'), 'utf-8'); + expect(out).toContain('[ai-color-palette]'); + expect(out).toContain('static scan of the rendered DOM'); + } finally { + server?.stop(true); server = null; + try { spawnSync(browseBin, ['stop'], { stdio: 'pipe', timeout: 10_000 }); } catch {} + for (const d of [site, reportDir, gstackHome]) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } + } + }, + CAPTURE_LONG_MS, + ); +}); + +describeIfSelected('Design HTML slop gate E2E', ['design-html-slop-gate'], () => { + let workDir: string; + let engineDir: string; + + beforeAll(() => { + workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-html-gate-')); + const run = (cmd: string, args: string[]) => spawnSync(cmd, args, { cwd: workDir, stdio: 'pipe', timeout: 5000 }); + run('git', ['init', '-b', 'main']); + run('git', ['config', 'user.email', 'test@test.com']); + run('git', ['config', 'user.name', 'Test']); + const css = fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'review-eval-design-slop.css'), 'utf-8'); + const html = fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'review-eval-design-slop.html'), 'utf-8') + .replace('', ``); + fs.writeFileSync(path.join(workDir, 'finalized.html'), html); + run('git', ['add', '.']); + run('git', ['commit', '-m', 'finalized html']); + engineDir = makeFakeEngine(); + const full = fs.readFileSync(path.join(ROOT, 'design-html', 'SKILL.md'), 'utf-8'); + const text = [ + sliceBetween(full, '**Design detector (optional, deterministic):**', '