import { describe, expect, test } from 'bun:test'; import type { Session } from '@opencode-ai/sdk/v2'; import { getRuntimeKey } from '@/lib/runtime-switch'; import { getPinnedSessionKey } from '@/stores/useSessionPinnedStore'; import { computeNodeStructureKey, nodeHasPinnedMembershipChange, selectFolderRootNodes, selectQuestionBadgeSessionScopes } from './sessionNodeItemUtils'; import type { SessionNode } from '../types'; const session = (id: string, title: string): Session => ({ id, title, time: { created: 1, updated: 1 }, } as Session); const rootWithChild = (childSession: Session): SessionNode => ({ session: session('root', 'Root'), children: [{ session: childSession, children: [], worktree: null }], worktree: null, }); describe('computeNodeStructureKey', () => { test('stays stable across grouping rebuilds that reuse session objects', () => { const child = session('child', 'Child'); expect(computeNodeStructureKey(rootWithChild(child))).toBe(computeNodeStructureKey(rootWithChild(child))); }); test('changes when a descendant session object changes', () => { const previous = session('child', 'Before'); const next = { ...previous, title: 'After' }; expect(computeNodeStructureKey(rootWithChild(previous))).not.toBe(computeNodeStructureKey(rootWithChild(next))); }); }); describe('selectQuestionBadgeSessionScopes', () => { const withDirectory = (node: SessionNode, directory: string | null): SessionNode => ({ ...node, session: { ...node.session, directory } as Session, }); test('rolls up the hidden subtree by owning directory when a parent is collapsed', () => { const grandchild = withDirectory({ session: session('grandchild', 'Grandchild'), children: [], worktree: null }, '/worktrees/feature'); const child = withDirectory({ session: session('child', 'Child'), children: [grandchild], worktree: null }, '/worktrees/feature'); const root = withDirectory({ session: session('root', 'Root'), children: [child], worktree: null }, '/repo'); expect(selectQuestionBadgeSessionScopes(root, false, '/repo')).toEqual([ { directory: '/repo', sessionIDs: ['root'] }, { directory: '/worktrees/feature', sessionIDs: ['child', 'grandchild'] }, ]); }); test('keeps expanded rows accurate to their own session only', () => { const child = withDirectory({ session: session('child', 'Child'), children: [], worktree: null }, '/worktrees/feature'); const root = withDirectory({ session: session('root', 'Root'), children: [child], worktree: null }, '/repo'); expect(selectQuestionBadgeSessionScopes(root, true, '/repo')).toEqual([ { directory: '/repo', sessionIDs: ['root'] }, ]); }); test('falls back to the group directory when the session has none', () => { const root: SessionNode = { session: session('root', 'Root'), children: [], worktree: null }; expect(selectQuestionBadgeSessionScopes(root, false, '/fallback')).toEqual([ { directory: '/fallback', sessionIDs: ['root'] }, ]); }); }); describe('nodeHasPinnedMembershipChange', () => { test('detects composite pin changes using the group directory fallback', () => { const node: SessionNode = { session: session('root', 'Root'), children: [], worktree: null, }; const pinnedKey = getPinnedSessionKey(getRuntimeKey(), '/repo', 'root'); expect(pinnedKey).not.toBeNull(); expect(nodeHasPinnedMembershipChange( node, node, new Set(), new Set([pinnedKey!]), '/repo', '/repo', )).toBe(true); }); test('ignores pin changes for the same session id in another directory', () => { const node: SessionNode = { session: session('root', 'Root'), children: [], worktree: null, }; const pinnedKey = getPinnedSessionKey(getRuntimeKey(), '/other-repo', 'root'); expect(pinnedKey).not.toBeNull(); expect(nodeHasPinnedMembershipChange( node, node, new Set(), new Set([pinnedKey!]), '/repo', '/repo', )).toBe(false); }); }); describe('selectFolderRootNodes', () => { test('does not render assigned descendants again beside their assigned parent tree', () => { const grandchild: SessionNode = { session: { ...session('grandchild', 'Grandchild'), parentID: 'child' } as Session, children: [], worktree: null, }; const child: SessionNode = { session: { ...session('child', 'Child'), parentID: 'root' } as Session, children: [grandchild], worktree: null, }; const root: SessionNode = { session: session('root', 'Root'), children: [child], worktree: null, }; const nodes = new Map([ ['root', root], ['child', child], ['grandchild', grandchild], ]); expect(selectFolderRootNodes(['root', 'child', 'grandchild'], nodes)).toEqual([root]); }); test('keeps a child as a folder root when none of its ancestors are assigned', () => { const child: SessionNode = { session: { ...session('child', 'Child'), parentID: 'root' } as Session, children: [], worktree: null, }; const root: SessionNode = { session: session('root', 'Root'), children: [child], worktree: null, }; expect(selectFolderRootNodes(['child'], new Map([['root', root], ['child', child]]))).toEqual([child]); }); test('keeps a child when an assigned ancestor is not available in the group', () => { const child: SessionNode = { session: { ...session('child', 'Child'), parentID: 'missing-root' } as Session, children: [], worktree: null, }; expect(selectFolderRootNodes(['missing-root', 'child'], new Map([['child', child]]))).toEqual([child]); }); });