coverage-tails.spec.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // @vitest-environment jsdom
  2. // Branch tails the acceptance specs do not reach: ToolRow stopped-state dot,
  3. // bash sample state dots, the node-half empty apply, and AssistantMarkdown
  4. // reasoning/unknown block arms.
  5. import { afterEach, describe, expect, it, vi } from 'vitest'
  6. import { cleanup, render } from '@testing-library/react'
  7. import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
  8. import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
  9. import type { RunningToolCall, SessionId, SessionListState, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
  10. import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
  11. import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
  12. import { apply as nodeApply } from '../src/index.ts'
  13. import { GenericToolCard, type GenericToolCardProps } from '../src/client/chat/GenericToolCard.tsx'
  14. import { ToolRow } from '../src/client/chat/ToolRow.tsx'
  15. import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
  16. import { BashRow } from '../src/client/toolviews/bash-sample.tsx'
  17. import { zh } from '../src/client/locales.ts'
  18. type BashRowProps = Parameters<typeof BashRow>[0]
  19. // Mirrors the real lookup chain (conversation namespace, then common).
  20. const t: GenericToolCardProps['t'] = makeTranslate(zh, commonZh)
  21. afterEach(cleanup)
  22. describe('tails', () => {
  23. it('node-half apply is an intentional no-op', () => {
  24. expect(() => { nodeApply() }).not.toThrow()
  25. })
  26. it('ToolRow stopped state renders the warning dot in the leading slot', () => {
  27. const view = render(
  28. <ToolRow t={t} variant="bash" icon={<i data-testid="icon" />} title="Bash" summary="s" body={null} state="stopped" />,
  29. )
  30. expect(view.queryByTestId('icon')).toBeNull()
  31. expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
  32. })
  33. it('AssistantMarkdown renders reasoning as a Think row and unknown blocks as JSON fallback', () => {
  34. const view = render(
  35. <AssistantMarkdown
  36. t={t}
  37. blocks={[
  38. { kind: 'reasoning', text: 'thinking hard\nsecond line' },
  39. { kind: 'tool-call', callId: 'c', name: 'bash', argsRaw: '{}' },
  40. { kind: 'other', block: { type: 'mystery' } },
  41. ]}
  42. streaming
  43. />,
  44. )
  45. expect(view.getByText('Think')).toBeTruthy()
  46. expect(view.getByText('thinking hard')).toBeTruthy()
  47. expect(view.getByText(/未知内容块/)).toBeTruthy()
  48. const stopped = render(
  49. <AssistantMarkdown t={t} blocks={[{ kind: 'text', text: 'partial words' }]} streaming={false} interrupted />,
  50. )
  51. expect(stopped.getByText('已停止')).toBeTruthy()
  52. })
  53. it('AssistantMarkdown skips the root shell when only tool-call heads remain', () => {
  54. // Tool heads are drawn by ChatView's tool groups; an empty root between
  55. // groups is layout noise (no text, no pulse, no interrupted marker).
  56. const empty = render(
  57. <AssistantMarkdown
  58. t={t}
  59. blocks={[{ kind: 'tool-call', callId: 'c', name: 'todo_write', argsRaw: '{}' }]}
  60. streaming={false}
  61. />,
  62. )
  63. expect(empty.container.firstChild).toBeNull()
  64. const blank = render(<AssistantMarkdown t={t} blocks={[]} streaming={false} />)
  65. expect(blank.container.firstChild).toBeNull()
  66. })
  67. it('a settled others-variant row renders the sparkle icon in the leading slot', () => {
  68. const settled: ToolResultNode = {
  69. kind: 'tool-result', seq: 2, time: 2_000, callId: 'c5',
  70. call: { name: 'todo_write', argsRaw: '{"note":"x"}' },
  71. callTime: 1_000,
  72. content: [], isError: false, callView: null, resultView: null,
  73. }
  74. const props: GenericToolCardProps = {
  75. callId: 'c5', toolName: 'todo_write', block: settled, openFile: vi.fn(), t,
  76. }
  77. const view = render(<GenericToolCard {...props} />)
  78. // Settled ok state keeps the variant icon (sparkle) instead of a StateDot.
  79. expect(view.container.querySelector('[data-variant="others"] svg')).not.toBeNull()
  80. expect(view.container.querySelector('[data-state="ok"]')).not.toBeNull()
  81. })
  82. it('BashRow carries data-state for running (row sweep) and StateDots for error/stopped', () => {
  83. const sid = 'root-1' as SessionId
  84. const list = createSnapshotStore<SessionListState>({
  85. ids: [sid],
  86. byId: { [sid]: { id: sid, title: 'r', displayTitle: 'r', running: false, blank: false, updatedAt: 0 } },
  87. current: undefined,
  88. phase: 'ready',
  89. subagentsByParent: {},
  90. currentAddress: undefined,
  91. })
  92. const props = (block: RunningToolCall | ToolResultNode) => ({
  93. callId: 'c1', toolName: 'bash', block, openFile: vi.fn(),
  94. sessionId: sid, useSessions: bindSnapshotSelector(list),
  95. t,
  96. } as unknown as BashRowProps)
  97. const running: RunningToolCall = {
  98. callId: 'c1', name: 'bash', argsRaw: '{"command":"ls","description":"List"}',
  99. turn: 1, step: 1, time: 1_000, callView: null,
  100. }
  101. const errorResult: ToolResultNode = {
  102. kind: 'tool-result', seq: 1, time: 1_000, callId: 'c1',
  103. call: { name: 'bash', argsRaw: '{"command":"boom"}' },
  104. callTime: 500,
  105. content: [], isError: true, callView: null, resultView: null,
  106. }
  107. const stoppedResult: ToolResultNode = {
  108. ...errorResult,
  109. error: { name: 'E', code: 'interrupted' },
  110. }
  111. const runningView = render(<BashRow {...props(running)} />)
  112. expect(runningView.container.querySelector('[data-state="running"]')).not.toBeNull()
  113. expect(runningView.getByText('Bash')).toBeTruthy()
  114. expect(runningView.getByText('List')).toBeTruthy()
  115. runningView.unmount()
  116. const errorView = render(<BashRow {...props(errorResult)} />)
  117. expect(errorView.container.querySelector('[data-sample="bash"]')).not.toBeNull()
  118. expect(errorView.container.querySelector('[data-state="error"]')).not.toBeNull()
  119. expect(errorView.getByText('失败')).toBeTruthy()
  120. errorView.unmount()
  121. const stoppedView = render(<BashRow {...props(stoppedResult)} />)
  122. expect(stoppedView.container.querySelector('[data-state="stopped"]')).not.toBeNull()
  123. expect(stoppedView.getByText('已停止')).toBeTruthy()
  124. })
  125. })