lineage.client.spec.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. /**
  2. * flattenLineage: root ordering, DFS child expansion, orphan degradation, and
  3. * cycle fail-soft (every entry always emitted, no infinite walk).
  4. */
  5. import { describe, expect, it, vi } from 'vitest'
  6. import type { SessionId, SessionSummary } from '@deepseek-ai/dsh-api-remotes/client'
  7. import { flattenLineage } from '../src/client/sessions/lineage.ts'
  8. const s = (id: string, updatedAt: number, parent?: string): SessionSummary => ({
  9. sessionId: id as SessionId, updatedAt, running: false, blank: false,
  10. ...(parent !== undefined ? { parentSessionId: parent as SessionId } : {}),
  11. })
  12. describe('Session lineage flattening', () => {
  13. it('keeps established root and sibling order while expanding children DFS with depth', () => {
  14. const out = flattenLineage([
  15. s('old-root', 10),
  16. s('new-root', 30),
  17. s('kid-old', 11, 'new-root'),
  18. s('kid-new', 12, 'new-root'),
  19. s('grandkid', 5, 'kid-new'),
  20. ])
  21. expect(out.map(e => [e.sessionId, e.depth])).toEqual([
  22. ['old-root', 0], ['new-root', 0], ['kid-old', 1], ['kid-new', 1], ['grandkid', 2],
  23. ])
  24. })
  25. it('degrades an orphan (absent parent) to root level without dropping it', () => {
  26. const out = flattenLineage([s('orphan', 20, 'ghost-parent'), s('root', 10)])
  27. expect(out.map(e => [e.sessionId, e.depth])).toEqual([['orphan', 0], ['root', 0]])
  28. })
  29. it('fails soft on a two-node cycle: all entries emitted, warn fired, no hang', () => {
  30. const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
  31. try {
  32. const out = flattenLineage([s('a', 20, 'b'), s('b', 10, 'a'), s('root', 30)])
  33. expect(out.map(e => e.sessionId).sort()).toEqual(['a', 'b', 'root'])
  34. expect(warnSpy).toHaveBeenCalled()
  35. } finally {
  36. warnSpy.mockRestore()
  37. }
  38. })
  39. it('handles a self-referencing entry as a cycle member', () => {
  40. const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
  41. try {
  42. const out = flattenLineage([s('self', 10, 'self')])
  43. expect(out.map(e => e.sessionId)).toEqual(['self'])
  44. expect(out[0]?.depth).toBe(0)
  45. } finally {
  46. warnSpy.mockRestore()
  47. }
  48. })
  49. it('projects the completion-reminder set into rows (absent = false)', () => {
  50. const out = flattenLineage([s('a', 10), s('b', 20)], new Set(['b' as SessionId]))
  51. expect(out.find(e => e.sessionId === 'a')?.completed).toBe(false)
  52. expect(out.find(e => e.sessionId === 'b')?.completed).toBe(true)
  53. expect(flattenLineage([s('a', 10)])[0]?.completed).toBe(false)
  54. })
  55. })