refactor(gui): route the composer chain on PendingWait currency
This commit is contained in:
@@ -8,7 +8,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { act } from '@testing-library/react'
|
||||
import type { SessionId, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { hookOf } from './hook.ts'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { ToolViewRegistry } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
@@ -65,7 +66,7 @@ describe('MessageItem arms', () => {
|
||||
describe('small branch tails', () => {
|
||||
it('PendingCard approval reason renders when present', () => {
|
||||
const view = render(
|
||||
<PendingCard item={{ kind: 'approval', rpcId: 'r1' as RpcId, approvalId: 'a1', toolName: 'rm', reason: 'careful' }} />,
|
||||
<PendingCard item={new PendingWait('approval', RpcId('r1'), 's1' as SessionId, { approvalId: 'a1', toolName: 'rm', reason: 'careful' } as PendingWait<'approval'>['payload'], vi.fn())} />,
|
||||
)
|
||||
expect(view.getByText('careful')).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -9,6 +9,8 @@ import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import type {
|
||||
AssistantMessageNode, ConversationNode, ConversationSnapshot, RunningToolCall, SessionId, ToolResultNode, UserMessageNode,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { hookOf } from './hook.ts'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { ConvViewProps, SelectionTarget } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
@@ -288,11 +290,10 @@ describe('ChatView', () => {
|
||||
it('renders approval cards while questions stay in the composer', () => {
|
||||
const h = makeHarness({
|
||||
pending: [
|
||||
{ kind: 'approval', rpcId: 'r1' as never, approvalId: 'ap1', toolName: 'bash' },
|
||||
{
|
||||
kind: 'question', rpcId: 'r2' as never,
|
||||
questions: [{ id: 'mode', question: 'Composer only?', options: [{ label: 'Yes' }] }],
|
||||
},
|
||||
new PendingWait('approval', RpcId('r1'), SID,
|
||||
{ approvalId: 'ap1', toolName: 'bash' } as PendingWait<'approval'>['payload'], vi.fn()),
|
||||
new PendingWait('question', RpcId('r2'), SID,
|
||||
{ questions: [{ id: 'mode', question: 'Composer only?', options: [{ label: 'Yes' }] }] } as PendingWait<'question'>['payload'], vi.fn()),
|
||||
],
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
|
||||
@@ -21,9 +21,12 @@ import { EmptyState } from '../src/client/skeleton/EmptyState.tsx'
|
||||
afterEach(cleanup)
|
||||
|
||||
const SID = 's1' as SessionId
|
||||
/** Fallback-only renderSlot stub (no takeover registered in these benches). */
|
||||
const fallbackRenderSlot: ConversationSlotProps['renderSlot'] =
|
||||
/** Fallback-only chain stub (no takeover registered in these benches). */
|
||||
const fallbackRenderSlotChain: ConversationSlotProps['renderSlotChain'] =
|
||||
(_key, _owner, opts) => opts?.fallback ?? null
|
||||
/** Non-chain renderSlot stub: ConversationRoot renders no non-chain child keys. */
|
||||
const unusedRenderSlot: ConversationSlotProps['renderSlot'] =
|
||||
(() => { throw new Error('no non-chain child keys') }) as unknown as ConversationSlotProps['renderSlot']
|
||||
/** Standard-seat stub: ConversationRoot never renders it, delivery is mandatory in the props type. */
|
||||
const StubSessionProvider: ConversationSlotProps['SessionProvider'] = ({ children }) => <>{children(SID)}</>
|
||||
|
||||
@@ -81,7 +84,8 @@ describe('ConversationRoot branches', () => {
|
||||
openDetails={vi.fn()}
|
||||
loadOlder={vi.fn()}
|
||||
open={open}
|
||||
renderSlot={fallbackRenderSlot}
|
||||
renderSlot={unusedRenderSlot}
|
||||
renderSlotChain={fallbackRenderSlotChain}
|
||||
SessionProvider={StubSessionProvider}
|
||||
/>,
|
||||
)
|
||||
@@ -141,7 +145,8 @@ describe('ConversationRoot branches', () => {
|
||||
openDetails={vi.fn()}
|
||||
loadOlder={vi.fn()}
|
||||
open={vi.fn()}
|
||||
renderSlot={fallbackRenderSlot}
|
||||
renderSlot={unusedRenderSlot}
|
||||
renderSlotChain={fallbackRenderSlotChain}
|
||||
SessionProvider={StubSessionProvider}
|
||||
/>,
|
||||
)
|
||||
|
||||
@@ -12,8 +12,9 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { FC } from 'react'
|
||||
import { hookOf } from './hook.ts'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createSnapshotStore, PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ConversationSnapshot, PendingInteraction, SessionId, SessionListState } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationSlotProps, SelectionTarget, ViewEntry } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
// Export discipline: packages/client/AGENTS.md.
|
||||
@@ -102,7 +103,7 @@ describe('EmptyState', () => {
|
||||
describe('ConversationRoot', () => {
|
||||
function bench(
|
||||
views: ViewEntry[], activeView?: string, init: Partial<FakeSnapshot> = {},
|
||||
renderSlot?: ConversationSlotProps['renderSlot'],
|
||||
renderSlotChain?: ConversationSlotProps['renderSlotChain'],
|
||||
) {
|
||||
const { useSession } = fakeSession({ nodes: [{ kind: 'user' }, { kind: 'user' }], ...init })
|
||||
const { useSessions } = fakeSessions([
|
||||
@@ -133,7 +134,8 @@ describe('ConversationRoot', () => {
|
||||
openDetails={openDetails}
|
||||
loadOlder={loadOlder}
|
||||
open={open}
|
||||
renderSlot={renderSlot ?? ((_key, _owner, opts) => opts?.fallback ?? null)}
|
||||
renderSlot={(() => { throw new Error('no non-chain child keys') }) as unknown as ConversationSlotProps['renderSlot']}
|
||||
renderSlotChain={renderSlotChain ?? ((_key, _owner, opts) => opts?.fallback ?? null)}
|
||||
SessionProvider={StubSessionProvider}
|
||||
/>)
|
||||
return { ui, chat, send, stop, open }
|
||||
@@ -195,20 +197,22 @@ describe('ConversationRoot', () => {
|
||||
expect(send).toHaveBeenCalledWith('hi', 'queue')
|
||||
})
|
||||
|
||||
it('dispatches a pending question to the composer slot instead of rendering InputBar', () => {
|
||||
const renderSlot = vi.fn(() => <div>question takeover</div>) as unknown as ConversationSlotProps['renderSlot']
|
||||
it('dispatches the pending list to the composer chain instead of rendering InputBar', () => {
|
||||
const renderSlotChain = vi.fn(() => <div>question takeover</div>) as unknown as ConversationSlotProps['renderSlotChain']
|
||||
bench([view('chat', 'Chat')], undefined, {
|
||||
pending: [{
|
||||
kind: 'question', rpcId: 'rq' as never,
|
||||
questions: [{ id: 'mode', question: 'Choose?', options: [{ label: 'Fast' }] }],
|
||||
}],
|
||||
}, renderSlot)
|
||||
pending: [new PendingWait('question', RpcId('rq'), sid('s1'),
|
||||
{ questions: [{ id: 'mode', question: 'Choose?', options: [{ label: 'Fast' }] }] } as PendingWait<'question'>['payload'], vi.fn())],
|
||||
}, renderSlotChain)
|
||||
expect(screen.getByText('question takeover')).toBeTruthy()
|
||||
expect(screen.queryByPlaceholderText(/输入消息/)).toBeNull()
|
||||
expect(renderSlot).toHaveBeenCalledWith(
|
||||
// The owner dispatches the raw pending list (chain currency); routing
|
||||
// lives in entry selectors, not here.
|
||||
expect(renderSlotChain).toHaveBeenCalledWith(
|
||||
'conversation.composer',
|
||||
expect.objectContaining({ interaction: expect.objectContaining({ rpcId: 'rq' }) }),
|
||||
expect.objectContaining({ entryKey: 'question' }),
|
||||
expect.objectContaining({
|
||||
interactions: expect.arrayContaining([expect.objectContaining({ key: 'q:rq' })]),
|
||||
}),
|
||||
expect.objectContaining({ fallback: expect.anything() }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user