diff --git a/test/fixtures/plan-seed-cli.ts b/test/fixtures/plan-seed-cli.ts index 349b66819..9323d5749 100644 --- a/test/fixtures/plan-seed-cli.ts +++ b/test/fixtures/plan-seed-cli.ts @@ -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; } diff --git a/test/helpers/plan-seed-submission.ts b/test/helpers/plan-seed-submission.ts index a68c2097e..91cea3419 100644 --- a/test/helpers/plan-seed-submission.ts +++ b/test/helpers/plan-seed-submission.ts @@ -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; }); } diff --git a/test/plan-seed-submission.test.ts b/test/plan-seed-submission.test.ts index bf2ec1041..398a9de1b 100644 --- a/test/plan-seed-submission.test.ts +++ b/test/plan-seed-submission.test.ts @@ -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', diff --git a/test/third-party-actions-recording.test.ts b/test/third-party-actions-recording.test.ts index 05b8823c0..5155363a7 100644 --- a/test/third-party-actions-recording.test.ts +++ b/test/third-party-actions-recording.test.ts @@ -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 = [];