mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Field feedback from two more Codex sessions drove both changes.
JIT instructions (live/instructions.mjs): every event live-poll prints
now carries _instructions, the authoritative next step for that exact
situation with real ids, paths, and line numbers substituted, and only
the active path's rules (a svelte-component session never sees JSX
guidance). The boot payload carries loop instructions the same way.
Instructions are versioned with the scripts, so they cannot drift from
behavior, and live.md's plumbing can keep shrinking toward contract plus
craft guidance. The Codex poll-discipline failure observed in the field
("the long poll was started, but I yielded the task instead of actively
servicing its result") gets a named anti-pattern in both the harness
policy and the boot instructions.
LLM e2e agent: default provider/model moves from Claude Haiku 4.5 to
OpenAI gpt-5.6-terra at medium reasoning effort via an Anthropic-shaped
shim over the ai SDK (the three call sites stay provider-agnostic;
Anthropic and DeepSeek remain selectable). The harness should exercise
the model tier that actually drives live sessions. Both the react and
sveltekit fixtures pass end to end with terra driving the trimmed
live.md and the new _instructions.
This work was produced with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
235 lines
9.4 KiB
JavaScript
235 lines
9.4 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import {
|
|
buildAcceptScriptArgs,
|
|
buildPollReplyPayload,
|
|
isEventPending,
|
|
manualApplyPollBanner,
|
|
normalizePollTypes,
|
|
parseReplyArgs,
|
|
requiresAgentReply,
|
|
} from '../skill/scripts/live-poll.mjs';
|
|
|
|
describe('live-poll reply payloads', () => {
|
|
it('preserves structured data for durable carbonize recovery acknowledgements', () => {
|
|
const payload = buildPollReplyPayload('token-1', {
|
|
id: 'carbonize-reply-1',
|
|
type: 'agent_done',
|
|
file: 'src/App.jsx',
|
|
data: { carbonize: true },
|
|
});
|
|
|
|
assert.deepEqual(
|
|
payload.data,
|
|
{ carbonize: true },
|
|
'event=live_poll.reply_data actor=agent operation=completion_ack risk=carbonize_flag_dropped_before_server_journal expected={"carbonize":true} actual=' + JSON.stringify(payload.data),
|
|
);
|
|
});
|
|
|
|
it('preserves the leased source event type when concurrent work shares a session id', () => {
|
|
const payload = buildPollReplyPayload('token-1', {
|
|
id: 'abc12345',
|
|
type: 'agent_done',
|
|
sourceEventType: 'accept',
|
|
});
|
|
assert.equal(payload.sourceEventType, 'accept');
|
|
});
|
|
});
|
|
|
|
describe('live-poll accept handling', () => {
|
|
it('forwards pageUrl to live-accept so staged manual edits are scrubbed by page', () => {
|
|
assert.deepEqual(
|
|
buildAcceptScriptArgs({
|
|
type: 'accept',
|
|
id: 'abc12345',
|
|
variantId: 2,
|
|
pageUrl: '/pricing',
|
|
}),
|
|
['--id', 'abc12345', '--variant', '2', '--page-url', '/pricing'],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('live-poll manual Apply guidance', () => {
|
|
it('prints a bounded stderr banner that keeps stdout JSON parseable', () => {
|
|
const banner = manualApplyPollBanner({ id: 'b79a4167' });
|
|
assert.match(banner, /Manual Apply action required/);
|
|
assert.match(banner, /--reply b79a4167 done --data '<json>'/);
|
|
assert.match(banner, /status, appliedEntryIds, failed, files, and notes/);
|
|
assert.match(banner, /summary counters are only a recovery fallback/);
|
|
assert.match(banner, /Do not run live-commit-manual-edits\.mjs/);
|
|
assert.match(banner, /Do not poll again before replying/);
|
|
assert.doesNotMatch(banner, /\{/);
|
|
});
|
|
});
|
|
|
|
describe('live-poll --reply arg parsing', () => {
|
|
it('returns null when --reply is absent', () => {
|
|
assert.equal(parseReplyArgs(['--timeout=600000']), null);
|
|
});
|
|
|
|
it('parses id, status, and a --data JSON payload for manual_edit_apply', () => {
|
|
const dataJson = JSON.stringify({
|
|
status: 'done',
|
|
appliedEntryIds: ['8hexid'],
|
|
failed: [],
|
|
files: ['src/page.html'],
|
|
notes: [],
|
|
});
|
|
const reply = parseReplyArgs(['--reply', '8hexid', 'done', '--data', dataJson]);
|
|
assert.equal(reply.id, '8hexid');
|
|
assert.equal(reply.type, 'done');
|
|
assert.deepEqual(reply.data, {
|
|
status: 'done',
|
|
appliedEntryIds: ['8hexid'],
|
|
failed: [],
|
|
files: ['src/page.html'],
|
|
notes: [],
|
|
});
|
|
// The --data value must not be misread as the trailing message positional.
|
|
assert.equal(reply.message, undefined);
|
|
});
|
|
|
|
it('reaches the POST body unchanged through buildPollReplyPayload', () => {
|
|
const reply = parseReplyArgs(['--reply', 'abc12345', 'done', '--data', '{"status":"partial","appliedEntryIds":["abc12345"]}']);
|
|
const payload = buildPollReplyPayload('tok', reply);
|
|
assert.equal(payload.id, 'abc12345');
|
|
assert.deepEqual(payload.data, { status: 'partial', appliedEntryIds: ['abc12345'] });
|
|
});
|
|
|
|
it('keeps a trailing error message distinct from flag values', () => {
|
|
const reply = parseReplyArgs(['--reply', 'abc12345', 'error', 'could not resolve sources']);
|
|
assert.equal(reply.type, 'error');
|
|
assert.equal(reply.message, 'could not resolve sources');
|
|
assert.equal(reply.data, undefined);
|
|
});
|
|
|
|
it('throws INVALID_DATA_JSON when --data is not valid JSON', () => {
|
|
assert.throws(
|
|
() => parseReplyArgs(['--reply', 'abc12345', 'done', '--data', '{not json}']),
|
|
(err) => err.code === 'INVALID_DATA_JSON' && /must be valid JSON/.test(err.message),
|
|
);
|
|
});
|
|
|
|
it('rejects the common malformed manual-apply ack that omits the event id', () => {
|
|
assert.throws(
|
|
() => parseReplyArgs(['--reply', 'done', '--file', 'site/pages/index.astro']),
|
|
(err) =>
|
|
err.code === 'INVALID_REPLY_ARGS'
|
|
&& /must be the event id/.test(err.message)
|
|
&& /--reply EVENT_ID done/.test(err.message),
|
|
);
|
|
});
|
|
|
|
it('requires an explicit reply status after the event id', () => {
|
|
assert.throws(
|
|
() => parseReplyArgs(['--reply', 'abc12345', '--file', 'src/App.tsx']),
|
|
(err) =>
|
|
err.code === 'INVALID_REPLY_ARGS'
|
|
&& /Missing reply status/.test(err.message),
|
|
);
|
|
});
|
|
|
|
it('coexists with --file without cross-contaminating values', () => {
|
|
const reply = parseReplyArgs(['--reply', 'abc12345', 'done', '--file', 'src/App.tsx', '--data', '{"status":"done"}']);
|
|
assert.equal(reply.file, 'src/App.tsx');
|
|
assert.deepEqual(reply.data, { status: 'done' });
|
|
assert.equal(reply.message, undefined);
|
|
});
|
|
});
|
|
|
|
describe('live-poll stream helpers', () => {
|
|
it('requiresAgentReply is true for work items that need agent acknowledgement', () => {
|
|
assert.equal(requiresAgentReply({ type: 'generate' }), true);
|
|
assert.equal(requiresAgentReply({ type: 'steer' }), true);
|
|
assert.equal(requiresAgentReply({ type: 'manual_edit_apply' }), true);
|
|
assert.equal(requiresAgentReply({ type: 'carbonize_cleanup' }), true);
|
|
assert.equal(requiresAgentReply({ type: 'prefetch' }), false);
|
|
assert.equal(requiresAgentReply({ type: 'accept' }), false);
|
|
assert.equal(requiresAgentReply({ type: 'timeout' }), false);
|
|
});
|
|
|
|
it('isEventPending matches pendingEvents by id', () => {
|
|
const status = {
|
|
pendingEvents: [
|
|
{ id: 'abc12345', type: 'steer', leased: true },
|
|
{ id: 'deadbeef', type: 'generate', leased: false },
|
|
],
|
|
};
|
|
assert.equal(isEventPending(status, 'abc12345'), true);
|
|
assert.equal(isEventPending(status, '00000000'), false);
|
|
});
|
|
|
|
it('normalizes a non-overlapping foreground control lane', () => {
|
|
assert.deepEqual(
|
|
normalizePollTypes('steer,manual_edit_apply,carbonize_cleanup,exit,steer'),
|
|
['steer', 'manual_edit_apply', 'carbonize_cleanup', 'exit'],
|
|
);
|
|
});
|
|
|
|
});
|
|
|
|
describe('just-in-time event instructions', () => {
|
|
it('attaches situation-specific _instructions per event type', async () => {
|
|
const { instructionsForEvent } = await import('../skill/scripts/live/instructions.mjs');
|
|
const sp = '/scripts';
|
|
|
|
const steer = instructionsForEvent({ type: 'steer', id: 'ev1', message: 'x' }, { scriptsPath: sp });
|
|
assert.match(steer, /--reply ev1 steer_done/);
|
|
|
|
const mountFailed = instructionsForEvent({ type: 'variant_mount_failed', id: 'ev2', variant: 2, url: 'http://x/v2.svelte', error: 'boom' }, { scriptsPath: sp });
|
|
assert.match(mountFailed, /variant 2/);
|
|
assert.match(mountFailed, /--reply ev2 done --file/);
|
|
|
|
// Svelte component generate: only svelte guidance, with concrete paths.
|
|
const svelteGen = instructionsForEvent({
|
|
type: 'generate', id: 'ev3', count: 3, action: 'impeccable',
|
|
scaffold: { previewMode: 'svelte-component', componentDir: 'node_modules/.impeccable-live/ev3', file: 'node_modules/.impeccable-live/ev3/manifest.json', sourceFile: 'src/routes/+page.svelte' },
|
|
}, { scriptsPath: sp });
|
|
assert.match(svelteGen, /EDIT the existing stubs node_modules\/\.impeccable-live\/ev3\/v1\.svelte/);
|
|
assert.match(svelteGen, /params\.json/);
|
|
assert.doesNotMatch(svelteGen, /JSX|template literal/);
|
|
assert.match(svelteGen, /--reply ev3 done --file/);
|
|
|
|
// Deferred source-preview generate: single-edit rule with real line numbers.
|
|
const deferredGen = instructionsForEvent({
|
|
type: 'generate', id: 'ev4', count: 3, action: 'bolder',
|
|
scaffold: { sourceWritten: false, file: 'src/App.tsx', wrapperBlock: 'x', replaceStartLine: 12, replaceEndLine: 40 },
|
|
}, { scriptsPath: sp });
|
|
assert.match(deferredGen, /replace lines 12-40/);
|
|
assert.match(deferredGen, /ONE edit/);
|
|
assert.match(deferredGen, /reference\/bolder\.md/);
|
|
assert.doesNotMatch(deferredGen, /params\.json sidecar|componentDir/);
|
|
|
|
// Carbonize accept: the five steps inline with the real file + complete cmd.
|
|
const accept = instructionsForEvent({
|
|
type: 'accept', id: 'ev5', _acceptResult: { handled: true, carbonize: true, file: 'public/index.html' }, _completionAck: { ok: true },
|
|
}, { scriptsPath: sp });
|
|
assert.match(accept, /public\/index\.html/);
|
|
assert.match(accept, /live-complete\.mjs --id ev5/);
|
|
|
|
const mechanicalAccept = instructionsForEvent({
|
|
type: 'accept', id: 'ev6', _acceptResult: { handled: true, carbonize: false }, _completionAck: { ok: true },
|
|
}, { scriptsPath: sp });
|
|
assert.match(mechanicalAccept, /nothing to clean up/i);
|
|
|
|
const timeout = instructionsForEvent({ type: 'timeout' }, { scriptsPath: sp });
|
|
assert.match(timeout, /poll again/i);
|
|
});
|
|
|
|
it('printPollEvent embeds _instructions in the emitted JSON', async () => {
|
|
const { printPollEvent } = await import('../skill/scripts/live-poll.mjs');
|
|
const lines = [];
|
|
const orig = console.log;
|
|
console.log = (s) => lines.push(s);
|
|
try {
|
|
printPollEvent({ type: 'steer', id: 'zz1', message: 'hello' });
|
|
} finally {
|
|
console.log = orig;
|
|
}
|
|
const parsed = JSON.parse(lines[0]);
|
|
assert.match(parsed._instructions, /--reply zz1 steer_done/);
|
|
});
|
|
});
|