import assert from 'node:assert/strict' import type { ChatMessage } from '../src/shared/types' import type { SubagentSnapshot } from '../src/shared/subagents' import { collectTurnMembers, projectMemberMessage, selectComposerTeam, sortMembers } from '../src/renderer/src/workbuddy/subagents/member-view' import { useWorkbenchStore } from '../src/renderer/src/stores/workbench' let checks = 0 const check = (name: string, condition: unknown): void => { assert.ok(condition, name); checks++; console.log('PASS ' + name) } const base: SubagentSnapshot = { id: 'a', name: '分析', task: '检查消息', status: 'running', access: 'read-only', files: [], modelName: 'local', createdAt: 1, events: [], tools: [] } const parent: ChatMessage = { id: 'parent-a', role: 'assistant', content: 'parent', createdAt: 1, parts: [ { type: 'subagent', agent: { ...base, status: 'completed' } }, { type: 'subagent', agent: { ...base, id: 'b', name: '验收', status: 'queued' } }, { type: 'subagent', agent: base } ] } const members = collectTurnMembers(parent) check('same child snapshot upserts without duplicating membership', members.length === 2 && members[0].agent === base) check('membership remains scoped to its own assistant turn', collectTurnMembers({ ...parent, id: 'next-turn', parts: [] }).length === 0) const next: ChatMessage = { ...parent, id: 'next-turn', parts: [], status: 'streaming' } check('input activity belongs to the active assistant rather than an older team', selectComposerTeam([parent, next], next.id) === undefined) check('an idle latest reply without children does not resurrect an earlier team', selectComposerTeam([parent, { ...next, status: 'done' }]) === undefined) check('hydration with a missing active assistant never falls back to stale members', selectComposerTeam([parent], 'not-loaded') === undefined) check('latest completed team remains reachable from the input area', selectComposerTeam([{ ...parent, status: 'done' }])?.assistantId === parent.id && selectComposerTeam([parent])?.members.length === 2) const completed = [{ ...members[0], agent: { ...base, status: 'completed' as const } }, members[1]] const sorted = sortMembers(completed) check('running or queued members sort first without changing task ordinals', sorted[0].agent.id === 'b' && sorted[0].ordinal === 2 && completed[0].agent.id === 'a') const tool = { id: '1:tool', name: 'read_file', args: { path: 'src/a.ts' }, argsRaw: '{"path":"src/a.ts"}', status: 'ok' as const, output: 'REAL_FILE_OUTPUT' } const child = { ...base, events: [ { id: 'thought', kind: 'reasoning' as const, text: '先核对入口' }, { id: tool.id, kind: 'tool' as const, name: tool.name, status: 'ok' as const, text: 'DISPLAY_SUMMARY' }, { id: 'reply', kind: 'text' as const, text: 'partial reply' } ], tools: [tool], result: 'complete reply', status: 'completed' as const, finishedAt: 2000 } const before = JSON.stringify(child) const projected = projectMemberMessage(child) check('detail preserves thinking, tool and reply order', projected.parts?.map(part => part.type).join(',') === 'reasoning,tool,text') check('detail uses actual child tool arguments and output', projected.tools?.[0] === tool && projected.tools[0].output === 'REAL_FILE_OUTPUT') check('complete child result replaces its truncated streaming tail exactly once', projected.content === 'complete reply' && projected.parts?.filter(part => part.type === 'text').length === 1) check('projection never mutates the persisted child snapshot', JSON.stringify(child) === before) check('member reasoning is finished only for a terminal child', projected.parts?.[0].type === 'reasoning' && projected.parts[0].finished === true && projectMemberMessage({ ...child, status: 'running' }).status === 'streaming') check('legacy snapshots retain real tool records when events are absent', projectMemberMessage({ ...base, tools: [tool] }).tools?.[0] === tool) check('cancelled children remain cancelled in the detail view', projectMemberMessage({ ...base, status: 'cancelled' }).status === 'cancelled') const api = useWorkbenchStore.getState() api.openView('session-a', 'fileTree') const tab = useWorkbenchStore.getState().sessions['session-a'].activeTabId! api.openMember('session-a', 'parent-a', 'a') check('opening a member preserves existing workbench tabs and opens its surface', useWorkbenchStore.getState().sessions['session-a'].member?.agentId === 'a' && useWorkbenchStore.getState().sessions['session-a'].open && useWorkbenchStore.getState().sessions['session-a'].tabs.length === 1) api.openMember('session-b', 'parent-b', 'b') check('member selection is isolated between conversations', useWorkbenchStore.getState().sessions['session-a'].member?.agentId === 'a' && useWorkbenchStore.getState().sessions['session-b'].member?.agentId === 'b') api.select('session-a', tab) check('selecting a workbench tab exits the member surface without removing child history', !useWorkbenchStore.getState().sessions['session-a'].member && useWorkbenchStore.getState().sessions['session-a'].activeTabId === tab && useWorkbenchStore.getState().sessions['session-b'].member?.agentId === 'b') api.openMember('session-a', 'parent-a', 'a') await api.openResource('session-a', 'session-a', 'artifact:demo', undefined, { kind: 'artifact', path: 'artifact:demo', name: '产物', content: 'content' }) check('opening an artifact from child tools replaces the member surface', !useWorkbenchStore.getState().sessions['session-a'].member && useWorkbenchStore.getState().sessions['session-a'].tabs.length === 2) api.closeMember('session-b') check('closing a member preserves its conversation workbench', !useWorkbenchStore.getState().sessions['session-b'].member) console.log('SUBAGENT_DISPLAY_PASS ' + JSON.stringify({ checks }))