context-branches.spec.ts 2.6 KB

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