virtual-rows.client.spec.ts 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /** Measurable virtual-row grouping and durable identity contracts. */
  2. import { describe, expect, it } from 'vitest'
  3. import type { TrajectoryCellProps } from '../src/client/trajectory-record.ts'
  4. import {
  5. groupTrajectoryVirtualRows, trajectoryVirtualRecordKey,
  6. type VirtualizableTrajectoryRecord,
  7. } from '../src/client/trajectory-virtual-rows.ts'
  8. function record(
  9. index: number,
  10. cell: Partial<TrajectoryCellProps> = {},
  11. collapsedSummaryKind?: 'turn' | 'assistant',
  12. ): VirtualizableTrajectoryRecord {
  13. return {
  14. cell: {
  15. index,
  16. kind: 'message',
  17. text: `record ${index}`,
  18. timeSeconds: 0,
  19. ...cell,
  20. },
  21. ...(collapsedSummaryKind === undefined ? {} : { collapsedSummaryKind }),
  22. }
  23. }
  24. describe('trajectory virtual rows', () => {
  25. it('groups zero-height request boundaries with the following content row', () => {
  26. const first = record(1, { requestOnly: true, sourceSeq: 10 })
  27. const second = record(2, { requestOnly: true, sourceSeq: 11 })
  28. const content = record(3, { sourceSeq: 12 })
  29. expect(groupTrajectoryVirtualRows([first, second, content])).toEqual([{
  30. entries: [
  31. { logicalIndex: 0, record: first },
  32. { logicalIndex: 1, record: second },
  33. { logicalIndex: 2, record: content },
  34. ],
  35. height: 30,
  36. key: trajectoryVirtualRecordKey(content),
  37. }])
  38. })
  39. it('retains terminal request-boundary clearance as a measurable row', () => {
  40. const content = record(1, { sourceSeq: 10 })
  41. const boundary = record(2, { requestOnly: true, sourceSeq: 11 })
  42. const rows = groupTrajectoryVirtualRows([content, boundary])
  43. expect(rows).toHaveLength(2)
  44. expect(rows[1]).toEqual({
  45. entries: [{ logicalIndex: 1, record: boundary }],
  46. height: 9,
  47. key: trajectoryVirtualRecordKey(boundary),
  48. })
  49. })
  50. it('uses the rendered collapsed-summary height', () => {
  51. const summary = record(1, { sourceSeq: 10 }, 'turn')
  52. expect(groupTrajectoryVirtualRows([summary])[0]?.height).toBe(20)
  53. })
  54. it('keeps an existing row key stable when older history is prepended', () => {
  55. const existing = record(2, { sourceSeq: 100 })
  56. const prepended = record(1, { sourceSeq: 10 })
  57. const before = groupTrajectoryVirtualRows([existing])[0]?.key
  58. const after = groupTrajectoryVirtualRows([prepended, existing])[1]?.key
  59. expect(after).toBe(before)
  60. })
  61. it('keeps the content key when a request boundary joins its row', () => {
  62. const content = record(2, { sourceSeq: 100 })
  63. const boundary = record(1, { requestOnly: true, sourceSeq: 99 })
  64. expect(groupTrajectoryVirtualRows([boundary, content])[0]?.key)
  65. .toBe(groupTrajectoryVirtualRows([content])[0]?.key)
  66. })
  67. it('distinguishes a folded summary from its source record', () => {
  68. const source = record(1, { sourceSeq: 10 })
  69. const summary = record(1, { sourceSeq: 10 }, 'assistant')
  70. expect(trajectoryVirtualRecordKey(summary)).not.toBe(trajectoryVirtualRecordKey(source))
  71. })
  72. it('exposes a DOM-safe semantic key', () => {
  73. const source = record(1, { callId: 'call with spaces/and?punctuation' })
  74. expect(trajectoryVirtualRecordKey(source)).toBe(
  75. 'message%00call%00call%20with%20spaces%2Fand%3Fpunctuation',
  76. )
  77. })
  78. })