context-branches.spec.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { describe, expect, it } from 'vitest'
  2. import type {
  3. ConversationContext, ConversationNode, RequestView,
  4. } from '@deepseek-ai/dsh-client-runtime/client'
  5. import {
  6. deriveTrajectoryContextBranches,
  7. trajectoryBranchContainsRequest,
  8. } from '../src/client/context-branches.ts'
  9. const checkpoint = {
  10. kind: 'context',
  11. seq: 100,
  12. time: 100,
  13. content: [],
  14. source: { kind: 'plugin', plugin: 'compact' },
  15. } as ConversationNode
  16. const abandoned = {
  17. kind: 'assistant',
  18. seq: 20,
  19. time: 20,
  20. turn: 1,
  21. step: 1,
  22. blocks: [{ kind: 'text', text: 'abandoned' }],
  23. } as ConversationNode
  24. const current = {
  25. kind: 'user',
  26. seq: 110,
  27. time: 110,
  28. content: [{ type: 'text', text: 'rewound' }],
  29. source: { kind: 'plugin', plugin: 'rewind' },
  30. } as ConversationNode
  31. function request(
  32. purpose: RequestView['purpose'],
  33. startSeq: number,
  34. resultSeq?: number,
  35. replacementSeq?: number,
  36. ): RequestView {
  37. const base = {
  38. startSeq,
  39. startedAt: startSeq,
  40. completedAt: startSeq + 1,
  41. status: 'complete' as const,
  42. ...(resultSeq === undefined ? {} : { resultSeq }),
  43. }
  44. return purpose === 'assistant'
  45. ? { ...base, purpose, turn: 1, step: 1 }
  46. : {
  47. ...base,
  48. purpose,
  49. turn: 1,
  50. step: 0,
  51. ...(replacementSeq === undefined ? {} : { replacementSeq }),
  52. }
  53. }
  54. describe('trajectory context branches', () => {
  55. it('inherits nodes and requests by retained surface position rather than seq cutoff', () => {
  56. const contexts: ConversationContext[] = [
  57. { id: 0, nodes: [checkpoint, abandoned] },
  58. {
  59. id: 1,
  60. parentId: 0,
  61. origin: 'rewind',
  62. originSeq: 110,
  63. nodes: [checkpoint, current],
  64. },
  65. ]
  66. const branches = deriveTrajectoryContextBranches(contexts)
  67. const successor = branches[1]!
  68. expect(successor.key).toBe('rewind:110')
  69. expect(successor.nodes.map(node => node.seq)).toEqual([110])
  70. expect(trajectoryBranchContainsRequest(
  71. successor,
  72. request('assistant', 10, 20),
  73. )).toBe(false)
  74. expect(trajectoryBranchContainsRequest(
  75. successor,
  76. request('compaction', 90, 95, 100),
  77. )).toBe(true)
  78. expect(trajectoryBranchContainsRequest(
  79. successor,
  80. request('assistant', 111),
  81. )).toBe(true)
  82. })
  83. it('keeps branch identity when prepended generations shift local ids', () => {
  84. const branch = (id: number) => deriveTrajectoryContextBranches([{
  85. id,
  86. origin: 'rewind',
  87. originSeq: 110,
  88. nodes: [current],
  89. }])[0]
  90. expect(branch(1)?.key).toBe(branch(9)?.key)
  91. })
  92. })