test: settle the post-response composer before seeding; give the TPA recorder adapter its infra helper

submitPlanSeed accepted a stale empty composer when the transcript recorded
end_turn before the CLI repainted (late-repaint-typed-current fails 5/5 on the
old helper, passes 5/5 now). The TPA recording fixture extracted recordE2E
without isPreTurnInfraFailure, so every failed case threw before recording.
This commit is contained in:
garrytan committed 2026-09-29 20:43:00 +00:00
1 parent 80c92c86ed
commit 3fc05932b7
4 files changed
+20 -9

No files matched your search

+3 -1
View File
@@ -85,8 +85,10 @@ process.stdin.on('data',chunk=>{
const current=scenario==='prose-question'?'\r\nWhich option do you prefer?\r\nA) Full review (recommended)\r\nB) Skip review\r\n❯ '
:scenario.endsWith('multiline-current')?'\r\n keep this draft'
:scenario.endsWith('typed-current')?'keep this draft':'';
frame(current,history,scenario.endsWith('missing-current-top')||scenario==='unframed-current'?'':rule,
// The native CLI can write end_turn before it repaints; the stale empty box must not count.
const paint=()=>frame(current,history,scenario.endsWith('missing-current-top')||scenario==='unframed-current'?'':rule,
scenario.endsWith('missing-current-bottom')||scenario==='unframed-current'?'':scenario==='mismatched-current-rules'?rule.slice(1):rule);
if(scenario==='late-repaint-typed-current')setTimeout(paint,60);else paint();
if(scenario==='stray-prompt-after-current')process.stdout.write('\r❯ keep this later draft');
},180);return;
}
+11 -3
View File
@@ -152,6 +152,9 @@ export async function submitPlanSeed(session: SeedSession, seed: string, opts: {
});
if (Date.now() >= opts.deadlineAt) throw new PlanSeedTimeout('Plan seed submission exhausted the existing case budget');
session.sendKey('Enter'); // Separate input event after the acknowledged paste.
// The transcript can record end_turn before the CLI repaints, so an empty
// composer counts only when the same frame survives one more poll.
let settled = '';
await until(async () => {
const owned = read();
if (!owned || owned.pendingBytes) return false;
@@ -178,13 +181,18 @@ export async function submitPlanSeed(session: SeedSession, seed: string, opts: {
}
if (row.type === 'user') for (const c of content(row)) if (c.type === 'tool_result') pending.delete(c.tool_use_id);
}
if (!complete || pending.size || owned.status.waitingFor) return false;
const unsettled = () => { settled = ''; return false; };
if (!complete || pending.size || owned.status.waitingFor) return unsettled();
const frame = await session.currentScreen!();
if (opts.isQuestionOrPermission(frame.text)) throw new Error('Plan seed response requires an answer before skill invocation');
const input = composer(frame.text);
if (frame.rawEnd !== session.mark() || !input
|| input.line.replace(/^❯[ \u00a0]*/, '').trim() !== '') return false;
|| input.line.replace(/^❯[ \u00a0]*/, '').trim() !== '') return unsettled();
const fresh = read();
return !!fresh && !fresh.pendingBytes && fresh.rows.length === owned.rows.length && !fresh.status.waitingFor;
if (!fresh || fresh.pendingBytes || fresh.rows.length !== owned.rows.length || fresh.status.waitingFor) return unsettled();
const signature = `${frame.rawEnd}:${fresh.rows.length}:${frame.text}`;
if (signature === settled) return true;
settled = signature;
return false;
});
}
+1 -1
View File
@@ -18,7 +18,7 @@ for (const scenario of ['success', 'completed-tool', 'status-updating', 'history
'startup-waiting', 'startup-prose-question', 'startup-permission', 'startup-fresh-waiting',
'no-ack', 'fused', 'duplicate', 'session-switch', 'foreign-cwd',
'pending-tool', 'question', 'prose-question', 'permission', 'no-end-turn', 'partial', 'wrong-pid',
'typed-current', 'multiline-current', 'history-box-typed-current', 'history-box-multiline-current',
'typed-current', 'late-repaint-typed-current', 'multiline-current', 'history-box-typed-current', 'history-box-multiline-current',
'missing-current-top', 'missing-current-bottom', 'mismatched-current-rules', 'unframed-current',
'history-no-current', 'history-missing-current-top', 'history-missing-current-bottom',
'stray-prompt-after-current', 'stale-response-frame',
+5 -4
View File
@@ -24,10 +24,11 @@ const recoveryResponses = JSON.parse(readFile(path.join(root, 'test/fixtures/thi
const writeFile = fs.writeFileSync.bind(fs);
const remove = fs.rmSync.bind(fs);
const source = readFile(path.join(root, 'test/helpers/e2e-helpers.ts'), 'utf8');
const start = source.indexOf('export function recordE2E(');
const end = source.indexOf('/**', start);
if (start < 0 || end < 0) throw new Error('recordE2E source boundary missing');
const recordSource = new Bun.Transpiler({ loader: 'ts' }).transformSync(source.slice(start, end).replace('export function', 'function'));
const start = source.indexOf('const INFRA_EXIT_REASONS');
const recordStart = source.indexOf('export function recordE2E(');
const end = source.indexOf('/**', recordStart);
if (start < 0 || recordStart < start || end < 0) throw new Error('recordE2E source boundary missing');
const recordSource = new Bun.Transpiler({ loader: 'ts' }).transformSync(source.slice(start, end).replaceAll('export function', 'function'));
const recordE2E = new Function(recordSource + '\\nreturn recordE2E;')();
const cases = new Map();
const records = [];