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,
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 = `
{#each stages as stage, i}
-
{stage.label}
{stage.detail}
{/each}
`;
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
block: lines 10-17 (1-indexed).
const originalLines = ROUTE_SOURCE.split('\n').slice(9, 17);
assert.match(originalLines[0], //);
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: [''],
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'), `
{#each stages as stage, i}
-
{stage.label}
{stage.detail}
{/each}
`);
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'), `
{#each stages as stage, i}
-
{stage.label}
{/each}
`);
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(''));
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(['
', ' ', ' '], ' ');
assert.deepEqual(out, ['
', ' ', ' ']);
});
it('mergeCssIntoSvelteSource creates a style block when none exists', () => {
const { text } = mergeCssIntoSvelteSource('
hi
', '.x { color: red; }');
assert.match(text, /