/**
* Tests for live-accept.mjs — the deterministic accept/discard helper.
* Run with: node --test tests/live-accept.test.mjs
*/
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { execFileSync, spawnSync } from 'node:child_process';
import { scaffoldSourceArtifactSession } from '../skill/scripts/live/source-artifact.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ACCEPT = resolve(__dirname, '..', 'skill/scripts/live-accept.mjs');
const WRAP = resolve(__dirname, '..', 'skill/scripts/live-wrap.mjs');
function runAccept(cwd, args) {
try {
const out = execFileSync('node', [ACCEPT, ...args], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return JSON.parse(out.trim());
} catch (err) {
const body = err.stdout?.toString().trim() || err.stderr?.toString().trim() || '';
return JSON.parse(body || '{}');
}
}
describe('live-accept — session id validation', () => {
let tmp;
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-accept-id-')); });
afterEach(() => rmSync(tmp, { recursive: true, force: true }));
// --id becomes a path segment for the accept receipt. Traversal here wrote
// JSON to arbitrary absolute paths (e.g. `--id ../../../../etc/evil`).
for (const id of ['../../../../etc/evil', 'a/b', '..', 'a\\b', '']) {
it(`refuses --id ${JSON.stringify(id)} without writing a receipt`, () => {
const res = spawnSync('node', [ACCEPT, '--id', id, '--discard'], {
cwd: tmp,
encoding: 'utf-8',
});
assert.equal(res.status, 1, 'must exit non-zero');
assert.match(res.stderr, /Invalid --id|Missing --id/);
assert.equal(existsSync(join(tmp, '.impeccable', 'live', 'accept-receipts')), false);
});
}
it('still accepts a well-formed id', () => {
const res = spawnSync('node', [ACCEPT, '--id', 'ab12cd34', '--discard'], {
cwd: tmp,
encoding: 'utf-8',
});
assert.doesNotMatch(res.stderr || '', /Invalid --id/);
});
// --variant is interpolated into a RegExp and into the markup written back to
// source. `.*` matched the `original` block first, so the CLI reported a
// successful accept while actually restoring the original.
for (const variant of ['.*', '[12]', 'original', '1e2', '']) {
it(`refuses --variant ${JSON.stringify(variant)} rather than matching by regex`, () => {
writeFileSync(join(tmp, 'page.html'), [
'',
'
ORIGINAL CONTENT
',
'VARIANT ONE
',
'',
'',
].join('\n'));
const res = spawnSync('node', [ACCEPT, '--id', 'ab12cd34', '--variant', variant], {
cwd: tmp,
encoding: 'utf-8',
});
assert.equal(res.status, 1);
assert.match(res.stderr, /Invalid --variant|Need --discard/);
assert.match(
readFileSync(join(tmp, 'page.html'), 'utf-8'),
/impeccable-variants-start/,
'a rejected variant must leave the wrapper untouched',
);
});
}
});
describe('live-accept — isolated source artifacts', () => {
let tmp;
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-accept-isolated-')); });
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
function scaffold(id) {
const original = '\n \n \n';
writeFileSync(join(tmp, 'page.html'), original);
const session = scaffoldSourceArtifactSession({
id,
count: 2,
sourceFile: 'page.html',
sourceStartLine: 2,
sourceEndLine: 2,
originalSource: '',
previewContent: `
`,
cwd: tmp,
});
return { original, session };
}
it('accepts one preview into true source exactly once', () => {
const { session } = scaffold('isolatedaccept');
const result = runAccept(tmp, ['--id', 'isolatedaccept', '--variant', '2']);
assert.equal(result.handled, true, JSON.stringify(result));
const source = readFileSync(join(tmp, 'page.html'), 'utf-8');
assert.match(source, /Accepted two/);
assert.doesNotMatch(source, /Accepted one|data-impeccable-variant/);
assert.equal(existsSync(join(tmp, session.sessionDir)), false);
});
it('discards the preview instantly without touching true source', () => {
const { original, session } = scaffold('isolateddiscard');
const result = runAccept(tmp, ['--id', 'isolateddiscard', '--discard']);
assert.equal(result.handled, true, JSON.stringify(result));
assert.equal(readFileSync(join(tmp, 'page.html'), 'utf-8'), original);
assert.equal(existsSync(join(tmp, session.sessionDir)), false);
});
});
describe('live-accept — style-element edge cases', () => {
let tmp;
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-accept-test-')); });
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
// Historical bug: extractVariant flipped into "inStyle" mode on block should also be treated as a
// single skipped unit; the line has both open and close tags.
it('finds the accepted variant after a single-line block', () => {
const html = `
`;
writeFileSync(join(tmp, 'page.html'), html);
const result = runAccept(tmp, ['--id', 'ONELINE', '--variant', '3']);
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
assert.ok(after.includes('data-impeccable-variant="3"'), 'accepted wrapper for variant 3 present');
assert.ok(after.includes('variant three'), 'variant 3 content kept');
assert.ok(!after.includes('variant two'), 'other variant content dropped');
});
// Baseline: the standard multi-line case must keep working.
it('finds the accepted variant after a multi-line block (regression baseline)', () => {
const html = `
`;
writeFileSync(join(tmp, 'page.html'), html);
const result = runAccept(tmp, ['--id', 'MULTI', '--variant', '1']);
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
assert.ok(after.includes('data-impeccable-variant="1"'), 'accepted wrapper for variant 1 present');
assert.ok(after.includes('variant one'), 'variant 1 content kept');
});
// Regression: the agent writes JSX and live-accept's
// extractCss used to capture the `{` … `` ` ``}` template-literal punctuation
// as CSS content. handleAccept then re-wrapped with another `{` …
// `` ` ``}`, producing nested template literals (`\n` +
` \n` +
` \n` +
` \n` +
` {/* impeccable-variants-end TPL */}\n` +
` >\n` +
` \n` +
` );\n` +
`}\n`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
const result = runAccept(tmp, ['--id', 'TPL', '--variant', '1']);
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
// Exactly one `{` opener after the carbonized \n` +
` \n` +
` \n` +
` \n` +
` {/* impeccable-variants-end INLINE */}\n` +
` >\n` +
` \n` +
` );\n` +
`}\n`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
const result = runAccept(tmp, ['--id', 'INLINE', '--variant', '1']);
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
const inner = after.match(/
{/* impeccable-variants-end ROOTIND */}
`;
writeFileSync(join(tmp, 'Root.tsx'), tsx);
const result = runAccept(tmp, ['--id', 'ROOTIND', '--variant', '1']);
assert.equal(result.handled, true, `accept should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'Root.tsx'), 'utf-8');
assert.match(
after,
/\n
\n nested text<\/span>\n <\/section>\n <\/div>/,
'column-0 JSX accept preserves relative indentation inside the carbonize variant wrapper',
);
});
// Cursor Bugbot regression (PR #118 review): the JSX wrapper places
// marker comments INSIDE the outer , so block.start sits 2 spaces
// deeper than the original element. Using block.start as the deindent
// base on JSX accept/discard pushes every restored line 2 spaces too far
// right. The fix anchors the indent on `replaceRange.start` (the outer
// wrapper line), which is at the original element's indent level for
// both HTML and JSX.
it('discard restores JSX content at the original indent (no 2-space drift from marker-inside layout)', () => {
// Run the real wrap CLI so we exercise the JSX-marker-inside-wrapper
// layout end to end, not a hand-rolled approximation.
const tsx = `export default function App() {
return (
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
execFileSync('node', [WRAP, '--id', 'INDENTDISC', '--count', '3', '--classes', 'card', '--tag', 'aside', '--file', join(tmp, 'App.tsx')], {
cwd: tmp,
encoding: 'utf-8',
});
runAccept(tmp, ['--id', 'INDENTDISC', '--discard']);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
// The aside opener should land at exactly 6 spaces — same as the
// original — and the
child at 8 (preserved relative depth).
// The earlier 6/6/6 collapse was caused by `originalLines.map(l =>
// indent + ' ' + l.trimStart())` in live-wrap stripping ALL
// leading whitespace before reindenting; the fix strips only the
// COMMON minimum so the relative structure is preserved.
assert.match(after, /^ $/m,
` opener must be at 6-space indent (was 8 before outer-indent fix), got:\n${after}`);
assert.match(after, /^ Hero<\/h1>$/m,
` child must be at 8-space indent — relative depth preserved through wrap+discard. Got:\n${after}`);
assert.match(after, /^ <\/aside>$/m,
` closer must be back at 6-space indent. Got:\n${after}`);
});
it('expandReplaceRange handles multi-line self-closing
inside the wrapped element', () => {
// Cursor Bugbot regression: per-line depth tracking in
// `expandReplaceRange` couldn't see across line boundaries, so a
// multi-line self-closing JSX `
` got
// counted as +1 with no compensating -1. The wrapper's outer
// never matched the depth-zero condition; replace-range stopped at
// block.end (the marker comment), leaving the wrapper's outer
// orphaned in the file after accept/discard — and worse, an
// unrelated right after the wrapper got
// its own
mis-counted as the wrapper close.
const tsx = `export default function App() {
return (
After
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
execFileSync('node', [WRAP, '--id', 'MULTILINESC', '--count', '3', '--classes', 'card', '--tag', 'aside', '--file', join(tmp, 'App.tsx')], {
cwd: tmp,
encoding: 'utf-8',
});
const result = runAccept(tmp, ['--id', 'MULTILINESC', '--discard']);
assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
// The wrapper scaffold must be fully gone — no orphan from
// the outer wrapper, and no impeccable markers/data attributes.
assert.ok(!after.includes('data-impeccable-variants'),
`outer wrapper div must be fully removed; got:\n${after}`);
assert.ok(!after.includes('data-impeccable-variant'),
`original-div wrapper must be fully removed; got:\n${after}`);
assert.ok(!after.includes('impeccable-variants-start'),
`start marker must be removed; got:\n${after}`);
// The unrelated After
sibling
// must survive intact — Bugbot's worst-case scenario was the depth
// walk eating its as the wrapper close.
assert.ok(after.includes('After
'),
`unrelated next-card sibling must be preserved; got:\n${after}`);
// The multi-line self-closing div inside the original element must
// survive too.
assert.match(after, /
/m,
`multi-line self-closing
inside original must survive; got:\n${after}`);
});
it('expandReplaceRange finds JSX wrapper openers with long multi-line attributes', () => {
const extraAttrs = Array.from({ length: 18 }, (_, i) => ` data-extra-${i}="x"`).join('\n');
const tsx = `export default function App() {
return (
{/* impeccable-variants-start LONGOPEN */}
{/* Original */}
{/* Variants: insert below this line */}
{/* impeccable-variants-end LONGOPEN */}
After
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
const result = runAccept(tmp, ['--id', 'LONGOPEN', '--discard']);
assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
assert.doesNotMatch(after, /data-impeccable-variants/);
assert.doesNotMatch(after, /impeccable-variants-start/);
assert.match(after, /\s*Original<\/h1>\s*<\/aside>/m);
assert.ok(after.includes(' After
'));
});
it('expandReplaceRange ignores unrelated prior end markers while finding the current JSX wrapper', () => {
const tsx = `export default function App() {
return (
{/* impeccable-variants-end OLD */}
{/* impeccable-variants-start ACTIVE */}
{/* Original */}
{/* Variants: insert below this line */}
{/* impeccable-variants-end ACTIVE */}
After
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
const result = runAccept(tmp, ['--id', 'ACTIVE', '--discard']);
assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
assert.doesNotMatch(after, /data-impeccable-variants/);
assert.doesNotMatch(after, /impeccable-variants-start ACTIVE/);
assert.doesNotMatch(after, /impeccable-variants-end OLD/);
assert.match(after, /\s*Original<\/h1>\s*<\/aside>/m);
assert.ok(after.includes(' After
'));
});
it('accept (no carbonize, raw HTML) restores at the original indent on JSX', () => {
// Manually craft a wrapped file in the JSX-marker-inside layout — this
// mirrors what wrap produces, but lets us exercise accept's indent
// logic without a full live cycle.
const tsx = `export default function App() {
return (
{/* impeccable-variants-start INDENTACC */}
{/* Original */}
{/* Variants: insert below this line */}
{/* impeccable-variants-end INDENTACC */}
);
}`;
writeFileSync(join(tmp, 'App.tsx'), tsx);
runAccept(tmp, ['--id', 'INDENTACC', '--variant', '1']);
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
// The accepted aside (variant-one) should land at 6-space indent, the
// same place the wrapper sat — not 2 spaces deeper.
assert.match(after, /^
/m,
`accepted must land at 6-space indent (the wrapper's level), got:\n${after}`);
});
// Discard must restore the original element after a self-closing ,
// proving extractOriginal also survives the style pattern.
it('discard restores the original element after a JSX self-closing ', () => {
const html = `
`;
writeFileSync(join(tmp, 'page.html'), html);
const result = runAccept(tmp, ['--id', 'DISC', '--discard']);
assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`);
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
assert.ok(after.includes('ORIGINAL CONTENT'), 'original restored');
assert.ok(!after.includes('impeccable-variants-start'), 'wrapper markers gone');
assert.ok(!after.includes('variant one'), 'variants dropped');
});
});
describe('live-accept — insert sessions', () => {
let tmp;
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-accept-insert-')); });
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
const insertHtml = (id) => `
`;
it('discard removes an insert wrapper without touching anchor sections', () => {
writeFileSync(join(tmp, 'page.html'), insertHtml('insaaa01'));
const result = runAccept(tmp, ['--id', 'insaaa01', '--discard']);
assert.equal(result.handled, true, JSON.stringify(result));
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
assert.ok(after.includes('Hero block'));
assert.ok(after.includes('Footer'));
assert.ok(!after.includes('impeccable-variants-start'));
assert.ok(!after.includes('Variant one'));
});
it('accept keeps the chosen insert variant and drops the wrapper', () => {
writeFileSync(join(tmp, 'page.html'), insertHtml('insbbb02'));
const result = runAccept(tmp, ['--id', 'insbbb02', '--variant', '2']);
assert.equal(result.handled, true, JSON.stringify(result));
const after = readFileSync(join(tmp, 'page.html'), 'utf-8');
assert.ok(after.includes('Variant two'));
assert.ok(!after.includes('Variant one'));
assert.ok(!after.includes('impeccable-variants-start'));
assert.ok(after.includes('Hero block'));
assert.ok(after.includes('Footer'));
});
});