import { beforeEach, describe, expect, it, vi } from 'vitest' import { createElement } from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { setPersistedOpen } from '@ui' import type { ActivityCard, HITLCard, Message } from '../src/contracts' import { Conversation } from '../src/components/Conversation' import { ConversationWork } from '../src/components/ConversationWork' import { ProcessTimeline } from '../src/components/ProcessTimeline' import { store, type LiveState } from '../src/store' // SSR has no browser subscription snapshot. Keep the production store/actions and // real UI renderers; substitute only the hook boundary with a complete snapshot. const snapshot = vi.hoisted(() => ({ state: undefined as ReturnType | undefined })) vi.mock('../src/store', async importOriginal => { const actual = await importOriginal() return { ...actual, useStore: (select: (s: ReturnType) => unknown) => select(snapshot.state!) } }) const timestamp = '2026-10-12T10:00:00Z' function card(over: Partial = {}): ActivityCard { return { id: 'card_1', projectId: 'prj_1', conversationId: 'cnv_1', kind: 'progress', title: '整理项目材料', status: 'active', tone: 'info', body: [{ type: 'kv', kv: [['状态', '进行中'], ['正在做', '核对会议结论'], ['材料键', 'private-material-key']] }], createdAt: timestamp, updatedAt: timestamp, ...over, } } function reply(parts: Message['parts']): Message { return { id: 'msg_1', conversationId: 'cnv_1', role: 'assistant', runId: 'run_1', createdAt: timestamp, parts } } function hitl(over: Partial = {}): HITLCard { return { id: 'hitl_detail1', runId: 'run_1', conversationId: 'cnv_1', kind: 'confirm', question: '执行工具 run_shell?', detail: '{"command":"printf confirm-record"}', choices: [{ id: 'yes', label: '执行', description: '按已确认的参数调用', impact: '会访问外部资源并返回结果' }, { id: 'no', label: '取消', description: '跳过本次调用', impact: '不会执行本次操作' }], allowFreeInput: false, createdAt: timestamp, resolved: false, ...over } } function render() { return renderToStaticMarkup(createElement(Conversation)) } function renderWork() { return renderToStaticMarkup(createElement(ConversationWork)) } function completedResults(): ActivityCard[] { // The host lists cards newest-first; submit_card publishes markdown results. return [ card({ id: 'result_latest', kind: 'result', title: '最新整理成果', status: 'done', tone: 'ok', updatedAt: '2026-10-12T12:00:00Z', body: [{ type: 'markdown', markdown: '最新成果的主要结论' }] }), card({ id: 'result_older', kind: 'result', title: '先前整理成果', status: 'done', tone: 'ok', pinned: true, updatedAt: '2026-10-12T10:00:00Z', body: [{ type: 'markdown', markdown: '先前成果的主要结论' }] }), ] } beforeEach(() => { setPersistedOpen('hitl-details:cnv_1:hitl_detail1', false) setPersistedOpen('conversation-work:prj_1:cnv_1', false) const base = store.getState() snapshot.state = { ...base, currentProjectId: 'prj_1', currentConvId: 'cnv_1', connected: true, tasks: [{ id: 'cnv_1', projectId: 'prj_1', title: '讨论项目', createdAt: timestamp, updatedAt: timestamp }], messages: [], cards: [], runs: [], activeHitl: [], live: null, artifacts: [], outbox: [], posting: false, ui: { ...base.ui, rightCollapsed: true, autoCollapseProcess: true }, cardActionStates: {}, } }) describe('conversation work reading', () => { it('keeps ordinary Q&A free of an empty progress object', () => { snapshot.state!.messages = [reply([{ type: 'text', text: '可以先确定项目目标' }])] const html = render() expect(html).toContain('可以先确定项目目标') expect(html).not.toContain('data-conversation-work') expect(html).not.toContain('data-testid="input-status"') }) it('shows the current real work with the auxiliary pane closed', () => { snapshot.state!.cards = [card()] const html = render() expect(html).toContain('整理项目材料') expect(html).toContain('核对会议结论') expect(html).not.toContain('private-material-key') }) it('shows the newest completed result by default with older results in closed history', () => { snapshot.state!.cards = completedResults() const html = renderWork() expect(html).toContain('data-card-id="result_latest"') expect(html).toContain('最新成果的主要结论') expect(html).not.toContain('data-card-id="result_older"') expect(html).not.toContain('先前成果的主要结论') expect(html).toContain('更多工作与成果 · 1 项') setPersistedOpen('conversation-work:prj_1:cnv_1', true) expect(renderWork()).toContain('先前成果的主要结论') }) it('keeps the newest result ahead of an already resolved confirmation', () => { snapshot.state!.cards = [card({ id: 'confirm_resolved', kind: 'confirm', status: 'resolved', title: '已处理确认', updatedAt: '2026-10-12T13:00:00Z' }), ...completedResults()] const html = renderWork() expect(html).toContain('最新成果的主要结论') expect(html).not.toContain('data-card-id="confirm_resolved"') expect(html).toContain('更多工作与成果 · 2 项') }) it.each([false, true])('keeps active work ahead of completed results (pinned=%s)', pinned => { const [latest, older] = completedResults() snapshot.state!.cards = [latest, card({ id: 'work_active', title: '正在核验材料', updatedAt: '2026-10-12T11:00:00Z', body: [{ type: 'kv', kv: [['正在做', '核对活动工作的结论']] }] }), older] if (pinned) snapshot.state!.cards.push(card({ id: 'work_pinned', pinned: true, title: '优先核验材料', updatedAt: '2026-10-12T09:00:00Z', body: [{ type: 'kv', kv: [['正在做', '核对固定工作的结论']] }] })) const html = renderWork() expect(html).toContain(pinned ? '核对固定工作的结论' : '核对活动工作的结论') expect(html).not.toContain('data-card-id="result_latest"') expect(html).not.toContain('data-card-id="result_older"') if (pinned) expect(html).not.toContain('data-card-id="work_active"') }) it('leaves completed results to the visible right work pane', () => { snapshot.state!.ui.rightCollapsed = false snapshot.state!.cards = completedResults() const html = renderWork() expect(html).toBe('') }) it('keeps the work card in one detail location when the right work pane is visible', () => { snapshot.state!.ui.rightCollapsed = false snapshot.state!.cards = [card()] const html = render() expect(html).not.toContain('data-conversation-work') expect(html).not.toContain('核对会议结论') snapshot.state!.ui.rightCollapsed = true expect(render()).toContain('核对会议结论') }) it('keeps the conversation detail when the right pane is reading files', () => { snapshot.state!.ui.rightCollapsed = false snapshot.state!.ui.rightTab = 'files' snapshot.state!.cards = [card()] expect(render()).toContain('核对会议结论') }) it('keeps secondary message operations available through progressive disclosure', () => { snapshot.state!.messages = [reply([{ type: 'text', text: '已经整理好要点' }])] const html = render() expect(html).toContain('已经整理好要点') expect(html).toContain('消息操作') expect(html).not.toContain('排除出上下文') expect(html).not.toContain('重新生成') }) it('keeps failure impact and coverage limits visible without expanding history', () => { snapshot.state!.cards = [card({ status: 'failed', tone: 'danger', body: [ { type: 'kv', kv: [['状态', '失败'], ['当前发现', '已整理两份材料'], ['剩余', '评审意见未能读取,完整结论尚未生成']] }, ] })] const html = render() expect(html).toContain('已整理两份材料') expect(html).toContain('评审意见未能读取,完整结论尚未生成') }) it('keeps a completed result limitation visible while another task is active', () => { snapshot.state!.cards = [card(), card({ id: 'card_partial', status: 'done', tone: 'warn', title: '已整理部分材料', body: [{ type: 'kv', kv: [['当前发现', '两份材料已整理'], ['剩余', '第三份材料未能读取,请补充清晰版本']] }], })] const html = render() expect(html).toContain('第三份材料未能读取,请补充清晰版本') }) it('hides running command records while keeping the final reply readable', () => { const live: LiveState = { runId: 'run_1', phase: 'responding', startedAt: Date.now(), agentNotes: {}, items: [ { kind: 'tool', name: 'run_shell', callId: 'call_1', args: 'secret-command-argument', output: 'raw-command-output', state: 'running', startedAt: Date.now(), endedAt: 0 }, { kind: 'final' }, { kind: 'text', index: 1, text: '已找到需要补充的两项要求' }, ] } snapshot.state!.live = live const html = render() expect(html).toContain('已找到需要补充的两项要求') expect(html).not.toContain('secret-command-argument') expect(html).not.toContain('raw-command-output') expect(html).toContain('执行记录') }) it('uses a real compact stage without exposing an internal tool name', () => { snapshot.state!.live = { runId: 'run_1', phase: 'acting', startedAt: Date.now(), agentNotes: {}, items: [ { kind: 'tool', name: 'run_shell', callId: 'call_1', args: '', output: '', state: 'running', startedAt: Date.now(), endedAt: 0 }, ] } const html = render() expect(html).toContain('正在处理') expect(html).not.toContain('run_shell') }) it('exposes anchored required confirmation outside work history', () => { snapshot.state!.messages = [{ ...reply([{ type: 'text', text: '请整理资料' }]), role: 'user' }] snapshot.state!.cards = [card({ kind: 'confirm', status: 'action_required', anchorMessageId: 'msg_1', title: '新增一份项目草稿', actions: [{ id: 'confirm', label: '确认新增', style: 'primary' }] })] const html = render() expect(html).toContain('新增一份项目草稿') expect(html).toContain('确认新增') }) it('shows only a location link for anchored confirmation when the work pane owns its actions', () => { snapshot.state!.ui.rightCollapsed = false snapshot.state!.messages = [{ ...reply([{ type: 'text', text: '请整理资料' }]), role: 'user' }] snapshot.state!.cards = [card({ kind: 'confirm', status: 'action_required', anchorMessageId: 'msg_1', title: '新增一份项目草稿', actions: [{ id: 'confirm', label: '确认新增', style: 'primary' }] })] const html = render() expect(html).toContain('新增一份项目草稿') expect(html).toContain('在工作栏处理') expect(html).not.toContain('确认新增') }) it('keeps a submitted card as a location link when its full body is in the work pane', () => { snapshot.state!.ui.rightCollapsed = false snapshot.state!.cards = [card({ id: 'result_1', kind: 'result', title: '材料核对结果', status: 'done', body: [{ type: 'markdown', markdown: '材料核对的完整正文' }] })] snapshot.state!.messages = [reply([{ type: 'card', card: { cardId: 'result_1', kind: 'result', data: [] } }])] const html = render() expect(html).toContain('材料核对结果') expect(html).not.toContain('展开全文') snapshot.state!.ui.rightCollapsed = true expect(render()).toContain('展开全文') }) it('keeps a required confirmation accessible when its anchor is an assistant message', () => { snapshot.state!.messages = [reply([{ type: 'text', text: '准备新增项目草稿' }])] snapshot.state!.cards = [card({ kind: 'confirm', status: 'action_required', anchorMessageId: 'msg_1', title: '新增一份项目草稿', actions: [{ id: 'confirm', label: '确认新增', style: 'primary' }] })] expect(render()).toContain('确认新增') }) it('keeps a thinking-only record available for deliberate review', () => { const html = renderToStaticMarkup(createElement(ProcessTimeline, { parts: [{ type: 'thinking', process: true, text: 'private-internal-deliberation' }], live: true, idKey: 'thinking-test', renderPart: () => null, })) expect(html).toContain('private-internal-deliberation') snapshot.state!.messages = [reply([{ type: 'thinking', process: true, text: 'private-internal-deliberation' }, { type: 'text', text: '已完成核对' }])] expect(render()).toContain('执行记录') }) it('lets readers reopen the full output of a grouped read action', () => { setPersistedOpen('tool-record:read-history:call-read:0', true) const html = renderToStaticMarkup(createElement(ProcessTimeline, { parts: [{ type: 'tool_call', process: true, toolCall: { id: 'call-read', name: 'fs_read_file', arguments: '{"path":"材料.txt"}' }, toolOutput: 'full-read-output' } as Message['parts'][number]], live: false, idKey: 'read-history', renderPart: () => null, })) expect(html).toContain('full-read-output') }) it('blocks repeated HITL choices when the shared action result is unknown', () => { snapshot.state!.cardActionStates = { 'hitl:hitl_1': 'unknown' } snapshot.state!.activeHitl = [{ id: 'hitl_1', runId: 'run_1', kind: 'choice', question: '选择整理范围', conversationId: 'cnv_1', allowFreeInput: false, createdAt: timestamp, resolved: false, choices: [{ id: 'all', label: '全部材料', description: '整理三份材料', impact: '生成一份草稿' }], }] const html = render() expect(html).toMatch(/]*disabled=""[^>]*>[\s\S]*?全部材料/) expect(html).toContain('正在核实处理结果') }) it('folds command details while retaining the confirmation and consequences', () => { snapshot.state!.activeHitl = [hitl()] const html = render() expect(html).not.toContain('confirm-record') expect(html).toContain('执行工具 run_shell?') expect(html).toContain('按已确认的参数调用') expect(html).toContain('会访问外部资源并返回结果') expect(html).toContain('操作详情') }) it('makes the original command detail readable after the user expands it', () => { snapshot.state!.activeHitl = [hitl()] setPersistedOpen('hitl-details:cnv_1:hitl_detail1', true) const html = render() expect(html).toMatch(/aria-expanded="true"[^>]*>[\s\S]*?操作详情/) expect(html).toContain('confirm-record') }) it('keeps a blocking restriction visible outside the operation detail fold', () => { snapshot.state!.activeHitl = [hitl({ blocked: true, blockReason: '目标文件需要先完成评审,当前不能覆盖' })] const html = render() expect(html).toContain('目标文件需要先完成评审,当前不能覆盖') expect(html).not.toContain('confirm-record') }) it('folds the exact workflow script suffix supplied in the question', () => { snapshot.state!.activeHitl = [hitl({ question: '运行这段脚本?\nprint("workflow-record-script")', detail: undefined })] const html = render() expect(html).toContain('运行这段脚本?') expect(html).toContain('会访问外部资源并返回结果') expect(html).not.toContain('workflow-record-script') expect(html).toContain('操作详情') }) it('retains the full workflow script and separate detail on deliberate expansion', () => { snapshot.state!.activeHitl = [hitl({ question: '运行这段脚本?\nprint("workflow-record-script")' })] setPersistedOpen('hitl-details:cnv_1:hitl_detail1', true) const html = render() expect(html).toContain('workflow-record-script') expect(html).toContain('confirm-record') expect(html).toMatch(/aria-expanded="true"[^>]*>[\s\S]*?操作详情/) }) it('retains multiline business consequences in an ordinary question', () => { snapshot.state!.activeHitl = [hitl({ question: '批准计划并继续执行?\n会覆盖旧草稿,原件保留', detail: undefined })] expect(render()).toContain('会覆盖旧草稿,原件保留') }) })