Files
pbakaus_impeccable/tests/live-svelte-component-accept.test.mjs
T
Paul BakausandClaude Code 5b6b331785 fix: preview-truth CSS supersession + cascade ordering on Svelte accept
Field failure from a real Codex session: accepting a variant into
Pitch.svelte appended 23 selectors and removed none, so the source's old
.decisions grid rules re-attached through the kept root class and forced
the accepted board into a stale three-column layout; some appended base
rules also landed after the source's media block, weakening the mobile
cascade.

Two mechanical fixes:
- Preview truth: the scaffolder records the seeded selectors (the source
  rules that styled the replaced selection, which the isolated preview
  never applied). On accept, any seeded selector the variant does not
  re-declare is removed; the selector-loss postcondition treats those
  removals like compiler prunes. A regression test reproduces the exact
  Pitch shape end to end.
- Cascade order: reconciliation inserts new base rules BEFORE existing
  top-level media blocks instead of appending after them.

Init-latency reductions from the same transcript:
- live.mjs inlines the resolved surface brief (removes three
  surface-brief.mjs round-trips including a --help miss before first poll).
- The wrap/scaffold payload carries componentStubMarkup, and live.md
  instructs editing stubs in place (the session read the manifest + stub
  back and then deleted/recreated the files).
- live.md notes that a busy default port usually means the dev server is
  already running (the session spawned a duplicate).

This work was produced with AI assistance (Claude Code).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-27 18:45:07 -07:00

356 lines
13 KiB
JavaScript

