From 00a9a138d8b3abb204a1f52fc3ded32183639a03 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Wed, 9 Sep 2026 14:54:28 -0700 Subject: [PATCH] test(claude): pin why the roster empties mid-turn in a sequential fan-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strip unmounting between two sequential subagents is truthful, not a swept row: A leaves on the provider's own terminal frame, B does not exist yet, and backgrounded work spanning the same gap holds the roster open — so an empty roster is never work the strip is hiding. Also pins the previous-turn rule against the one the subagent roster already applies on the same frame: a still-working FOREGROUND child becomes `unverifiable` there and a backgrounded one is left alone, so the strip drops the first and keeps the second rather than asserting `live` for either. --- .../claude-background-task-tracker.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/main/claude/claude-background-task-tracker.test.ts b/src/main/claude/claude-background-task-tracker.test.ts index 4e1fae6ff8a..733ea49a582 100644 --- a/src/main/claude/claude-background-task-tracker.test.ts +++ b/src/main/claude/claude-background-task-tracker.test.ts @@ -114,6 +114,56 @@ describe('ClaudeBackgroundTaskTracker', () => { }) }) + it('empties only between one task retiring and the next starting', () => { + // The strip's mid-turn unmount in a sequential fan-out is TRUTHFUL: A leaves + // on the provider's own terminal frame for A, B does not exist yet, and + // nothing sweeps A early. An empty roster means no task is running. + const tracker = new ClaudeBackgroundTaskTracker() + tracker.observe({ type: 'user' }, true) + tracker.observe( + system('task_started', { task_id: 'A', task_type: 'local_agent', is_backgrounded: false }) + ) + expect(tracker.state?.tasks).toEqual([{ id: 'A', kind: 'agent', stoppable: false }]) + tracker.observe(system('task_notification', { task_id: 'A', status: 'completed' })) + expect(tracker.state).toBeNull() + tracker.observe( + system('task_started', { task_id: 'B', task_type: 'local_agent', is_backgrounded: false }) + ) + expect(tracker.state?.tasks).toEqual([{ id: 'B', kind: 'agent', stoppable: false }]) + + // Backgrounded work spanning the same gap holds the roster open, so an + // empty one is never work the strip is hiding. + const spanned = new ClaudeBackgroundTaskTracker() + spanned.observe({ type: 'user' }, true) + spanned.observe( + system('task_started', { task_id: 'bg', task_type: 'local_bash', is_backgrounded: true }) + ) + spanned.observe( + system('task_started', { task_id: 'A', task_type: 'local_agent', is_backgrounded: false }) + ) + spanned.observe(system('task_notification', { task_id: 'A', status: 'completed' })) + expect(spanned.state?.tasks).toEqual([{ id: 'bg', kind: 'command' }]) + }) + + it('settles a previous turn the way the subagent roster settles it', () => { + // On this same frame the roster's `settleTurn` moves a still-working + // FOREGROUND child to `unverifiable` and leaves a backgrounded one alone. + // The strip has no `unverifiable` row, so keeping one would assert `live` + // for work Orca has already stopped vouching for. + const tracker = new ClaudeBackgroundTaskTracker() + tracker.observe({ type: 'user' }, true) + tracker.observe( + system('task_started', { task_id: 'fore', task_type: 'local_agent', is_backgrounded: false }) + ) + tracker.observe( + system('task_started', { task_id: 'back', task_type: 'local_bash', is_backgrounded: true }) + ) + + // No `result` for that turn; the next one starting is its only end. + tracker.observe({ type: 'user' }, true) + expect(tracker.state?.tasks).toEqual([{ id: 'back', kind: 'command' }]) + }) + it('retires a phantom foreground row when the next turn starts', () => { // A foreground `task_started` with no turn open has no `result` coming to // retire it, so it would sit in the strip — with no stop of its own — and