- Add flow-runner-decisions.ts: decision-aware step execution - Extend flow-runner.ts: dynamic step decisions - Extend conductor types: additional flow state types - Add collision-detector.test.ts: edit collision unit tests - Add conflict-index.test.ts: conflict resolution index tests
147 lines
5.7 KiB
TypeScript
147 lines
5.7 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { ConflictIndex } from '../conflict-index.js';
|
|
|
|
describe('ConflictIndex', () => {
|
|
let idx: ConflictIndex;
|
|
|
|
beforeEach(() => {
|
|
idx = new ConflictIndex();
|
|
});
|
|
|
|
describe('registerChange', () => {
|
|
it('adds an entry for a file path', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
|
|
const entries = idx.getEntriesFor('src/a.ts');
|
|
expect(entries.size).toBe(1);
|
|
const entry = [...entries][0]!;
|
|
expect(entry.worktreeId).toBe('wt-1');
|
|
expect(entry.agent).toBe('agent-a');
|
|
expect(entry.lineRange).toEqual({ start: 1, end: 10 });
|
|
expect(entry.status).toBe('pending');
|
|
expect(entry.timestamp).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('supports multiple entries for the same file path', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
|
|
idx.registerChange('src/a.ts', 'wt-2', 'agent-b', { start: 20, end: 30 });
|
|
expect(idx.getEntriesFor('src/a.ts').size).toBe(2);
|
|
});
|
|
|
|
it('allows a worktree to have multiple entries (several edits to same file)', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 20, end: 30 });
|
|
// Duplicate entries with same fields — the Set dedupes by ref,
|
|
// so a second identical call is still a distinct object (allowed).
|
|
expect(idx.getEntriesFor('src/a.ts').size).toBe(2);
|
|
});
|
|
|
|
it('separates files into distinct keys', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
idx.registerChange('src/b.ts', 'wt-2', 'agent-b');
|
|
expect(idx.getEntriesFor('src/a.ts').size).toBe(1);
|
|
expect(idx.getEntriesFor('src/b.ts').size).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('removeWorktree', () => {
|
|
it('removes all entries for a given worktree', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
idx.registerChange('src/a.ts', 'wt-2', 'agent-b');
|
|
idx.registerChange('src/b.ts', 'wt-1', 'agent-a');
|
|
idx.removeWorktree('wt-1');
|
|
expect(idx.getEntriesFor('src/a.ts').size).toBe(1);
|
|
expect([...idx.getEntriesFor('src/a.ts')][0]!.worktreeId).toBe('wt-2');
|
|
expect(idx.getEntriesFor('src/b.ts').size).toBe(0);
|
|
});
|
|
|
|
it('is a no-op when worktree has no entries', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
idx.removeWorktree('wt-ghost');
|
|
expect(idx.getEntriesFor('src/a.ts').size).toBe(1);
|
|
});
|
|
|
|
it('cleans up file key when last entry is removed', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
idx.removeWorktree('wt-1');
|
|
// After removal the key should be gone
|
|
expect(idx.snapshot().has('src/a.ts')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('sweepStale', () => {
|
|
it('removes entries older than maxAgeMs', async () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
idx.registerChange('src/b.ts', 'wt-2', 'agent-b');
|
|
// Wait a tick so timestamps diverge
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
idx.registerChange('src/c.ts', 'wt-3', 'agent-c');
|
|
const removed = idx.sweepStale(5); // 5ms cutoff — entries from before the await are stale
|
|
expect(removed).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('removes file key when all entries swept', async () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
// Wait so timestamp is definitely older than cutoff
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
const removed = idx.sweepStale(5);
|
|
expect(removed).toBe(1);
|
|
expect(idx.snapshot().has('src/a.ts')).toBe(false);
|
|
});
|
|
|
|
it('returns 0 when no entries are stale', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
const removed = idx.sweepStale(86_400_000); // 24h
|
|
expect(removed).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('getConflictsFor', () => {
|
|
it('returns conflicts between worktrees', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
|
|
idx.registerChange('src/a.ts', 'wt-2', 'agent-b', { start: 5, end: 15 });
|
|
const conflicts = idx.getConflictsFor('src/a.ts');
|
|
expect(conflicts).toHaveLength(1);
|
|
expect(conflicts[0]!.filePath).toBe('src/a.ts');
|
|
// getConflictsFor doesn't know the caller's line range,
|
|
// so severity defaults to 'different_area'
|
|
expect(conflicts[0]!.severity).toBe('different_area');
|
|
});
|
|
|
|
it('returns empty for files with only one worktree', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
expect(idx.getConflictsFor('src/a.ts')).toEqual([]);
|
|
});
|
|
|
|
it('returns empty for files not in index', () => {
|
|
expect(idx.getConflictsFor('src/never-touched.ts')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('query', () => {
|
|
it('delegates to findConflicts with proper data', () => {
|
|
idx.registerChange('src/a.ts', 'wt-2', 'agent-b', { start: 5, end: 15 });
|
|
const ranges = new Map([['src/a.ts', { start: 10, end: 20 }]]);
|
|
const result = idx.query(['src/a.ts'], 'wt-1', ranges);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]!.severity).toBe('same_line');
|
|
});
|
|
|
|
it('returns empty when no conflicts', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a', { start: 1, end: 10 });
|
|
const result = idx.query(['src/a.ts'], 'wt-1', new Map());
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('snapshot', () => {
|
|
it('returns a copy of the internal map', () => {
|
|
idx.registerChange('src/a.ts', 'wt-1', 'agent-a');
|
|
const snap = idx.snapshot();
|
|
expect(snap.has('src/a.ts')).toBe(true);
|
|
// Mutating the snapshot doesn't affect the original
|
|
idx.removeWorktree('wt-1');
|
|
expect(snap.has('src/a.ts')).toBe(true);
|
|
});
|
|
});
|
|
});
|