mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 18:47:02 +03:00
Add /impeccable generate: agent-initiated live variants (Node-era squash)
Squash of the ten commits reviewed on PR #626, plus the last review round's connection-aware roll call, before the rebase onto the Rust engine: the generate command reference and router row, the overlay's agent-target handling (roll call, leases, replay, rescue), the Node-era live-server routes and live-generate CLI, the hook stand-down, the pricing cards e2e fixture, and the unit, contract, e2e, and skill-behavior tests. The server, CLI, hook, and pin halves are ported to the engine crates in the commits that follow. AI-assisted: implemented and tested with Claude Code under maintainer direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude Fable 5
parent
1c043ea7c9
commit
fc89b0ed62
@@ -810,6 +810,130 @@ for (const { name, fixture } of fixtures) {
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Agent-initiated targeting (the `generate` command). The agent names
|
||||
// the element over the live-generate CLI; the overlay resolves the
|
||||
// selector, scrolls to it, enters the picked state, and fires Go with
|
||||
// no user click. Everything downstream (generate event, variants,
|
||||
// cycling, accept, carbonize) is the standard pipeline, so the second
|
||||
// half of this test reuses the core helpers unchanged.
|
||||
// -----------------------------------------------------------------
|
||||
if (shouldRunScenario('agent-target') && fixture.runtime.agentTargetScenario) {
|
||||
it('agent-initiated target scrolls, picks, and generates without a user click', liveE2eTestOptions, async (t) => {
|
||||
if (!canRunFakeAgentScenario(t)) return;
|
||||
const scenario = fixture.runtime.agentTargetScenario;
|
||||
const session = await bootFixtureSession({
|
||||
name,
|
||||
fixture,
|
||||
browser,
|
||||
agent: createFakeAgent(),
|
||||
wrapTarget: wrapTargetFromPickedElement,
|
||||
atomicDelayMs,
|
||||
log: (m) => t.diagnostic(m),
|
||||
});
|
||||
const { page, appRoot, teardown } = session;
|
||||
try {
|
||||
await waitForHandshake(page);
|
||||
|
||||
// Failure contracts first; none of these may start a session.
|
||||
const miss = runLiveGenerate(appRoot, { selector: scenario.missSelector, action: scenario.action });
|
||||
assert.equal(miss.ok, false);
|
||||
assert.equal(miss.error, 'no_match');
|
||||
|
||||
const ambiguous = runLiveGenerate(appRoot, { selector: scenario.ambiguousSelector, action: scenario.action });
|
||||
assert.equal(ambiguous.ok, false);
|
||||
assert.equal(ambiguous.error, 'ambiguous');
|
||||
assert.ok(ambiguous.matchCount >= 2, 'ambiguous reports the match count');
|
||||
assert.ok(
|
||||
Array.isArray(ambiguous.candidates) && ambiguous.candidates.length >= 2,
|
||||
'ambiguous lists candidate descriptors',
|
||||
);
|
||||
|
||||
const dry = runLiveGenerate(appRoot, {
|
||||
selector: scenario.selector,
|
||||
action: scenario.action,
|
||||
'dry-run': true,
|
||||
});
|
||||
assert.equal(dry.ok, true, `dry-run resolved: ${JSON.stringify(dry)}`);
|
||||
assert.equal(dry.dryRun, true);
|
||||
assert.equal(dry.matchCount, 1);
|
||||
|
||||
const noSession = await page.evaluate(() => window.__IMPECCABLE_LIVE_CHROME_CORE__.debugState());
|
||||
assert.equal(noSession.currentSessionId, null, 'failed and dry-run targets start no session');
|
||||
assert.equal(await page.evaluate(() => Math.round(window.scrollY)), 0, 'page has not scrolled yet');
|
||||
|
||||
// The happy path: resolve, scroll, pick, Go.
|
||||
t.diagnostic(`Agent-targeting ${scenario.selector} (${scenario.action} x${scenario.count || 3})`);
|
||||
const res = runLiveGenerate(appRoot, {
|
||||
selector: scenario.selector,
|
||||
action: scenario.action,
|
||||
count: scenario.count || 3,
|
||||
...(scenario.prompt ? { prompt: scenario.prompt } : {}),
|
||||
});
|
||||
assert.equal(res.ok, true, `live-generate succeeded: ${JSON.stringify(res)}`);
|
||||
assert.match(res.sessionId, /^[0-9a-f]{8}$/, 'a session id came back');
|
||||
assert.equal(res.action, scenario.action);
|
||||
|
||||
const scrolled = await page.evaluate(() => Math.round(window.scrollY));
|
||||
assert.ok(
|
||||
scrolled >= (scenario.minScrollY || 100),
|
||||
`browser scrolled to the target (scrollY=${scrolled})`,
|
||||
);
|
||||
const dbg = await page.evaluate(() => window.__IMPECCABLE_LIVE_CHROME_CORE__.debugState());
|
||||
assert.equal(dbg.currentSessionId, res.sessionId, 'overlay session matches the CLI result');
|
||||
|
||||
if (scenario.prompt) {
|
||||
// The configure bar rebuild once discarded the preset prompt, so
|
||||
// pin the regression at the wire: the journaled generate event
|
||||
// must carry the prompt the CLI was given.
|
||||
const journalPath = join(appRoot, '.impeccable/live/sessions', `${res.sessionId}.jsonl`);
|
||||
const journaled = readFileSync(journalPath, 'utf-8').trim().split('\n').map((l) => JSON.parse(l));
|
||||
const generateEvent = journaled.find((entry) => entry.type === 'generate')?.event;
|
||||
assert.equal(
|
||||
generateEvent?.freeformPrompt,
|
||||
scenario.prompt,
|
||||
'the prompt reached the generate event',
|
||||
);
|
||||
}
|
||||
|
||||
// A second target while the session is mid-flight must refuse.
|
||||
const busy = runLiveGenerate(appRoot, { selector: scenario.selector, action: scenario.action });
|
||||
assert.equal(busy.ok, false);
|
||||
assert.equal(busy.error, 'busy');
|
||||
|
||||
await waitForCyclingRobust(page, 3, { agentMode: 'fake', log: (m) => t.diagnostic(m) });
|
||||
const sourceFile = await locateSessionFile(appRoot);
|
||||
assert.ok(sourceFile, 'the variants wrapper landed in a source file');
|
||||
|
||||
await cycleToVariant(page, 2, 3);
|
||||
t.diagnostic('Accepting variant 2');
|
||||
await clickAccept(page, { expectedVariant: 2 });
|
||||
await waitForBarHidden(page);
|
||||
const final = await waitForSourceClean(sourceFile, 20_000, {});
|
||||
assert.match(
|
||||
final,
|
||||
new RegExp(fixture.runtime.acceptedSourcePattern),
|
||||
'accepted source element survives',
|
||||
);
|
||||
for (const needle of fixture.runtime.assertSourceContains || []) {
|
||||
assert.ok(final.includes(needle), `source still contains ${JSON.stringify(needle)} after accept`);
|
||||
}
|
||||
assert.doesNotMatch(final, /data-impeccable-variants="/, 'variants wrapper removed');
|
||||
assert.doesNotMatch(final, /impeccable-carbonize-start/, 'carbonize block rewritten');
|
||||
|
||||
// The overlay is reusable after accept: a fresh dry-run resolves.
|
||||
const post = runLiveGenerate(appRoot, {
|
||||
selector: scenario.selector,
|
||||
action: scenario.action,
|
||||
'dry-run': true,
|
||||
});
|
||||
assert.equal(post.ok, true, 'a new target resolves after accept');
|
||||
} finally {
|
||||
await teardownAndResetBrowser(teardown);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Failure injection for component previews.
|
||||
// -----------------------------------------------------------------
|
||||
@@ -1363,6 +1487,29 @@ function canRunFakeAgentScenario(t) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the live-generate verb (agent-initiated targeting) against a staged
|
||||
* fixture and parse its JSON verdict. The CLI exits 1 for every ok:false
|
||||
* outcome, so the JSON is read from the thrown error's stdout in that case.
|
||||
*/
|
||||
function runLiveGenerate(appRoot, flags) {
|
||||
const args = [];
|
||||
for (const [key, value] of Object.entries(flags)) {
|
||||
if (value === true) args.push(`--${key}`);
|
||||
else if (value !== undefined && value !== null) args.push(`--${key}`, String(value));
|
||||
}
|
||||
let stdout;
|
||||
try {
|
||||
stdout = runEngineSync('live-generate', args, { cwd: appRoot });
|
||||
} catch (err) {
|
||||
// The verb exits non-zero on every failure verdict but still prints the
|
||||
// JSON the scenario asserts on.
|
||||
stdout = err.stdout || '';
|
||||
if (!stdout.trim()) throw err;
|
||||
}
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot a fixture straight to CYCLING on a component-preview session and hand
|
||||
* back the handles the scenarios need: the manifest, the route source, and the
|
||||
|
||||
Reference in New Issue
Block a user