queue-dock.spec.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. // @vitest-environment jsdom
  2. /**
  3. * QueueDock rendering (web input-triggers queue cut 1): empty queue renders
  4. * nothing, rows render one preview line each keyed by rpcId, and the strip
  5. * follows queue changes through the useSession selector.
  6. */
  7. import { afterEach, describe, expect, it } from 'vitest'
  8. import { act, cleanup, render } from '@testing-library/react'
  9. import { useSyncExternalStore } from 'react'
  10. import type { ConversationSnapshot, QueuedMessage, SessionId, SessionListState } from '@deepseek-ai/dsh-client-runtime/client'
  11. import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
  12. import type { InputState } from '../src/client/input/contract.ts'
  13. import { QueueDock, queueDockEntry } from '../src/client/queue/QueueDock.tsx'
  14. afterEach(cleanup)
  15. const SID = 's1' as SessionId
  16. function snapshotWith(queue: QueuedMessage[]): ConversationSnapshot {
  17. return {
  18. sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
  19. pending: [], queue, todos: [], running: true, composerPhase: 'active', removed: false, openState: 'open', openError: null,
  20. hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
  21. }
  22. }
  23. /** Minimal live source backing the useSession stub (queue swaps notify subscribers). */
  24. function liveSession(initial: ConversationSnapshot) {
  25. let snapshot = initial
  26. const listeners = new Set<() => void>()
  27. const useSession: SnapshotSelectorHook<ConversationSnapshot> = sel =>
  28. useSyncExternalStore(
  29. (fn) => {
  30. listeners.add(fn)
  31. return () => listeners.delete(fn)
  32. },
  33. () => sel(snapshot),
  34. )
  35. return {
  36. useSession,
  37. push(next: ConversationSnapshot): void {
  38. snapshot = next
  39. for (const fn of [...listeners]) fn()
  40. },
  41. }
  42. }
  43. /** InputZone owner stub (the dock reads useSession only; the zone fields satisfy the owner share). */
  44. const INPUT_STATE: InputState = { draft: '', draftRev: 0, phase: 'plain', occurrences: [], queue: [] }
  45. function kitFor(snapshot: ConversationSnapshot) {
  46. return {
  47. sessionId: SID,
  48. useSessions: (() => { throw new Error('unused') }) as unknown as SnapshotSelectorHook<SessionListState>,
  49. useWorkspaces: (() => { throw new Error('unused') }) as never,
  50. useInput: (() => { throw new Error('unused') }) as never,
  51. inputActions: { setDraft: () => {}, submit: () => {} } as never,
  52. session: snapshot,
  53. input: INPUT_STATE,
  54. }
  55. }
  56. describe('QueueDock', () => {
  57. it('renders null while the queue is empty', () => {
  58. const snap = snapshotWith([])
  59. const source = liveSession(snap)
  60. const { container } = render(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
  61. expect(container.innerHTML).toBe('')
  62. })
  63. it('renders one preview row per queued message with the count strip', () => {
  64. const snap = snapshotWith([
  65. { key: 'p-1', preview: '第一条排队消息' },
  66. { key: 'p-2', preview: 'second queued line' },
  67. ])
  68. const source = liveSession(snap)
  69. const { container } = render(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
  70. expect(container.textContent).toContain('已排队 2 条')
  71. const rows = [...container.querySelectorAll('li')]
  72. expect(rows.map(r => r.textContent)).toEqual(['第一条排队消息', 'second queued line'])
  73. })
  74. it('follows queue changes: retirement empties the strip back to null', () => {
  75. const snap = snapshotWith([{ key: 'p-1', preview: '在场' }])
  76. const source = liveSession(snap)
  77. const { container } = render(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
  78. expect(container.textContent).toContain('在场')
  79. act(() => { source.push(snapshotWith([])) })
  80. expect(container.innerHTML).toBe('')
  81. })
  82. it('ships the registrant plugin shape (list entry into conversation.input.dock)', () => {
  83. // Registration itself runs under T5's slot declaration; here we pin the
  84. // frozen registration surface so the wiring layer can mount it verbatim.
  85. expect(queueDockEntry.name).toBe('conversation-queue-dock')
  86. expect(queueDockEntry.inject).toEqual(['slots', 'conversation'])
  87. expect(typeof queueDockEntry.apply).toBe('function')
  88. })
  89. })