import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, symlinkSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import {
extractMatchingSourceCss,
removeSelectorsFromSvelteSource,
findSvelteComponentManifest,
inlineSvelteComponentAccept,
mergeCssIntoSvelteSource,
reindentPreservingStructure,
scaffoldSvelteComponentSession,
} from '../skill/scripts/live/svelte-component.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_NODE_MODULES = join(__dirname, '..', 'node_modules');
const ROUTE_SOURCE = `<script>
let stages = [
{ label: 'Review', detail: 'Bots comment', active: true },
{ label: 'Resolve', detail: 'Agent fixes', active: false },
];
let footerNote = 'All quiet.';
</script>
<main>
<ol class="pit-board">
{#each stages as stage, i}
<li class="stage">
<span class="label">{stage.label}</span>
<p class="detail">{stage.detail}</p>
</li>
{/each}
</ol>
<p class="footer">{footerNote}</p>
</main>
<style>
.pit-board {
border-top: 1px solid #333;
display: flex;
}
.stage { padding: 8px; }
.footer { color: gray; }
</style>
`;
function write(root, rel, content) {
const abs = join(root, rel);
mkdirSync(dirname(abs), { recursive: true });
writeFileSync(abs, content);
}
describe('svelte component scaffold + accept pipeline', () => {
let tmp;
beforeEach(() => {
tmp = realpathSync(mkdtempSync(join(tmpdir(), 'impeccable-svelte-accept-')));
// The scaffolder resolves the app's svelte compiler; link this repo's.
mkdirSync(join(tmp, 'node_modules'), { recursive: true });
try {
symlinkSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp, 'node_modules', 'svelte'), 'dir');
} catch {
cpSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp, 'node_modules', 'svelte'), { recursive: true });
}
write(tmp, 'package.json', JSON.stringify({ name: 'app', dependencies: { svelte: '^5' } }));
write(tmp, 'src/routes/+page.svelte', ROUTE_SOURCE);
});
afterEach(() => {
rmSync(tmp, { recursive: true, force: true });
});
function scaffold(id = 'testacc1') {
// The picked element spans the <ol> block: lines 10-17 (1-indexed).
const originalLines = ROUTE_SOURCE.split('\n').slice(9, 17);
assert.match(originalLines[0], /<ol/);
assert.match(originalLines[originalLines.length - 1], /<\/ol>/);
return scaffoldSvelteComponentSession({
id,
count: 2,
sourceFile: 'src/routes/+page.svelte',
sourceStartLine: 10,
sourceEndLine: 17,
originalLines,
cwd: tmp,
});
}
it('scaffolds a v2 contract with the each collection as one structured prop', () => {
const session = scaffold();
assert.equal(session.fallback, undefined);
assert.equal(session.manifest.contractVersion, 2);
const collection = session.propContract.find((c) => c.kind === 'collection');
assert.equal(collection.prop, 'stages');
assert.equal(collection.item.rootTag, 'li');
const v1 = readFileSync(join(tmp, session.componentDir, 'v1.svelte'), 'utf-8');
assert.match(v1, /\{#each stages as stage, i\}/);
assert.match(v1, /\{stage\.label\}/);
assert.match(v1, /let \{ stages = \[\] \} = \$props\(\)/);
// Stub CSS is seeded from the route's matching rules.
assert.match(v1, /border-top: 1px solid #333/);
});
it('falls back to source-preview for markup with component tags', () => {
const res = scaffoldSvelteComponentSession({
id: 'fallb1',
count: 3,
sourceFile: 'src/routes/+page.svelte',
sourceStartLine: 1,
sourceEndLine: 1,
originalLines: ['<Card title={x} />'],
cwd: tmp,
});
assert.equal(res.fallback, 'source-preview');
assert.match(res.reason, /component tag/);
});
it('accept merges CSS instead of appending: superseded rules are replaced, dead branches pruned', () => {
const session = scaffold('acc2');
// Agent authors variant 1: arrows instead of divider borders, one param.
write(tmp, join(session.componentDir, 'v1.svelte'), `<script>
/** @type {{ stages?: Array<Record<string, unknown>> }} */
let { stages = [] } = $props();
</script>
<ol class="pit-board">
{#each stages as stage, i}
<li class="stage">
<span class="label">{stage.label}</span>
<p class="detail">{stage.detail}</p>
</li>
{/each}
</ol>
<style>
.pit-board {
display: flex;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
.stage { padding: calc(var(--p-depth, 6px) + 2px); }
:global([data-p-density="airy"]) .stage { margin: 16px; }
:global([data-p-density="snug"]) .stage { margin: 4px; }
</style>
`);
write(tmp, join(session.componentDir, 'params.json'), JSON.stringify({
1: [
{ id: 'depth', kind: 'range', min: 0, max: 20, step: 1, default: 6, label: 'Depth' },
{ id: 'density', kind: 'steps', default: 'airy', label: 'Density', options: [
{ value: 'airy', label: 'Airy' }, { value: 'snug', label: 'Snug' },
] },
],
}));
const manifest = findSvelteComponentManifest('acc2', tmp);
const result = inlineSvelteComponentAccept(manifest, 1, { depth: 10, density: 'snug' }, tmp);
assert.equal(result.handled, true, result.error);
const out = readFileSync(join(tmp, 'src/routes/+page.svelte'), 'utf-8');
// Loop restored with original expressions, one each block only.
assert.equal(out.split('{#each stages as stage, i}').length - 1, 1);
// Superseded divider border is GONE (replaced, not shadowed).
assert.doesNotMatch(out, /border-top: 1px solid #333/);
assert.match(out, /clip-path/);
// Exactly one .pit-board rule.
assert.equal(out.split('.pit-board {').length - 1, 1);
// Range baked with paren-aware substitution.
assert.match(out, /padding: calc\(10 \+ 2px\)/);
// Steps: chosen branch folded into the .stage rule, other branch dropped,
// no data-p attributes anywhere.
assert.match(out, /margin: 4px/);
assert.equal(out.split(/\.stage \{/).length - 1, 1);
assert.doesNotMatch(out, /margin: 16px/);
assert.doesNotMatch(out, /data-p-/);
assert.doesNotMatch(out, /var\(--p-/);
// The untouched .footer rule survives.
assert.match(out, /\.footer \{ color: gray; \}/);
// Self-check reports clean.
assert.equal(result.verify.clean, true, JSON.stringify(result.verify.findings));
});
it('preserves the variant markup indentation structure', () => {
const session = scaffold('acc3');
write(tmp, join(session.componentDir, 'v1.svelte'), `<script>
let { stages = [] } = $props();
</script>
<ol class="pit-board">
{#each stages as stage, i}
<li class="stage">
<div class="deep">
<span class="label">{stage.label}</span>
</div>
</li>
{/each}
</ol>
<style>
.deep { display: block; }
</style>
`);
const manifest = findSvelteComponentManifest('acc3', tmp);
const result = inlineSvelteComponentAccept(manifest, 1, null, tmp);
assert.equal(result.handled, true, result.error);
const out = readFileSync(join(tmp, 'src/routes/+page.svelte'), 'utf-8');
const lines = out.split('\n');
const deepIdx = lines.findIndex((l) => l.includes('<div class="deep">'));
const labelIdx = lines.findIndex((l) => l.includes('span class="label"'));
const deepIndent = lines[deepIdx].match(/^\s*/)[0].length;
const labelIndent = lines[labelIdx].match(/^\s*/)[0].length;
// Nested structure survives: label sits deeper than its parent div.
assert.equal(labelIndent > deepIndent, true, `expected nesting, got ${deepIndent} vs ${labelIndent}`);
});
it('reindentPreservingStructure keeps relative depth', () => {
const out = reindentPreservingStructure([' <a>', ' <b>', ' </a>'], ' ');
assert.deepEqual(out, [' <a>', ' <b>', ' </a>']);
});
it('mergeCssIntoSvelteSource creates a style block when none exists', () => {
const { text } = mergeCssIntoSvelteSource('<div class="x">hi</div>', '.x { color: red; }');
assert.match(text, /<style>\n\s+\.x \{ color: red; \}\n<\/style>/);
});
it('extractMatchingSourceCss picks only rules that style the selection', () => {
const css = extractMatchingSourceCss(ROUTE_SOURCE, '<ol class="pit-board"><li class="stage">x</li></ol>');
assert.match(css, /\.pit-board/);
assert.match(css, /\.stage/);
assert.doesNotMatch(css, /\.footer/);
});
});
describe('review regressions: preview-truth supersession (the Pitch mangle)', () => {
const PITCH_SOURCE = `<script>
let verdicts = [
{ label: 'True positive', detail: 'Fix it' },
{ label: 'False positive', detail: 'Dismiss it' },
];
</script>
<section class="pitch">
<div class="decisions">
{#each verdicts as verdict}
<div class="cell">
<h3>{verdict.label}</h3>
<p>{verdict.detail}</p>
</div>
{/each}
</div>
</section>
<style>
.pitch { padding: 40px; }
.decisions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.decisions > .cell { border: 1px solid #333; }
@media (max-width: 700px) {
.decisions { grid-template-columns: 1fr; }
.pitch { padding: 16px; }
}
</style>
`;
it('removes seeded rules the variant did not re-declare and orders new base rules before media blocks', () => {
const tmp2 = realpathSync(mkdtempSync(join(tmpdir(), 'impeccable-pitch-mangle-')));
try {
mkdirSync(join(tmp2, 'node_modules'), { recursive: true });
try {
symlinkSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp2, 'node_modules', 'svelte'), 'dir');
} catch {
cpSync(join(REPO_NODE_MODULES, 'svelte'), join(tmp2, 'node_modules', 'svelte'), { recursive: true });
}
write(tmp2, 'package.json', JSON.stringify({ name: 'app' }));
write(tmp2, 'src/lib/Pitch.svelte', PITCH_SOURCE);
// Picked element: the .decisions block (lines 10-17, 1-indexed).
const lines = PITCH_SOURCE.split('\n');
const startLine = lines.findIndex((l) => l.includes('class="decisions"')) + 1;
const endLine = lines.findIndex((l, i) => i >= startLine && l.trim() === '</div>' && lines[i + 1]?.includes('</section>')) + 1;
const originalLines = lines.slice(startLine - 1, endLine);
const session = scaffoldSvelteComponentSession({
id: 'pitchm1',
count: 1,
sourceFile: 'src/lib/Pitch.svelte',
sourceStartLine: startLine,
sourceEndLine: endLine,
originalLines,
cwd: tmp2,
});
assert.equal(session.fallback, undefined, session.reason);
// Seeded selectors recorded for accept-time supersession.
assert.equal(session.manifest.seededSelectors.includes('.decisions'), true);
// The agent's variant: a NEW class, no re-declaration of .decisions.
write(tmp2, join(session.componentDir, 'v1.svelte'), `<script>
let { verdicts = [] } = $props();
</script>
<div class="disposition-board">
{#each verdicts as verdict}
<div class="lane">
<h3>{verdict.label}</h3>
<p>{verdict.detail}</p>
</div>
{/each}
</div>
<style>
.disposition-board { display: flex; flex-direction: column; gap: 8px; }
.disposition-board .lane { border-left: 3px solid #7df; padding: 8px 12px; }
@media (max-width: 700px) {
.disposition-board .lane { padding: 6px 8px; }
}
</style>
`);
const manifest = findSvelteComponentManifest('pitchm1', tmp2);
const result = inlineSvelteComponentAccept(manifest, 1, null, tmp2);
assert.equal(result.handled, true, result.error);
const out = readFileSync(join(tmp2, 'src/lib/Pitch.svelte'), 'utf-8');
// The superseded grid rules are GONE: they never applied in the
// preview the user approved, and the root keeps the old class.
assert.doesNotMatch(out, /grid-template-columns: repeat\(3, 1fr\)/);
assert.doesNotMatch(out, /\.decisions > \.cell/);
assert.equal(result.css.superseded.includes('.decisions'), true);
// The untouched sibling rule survives.
assert.match(out, /\.pitch \{ padding: 40px; \}/);
// Source media block survives for the surviving class...
assert.match(out, /\.pitch \{ padding: 16px; \}/);
// ...and no longer carries the superseded selector.
assert.doesNotMatch(out, /\.decisions \{ grid-template-columns: 1fr; \}/);
// New base rules sit BEFORE the source's @media block (cascade order).
const baseIdx = out.indexOf('.disposition-board {');
const mediaIdx = out.indexOf('@media (max-width: 700px)');
assert.equal(baseIdx > -1 && mediaIdx > -1 && baseIdx < mediaIdx, true,
`expected base rules before media, got base@${baseIdx} media@${mediaIdx}`);
assert.equal(result.verify.clean, true, JSON.stringify(result.verify.findings));
} finally {
rmSync(tmp2, { recursive: true, force: true });
}
});
it('keeps seeded rules the variant re-declares', () => {
const { text, removed } = removeSelectorsFromSvelteSource('<div class="a">x</div>\n<style>\n .a { color: red; }\n .b { color: blue; }\n</style>', new Set(['.b']));
assert.match(text, /\.a \{ color: red; \}/);
assert.doesNotMatch(text, /color: blue/);
assert.deepEqual(removed, ['.b']);
});
});