mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
This bundles the pending desktop/server team-session fixes with the local adapter recovery changes already in the worktree. The team path now keeps teammate membership stable under concurrent spawns, surfaces real teammate identities in the desktop UI, and allows direct interaction with member transcripts. The adapter changes recover automatically when stale thinking signatures invalidate an existing session. Constraint: Team config writes can happen concurrently while multiple reviewers spawn in parallel Constraint: Desktop member views must follow mailbox/transcript semantics rather than hijacking teammate runtime sessions Rejected: Keep relying on config.json alone for member discovery | in-process teammates can be lost after concurrent writes Rejected: Open teammate sessionIds as normal desktop sessions | would attach a second CLI instead of the running teammate Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Preserve locked team-file mutation for any future teammate registration path and keep teammate labels sourced from member names before agent types Tested: bun test src/server/__tests__/teams.test.ts src/server/__tests__/team-watcher.test.ts Tested: cd desktop && bun run test --run src/stores/chatStore.test.ts src/pages/ActiveSession.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Not-tested: Manual end-to-end validation against a live Agent Teams run in the desktop app
180 lines
5.2 KiB
TypeScript
180 lines
5.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { render } from '@testing-library/react'
|
|
import '@testing-library/jest-dom'
|
|
import { act } from 'react'
|
|
|
|
vi.mock('../components/chat/MessageList', () => ({
|
|
MessageList: () => <div data-testid="message-list" />,
|
|
}))
|
|
|
|
vi.mock('../components/chat/ChatInput', () => ({
|
|
ChatInput: () => <div data-testid="chat-input" />,
|
|
}))
|
|
|
|
vi.mock('../components/teams/TeamStatusBar', () => ({
|
|
TeamStatusBar: () => <div data-testid="team-status-bar" />,
|
|
}))
|
|
|
|
vi.mock('../components/chat/SessionTaskBar', () => ({
|
|
SessionTaskBar: () => <div data-testid="session-task-bar" />,
|
|
}))
|
|
|
|
import { ActiveSession } from './ActiveSession'
|
|
import { useChatStore } from '../stores/chatStore'
|
|
import { useCLITaskStore } from '../stores/cliTaskStore'
|
|
import { useSessionStore } from '../stores/sessionStore'
|
|
import { useTabStore } from '../stores/tabStore'
|
|
import { useTeamStore } from '../stores/teamStore'
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
useTabStore.setState({ tabs: [], activeTabId: null })
|
|
useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null })
|
|
useChatStore.setState({ sessions: {} })
|
|
useTeamStore.setState({ teams: [], activeTeam: null, memberColors: new Map(), error: null })
|
|
})
|
|
|
|
describe('ActiveSession task polling', () => {
|
|
it('refreshes CLI tasks repeatedly while a turn is active', async () => {
|
|
vi.useFakeTimers()
|
|
|
|
const sessionId = 'polling-session'
|
|
const originalCliTaskState = useCLITaskStore.getState()
|
|
const fetchSessionTasks = vi.fn().mockResolvedValue(undefined)
|
|
|
|
useCLITaskStore.setState({
|
|
sessionId,
|
|
tasks: [],
|
|
fetchSessionTasks,
|
|
})
|
|
|
|
useSessionStore.setState({
|
|
sessions: [{
|
|
id: sessionId,
|
|
title: 'Polling Session',
|
|
createdAt: '2026-04-10T00:00:00.000Z',
|
|
modifiedAt: '2026-04-10T00:00:00.000Z',
|
|
messageCount: 1,
|
|
projectPath: '',
|
|
workDir: null,
|
|
workDirExists: true,
|
|
}],
|
|
activeSessionId: sessionId,
|
|
isLoading: false,
|
|
error: null,
|
|
})
|
|
useTabStore.setState({
|
|
tabs: [{ sessionId, title: 'Polling Session', type: 'session', status: 'idle' }],
|
|
activeTabId: sessionId,
|
|
})
|
|
useChatStore.setState({
|
|
sessions: {
|
|
[sessionId]: {
|
|
messages: [],
|
|
chatState: 'thinking',
|
|
connectionState: 'connected',
|
|
streamingText: '',
|
|
streamingToolInput: '',
|
|
activeToolUseId: null,
|
|
activeToolName: null,
|
|
activeThinkingId: null,
|
|
pendingPermission: null,
|
|
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
|
elapsedSeconds: 0,
|
|
statusVerb: '',
|
|
slashCommands: [],
|
|
agentTaskNotifications: {},
|
|
elapsedTimer: null,
|
|
},
|
|
},
|
|
})
|
|
|
|
const { unmount } = render(<ActiveSession />)
|
|
|
|
expect(fetchSessionTasks).toHaveBeenCalledWith(sessionId)
|
|
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(2200)
|
|
})
|
|
|
|
expect(
|
|
fetchSessionTasks.mock.calls.filter(([currentSessionId]) => currentSessionId === sessionId),
|
|
).toHaveLength(3)
|
|
|
|
unmount()
|
|
useCLITaskStore.setState(originalCliTaskState)
|
|
})
|
|
|
|
it('keeps member sessions interactive and skips leader task polling', () => {
|
|
const memberSessionId = 'team-member:security-reviewer@test-team'
|
|
const originalCliTaskState = useCLITaskStore.getState()
|
|
const fetchSessionTasks = vi.fn().mockResolvedValue(undefined)
|
|
|
|
useCLITaskStore.setState({
|
|
sessionId: null,
|
|
tasks: [],
|
|
fetchSessionTasks,
|
|
})
|
|
|
|
useTeamStore.setState({
|
|
teams: [],
|
|
activeTeam: {
|
|
name: 'test-team',
|
|
leadAgentId: 'team-lead@test-team',
|
|
leadSessionId: 'leader-session',
|
|
members: [
|
|
{
|
|
agentId: 'team-lead@test-team',
|
|
role: 'team-lead',
|
|
status: 'running',
|
|
sessionId: 'leader-session',
|
|
},
|
|
{
|
|
agentId: 'security-reviewer@test-team',
|
|
role: 'security-reviewer',
|
|
status: 'running',
|
|
},
|
|
],
|
|
},
|
|
memberColors: new Map(),
|
|
error: null,
|
|
})
|
|
|
|
useTabStore.setState({
|
|
tabs: [{ sessionId: memberSessionId, title: 'security-reviewer', type: 'session', status: 'idle' }],
|
|
activeTabId: memberSessionId,
|
|
})
|
|
|
|
useChatStore.setState({
|
|
sessions: {
|
|
[memberSessionId]: {
|
|
messages: [],
|
|
chatState: 'thinking',
|
|
connectionState: 'connected',
|
|
streamingText: '',
|
|
streamingToolInput: '',
|
|
activeToolUseId: null,
|
|
activeToolName: null,
|
|
activeThinkingId: null,
|
|
pendingPermission: null,
|
|
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
|
elapsedSeconds: 0,
|
|
statusVerb: '',
|
|
slashCommands: [],
|
|
agentTaskNotifications: {},
|
|
elapsedTimer: null,
|
|
},
|
|
},
|
|
})
|
|
|
|
const { queryByTestId, unmount } = render(<ActiveSession />)
|
|
|
|
expect(queryByTestId('chat-input')).toBeInTheDocument()
|
|
expect(queryByTestId('session-task-bar')).not.toBeInTheDocument()
|
|
expect(fetchSessionTasks).not.toHaveBeenCalled()
|
|
|
|
unmount()
|
|
useCLITaskStore.setState(originalCliTaskState)
|
|
})
|
|
})
|