cc-haha/desktop/src/lib/sessionTree.test.ts
moyu12-ae 1b52da3587 fix: session branch visibility and parent-child tree display (#561)
- Fix branch button hidden when AI is responding by decoupling
  branchActionsDisabled from chatState (MessageList.tsx)
- Add sourceSessionId/sourceMessageId to SessionListItem type
  (both server and desktop)
- Scan forkedFrom in listSessions to expose parent-child relationships
- Preserve sourceSessionId/sourceMessageId in branchSession optimistic store
- Add buildSessionTree utility for tree-flattened session list with
  depth and isLastChild markers
- Render forked sessions indented with GitBranch icon in sidebar
- Add comprehensive tests (187 total: 81 desktop + 106 server)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:40:04 +08:00

121 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { buildSessionTree } from './sessionTree'
import type { SessionListItem } from '../types/session'
function makeSession(
id: string,
title: string,
sourceSessionId?: string,
sourceMessageId?: string,
): SessionListItem {
return {
id,
title,
createdAt: '2026-01-01T00:00:00.000Z',
modifiedAt: '2026-01-01T00:00:00.000Z',
messageCount: 1,
projectPath: '/workspace/repo',
projectRoot: '/workspace/repo',
workDir: '/workspace/repo',
workDirExists: true,
sourceSessionId,
sourceMessageId,
}
}
describe('buildSessionTree', () => {
it('returns flat list unchanged when no sessions have parents', () => {
const sessions = [
makeSession('s1', 'Session 1'),
makeSession('s2', 'Session 2'),
makeSession('s3', 'Session 3'),
]
const tree = buildSessionTree(sessions)
expect(tree.map((t) => t.id)).toEqual(['s1', 's2', 's3'])
expect(tree.every((t) => t.depth === 0)).toBe(true)
})
it('nests forked sessions under their parent', () => {
const sessions = [
makeSession('parent', 'Parent Session'),
makeSession('child', 'Forked Child', 'parent', 'msg-1'),
makeSession('sibling', 'Sibling Session'),
]
const tree = buildSessionTree(sessions)
expect(tree.map((t) => ({ id: t.id, depth: t.depth }))).toEqual([
{ id: 'parent', depth: 0 },
{ id: 'child', depth: 1 },
{ id: 'sibling', depth: 0 },
])
})
it('supports multi-level nesting (fork of a fork)', () => {
const sessions = [
makeSession('root', 'Root'),
makeSession('fork1', 'Fork Level 1', 'root', 'msg-a'),
makeSession('fork2', 'Fork Level 2', 'fork1', 'msg-b'),
]
const tree = buildSessionTree(sessions)
expect(tree.map((t) => ({ id: t.id, depth: t.depth }))).toEqual([
{ id: 'root', depth: 0 },
{ id: 'fork1', depth: 1 },
{ id: 'fork2', depth: 2 },
])
})
it('groups multiple children of the same parent', () => {
const sessions = [
makeSession('root', 'Root'),
makeSession('child-a', 'Child A', 'root', 'msg-1'),
makeSession('child-b', 'Child B', 'root', 'msg-2'),
]
const tree = buildSessionTree(sessions)
expect(tree.map((t) => ({ id: t.id, depth: t.depth }))).toEqual([
{ id: 'root', depth: 0 },
{ id: 'child-a', depth: 1 },
{ id: 'child-b', depth: 1 },
])
})
it('handles sessions whose parent is not in the list', () => {
// This can happen when a parent session is deleted
const sessions = [
makeSession('orphan', 'Orphan Fork', 'nonexistent-parent', 'msg-1'),
]
const tree = buildSessionTree(sessions)
// Orphan sessions should be rendered at root level
expect(tree.map((t) => ({ id: t.id, depth: t.depth }))).toEqual([
{ id: 'orphan', depth: 0 },
])
})
it('marks the last child of a parent for visual connecting line', () => {
const sessions = [
makeSession('root', 'Root'),
makeSession('child-a', 'Child A', 'root', 'msg-1'),
makeSession('child-b', 'Child B', 'root', 'msg-2'),
makeSession('other', 'Other'),
]
const tree = buildSessionTree(sessions)
const childA = tree.find((t) => t.id === 'child-a')!
const childB = tree.find((t) => t.id === 'child-b')!
const other = tree.find((t) => t.id === 'other')!
expect(childA.isLastChild).toBe(false)
expect(childB.isLastChild).toBe(true)
expect(other.isLastChild).toBe(false)
})
it('preserves original sort order within same depth level', () => {
const sessions = [
makeSession('a', 'A'),
makeSession('b-child', 'B Child', 'a', 'msg-1'),
makeSession('c', 'C'),
makeSession('d', 'D'),
]
const tree = buildSessionTree(sessions)
expect(tree.map((t) => t.id)).toEqual(['a', 'b-child', 'c', 'd'])
})
})