Files
pbakaus_impeccable/tests/live-generation-preflight.test.mjs
T
Paul BakausandClaude Fable 5 dbe0c12b91 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>
2026-07-22 22:49:44 -07:00

227 lines
7.1 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import path from 'node:path';
import {
buildGenerationPreflight,
runGenerationPreflight,
clearSourceResolutionCache,
} from '../skill/scripts/live/generation-preflight.mjs';
const SCRIPTS_DIR = path.resolve('skill/scripts');
test('builds a replace preflight from the picker locator', () => {
const command = buildGenerationPreflight({
type: 'generate',
id: 'session-1',
count: 3,
pageUrl: '/pricing',
element: {
id: 'hero',
classes: ['hero', 'hero--dark'],
tagName: 'SECTION',
textContent: 'A faster way to ship',
},
}, SCRIPTS_DIR);
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',
'--text', 'A faster way to ship',
'--page-url', '/pricing',
]);
});
test('builds an insert preflight from the anchor locator', () => {
const command = buildGenerationPreflight({
type: 'generate',
id: 'session-2',
count: 2,
mode: 'insert',
insert: {
position: 'before',
anchor: { classes: ['card'], tagName: 'ARTICLE', textContent: 'Plan' },
},
}, SCRIPTS_DIR);
assert.equal(command.mode, 'insert');
assert.deepEqual(command.args.slice(1), [
'--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({
type: 'generate',
id: 'session-3',
count: 1,
element: { classes: ['hero'] },
}, {
scriptsDir: SCRIPTS_DIR,
cwd: '/tmp/example',
async execFileImpl(file, args, options) {
calls.push({ file, args, options });
return { stdout: '{"file":"src/App.jsx","insertLine":12}\n', stderr: '' };
},
});
assert.equal(result.ok, true);
assert.deepEqual(result.scaffold, { file: 'src/App.jsx', insertLine: 12 });
assert.equal(calls[0].file, process.execPath);
assert.equal(calls[0].options.cwd, '/tmp/example');
});
test('skips preflight when the picker has no source locator', async () => {
const result = await runGenerationPreflight({
type: 'generate',
id: 'session-4',
count: 3,
element: { tagName: 'DIV' },
}, { scriptsDir: SCRIPTS_DIR });
assert.deepEqual(result, { ok: false, skipped: true, reason: 'insufficient_locator' });
});
test('yields to the event loop instead of blocking on the child process', async () => {
// The server is single-threaded and leases polls through this call. A
// synchronous spawn froze every other request (Accept, Discard, SSE) for the
// scaffold's full duration — measured at ~7.6s on a large repo.
let tickedDuringPreflight = false;
const pending = runGenerationPreflight({
type: 'generate',
id: 'session-async',
count: 1,
element: { classes: ['hero'] },
}, {
scriptsDir: SCRIPTS_DIR,
execFileImpl: () => new Promise((resolve) => {
setTimeout(() => resolve({ stdout: '{"file":"src/App.jsx"}\n', stderr: '' }), 25);
}),
});
setTimeout(() => { tickedDuringPreflight = true; }, 5);
const result = await pending;
assert.equal(result.ok, true);
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';
const result = await runGenerationPreflight({
type: 'generate',
id: 'session-fail',
count: 1,
element: { classes: ['hero'] },
}, {
scriptsDir: SCRIPTS_DIR,
execFileImpl: () => Promise.reject(error),
});
assert.equal(result.ok, false);
assert.equal(result.error, 'live-wrap.mjs: element not found');
assert.ok(typeof result.durationMs === 'number');
});