import { describe, it, expect } from 'vitest'; import { findConflicts } from '../collision-detector.js'; import type { ConflictEntry, ConflictIndexData } from '../collision-detector.js'; function entry(worktreeId: string, agent: string, start?: number, end?: number): ConflictEntry { return { worktreeId, agent, lineRange: start !== undefined && end !== undefined ? { start, end } : undefined, status: 'pending' as const, timestamp: 1000, }; } function index(entries: Array<[string, ConflictEntry[]]>): ConflictIndexData { return new Map(entries.map(([path, es]) => [path, new Set(es)] as const)); } describe('findConflicts', () => { it('returns empty when no files in index', () => { const result = findConflicts(['src/a.ts'], 'wt-1', new Map(), new Map()); expect(result).toEqual([]); }); it('returns empty when only own worktree has the file', () => { const idx = index([['src/a.ts', [entry('wt-1', 'agent-a', 1, 10)]]]); const result = findConflicts(['src/a.ts'], 'wt-1', new Map(), idx); expect(result).toEqual([]); }); it('detects same_file conflict from another worktree', () => { const idx = index([['src/a.ts', [entry('wt-2', 'agent-b', 5, 15)]]]); const result = findConflicts(['src/a.ts'], 'wt-1', new Map(), idx); expect(result).toHaveLength(1); expect(result[0]!.filePath).toBe('src/a.ts'); expect(result[0]!.worktrees).toEqual(['wt-2']); expect(result[0]!.agents).toEqual(['agent-b']); }); it('reports same_line severity when ranges overlap', () => { const idx = index([['src/a.ts', [entry('wt-2', 'agent-b', 10, 20)]]]); const ranges = new Map([['src/a.ts', { start: 15, end: 25 }]]); const result = findConflicts(['src/a.ts'], 'wt-1', ranges, idx); expect(result[0]!.severity).toBe('same_line'); }); it('reports different_area severity when ranges are far apart', () => { const idx = index([['src/a.ts', [entry('wt-2', 'agent-b', 1, 10)]]]); const ranges = new Map([['src/a.ts', { start: 100, end: 200 }]]); const result = findConflicts(['src/a.ts'], 'wt-1', ranges, idx); expect(result[0]!.severity).toBe('different_area'); }); it('reports adjacent_line severity when ranges are 3 lines apart', () => { const idx = index([['src/a.ts', [entry('wt-2', 'agent-b', 10, 15)]]]); const ranges = new Map([['src/a.ts', { start: 19, end: 25 }]]); const result = findConflicts(['src/a.ts'], 'wt-1', ranges, idx); expect(result[0]!.severity).toBe('adjacent_line'); }); it('returns entry for each conflicting file', () => { const idx = index([ ['src/a.ts', [entry('wt-2', 'agent-b', 1, 10)]], ['src/b.ts', [entry('wt-3', 'agent-c', 1, 10)]], ]); const result = findConflicts(['src/a.ts', 'src/b.ts', 'src/c.ts'], 'wt-1', new Map(), idx); expect(result).toHaveLength(2); expect(result.map((v) => v.filePath).sort()).toEqual(['src/a.ts', 'src/b.ts']); }); it('excludes entries from the same worktree', () => { const idx = index([['src/a.ts', [entry('wt-1', 'agent-a', 1, 10), entry('wt-2', 'agent-b', 5, 15)]]]); const result = findConflicts(['src/a.ts'], 'wt-1', new Map(), idx); expect(result).toHaveLength(1); expect(result[0]!.worktrees).toEqual(['wt-2']); }); it('deduplicates worktree IDs in verdict', () => { const idx = index([['src/a.ts', [entry('wt-2', 'agent-b', 1, 5), entry('wt-2', 'agent-b', 10, 15)]]]); const result = findConflicts(['src/a.ts'], 'wt-1', new Map(), idx); expect(result[0]!.worktrees).toEqual(['wt-2']); }); it('reports same_line when no lineRange on either side (create/delete conflates)', () => { const idx = index([['src/a.ts', [entry('wt-2', 'agent-b')]]]); const result = findConflicts(['src/a.ts'], 'wt-1', new Map(), idx); expect(result).toHaveLength(1); expect(result[0]!.severity).toBe('different_area'); }); });