test(sidebar): pin the bounded false positive spinner attribution accepts (#9040)

launchAgent usually clears when the agent exits — clearTabLaunchAgent fires from
use-tab-agent.ts once resolveLaunchedAgentExitEvidence proves it via OSC 133;D
shell-foreground, a vanished local agent signal, or a completion hook. But both
signal paths are gated on !isRemote and a hookless remote agent has no completion
hook, so its launch identity can outlive it. A later non-agent process in that tab
whose title carries an ora-style braille spinner (pnpm, vite, cargo) then reads as
working.

That is the trade-off #9647 already made for the sidebar row; this makes the dot
match rather than diverge. Pin it with its two bounds — no launch identity cannot
spin, and a dead PTY drops out — so it stays a deliberate choice under review
instead of drifting into an unexamined default.

Also drop an overstated comment: the gate stops a spinner in a tab no agent was
launched in, not every frozen spinner frame.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-25 22:05:21 -07:00
co-authored by Orca
parent e841974b42
commit 103e9acc7d
@@ -56,7 +56,7 @@ describe('#9040 worktree dot attributes spinner titles to the launched agent', (
})
// Why: pins the #9647 gate — spinner attribution needs a launch identity, so a
// frozen spinner frame left by an exited agent still cannot spin the dot forever.
// spinner in a tab no agent was launched in cannot spin the dot.
it('stays active for a spinner title with no launch identity', () => {
const status = getWorktreeStatus(
[{ id: 'tab-1', title: '⠐ Review branch for regressions' }],
@@ -78,6 +78,30 @@ describe('#9040 worktree dot attributes spinner titles to the launched agent', (
})
})
// Why: launchAgent usually clears on exit (clearTabLaunchAgent via
// resolveLaunchedAgentExitEvidence), but not on every path — a hookless remote agent has
// no completion hook and no shell-foreground producer, so its launch identity can outlive
// it and a later ora-style spinner then reads as working. Documented trade-off, not an
// oversight — the row builder has behaved this way since #9647. Pinned so it stays a
// deliberate choice rather than drifting silently.
describe('#9040 spinner attribution trade-off is bounded', () => {
it('over-reports a non-agent spinner in a tab an agent was launched in', () => {
const tab = {
id: 'tab-1',
title: '⠋ Progress: resolved 42',
launchAgent: 'claude'
} satisfies Partial<TerminalTab>
expect(getWorktreeStatus([tab], [], livePtyMap('tab-1'))).toBe('working')
// Bound 1: the same title cannot spin a tab with no launch identity.
expect(getWorktreeStatus([{ id: 'tab-1', title: tab.title }], [], livePtyMap('tab-1'))).toBe(
'active'
)
// Bound 2: a dead PTY drops out regardless of launch identity.
expect(getWorktreeStatus([tab], [], {})).toBe('inactive')
})
})
// Why: the gate's stated purpose is that the dot never spins with no matching sidebar
// row. Claude's spinner title must reach the same dot/row agreement a named provider
// already gets — no better, and no worse.