Live: stop the preflight writing source, cache the resolution

The polling-rework preflight wrote the variant scaffold into source during the
poll lease, before the agent acted. On source-preview targets (React/Vue/Vite,
everything but the svelte-component path) that write full-reloaded the
framework; a browser caught mid-reload missed the agent's variant write and the
SSE done, and sat stranded at 0/N.

Restore the 3.5 single-atomic-edit semantics: the preflight still resolves the
element location and computes the scaffold, but --defer-source-write leaves
source untouched and hands the agent the wrapper text plus the picked source
range. The agent splices variants into the wrapper and replaces the range in
one write, so the framework reloads exactly once. The svelte-component path is
untouched (it never writes route source). The missed-completion recovery stays
as defense in depth.

Also cache the resolved source file per target signature (locator + route):
the ~7.6s tree search re-ran on every generate for the same element; a hit now
points the helper straight at the file via --file, invalidated when the target
changes or a resolution fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-22 22:49:44 -07:00
co-authored by Claude Fable 5
parent 4cd5ea7547
commit dbe0c12b91
8 changed files with 314 additions and 33 deletions
+5 -3
View File
@@ -116,7 +116,7 @@ node {{scripts_path}}/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --positi
- `--position``event.insert.position` (`before` | `after`)
- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text)
The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Decide the visitor mode from the surface and load [craft-floor.md](craft-floor.md) before writing net-new markup (freeform only, no action sub-command). Deliver using the harness policy, then `--reply done`.
The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. On source-preview targets the scaffold carries `sourceWritten: false` with `wrapperBlock`, `replaceStartLine`, and `replaceEndLine` (here `replaceEndLine < replaceStartLine`, an insertion): splice your variants into `wrapperBlock` at the marker and insert the result at `replaceStartLine` in one edit, exactly as the wrap section describes, so the framework reloads once. Decide the visitor mode from the surface and load [craft-floor.md](craft-floor.md) before writing net-new markup (freeform only, no action sub-command). Deliver using the harness policy, then `--reply done`.
For Svelte/SvelteKit targets, `live-insert.mjs` returns `previewMode: "svelte-component"` with `mode: "insert"`, `file` pointing at a temporary `node_modules/.impeccable-live/<id>/manifest.json`, `componentDir` pointing at the variant component files, and `sourceFile` pointing at the real `.svelte` route. Write each inserted variant as a real Svelte component (`v1.svelte`, `v2.svelte`, …) under `componentDir`. Insert variants must be non-empty net-new content with a single top-level root, no `data-impeccable-*` attributes, and CSS in each component's `<style>` block. Do **not** edit the route source during generation; the browser mounts the temporary component before/after the live anchor while the user cycles variants. On Accept, `live-accept.mjs` inserts the selected component markup into `sourceFile` immediately and deletes the temp session after the source write succeeds.
@@ -141,7 +141,9 @@ Reading annotations precisely:
### 2. Wrap the element
When `event.scaffold` is present, the local helper already found and wrapped the source before the poll returned. Treat `event.scaffold` as the successful helper output and skip this command entirely. `event.scaffoldAttempted` with `scaffoldError` means local preflight could not finish; use the command/fallback path below. This optimization removes a deterministic tool round trip without changing the generated design.
When `event.scaffold` is present, the local helper already found the source and computed the wrapper before the poll returned. Treat `event.scaffold` as the successful helper output and skip this command entirely. `event.scaffoldAttempted` with `scaffoldError` means local preflight could not finish; use the command/fallback path below. This optimization removes a deterministic tool round trip without changing the generated design.
**On source-preview targets `event.scaffold` carries `sourceWritten: false`.** The helper did NOT write the wrapper into source; it hands you the wrapper as `scaffold.wrapperBlock` plus the picked element's source range (`scaffold.replaceStartLine`, `scaffold.replaceEndLine`, 1-indexed). Write the wrapper **and** all variants in ONE edit: splice your variants into `wrapperBlock` at the "Variants: insert below this line" marker, then replace source lines `[replaceStartLine, replaceEndLine]` with the result. A separate scaffold write reloads the framework before your variant write lands, and a browser caught mid-reload misses the `done` and sits at 0/N; the single edit avoids it. (`replaceEndLine < replaceStartLine` means insert mode: insert `wrapperBlock`, remove nothing.) The `svelte-component` path never sets `sourceWritten`; it follows the component-preview flow below unchanged.
```bash
node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
@@ -158,7 +160,7 @@ The helper searches ID first, then classes, then tag + class combo. If `event.pa
If `--text` matches multiple candidates equally well, wrap exits with `{ error: "element_ambiguous", candidates: [...] }` and `fallback: "agent-driven"`: read the candidate line ranges, decide which one matches the picked element from page context, and write the wrapper manually per the fallback flow.
Output on success: `{ file, insertLine, commentSyntax, styleMode, styleTag, cssSelectorPrefixExamples, cssAuthoring }`.
Output on success: `{ file, insertLine, commentSyntax, styleMode, styleTag, cssSelectorPrefixExamples, cssAuthoring }`. On source-preview targets it also returns `sourceWritten: false`, `wrapperBlock`, `replaceStartLine`, and `replaceEndLine` (write it yourself per the `event.scaffold` note above). When you run this command directly (no preflight scaffold), it writes the wrapper into source itself, so there is no `wrapperBlock` and you splice variants at `insertLine`.
For Svelte/SvelteKit targets, `live-wrap.mjs` returns `previewMode: "svelte-component"` with `file` pointing at a temporary `node_modules/.impeccable-live/<id>/manifest.json`, `componentDir` pointing at the variant component files, and `sourceFile` pointing at the real `.svelte` route. Write each variant as a real Svelte component (`v1.svelte`, `v2.svelte`, …) under `componentDir`; use the `propContract` prop names for dynamic text (`{propName}`), not literal snapshot strings. Put variant CSS in each component's `<style>` block with semantic class selectors (no `@scope`, no `data-impeccable-*`). Reply with `--file` set to the manifest path; the browser dynamically imports and mounts the compiled components so Svelte HMR does not reset page state while the user cycles variants. On Accept, `live-accept.mjs` inlines the accepted component back into `sourceFile` immediately after source promotion succeeds.
+24 -6
View File
@@ -131,6 +131,9 @@ Output (JSON):
const query = argVal(args, '--query');
const filePath = argVal(args, '--file');
const text = argVal(args, '--text');
// See live-wrap.mjs: preflight computes the scaffold but leaves source
// untouched so the agent's single edit is the only framework reload.
const deferSourceWrite = args.includes('--defer-source-write');
if (!id) { console.error('Missing --id'); process.exit(1); }
if (!position) { console.error('Missing --position (before | after)'); process.exit(1); }
@@ -244,12 +247,23 @@ Output (JSON):
isJsx,
});
const newLines = [
...lines.slice(0, spliceIndex),
...wrapperLines,
...lines.slice(spliceIndex),
];
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
let deferredWrapper = null;
if (deferSourceWrite) {
// Insert-as-empty-range: the agent inserts `wrapperBlock` (variants spliced
// at the marker) at spliceIndex without removing any source line.
deferredWrapper = {
block: wrapperLines.join('\n'),
replaceStartLine: spliceIndex + 1,
replaceEndLine: spliceIndex, // empty range (endLine < startLine) => insertion
};
} else {
const newLines = [
...lines.slice(0, spliceIndex),
...wrapperLines,
...lines.slice(spliceIndex),
];
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
}
const insertLine = spliceIndex + 3;
@@ -257,6 +271,10 @@ Output (JSON):
mode: 'insert',
position,
file: relTargetFile,
sourceWritten: deferredWrapper ? false : undefined,
wrapperBlock: deferredWrapper ? deferredWrapper.block : undefined,
replaceStartLine: deferredWrapper ? deferredWrapper.replaceStartLine : undefined,
replaceEndLine: deferredWrapper ? deferredWrapper.replaceEndLine : undefined,
insertLine: insertLine + 1,
commentSyntax,
styleMode: styleMode.mode,
+6 -5
View File
@@ -235,11 +235,12 @@ function recordAgentPhase(id, phase, details = {}) {
/**
* Detect a browser that missed the generation `done` broadcast.
*
* The preflight scaffold write triggers a framework full-reload (Astro reloads
* the page for any .astro edit). If the agent's variant write + `done` land
* while the browser is mid-reload, the new page misses both the second HMR
* reload and the SSE `done` — it resumes from the scaffold-only source and
* sits in GENERATING at 0/N forever. That resumed page always checkpoints
* The preflight no longer writes the scaffold into source for source-preview
* targets (the agent writes wrapper + variants in one atomic edit), so the old
* scaffold-write full-reload that opened the "stranded at 0/N" race is gone.
* This recovery stays as defense in depth: any framework reload that drops the
* agent's variant write + `done` while the browser is mid-reload leaves the new
* page in GENERATING at 0/N. That resumed page always checkpoints
* (`browser_resumed`), so a checkpoint claiming "still generating, variants
* missing" for a session whose generation already completed is direct
* evidence of the miss. Rebuild the `done` payload from the snapshot so the
+29
View File
@@ -68,6 +68,13 @@ The agent should insert variant HTML at insertLine.`);
const filePath = argVal(args, '--file');
const text = argVal(args, '--text');
const pageUrl = argVal(args, '--page-url');
// Preflight passes this for source-preview targets. It computes the scaffold
// (element location + wrapper text) but does NOT write it into source. The
// agent then writes the wrapper + all variants in one atomic edit. The
// premature server-side write full-reloaded the framework mid-generate and
// stranded the browser at 0/N (live-server.mjs missed-completion note). It is
// a no-op on the svelte-component path, which never writes the route source.
const deferSourceWrite = args.includes('--defer-source-write');
if (!id) { console.error('Missing --id'); process.exit(1); }
if (!elementId && !classes && !query) {
@@ -334,6 +341,7 @@ The agent should insert variant HTML at insertLine.`);
let outputEndLine = startLine + wrapperLines.length + (originalLines.length - 1);
let insertLine;
let svelteSession = null;
let deferredWrapper = null;
if (useSvelteComponent) {
// Svelte/SvelteKit resets component-local state on markup HMR updates.
@@ -353,6 +361,20 @@ The agent should insert variant HTML at insertLine.`);
outputStartLine = 1;
outputEndLine = 1;
insertLine = 1;
} else if (deferSourceWrite) {
// Deferred source write: compute the scaffold text but leave source
// untouched. The agent replaces the picked element's source range with
// `wrapperBlock` (variants spliced at the marker) in one edit. Writing the
// scaffold here first would reload the framework before the agent's write
// lands, and a browser caught mid-reload misses the `done` and sits at 0/N.
deferredWrapper = {
block: wrapperLines.join('\n'),
replaceStartLine: startLine + 1, // 1-indexed picked-element range the
replaceEndLine: endLine + 1, // agent's wrapper block replaces
};
// insertLine matches the final file position the wrapper occupies once the
// agent replaces the picked range, so downstream consumers stay consistent.
insertLine = startLine + 6 + (originalLines.length - 1) + 1;
} else {
// Replace the original element with the wrapper
const newLines = [
@@ -383,6 +405,13 @@ The agent should insert variant HTML at insertLine.`);
file: outputRelFile,
sourceFile: useFrameworkComponent ? relTargetFile : undefined,
previewMode,
// Deferred source write: the wrapper is NOT yet in source. The agent
// replaces [replaceStartLine, replaceEndLine] with `wrapperBlock` (variants
// spliced at the "insert below this line" marker) in one atomic edit.
sourceWritten: deferredWrapper ? false : undefined,
wrapperBlock: deferredWrapper ? deferredWrapper.block : undefined,
replaceStartLine: deferredWrapper ? deferredWrapper.replaceStartLine : undefined,
replaceEndLine: deferredWrapper ? deferredWrapper.replaceEndLine : undefined,
componentDir: componentSession?.componentDir,
propContract: componentSession?.propContract,
sourceStartLine: useFrameworkComponent ? startLine + 1 : undefined,
+51 -4
View File
@@ -5,7 +5,32 @@ import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const PREFLIGHT_TIMEOUT_MS = 15_000;
export function buildGenerationPreflight(event, scriptsDir) {
// Per-target cache of the resolved source file. The wrap search walks the whole
// project tree and was measured at ~7.6s on a large repo; it re-ran on every
// generate for the same picked element (re-rolls, param passes). Keyed by the
// target signature (locator + route), so it invalidates automatically when the
// element or route changes; a failed resolution evicts its entry (see below).
const sourceResolutionCache = new Map();
/** Test/lifecycle hook: drop all cached source resolutions. */
export function clearSourceResolutionCache() {
sourceResolutionCache.clear();
}
function targetSignature(event) {
const isInsert = event.mode === 'insert';
const target = isInsert ? insertTarget(event) : replaceTarget(event);
return JSON.stringify({
mode: isInsert ? 'insert' : 'replace',
position: isInsert ? target.position : null,
elementId: target.elementId || null,
classes: target.classes || null,
tag: target.tag || null,
pageUrl: event.pageUrl || null,
});
}
export function buildGenerationPreflight(event, scriptsDir, { cache = null } = {}) {
if (!event || event.type !== 'generate' || !event.id) return null;
const isInsert = event.mode === 'insert';
@@ -14,13 +39,24 @@ export function buildGenerationPreflight(event, scriptsDir) {
const script = path.join(scriptsDir, isInsert ? 'live-insert.mjs' : 'live-wrap.mjs');
const args = [script, '--id', event.id, '--count', String(event.count || 3)];
// Compute the scaffold but do not write it into source for source-preview
// targets. The agent writes wrapper + variants atomically; a premature
// server-side write reloads the framework and strands the browser at 0/N.
// No-op on the svelte-component path, which never writes the route source.
args.push('--defer-source-write');
if (isInsert) args.push('--position', target.position);
if (target.elementId) args.push('--element-id', target.elementId);
if (target.classes) args.push('--classes', target.classes);
if (target.tag) args.push('--tag', target.tag);
if (target.text) args.push('--text', target.text);
if (!isInsert && event.pageUrl) args.push('--page-url', event.pageUrl);
return { script, args, mode: isInsert ? 'insert' : 'replace' };
const signature = targetSignature(event);
// A cached resolution points the helper straight at the file, skipping the
// tree search. The helper still reads current content, so line ranges stay
// fresh; only discovery is cached.
const cachedFile = cache ? cache.get(signature) : null;
if (cachedFile) args.push('--file', cachedFile);
return { script, args, mode: isInsert ? 'insert' : 'replace', signature };
}
/**
@@ -38,8 +74,9 @@ export async function runGenerationPreflight(event, {
scriptsDir,
execFileImpl = execFileAsync,
timeoutMs = PREFLIGHT_TIMEOUT_MS,
cache = sourceResolutionCache,
} = {}) {
const command = buildGenerationPreflight(event, scriptsDir);
const command = buildGenerationPreflight(event, scriptsDir, { cache });
if (!command) {
return { ok: false, skipped: true, reason: 'insufficient_locator' };
}
@@ -53,13 +90,23 @@ export async function runGenerationPreflight(event, {
});
const line = String(stdout).trim().split('\n').filter(Boolean).pop();
if (!line) throw new Error('preflight returned no scaffold metadata');
const scaffold = JSON.parse(line);
// Cache the resolved SOURCE file (route source, not the svelte manifest) so
// the next generate on this target skips the tree search.
const resolvedSource = scaffold.sourceFile || scaffold.file;
if (cache && command.signature && typeof resolvedSource === 'string') {
cache.set(command.signature, resolvedSource);
}
return {
ok: true,
mode: command.mode,
durationMs: performance.now() - startedAt,
scaffold: JSON.parse(line),
scaffold,
};
} catch (error) {
// Evict a stale/failed resolution so the next attempt does a full search
// (the element may have moved out of the previously cached file).
if (cache && command.signature) cache.delete(command.signature);
return {
ok: false,
mode: command.mode,
+61 -14
View File
@@ -1292,14 +1292,12 @@ function renderVariantsBlock({ sessionId, indent, output, commentSyntax, file, s
}
/**
* Read the wrapped file, find the "insert below this line" marker, splice in
* the rendered variants block, write back.
* Splice the rendered variants block into an array of wrapper lines at the
* "insert below this line" marker. Pure: returns the new lines array. Used
* both against a whole file (wrapper already in source) and against a
* standalone wrapper block (deferred source write, agent writes it now).
*/
async function spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId, output }) {
const filePath = path.join(tmp, wrapInfo.file);
const src = await fs.readFile(filePath, 'utf-8');
const lines = src.split('\n');
function spliceVariantsIntoLines(lines, { sessionId, output, commentSyntax, file, styleMode }) {
// Find the "Variants: insert below this line" comment line — definitive
// marker, robust to any indentation off-by-one. Matches in any comment
// style (HTML / JSX / Astro).
@@ -1307,7 +1305,7 @@ async function spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId, output }) {
l.includes('Variants: insert below this line'),
);
if (markerIdx === -1) {
throw new Error('insert marker not found in ' + wrapInfo.file);
throw new Error('insert marker not found in ' + file);
}
const indent = (lines[markerIdx].match(/^\s*/) || [''])[0];
@@ -1320,26 +1318,73 @@ async function spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId, output }) {
sessionId,
indent: wrapperIndent,
output,
commentSyntax: wrapInfo.commentSyntax,
file: wrapInfo.file,
styleMode: wrapInfo.styleMode,
commentSyntax,
file,
styleMode,
});
const endMarkerIdx = lines.findIndex((line, index) =>
index > markerIdx && line.includes('impeccable-variants-end ' + sessionId),
);
if (endMarkerIdx === -1) {
throw new Error('end marker not found in ' + wrapInfo.file);
throw new Error('end marker not found in ' + file);
}
const tailIdx = wrapInfo.commentSyntax.open === '{/*'
const tailIdx = commentSyntax.open === '{/*'
? endMarkerIdx
: endMarkerIdx - 1;
const next = [
return [
...lines.slice(0, markerIdx + 1),
block,
...lines.slice(tailIdx),
];
}
/**
* Read the wrapped file, find the "insert below this line" marker, splice in
* the rendered variants block, write back. Used when the wrapper is already
* present in source (agent's own wrap fallback, no preflight).
*/
async function spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId, output }) {
const filePath = path.join(tmp, wrapInfo.file);
const src = await fs.readFile(filePath, 'utf-8');
const lines = src.split('\n');
const next = spliceVariantsIntoLines(lines, {
sessionId,
output,
commentSyntax: wrapInfo.commentSyntax,
file: wrapInfo.file,
styleMode: wrapInfo.styleMode,
});
await fs.writeFile(filePath, next.join('\n'), 'utf-8');
}
/**
* Deferred source write (preflight computed the scaffold but left source
* untouched). Splice the variants into the scaffold's `wrapperBlock`, then
* replace the picked element's source range with the result in ONE write
* the 3.5 atomic single-edit semantics. `replaceEndLine < replaceStartLine`
* expresses a pure insertion (insert mode).
*/
async function writeDeferredWrapperWithVariants({ tmp, wrapInfo, sessionId, output }) {
const filePath = path.join(tmp, wrapInfo.file);
const src = await fs.readFile(filePath, 'utf-8');
const lines = src.split('\n');
const wrapperLines = String(wrapInfo.wrapperBlock).split('\n');
const splicedWrapper = spliceVariantsIntoLines(wrapperLines, {
sessionId,
output,
commentSyntax: wrapInfo.commentSyntax,
file: wrapInfo.file,
styleMode: wrapInfo.styleMode,
});
const startIdx = wrapInfo.replaceStartLine - 1;
const endIdx = wrapInfo.replaceEndLine - 1; // may be startIdx-1 for insertion
const next = [
...lines.slice(0, startIdx),
...splicedWrapper,
...lines.slice(endIdx + 1),
];
await fs.writeFile(filePath, next.join('\n'), 'utf-8');
}
@@ -1661,6 +1706,8 @@ export async function runAgentLoop({
trace('agent.write.start', { id: event.id, file: wrapInfo.file });
if (wrapInfo.previewMode === 'svelte-component') {
await writeSvelteComponentVariants({ tmp, wrapInfo, event, output, writeParams: true });
} else if (wrapInfo.sourceWritten === false) {
await writeDeferredWrapperWithVariants({ tmp, wrapInfo, sessionId: event.id, output });
} else {
await spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId: event.id, output });
}
+101 -1
View File
@@ -5,6 +5,7 @@ import path from 'node:path';
import {
buildGenerationPreflight,
runGenerationPreflight,
clearSourceResolutionCache,
} from '../skill/scripts/live/generation-preflight.mjs';
const SCRIPTS_DIR = path.resolve('skill/scripts');
@@ -26,6 +27,7 @@ test('builds a replace preflight from the picker locator', () => {
assert.equal(command.mode, 'replace');
assert.deepEqual(command.args.slice(1), [
'--id', 'session-1', '--count', '3',
'--defer-source-write',
'--element-id', 'hero',
'--classes', 'hero hero--dark',
'--tag', 'SECTION',
@@ -48,11 +50,22 @@ test('builds an insert preflight from the anchor locator', () => {
assert.equal(command.mode, 'insert');
assert.deepEqual(command.args.slice(1), [
'--id', 'session-2', '--count', '2', '--position', 'before',
'--id', 'session-2', '--count', '2',
'--defer-source-write', '--position', 'before',
'--classes', 'card', '--tag', 'ARTICLE', '--text', 'Plan',
]);
});
test('replace preflight always requests a deferred source write', () => {
const command = buildGenerationPreflight({
type: 'generate',
id: 'session-defer',
count: 3,
element: { classes: ['hero'] },
}, SCRIPTS_DIR);
assert.ok(command.args.includes('--defer-source-write'));
});
test('returns scaffold metadata without exposing child-process details', async () => {
const calls = [];
const result = await runGenerationPreflight({
@@ -108,6 +121,93 @@ test('yields to the event loop instead of blocking on the child process', async
assert.equal(tickedDuringPreflight, true, 'the event loop must stay responsive during preflight');
});
test('caches the resolved source file and reuses it via --file on the next generate', async () => {
clearSourceResolutionCache();
const cache = new Map();
const event = {
type: 'generate',
id: 'sess-a',
count: 3,
pageUrl: '/pricing',
element: { classes: ['hero'], tagName: 'SECTION' },
};
const firstArgs = [];
const first = await runGenerationPreflight(event, {
scriptsDir: SCRIPTS_DIR,
cache,
async execFileImpl(_file, args) {
firstArgs.push(...args);
return { stdout: '{"file":"src/Pricing.jsx","sourceWritten":false}\n', stderr: '' };
},
});
assert.equal(first.ok, true);
assert.ok(!firstArgs.includes('--file'), 'first pass does the tree search, no --file');
// Second generate on the SAME target (new session id) should point --file at
// the cached resolution and skip the search.
const secondArgs = [];
const second = await runGenerationPreflight({ ...event, id: 'sess-b' }, {
scriptsDir: SCRIPTS_DIR,
cache,
async execFileImpl(_file, args) {
secondArgs.push(...args);
return { stdout: '{"file":"src/Pricing.jsx","sourceWritten":false}\n', stderr: '' };
},
});
assert.equal(second.ok, true);
const fileIdx = secondArgs.indexOf('--file');
assert.notEqual(fileIdx, -1, 'cached resolution injects --file');
assert.equal(secondArgs[fileIdx + 1], 'src/Pricing.jsx');
});
test('evicts the cached resolution when the preflight fails', async () => {
const cache = new Map();
const event = {
type: 'generate',
id: 'sess-c',
count: 3,
pageUrl: '/pricing',
element: { classes: ['hero'] },
};
await runGenerationPreflight(event, {
scriptsDir: SCRIPTS_DIR,
cache,
async execFileImpl() { return { stdout: '{"file":"src/Pricing.jsx"}\n', stderr: '' }; },
});
assert.equal(cache.size, 1);
const error = new Error('spawn failed');
error.stderr = 'live-wrap.mjs: element not found\n';
await runGenerationPreflight(event, {
scriptsDir: SCRIPTS_DIR,
cache,
execFileImpl: () => Promise.reject(error),
});
assert.equal(cache.size, 0, 'a failed resolution is evicted so the next run re-searches');
});
test('caches the route source file, not the svelte-component manifest', async () => {
const cache = new Map();
const event = {
type: 'generate',
id: 'sess-svelte',
count: 3,
pageUrl: '/',
element: { classes: ['hero'] },
};
await runGenerationPreflight(event, {
scriptsDir: SCRIPTS_DIR,
cache,
async execFileImpl() {
return {
stdout: '{"file":"node_modules/.impeccable-live/x/manifest.json","sourceFile":"src/routes/+page.svelte","previewMode":"svelte-component"}\n',
stderr: '',
};
},
});
assert.deepEqual([...cache.values()], ['src/routes/+page.svelte']);
});
test('reports a child-process failure without leaking internals or throwing', async () => {
const error = new Error('spawn failed');
error.stderr = 'live-wrap.mjs: element not found\n';
+37
View File
@@ -254,6 +254,43 @@ describe('wrapCli integration', () => {
});
it('--defer-source-write leaves source untouched and returns the wrapper block', () => {
const html = `<!DOCTYPE html>
<html>
<body>
<div class="hero-section">
<h1>Hello World</h1>
<p>Welcome to our site.</p>
</div>
</body>
</html>`;
const file = join(tmp, 'index.html');
writeFileSync(file, html);
const result = JSON.parse(execSync(
`node skill/scripts/live-wrap.mjs --id defer1 --count 3 --classes "hero-section" --defer-source-write --file "${file}"`,
{ cwd: process.cwd(), encoding: 'utf-8' }
));
// Source is NOT written by the preflight (no reload storm).
assert.equal(readFileSync(file, 'utf-8'), html);
// Deferred contract fields present for the agent's atomic edit.
assert.equal(result.sourceWritten, false);
assert.ok(typeof result.wrapperBlock === 'string' && result.wrapperBlock.length > 0);
assert.ok(result.wrapperBlock.includes('data-impeccable-variants="defer1"'));
assert.ok(result.wrapperBlock.includes('Variants: insert below this line'));
assert.ok(result.wrapperBlock.includes('impeccable-variants-end defer1'));
assert.equal(typeof result.replaceStartLine, 'number');
assert.equal(typeof result.replaceEndLine, 'number');
// The replace range points at the picked <div class="hero-section"> block
// (1-indexed lines 4..7 of the source above).
const lines = html.split('\n');
assert.ok(lines[result.replaceStartLine - 1].includes('class="hero-section"'));
assert.ok(lines[result.replaceEndLine - 1].includes('</div>'));
});
it('wraps a JSX element and uses JSX comment syntax', () => {
const jsx = `export default function App() {
return (