mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
Extract live-worktree-mismatch tests into a dedicated file, add deferred
This commit is contained in:
@@ -6182,10 +6182,7 @@ describe('AgentHookServer reported cwd merge', () => {
|
||||
envelope: Record<string, unknown>,
|
||||
connectionId = 'conn-1'
|
||||
): void => {
|
||||
server.ingestRemote(
|
||||
envelope as Parameters<AgentHookServer['ingestRemote']>[0],
|
||||
connectionId
|
||||
)
|
||||
server.ingestRemote(envelope as Parameters<AgentHookServer['ingestRemote']>[0], connectionId)
|
||||
}
|
||||
const workingPayload = (overrides: Record<string, unknown> = {}) => ({
|
||||
state: 'working',
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import type { ReactNode } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
type MockAgentOptions = {
|
||||
paneKey?: string
|
||||
agentType?: string
|
||||
startedAt?: number
|
||||
stateStartedAt?: number
|
||||
prompt?: string
|
||||
lastAssistantMessage?: string
|
||||
liveWorktreeMismatch?: { destinationWorktreeId: string; destinationLabel: string }
|
||||
}
|
||||
|
||||
function mockAgent({
|
||||
paneKey = 'tab-1:1',
|
||||
agentType,
|
||||
startedAt,
|
||||
stateStartedAt = 1000,
|
||||
prompt,
|
||||
lastAssistantMessage,
|
||||
liveWorktreeMismatch
|
||||
}: MockAgentOptions = {}): unknown {
|
||||
return {
|
||||
paneKey,
|
||||
tab: { id: paneKey.split(':')[0] },
|
||||
agentType,
|
||||
rowSource: 'live',
|
||||
liveWorktreeMismatch,
|
||||
state: 'working',
|
||||
startedAt,
|
||||
entry: {
|
||||
prompt,
|
||||
lastAssistantMessage,
|
||||
state: 'working',
|
||||
stateStartedAt,
|
||||
stateHistory: prompt === undefined ? undefined : []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mockAgents: unknown[] = []
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
agentActivityDisplayMode: 'compact',
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
cacheTimerByKey: {},
|
||||
dropAgentStatus: vi.fn(),
|
||||
dismissRetainedAgent: vi.fn(),
|
||||
acknowledgeAgents: vi.fn(),
|
||||
agentSendPopoverTargetMode: null,
|
||||
agentStatusByPaneKey: {},
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
sendPromptToSidebarAgentTarget: vi.fn(),
|
||||
settings: { promptCacheTimerEnabled: true, promptCacheTtlMs: 60_000 }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('./useWorktreeAgentRows', () => ({
|
||||
useWorktreeAgentRows: vi.fn(() => mockAgents)
|
||||
}))
|
||||
|
||||
vi.mock('@/components/dashboard/useNow', () => ({
|
||||
useNow: vi.fn(() => 2000)
|
||||
}))
|
||||
|
||||
vi.mock('./prompt-cache-countdown-clock', () => ({
|
||||
usePromptCacheCountdownNow: vi.fn(() => 10_000)
|
||||
}))
|
||||
|
||||
vi.mock('@/components/dashboard/DashboardAgentRow', () => ({
|
||||
default: ({ agent }: { agent: { paneKey: string } }) => (
|
||||
<div data-testid="agent-row" data-pane-key={agent.paneKey} />
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('./focused-agent-row-highlight', () => ({
|
||||
useFocusedAgentPaneKey: vi.fn(() => null)
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
describe('WorktreeCardAgents live worktree mismatch', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockAgents = []
|
||||
})
|
||||
|
||||
it('notes the destination worktree on a compact row without dropping its details', async () => {
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
startedAt: 1000,
|
||||
prompt: 'Run tests',
|
||||
lastAssistantMessage: 'Inspecting changes',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('data-agent-live-worktree-mismatch')
|
||||
expect(markup).toContain(' · in scratch-fix')
|
||||
expect(markup).toContain('<span class="text-muted-foreground/90">Run tests</span>')
|
||||
expect(markup).toContain('<span class="text-muted-foreground/65"> - Inspecting changes</span>')
|
||||
expect(markup).toContain('title="Run tests · in scratch-fix - Inspecting changes"')
|
||||
}, 30_000)
|
||||
|
||||
it('summarizes hidden panes running elsewhere on the collapsed compact affordance', async () => {
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
startedAt: 1000,
|
||||
prompt: 'One',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:2',
|
||||
agentType: 'claude',
|
||||
startedAt: 1500,
|
||||
stateStartedAt: 1500,
|
||||
prompt: 'Two',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-other',
|
||||
destinationLabel: 'other-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:3',
|
||||
agentType: 'gemini',
|
||||
startedAt: 1700,
|
||||
stateStartedAt: 1700,
|
||||
prompt: 'Three'
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('aria-expanded="false"')
|
||||
expect(markup).toContain('data-agent-live-worktree-mismatch-summary')
|
||||
expect(markup).toContain('2 elsewhere')
|
||||
expect(markup).toContain('2 agents in another worktree')
|
||||
expect(markup).not.toContain('3 agents in another worktree')
|
||||
})
|
||||
|
||||
it('uses the singular hidden-elsewhere phrasing for a single mismatched pane', async () => {
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
startedAt: 1000,
|
||||
prompt: 'One',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:2',
|
||||
agentType: 'claude',
|
||||
startedAt: 1500,
|
||||
stateStartedAt: 1500,
|
||||
prompt: 'Two'
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('1 elsewhere')
|
||||
expect(markup).toContain('1 agent in another worktree')
|
||||
})
|
||||
})
|
||||
@@ -235,4 +235,40 @@ describe('WorktreeCardAgents send targets', () => {
|
||||
expect(markup).not.toContain('data-agent-send-target="disabled"')
|
||||
expect(markup).not.toContain('title="Agent is working"')
|
||||
})
|
||||
|
||||
it('keeps every compact send target visible instead of summarizing panes elsewhere', async () => {
|
||||
const now = Date.now()
|
||||
mockAgents = [
|
||||
{
|
||||
...(agentRow(READY_PANE_KEY, 'done', now) as Record<string, unknown>),
|
||||
rowSource: 'live',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
},
|
||||
{
|
||||
...(agentRow(WORKING_PANE_KEY, 'working', now) as Record<string, unknown>),
|
||||
rowSource: 'live',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-other',
|
||||
destinationLabel: 'other-fix'
|
||||
}
|
||||
}
|
||||
]
|
||||
mockStoreState = {
|
||||
...targetStoreState(now),
|
||||
agentActivityDisplayMode: 'compact',
|
||||
agentSendPopoverTargetMode: activeTargetMode()
|
||||
}
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
// Why: target selection lists every pane, so a collapsed "N elsewhere" badge would hide choices.
|
||||
expect(markup).not.toContain('data-agent-live-worktree-mismatch-summary')
|
||||
expect(markup).not.toContain('elsewhere')
|
||||
expect(markup).toContain('in scratch-fix')
|
||||
expect(markup).toContain('in other-fix')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,7 +29,6 @@ type MockAgentOptions = {
|
||||
isLastSibling: boolean
|
||||
childCount: number
|
||||
}
|
||||
liveWorktreeMismatch?: { destinationWorktreeId: string; destinationLabel: string }
|
||||
}
|
||||
|
||||
function mockAgent({
|
||||
@@ -44,15 +43,13 @@ function mockAgent({
|
||||
stateStartedAt = 1000,
|
||||
terminalHandle,
|
||||
orchestration,
|
||||
lineage,
|
||||
liveWorktreeMismatch
|
||||
lineage
|
||||
}: MockAgentOptions = {}): unknown {
|
||||
return {
|
||||
paneKey,
|
||||
tab: { id: tabId },
|
||||
agentType,
|
||||
rowSource,
|
||||
liveWorktreeMismatch,
|
||||
state,
|
||||
startedAt,
|
||||
entry: {
|
||||
@@ -898,105 +895,4 @@ describe('WorktreeCardAgents', () => {
|
||||
expect(markup).not.toContain('Child A')
|
||||
expect(markup).not.toContain('compact-agent-row')
|
||||
})
|
||||
|
||||
it('notes the destination worktree on a compact row without dropping its details', async () => {
|
||||
mockAgentActivityDisplayMode = 'compact'
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
rowSource: 'live',
|
||||
startedAt: 1000,
|
||||
prompt: 'Run tests',
|
||||
lastAssistantMessage: 'Inspecting changes',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('data-agent-live-worktree-mismatch')
|
||||
expect(markup).toContain(' · in scratch-fix')
|
||||
expect(markup).toContain('<span class="text-muted-foreground/90">Run tests</span>')
|
||||
expect(markup).toContain('<span class="text-muted-foreground/65"> - Inspecting changes</span>')
|
||||
expect(markup).toContain('title="Run tests · in scratch-fix - Inspecting changes"')
|
||||
})
|
||||
|
||||
it('summarizes hidden panes running elsewhere on the collapsed compact affordance', async () => {
|
||||
mockAgentActivityDisplayMode = 'compact'
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
rowSource: 'live',
|
||||
startedAt: 1000,
|
||||
prompt: 'One',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:2',
|
||||
agentType: 'claude',
|
||||
rowSource: 'live',
|
||||
startedAt: 1500,
|
||||
stateStartedAt: 1500,
|
||||
prompt: 'Two',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-other',
|
||||
destinationLabel: 'other-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:3',
|
||||
agentType: 'gemini',
|
||||
rowSource: 'live',
|
||||
startedAt: 1700,
|
||||
stateStartedAt: 1700,
|
||||
prompt: 'Three'
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('aria-expanded="false"')
|
||||
expect(markup).toContain('data-agent-live-worktree-mismatch-summary')
|
||||
expect(markup).toContain('2 elsewhere')
|
||||
expect(markup).toContain('2 agents in another worktree')
|
||||
expect(markup).not.toContain('3 agents in another worktree')
|
||||
})
|
||||
|
||||
it('uses the singular hidden-elsewhere phrasing for a single mismatched pane', async () => {
|
||||
mockAgentActivityDisplayMode = 'compact'
|
||||
mockAgents = [
|
||||
mockAgent({
|
||||
agentType: 'codex',
|
||||
rowSource: 'live',
|
||||
startedAt: 1000,
|
||||
prompt: 'One',
|
||||
liveWorktreeMismatch: {
|
||||
destinationWorktreeId: 'wt-scratch',
|
||||
destinationLabel: 'scratch-fix'
|
||||
}
|
||||
}),
|
||||
mockAgent({
|
||||
paneKey: 'tab-1:2',
|
||||
agentType: 'claude',
|
||||
rowSource: 'live',
|
||||
startedAt: 1500,
|
||||
stateStartedAt: 1500,
|
||||
prompt: 'Two'
|
||||
})
|
||||
]
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
const markup = renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
|
||||
expect(markup).toContain('1 elsewhere')
|
||||
expect(markup).toContain('1 agent in another worktree')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -104,11 +104,12 @@ function resolve(
|
||||
|
||||
describe('resolveAgentLiveWorktreeMismatch', () => {
|
||||
it('resolves a nested scratch destination over the owner checkout', () => {
|
||||
expect(resolve('/repo/.claude/worktrees/scratch/src', [candidate(OWNER), candidate(SCRATCH)]))
|
||||
.toEqual({
|
||||
destinationWorktreeId: SCRATCH.id,
|
||||
destinationLabel: '10572-live-repro'
|
||||
})
|
||||
expect(
|
||||
resolve('/repo/.claude/worktrees/scratch/src', [candidate(OWNER), candidate(SCRATCH)])
|
||||
).toEqual({
|
||||
destinationWorktreeId: SCRATCH.id,
|
||||
destinationLabel: '10572-live-repro'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns no mismatch for the owner checkout, unknown paths, and folder workspaces', () => {
|
||||
@@ -148,8 +149,9 @@ describe('resolveAgentLiveWorktreeMismatch', () => {
|
||||
path: '/repo/.claude/worktrees/scratch',
|
||||
hostId: 'ssh:target-b'
|
||||
})
|
||||
expect(resolve('/repo/.claude/worktrees/scratch', [candidate(sameIdOtherHost)], sshOwner))
|
||||
.toBeNull()
|
||||
expect(
|
||||
resolve('/repo/.claude/worktrees/scratch', [candidate(sameIdOtherHost)], sshOwner)
|
||||
).toBeNull()
|
||||
// Why: matching is by current path only; a renamed folder's old id must not match.
|
||||
const renamed = makeWorktree({
|
||||
id: 'repo-1::/repo/.claude/worktrees/renamed',
|
||||
@@ -158,7 +160,9 @@ describe('resolveAgentLiveWorktreeMismatch', () => {
|
||||
priorWorktreeIds: ['repo-1::/repo/.claude/worktrees/old'],
|
||||
branch: 'refs/heads/renamed'
|
||||
})
|
||||
expect(resolve('/repo/.claude/worktrees/old', [candidate(OWNER), candidate(renamed)])).toBeNull()
|
||||
expect(
|
||||
resolve('/repo/.claude/worktrees/old', [candidate(OWNER), candidate(renamed)])
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects a candidate from another repo bucket', () => {
|
||||
@@ -244,7 +248,10 @@ describe('resolveAgentLiveWorktreeMismatch', () => {
|
||||
|
||||
it('collapses dot segments, duplicate and trailing separators', () => {
|
||||
expect(
|
||||
resolve('/repo/./.claude//worktrees/other/../scratch/', [candidate(OWNER), candidate(SCRATCH)])
|
||||
resolve('/repo/./.claude//worktrees/other/../scratch/', [
|
||||
candidate(OWNER),
|
||||
candidate(SCRATCH)
|
||||
])
|
||||
).toEqual({ destinationWorktreeId: SCRATCH.id, destinationLabel: '10572-live-repro' })
|
||||
})
|
||||
|
||||
@@ -304,7 +311,11 @@ describe('resolveAgentLiveWorktreeMismatch', () => {
|
||||
// Why: the distro alias folds case, the Linux tail does not.
|
||||
expect(resolve('/home/dev/repo/Scratch', [candidate(wslScratch)], wslOwner)).toBeNull()
|
||||
expect(
|
||||
resolve('\\\\wsl.localhost\\Debian\\home\\dev\\repo\\scratch', [candidate(wslScratch)], wslOwner)
|
||||
resolve(
|
||||
'\\\\wsl.localhost\\Debian\\home\\dev\\repo\\scratch',
|
||||
[candidate(wslScratch)],
|
||||
wslOwner
|
||||
)
|
||||
).toBeNull()
|
||||
// Why: a bare Linux cwd only bridges into the owner's proven distro.
|
||||
expect(resolve('/home/dev/repo/scratch', [candidate(otherDistro)], wslOwner)).toBeNull()
|
||||
@@ -321,11 +332,11 @@ describe('resolveAgentLiveWorktreeMismatch', () => {
|
||||
})
|
||||
|
||||
it('falls back from branch to display name to path basename for the label', () => {
|
||||
const noBranch = { ...SCRATCH, branch: undefined } as Worktree
|
||||
expect(resolve('/repo/.claude/worktrees/scratch', [candidate(noBranch)])?.destinationLabel).toBe(
|
||||
'scratch'
|
||||
)
|
||||
const noNames = { ...SCRATCH, branch: undefined, displayName: '' } as Worktree
|
||||
const noBranch: Worktree = { ...SCRATCH, branch: '' }
|
||||
expect(
|
||||
resolve('/repo/.claude/worktrees/scratch', [candidate(noBranch)])?.destinationLabel
|
||||
).toBe('scratch')
|
||||
const noNames: Worktree = { ...SCRATCH, branch: '', displayName: '' }
|
||||
expect(resolve('/repo/.claude/worktrees/scratch', [candidate(noNames)])?.destinationLabel).toBe(
|
||||
'scratch'
|
||||
)
|
||||
@@ -402,7 +413,10 @@ describe('buildAgentLiveWorktreeMismatchCandidates', () => {
|
||||
|
||||
describe('attachAgentLiveWorktreeMismatch', () => {
|
||||
function makeRow(
|
||||
overrides: Partial<DashboardAgentRow> & { paneKey: string }
|
||||
overrides: Omit<Partial<DashboardAgentRow>, 'entry'> & {
|
||||
paneKey: string
|
||||
entry?: Partial<AgentStatusEntry>
|
||||
}
|
||||
): DashboardAgentRow {
|
||||
const entry = {
|
||||
paneKey: overrides.paneKey,
|
||||
@@ -411,10 +425,9 @@ describe('attachAgentLiveWorktreeMismatch', () => {
|
||||
stateStartedAt: 1,
|
||||
stateHistory: [],
|
||||
agentType: 'claude',
|
||||
...(overrides.entry ?? {})
|
||||
...overrides.entry
|
||||
} as AgentStatusEntry
|
||||
return {
|
||||
paneKey: overrides.paneKey,
|
||||
tab: { id: 'tab-1', worktreeId: OWNER.id } as TerminalTab,
|
||||
agentType: 'claude',
|
||||
rowSource: 'live',
|
||||
@@ -466,4 +479,42 @@ describe('attachAgentLiveWorktreeMismatch', () => {
|
||||
]
|
||||
expect(attachAgentLiveWorktreeMismatch(rows, args)).toBe(rows)
|
||||
})
|
||||
|
||||
it('starts annotating the same row once the destination is discovered', () => {
|
||||
const rows = [
|
||||
makeRow({
|
||||
paneKey: 'tab-1:pane-a',
|
||||
entry: { reportedCwd: '/repo/.claude/worktrees/scratch' } as Partial<AgentStatusEntry>
|
||||
})
|
||||
]
|
||||
// Why: the scratch checkout usually appears in a later catalog refresh than
|
||||
// the hook event that first reports its path.
|
||||
const beforeDiscovery = buildAgentLiveWorktreeMismatchCandidates({
|
||||
ownerWorktree: OWNER,
|
||||
ownerRepo: OWNER_REPO,
|
||||
visibleWorktrees: [OWNER],
|
||||
detected: undefined
|
||||
})
|
||||
expect(
|
||||
attachAgentLiveWorktreeMismatch(rows, {
|
||||
ownerWorktree: OWNER,
|
||||
ownerRepo: OWNER_REPO,
|
||||
candidates: beforeDiscovery
|
||||
})[0].liveWorktreeMismatch
|
||||
).toBeUndefined()
|
||||
|
||||
const afterDiscovery = buildAgentLiveWorktreeMismatchCandidates({
|
||||
ownerWorktree: OWNER,
|
||||
ownerRepo: OWNER_REPO,
|
||||
visibleWorktrees: [OWNER],
|
||||
detected: makeDetected([SCRATCH], { local: { authoritative: true, source: 'git' } })
|
||||
})
|
||||
expect(
|
||||
attachAgentLiveWorktreeMismatch(rows, {
|
||||
ownerWorktree: OWNER,
|
||||
ownerRepo: OWNER_REPO,
|
||||
candidates: afterDiscovery
|
||||
})[0].liveWorktreeMismatch?.destinationLabel
|
||||
).toBe('10572-live-repro')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ function seedTabs(store: ReturnType<typeof createTestStore>): void {
|
||||
|
||||
function setStatusWithMetadata(
|
||||
store: ReturnType<typeof createTestStore>,
|
||||
payload: { state: 'working' | 'done'; prompt?: string; agentType?: 'claude' | 'codex' | 'pi' },
|
||||
payload: { state: 'working' | 'done'; prompt: string; agentType?: 'claude' | 'codex' | 'pi' },
|
||||
metadata?: { reportedCwd?: string | null }
|
||||
): void {
|
||||
store
|
||||
@@ -46,31 +46,76 @@ describe('agent status reported cwd', () => {
|
||||
const store = createTestStore()
|
||||
seedTabs(store)
|
||||
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'go', agentType: 'claude' }, {
|
||||
reportedCwd: SCRATCH_CWD
|
||||
})
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'go', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD
|
||||
}
|
||||
)
|
||||
expect(reportedCwd(store)).toBe(SCRATCH_CWD)
|
||||
|
||||
// Why: a same-root transition with no cwd must not blank the known location.
|
||||
setStatusWithMetadata(store, { state: 'done', prompt: 'go', agentType: 'claude' })
|
||||
expect(reportedCwd(store)).toBe(SCRATCH_CWD)
|
||||
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'again', agentType: 'claude' }, {
|
||||
reportedCwd: OWNER_CWD
|
||||
})
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'again', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: OWNER_CWD
|
||||
}
|
||||
)
|
||||
expect(reportedCwd(store)).toBe(OWNER_CWD)
|
||||
})
|
||||
|
||||
it('publishes a fresh entry for an update that only moves the location', () => {
|
||||
const store = createTestStore()
|
||||
seedTabs(store)
|
||||
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'go', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: OWNER_CWD
|
||||
}
|
||||
)
|
||||
const before = store.getState().agentStatusByPaneKey[PANE_KEY]
|
||||
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'go', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD
|
||||
}
|
||||
)
|
||||
const after = store.getState().agentStatusByPaneKey[PANE_KEY]
|
||||
|
||||
// Why: sidebar cards diff entry identity, so a cwd-only move must publish a new object.
|
||||
expect(after).not.toBe(before)
|
||||
expect(after.reportedCwd).toBe(SCRATCH_CWD)
|
||||
expect(after.state).toBe(before.state)
|
||||
expect(after.prompt).toBe(before.prompt)
|
||||
})
|
||||
|
||||
it('clears the location when main sends an explicit null', () => {
|
||||
const store = createTestStore()
|
||||
seedTabs(store)
|
||||
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'go', agentType: 'claude' }, {
|
||||
reportedCwd: SCRATCH_CWD
|
||||
})
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'go', agentType: 'claude' }, {
|
||||
reportedCwd: null
|
||||
})
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'go', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD
|
||||
}
|
||||
)
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'go', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: null
|
||||
}
|
||||
)
|
||||
|
||||
expect(reportedCwd(store)).toBeUndefined()
|
||||
expect('reportedCwd' in store.getState().agentStatusByPaneKey[PANE_KEY]).toBe(false)
|
||||
@@ -81,13 +126,21 @@ describe('agent status reported cwd', () => {
|
||||
const store = createTestStore()
|
||||
seedTabs(store)
|
||||
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'root', agentType: 'claude' }, {
|
||||
reportedCwd: OWNER_CWD
|
||||
})
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'root', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: OWNER_CWD
|
||||
}
|
||||
)
|
||||
// Why: a foreign agentType while the root turn is live is nested child traffic.
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'child', agentType: 'codex' }, {
|
||||
reportedCwd: SCRATCH_CWD
|
||||
})
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'child', agentType: 'codex' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD
|
||||
}
|
||||
)
|
||||
|
||||
expect(store.getState().agentStatusByPaneKey[PANE_KEY].agentType).toBe('claude')
|
||||
expect(reportedCwd(store)).toBe(OWNER_CWD)
|
||||
@@ -98,9 +151,13 @@ describe('agent status reported cwd', () => {
|
||||
const store = createTestStore()
|
||||
seedTabs(store)
|
||||
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'root', agentType: 'claude' }, {
|
||||
reportedCwd: SCRATCH_CWD
|
||||
})
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'working', prompt: 'root', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD
|
||||
}
|
||||
)
|
||||
setStatusWithMetadata(store, { state: 'done', prompt: 'root', agentType: 'claude' })
|
||||
setStatusWithMetadata(store, { state: 'working', prompt: 'next', agentType: 'codex' })
|
||||
|
||||
@@ -112,9 +169,13 @@ describe('agent status reported cwd', () => {
|
||||
const store = createTestStore()
|
||||
seedTabs(store)
|
||||
|
||||
setStatusWithMetadata(store, { state: 'done', prompt: 'finished', agentType: 'claude' }, {
|
||||
reportedCwd: SCRATCH_CWD
|
||||
})
|
||||
setStatusWithMetadata(
|
||||
store,
|
||||
{ state: 'done', prompt: 'finished', agentType: 'claude' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD
|
||||
}
|
||||
)
|
||||
|
||||
const retained = collectHibernatedCompletionEvidenceForWorktree(store.getState(), 'wt-1', [
|
||||
PANE_KEY
|
||||
@@ -130,19 +191,17 @@ describe('agent status reported cwd', () => {
|
||||
const store = createTestStore()
|
||||
seedTabs(store)
|
||||
|
||||
store
|
||||
.getState()
|
||||
.setAgentStatus(
|
||||
PANE_KEY,
|
||||
{ state: 'working', prompt: 'sleep me', agentType: 'codex' },
|
||||
undefined,
|
||||
undefined,
|
||||
{ tabId: 'tab-1', worktreeId: 'wt-1' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD,
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' }
|
||||
}
|
||||
)
|
||||
store.getState().setAgentStatus(
|
||||
PANE_KEY,
|
||||
{ state: 'working', prompt: 'sleep me', agentType: 'codex' },
|
||||
undefined,
|
||||
undefined,
|
||||
{ tabId: 'tab-1', worktreeId: 'wt-1' },
|
||||
{
|
||||
reportedCwd: SCRATCH_CWD,
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' }
|
||||
}
|
||||
)
|
||||
|
||||
const records = collectSleepingAgentSessionRecordsForWorktree(store.getState(), 'wt-1')
|
||||
expect(records[PANE_KEY]).toBeDefined()
|
||||
|
||||
@@ -56,9 +56,9 @@ describe('extractAgentReportedCwdUpdate', () => {
|
||||
it('accepts the workspaceRoot aliases and honours alias precedence', () => {
|
||||
expect(extractAgentReportedCwdUpdate({ workspaceRoot: '/repo/a' })).toBe('/repo/a')
|
||||
expect(extractAgentReportedCwdUpdate({ workspace_root: '/repo/b' })).toBe('/repo/b')
|
||||
expect(
|
||||
extractAgentReportedCwdUpdate({ cwd: '/repo/a', workspaceRoot: '/repo/b' })
|
||||
).toBe('/repo/a')
|
||||
expect(extractAgentReportedCwdUpdate({ cwd: '/repo/a', workspaceRoot: '/repo/b' })).toBe(
|
||||
'/repo/a'
|
||||
)
|
||||
expect(
|
||||
extractAgentReportedCwdUpdate({ workspaceRoot: '/repo/b', workspace_root: '/repo/c' })
|
||||
).toBe('/repo/b')
|
||||
|
||||
Reference in New Issue
Block a user