history-fold.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { createMessage } from '@deepseek-ai/dsh-llm'
  2. import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
  3. import { describe, expect, it } from 'vitest'
  4. import { projectConversationHistory } from '../src/client/session-history/history-fold.ts'
  5. import { compactHistoryInspectionEntries } from '../src/client/sessions/history.ts'
  6. import { inspectRequests } from '../src/client/sessions/request-inspection.ts'
  7. import { ev } from './event-script.ts'
  8. const at = (seq: number, event: Record<string, unknown>): SessionEvent =>
  9. ({ seq, time: 1_700_000_000_000 + seq, ...event }) as unknown as SessionEvent
  10. describe('projectConversationHistory', () => {
  11. it('projects a high-sequence history window without synthesizing its unloaded prefix', () => {
  12. const baseSeq = 400_000
  13. const events = [
  14. ev.user(baseSeq, 'loaded tail'),
  15. at(baseSeq + 1, {
  16. type: 'assistant/message',
  17. surfaceOp: { op: 'replace', start: baseSeq, end: baseSeq },
  18. sourceEventSeqs: [baseSeq],
  19. data: {
  20. turn: 80,
  21. step: 1,
  22. message: createMessage({
  23. role: 'assistant',
  24. content: [{ type: 'text', text: 'tail summary' }],
  25. source: { kind: 'model', provider: 'fake', model: 'fake' },
  26. }),
  27. },
  28. }),
  29. ]
  30. const projection = projectConversationHistory(events.map(event => ({ event })))
  31. expect(projection.eventNodes.map(node => node.seq)).toEqual([baseSeq, baseSeq + 1])
  32. expect(projection.contexts.map(context => ({
  33. originSeq: context.originSeq,
  34. nodes: context.nodes.map(node => node.seq),
  35. }))).toEqual([
  36. { originSeq: undefined, nodes: [baseSeq] },
  37. { originSeq: baseSeq + 1, nodes: [baseSeq + 1] },
  38. ])
  39. })
  40. it('projects frozen surface generations without widening the core live surface', () => {
  41. const events = [
  42. ev.user(0, 'a'),
  43. ev.user(1, 'b'),
  44. at(2, {
  45. type: 'assistant/message',
  46. surfaceOp: { op: 'replace', start: 0, end: 0 },
  47. sourceEventSeqs: [0],
  48. data: {
  49. turn: 1,
  50. step: 1,
  51. message: createMessage({
  52. role: 'assistant',
  53. content: [{ type: 'text', text: 'summary' }],
  54. source: { kind: 'model', provider: 'fake', model: 'fake' },
  55. }),
  56. },
  57. }),
  58. at(3, {
  59. type: 'assistant/message',
  60. surfaceOp: { op: 'replace', start: 2, end: 1 },
  61. sourceEventSeqs: [2, 1],
  62. data: {
  63. turn: 1,
  64. step: 2,
  65. message: createMessage({
  66. role: 'assistant',
  67. content: [{ type: 'text', text: 'summary 2' }],
  68. source: { kind: 'model', provider: 'fake', model: 'fake' },
  69. }),
  70. },
  71. }),
  72. ]
  73. expect(projectConversationHistory(events.map(event => ({ event }))).contexts.map(context => ({
  74. id: context.id,
  75. parentId: context.parentId,
  76. originSeq: context.originSeq,
  77. nodes: context.nodes.map(node => node.seq),
  78. }))).toEqual([
  79. { id: 0, parentId: undefined, originSeq: undefined, nodes: [0, 1] },
  80. { id: 1, parentId: 0, originSeq: 2, nodes: [2, 1] },
  81. { id: 2, parentId: 1, originSeq: 3, nodes: [3] },
  82. ])
  83. })
  84. it('projects assistant timing and the active request header from history', () => {
  85. const projection = projectConversationHistory([
  86. ev.stepStart(0, 1, 2),
  87. at(1, { type: 'request/header', data: {
  88. reason: 'initial',
  89. header: {
  90. config: { provider: 'fake', model: 'first' },
  91. tools: [],
  92. },
  93. } }),
  94. ev.chunkStart(2, 1, 2),
  95. ev.chunkText(3, 1, 'token', 2),
  96. ev.assistant(4, 1, 'done', 2),
  97. ev.stepStart(5, 2, 1),
  98. ev.chunkText(6, 2, 'next', 1),
  99. ev.assistant(7, 2, 'next done', 1),
  100. ].map(event => ({ event })))
  101. expect(projection.eventNodes[0]).toMatchObject({
  102. kind: 'assistant',
  103. timing: {
  104. stepStartTime: 1_700_000_000_000,
  105. firstTokenTime: 1_700_000_000_003,
  106. completedTime: 1_700_000_000_004,
  107. },
  108. requestConfig: { provider: 'fake', model: 'first' },
  109. })
  110. expect(projection.eventNodes.at(-1)).toMatchObject({
  111. timing: {
  112. stepStartTime: 1_700_000_000_005,
  113. firstTokenTime: 1_700_000_000_006,
  114. completedTime: 1_700_000_000_007,
  115. },
  116. requestConfig: { provider: 'fake', model: 'first' },
  117. })
  118. })
  119. it('drops completed token payloads without changing inspection projections', () => {
  120. const events = [
  121. ev.user(0, 'before'),
  122. ev.stepStart(1, 1, 0),
  123. ev.chunkStart(2, 1),
  124. ev.chunkText(3, 1, ''),
  125. ev.chunkText(4, 1, 'first'),
  126. ev.chunkText(5, 1, ' discarded'),
  127. at(6, { type: 'assistant/chunk', data: {
  128. turn: 1,
  129. step: 0,
  130. chunk: { type: 'usage', usage: { inputTokens: 4, outputTokens: 2 } },
  131. } }),
  132. ev.assistant(7, 1, 'first discarded'),
  133. ev.compactSummary(8, 'summary', 0, 7),
  134. ev.compactCheckpoint(9, 8, 0, 7),
  135. ev.stepStart(10, 2, 0),
  136. ev.chunkStart(11, 2),
  137. ev.chunkText(12, 2, 'interrupted'),
  138. ev.turnEnd(13, 2, 'aborted'),
  139. ]
  140. const raw = events.map(event => ({ event }))
  141. const compacted = compactHistoryInspectionEntries(raw)
  142. expect(compacted.map(entry => entry.event.seq)).toEqual([
  143. 0, 1, 4, 6, 7, 8, 9, 10, 11, 12, 13,
  144. ])
  145. expect(projectConversationHistory(compacted)).toEqual(projectConversationHistory(raw))
  146. expect(inspectRequests(compacted)).toEqual(inspectRequests(raw))
  147. })
  148. })