mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
635a966c3e
This folds together the desktop-side fixes needed before broader rollout. Session resume no longer deadlocks waiting on init, Mermaid and inline image output render inside chat, task and sub-agent state stay visible during execution, local build/release paths are safer, and Feishu/Telegram now expose lightweight mobile commands (/help, /status, /clear) without adding a new adapter-specific protocol. Constraint: Desktop releases must publish updater artifacts from non-draft GitHub releases Constraint: IM commands need short, phone-friendly responses and low operational complexity Rejected: Add a dedicated IM command API surface | re-used existing slash commands and session/task REST endpoints to keep adapters thin Rejected: Wait for task_update push events in WebUI | added low-risk polling because the current frontend ignores that event path Confidence: medium Scope-risk: broad Reversibility: clean Directive: Keep IM command replies terse and mobile-first, and merge local fallback slash commands when server-provided lists are partial Tested: cd desktop && bun x vitest run src/components/chat/MermaidRenderer.test.tsx src/components/markdown/MarkdownRenderer.test.tsx Tested: cd desktop && bun x vitest run src/components/chat/composerUtils.test.ts src/pages/ActiveSession.test.tsx src/stores/chatStore.test.ts Tested: cd desktop && bun run lint Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "SDK init arrives only after the first user turn" --timeout 60000 Tested: cd adapters && bun test common/ feishu/ telegram/ Tested: cd adapters && bunx tsc --noEmit Not-tested: Full GitHub Actions release run on all three desktop platforms Not-tested: Local DMG packaging end-to-end on Apple Silicon Not-tested: Real Feishu/Telegram device sessions against a live adapter process
251 lines
6.5 KiB
TypeScript
251 lines
6.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { MessageEntry } from '../types/session'
|
|
|
|
const { sendMock } = vi.hoisted(() => ({
|
|
sendMock: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../api/websocket', () => ({
|
|
wsManager: {
|
|
connect: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
onMessage: vi.fn(() => () => {}),
|
|
clearHandlers: vi.fn(),
|
|
send: sendMock,
|
|
},
|
|
}))
|
|
|
|
vi.mock('../api/sessions', () => ({
|
|
sessionsApi: {
|
|
getMessages: vi.fn(async () => ({ messages: [] })),
|
|
getSlashCommands: vi.fn(async () => ({ commands: [] })),
|
|
},
|
|
}))
|
|
|
|
vi.mock('./teamStore', () => ({
|
|
useTeamStore: {
|
|
getState: () => ({
|
|
handleTeamCreated: vi.fn(),
|
|
handleTeamUpdate: vi.fn(),
|
|
handleTeamDeleted: vi.fn(),
|
|
}),
|
|
},
|
|
}))
|
|
|
|
vi.mock('./tabStore', () => ({
|
|
useTabStore: {
|
|
getState: () => ({
|
|
updateTabStatus: vi.fn(),
|
|
updateTabTitle: vi.fn(),
|
|
}),
|
|
},
|
|
}))
|
|
|
|
vi.mock('./sessionStore', () => ({
|
|
useSessionStore: {
|
|
getState: () => ({
|
|
updateSessionTitle: vi.fn(),
|
|
}),
|
|
},
|
|
}))
|
|
|
|
vi.mock('./cliTaskStore', () => ({
|
|
useCLITaskStore: {
|
|
getState: () => ({
|
|
fetchSessionTasks: vi.fn(),
|
|
tasks: [],
|
|
clearTasks: vi.fn(),
|
|
setTasksFromTodos: vi.fn(),
|
|
markCompletedAndDismissed: vi.fn(),
|
|
refreshTasks: vi.fn(),
|
|
}),
|
|
},
|
|
}))
|
|
|
|
import { mapHistoryMessagesToUiMessages, useChatStore } from './chatStore'
|
|
|
|
const TEST_SESSION_ID = 'test-session-1'
|
|
const initialState = useChatStore.getState()
|
|
|
|
describe('chatStore history mapping', () => {
|
|
beforeEach(() => {
|
|
sendMock.mockReset()
|
|
useChatStore.setState({
|
|
...initialState,
|
|
sessions: {},
|
|
})
|
|
})
|
|
|
|
it('preserves thinking blocks when restoring transcript history', () => {
|
|
const messages: MessageEntry[] = [
|
|
{
|
|
id: 'assistant-1',
|
|
type: 'assistant',
|
|
timestamp: '2026-04-06T00:00:00.000Z',
|
|
model: 'opus',
|
|
parentToolUseId: 'agent-1',
|
|
content: [
|
|
{ type: 'thinking', thinking: 'internal reasoning' },
|
|
{ type: 'text', text: '目录结构分析' },
|
|
{ type: 'tool_use', name: 'Read', id: 'tool-1', input: { file_path: 'src/App.tsx' } },
|
|
],
|
|
},
|
|
{
|
|
id: 'user-1',
|
|
type: 'user',
|
|
timestamp: '2026-04-06T00:00:01.000Z',
|
|
parentToolUseId: 'agent-1',
|
|
content: [
|
|
{ type: 'tool_result', tool_use_id: 'tool-1', content: 'ok', is_error: false },
|
|
],
|
|
},
|
|
]
|
|
|
|
const mapped = mapHistoryMessagesToUiMessages(messages)
|
|
|
|
expect(mapped.map((message) => message.type)).toEqual([
|
|
'thinking',
|
|
'assistant_text',
|
|
'tool_use',
|
|
'tool_result',
|
|
])
|
|
expect(mapped[2]).toMatchObject({ parentToolUseId: 'agent-1' })
|
|
expect(mapped[3]).toMatchObject({ parentToolUseId: 'agent-1' })
|
|
})
|
|
|
|
it('keeps parent tool linkage for live tool events', () => {
|
|
// Initialize the session first
|
|
useChatStore.setState({
|
|
sessions: {
|
|
[TEST_SESSION_ID]: {
|
|
messages: [],
|
|
chatState: 'idle',
|
|
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,
|
|
},
|
|
},
|
|
})
|
|
|
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
|
type: 'tool_use_complete',
|
|
toolName: 'Read',
|
|
toolUseId: 'tool-1',
|
|
input: { file_path: 'src/App.tsx' },
|
|
parentToolUseId: 'agent-1',
|
|
})
|
|
|
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
|
type: 'tool_result',
|
|
toolUseId: 'tool-1',
|
|
content: 'ok',
|
|
isError: false,
|
|
parentToolUseId: 'agent-1',
|
|
})
|
|
|
|
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
|
|
{
|
|
type: 'tool_use',
|
|
toolUseId: 'tool-1',
|
|
parentToolUseId: 'agent-1',
|
|
},
|
|
{
|
|
type: 'tool_result',
|
|
toolUseId: 'tool-1',
|
|
parentToolUseId: 'agent-1',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('sends permission mode updates to the active session only', () => {
|
|
useChatStore.getState().setSessionPermissionMode('nonexistent-session', 'acceptEdits')
|
|
expect(sendMock).not.toHaveBeenCalled()
|
|
|
|
useChatStore.setState({
|
|
sessions: {
|
|
'session-1': {
|
|
messages: [],
|
|
chatState: 'idle',
|
|
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,
|
|
},
|
|
},
|
|
})
|
|
useChatStore.getState().setSessionPermissionMode('session-1', 'acceptEdits')
|
|
|
|
expect(sendMock).toHaveBeenCalledWith('session-1', {
|
|
type: 'set_permission_mode',
|
|
mode: 'acceptEdits',
|
|
})
|
|
})
|
|
|
|
it('stores terminal task notifications for agent tool cards', () => {
|
|
useChatStore.setState({
|
|
sessions: {
|
|
[TEST_SESSION_ID]: {
|
|
messages: [],
|
|
chatState: 'idle',
|
|
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,
|
|
},
|
|
},
|
|
})
|
|
|
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
|
type: 'system_notification',
|
|
subtype: 'task_notification',
|
|
data: {
|
|
task_id: 'agent-task-1',
|
|
tool_use_id: 'agent-tool-1',
|
|
status: 'completed',
|
|
summary: 'Agent "修复异常处理" completed',
|
|
output_file: '/tmp/agent-output.txt',
|
|
},
|
|
})
|
|
|
|
expect(
|
|
useChatStore.getState().sessions[TEST_SESSION_ID]?.agentTaskNotifications[
|
|
'agent-tool-1'
|
|
],
|
|
).toMatchObject({
|
|
taskId: 'agent-task-1',
|
|
toolUseId: 'agent-tool-1',
|
|
status: 'completed',
|
|
summary: 'Agent "修复异常处理" completed',
|
|
outputFile: '/tmp/agent-output.txt',
|
|
})
|
|
})
|
|
})
|