/** * test/http/session-groups.test.ts (W5) — pure grouping helpers, no I/O. * * deriveRepoRoot strips a `-worktrees/` parent to `<...>/`; * groupSessionsByRepo clusters a repo + its worktree lanes into one SessionGroup, * skips cwd:null, and preserves manager.list() (newest-first) order. */ import { describe, it, expect } from 'vitest' import { deriveRepoRoot, groupSessionsByRepo } from '../../src/http/session-groups.js' import type { LiveSessionInfo } from '../../src/types.js' function live(id: string, cwd: string | null): LiveSessionInfo { return { id, createdAt: 0, clientCount: 0, status: 'unknown', exited: false, cwd, cols: 80, rows: 24, } } describe('deriveRepoRoot', () => { it('strips a *-worktrees parent to the repo root', () => { expect(deriveRepoRoot('/a/proj-worktrees/lane-1')).toBe('/a/proj') }) it('returns the cwd unchanged when the parent is not a *-worktrees dir', () => { expect(deriveRepoRoot('/a/proj')).toBe('/a/proj') }) it('returns null for a null cwd', () => { expect(deriveRepoRoot(null)).toBe(null) }) it('returns null for an empty cwd', () => { expect(deriveRepoRoot('')).toBe(null) }) it('handles a nested repo path with a *-worktrees parent', () => { expect(deriveRepoRoot('/home/me/code/web-terminal-worktrees/feat-x-lane-2')).toBe( '/home/me/code/web-terminal', ) }) it('does not treat a bare "-worktrees" segment (no repo name) specially', () => { // parentName === '-worktrees' has no repo name to keep → falls through to cwd. expect(deriveRepoRoot('/a/-worktrees/x')).toBe('/a/-worktrees/x') }) }) describe('groupSessionsByRepo', () => { it('returns [] for no sessions', () => { expect(groupSessionsByRepo([])).toEqual([]) }) it('clusters a repo and its worktree lanes into ONE group', () => { const sessions = [ live('s1', '/a/proj-worktrees/lane-1'), live('s2', '/a/proj-worktrees/lane-2'), ] const groups = groupSessionsByRepo(sessions) expect(groups).toHaveLength(1) expect(groups[0]!.repoRoot).toBe('/a/proj') expect(groups[0]!.label).toBe('proj') expect(groups[0]!.sessions.map((s) => s.id)).toEqual(['s1', 's2']) }) it('groups the main repo cwd together with its worktree lanes', () => { const sessions = [ live('main', '/a/proj'), live('lane', '/a/proj-worktrees/feat-lane-1'), ] const groups = groupSessionsByRepo(sessions) expect(groups).toHaveLength(1) expect(groups[0]!.repoRoot).toBe('/a/proj') expect(groups[0]!.sessions.map((s) => s.id)).toEqual(['main', 'lane']) }) it('puts an unrelated cwd in its own group', () => { const groups = groupSessionsByRepo([ live('a', '/a/proj-worktrees/lane-1'), live('b', '/b/other'), ]) expect(groups).toHaveLength(2) expect(groups[0]!.repoRoot).toBe('/a/proj') expect(groups[1]!.repoRoot).toBe('/b/other') expect(groups[1]!.label).toBe('other') }) it('skips sessions with a null cwd (not attributable to a repo)', () => { const groups = groupSessionsByRepo([ live('nocwd', null), live('real', '/a/proj'), ]) expect(groups).toHaveLength(1) expect(groups[0]!.repoRoot).toBe('/a/proj') expect(groups[0]!.sessions.map((s) => s.id)).toEqual(['real']) }) it('preserves the input (newest-first) order across and within groups', () => { const groups = groupSessionsByRepo([ live('newest', '/a/proj-worktrees/lane-2'), live('older', '/a/proj-worktrees/lane-1'), ]) expect(groups[0]!.sessions.map((s) => s.id)).toEqual(['newest', 'older']) }) it('does not mutate the input array', () => { const input = [live('a', '/a/proj')] const copy = [...input] groupSessionsByRepo(input) expect(input).toEqual(copy) }) })