Files
deepseek-harness/packages/client/ui-conversation/tests/coverage-tails.spec.tsx
imccyu bbde18caff refactor(gui): dissolve the tool ring into per-view keyed slots
Four rounds of structural rework on the conversation surface, converging
on one registration model for the whole client:

- Review fixes: open() leaves the inject factory (SessionsService owns
  the semantic); ConversationService mounts via ctx.plugin(); the
  bespoke view registry retires into the 'conversation.view' list slot.
- Ring alignment: createChatView factory retired (components get
  everything through checkable shares at the register call site); the
  hand-rolled t/i18n threading is deleted wholesale — a future
  framework-level i18n will supply t as a standard prop keyed by slot
  name, so no interim manual channel.
- Toolview dissolution: ToolViewRegistry / ToolViewResolver /
  ToolViewOutlet / ctx.toolviews retire. Tool rows are entries of the
  'conversation.chat.toolview' keyed slot (scope: session) declared by
  the chat entry; ToolRowOwnerProps is the unified owner payload;
  GenericToolCard becomes the call-site fallback; registrants are plain
  plugins (inject ['slots','conversation'] as the load-order seam);
  session-dimension dispatch moves into components (useSessions reads
  parentId); trajectory/waterfall gain same-shape slots the day they
  render tool rows (RendersCheck rejects empty declarations). Slot
  names mirror the composition path (<domain>.<entry>.<hole>).
- Staging follows current: cell()/binding() are pure resolution
  (render-safe); the constructor subscribes to the list store and
  followCurrent opens the event window when the current session
  changes — staging IS the open signal, business verbs are the timing,
  React render/commit is decoupled from window lifecycle. A masked
  current (projection gap) keeps the stage untouched so deferred
  teardown semantics survive reconnects.

Agent Note: .agents/notes/implemented/architecture/
2026-07-23-toolview-dissolution.md (bilingual pair) records the
decision, the four rejected alternatives, and the accepted semantic
changes; the web client architecture note and packages/client/AGENTS.md
carry the current-state narrative.

Verified: typecheck 0, duplication 0 clones (478 files), full coverage
run 6190 passed with zero threshold errors, knip 0, doc-sync 24/24,
client aggregate tsc 0, render-count checks (one commit per chunk, zero
row re-renders under streaming) green.
2026-07-23 17:49:47 +08:00

99 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
// Branch tails the acceptance specs do not reach: ToolRow stopped-state dot,
// PendingCard question arm, bash sample error pill, the node-half empty
// apply, and AssistantMarkdown reasoning/unknown block arms.
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, render } from '@testing-library/react'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type { SessionId, SessionListState, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
import type { RpcId } from '@deepseek-ai/dsh-client-connection/client'
import type { ToolRowOwnerProps, ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { apply as nodeApply } from '../src/index.ts'
import { GenericToolCard } from '../src/client/chat/GenericToolCard.tsx'
import { ToolRow } from '../src/client/chat/ToolRow.tsx'
import { PendingCard } from '../src/client/chat/PendingCard.tsx'
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
import { BashRow } from '../src/client/toolviews/bash-sample.tsx'
afterEach(cleanup)
describe('tails', () => {
it('node-half apply is an intentional no-op', () => {
expect(nodeApply()).toBeUndefined()
})
it('ToolRow stopped state renders the warning dot in the leading slot', () => {
const view = render(
<ToolRow variant="bash" icon={<i data-testid="icon" />} title="Bash" summary="s" body={null} state="stopped" />,
)
expect(view.queryByTestId('icon')).toBeNull()
expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
})
it('PendingCard renders the question arm with its count', () => {
const view = render(
<PendingCard item={{ kind: 'question', rpcId: 'r1' as RpcId, questions: [{}, {}] }} />,
)
expect(view.getByText(/等待回答2 题)/)).toBeTruthy()
})
it('AssistantMarkdown renders reasoning as a Think row and unknown blocks as JSON fallback', () => {
const view = render(
<AssistantMarkdown
blocks={[
{ kind: 'reasoning', text: 'thinking hard\nsecond line' },
{ kind: 'tool-call', callId: 'c', name: 'bash', argsRaw: '{}' },
{ kind: 'other', block: { type: 'mystery' } },
]}
streaming
/>,
)
expect(view.getByText('Think')).toBeTruthy()
expect(view.getByText('thinking hard')).toBeTruthy()
expect(view.getByText(/未知内容块/)).toBeTruthy()
const stopped = render(
<AssistantMarkdown blocks={[{ kind: 'text', text: 'partial words' }]} streaming={false} interrupted />,
)
expect(stopped.getByText('已停止')).toBeTruthy()
})
it('a settled others-variant row renders the sparkle icon in the leading slot', () => {
const settled: ToolResultNode = {
kind: 'tool-result', seq: 2, callId: 'c5',
call: { name: 'todo_write', argsRaw: '{"note":"x"}' },
content: [], isError: false, callView: null, resultView: null,
}
const props: ToolRowOwnerProps = {
callId: 'c5', toolName: 'todo_write', block: settled, openDetails: vi.fn(),
}
const view = render(<GenericToolCard {...props} />)
// Settled ok state keeps the variant icon (sparkle) instead of a StateDot.
expect(view.container.querySelector('[data-variant="others"] svg')).not.toBeNull()
expect(view.container.querySelector('[data-state="ok"]')).not.toBeNull()
})
it('BashRow shows the failed pill on error results (root session arm)', () => {
const errorResult: ToolResultNode = {
kind: 'tool-result', seq: 1, callId: 'c1',
call: { name: 'bash', argsRaw: '{"command":"boom"}' },
content: [], isError: true, callView: null, resultView: null,
}
// Root session (no parentId): the global arm renders, error pill visible.
const sid = 'root-1' as SessionId
const list = createSnapshotStore<SessionListState>({
ids: [sid],
byId: { [sid]: { id: sid, title: 'r', running: false, updatedAt: 0 } },
current: undefined,
} as SessionListState)
const props = {
callId: 'c1', toolName: 'bash', block: errorResult, openDetails: vi.fn(),
sessionId: sid, useSessions: bindSnapshotSelector(list),
} as unknown as ToolRowProps
const view = render(<BashRow {...props} />)
expect(view.container.querySelector('[data-sample="bash-global"]')).not.toBeNull()
expect(view.getByText('failed')).toBeTruthy()
})
})