agent-ecosystem/test/main/services/editor/conflictDetection.test.ts
iliya 5b0c7d13fc feat: add project editor with drag & drop file management
- Backend: ProjectFileService with file CRUD, search, git status, file watcher
- IPC: 12 editor channels with security validation and path containment
- Store: editorSlice with multi-tab management, draft persistence, conflict detection
- UI: CodeMirror 6 editor, file tree with DnD, search-in-files, context menus
- Move: fs.rename with EXDEV fallback, full path remapping across all caches
- Tests: comprehensive coverage for services, IPC handlers, store, and utilities
2026-02-28 23:40:41 +02:00

95 lines
2.8 KiB
TypeScript

/**
* Tests for conflictDetection — mtime comparison, deleted files, tolerance.
*/
import { describe, expect, it, vi } from 'vitest';
vi.mock('fs/promises', () => ({
stat: vi.fn(),
}));
import * as fs from 'fs/promises';
import { checkFileConflict } from '../../../../src/main/services/editor/conflictDetection';
// =============================================================================
// Helpers
// =============================================================================
function mockStat(mtimeMs: number): void {
vi.mocked(fs.stat).mockResolvedValue({ mtimeMs } as Awaited<ReturnType<typeof fs.stat>>);
}
function mockStatError(code: string): void {
const err = new Error(`${code}: no such file`) as NodeJS.ErrnoException;
err.code = code;
vi.mocked(fs.stat).mockRejectedValue(err);
}
// =============================================================================
// Tests
// =============================================================================
describe('checkFileConflict', () => {
it('returns no conflict when mtime matches exactly', async () => {
mockStat(1000);
const result = await checkFileConflict('/test/file.ts', 1000);
expect(result.hasConflict).toBe(false);
expect(result.currentMtimeMs).toBe(1000);
expect(result.deleted).toBe(false);
});
it('returns no conflict within 1ms tolerance', async () => {
mockStat(1000.5);
const result = await checkFileConflict('/test/file.ts', 1000);
expect(result.hasConflict).toBe(false);
});
it('detects conflict when mtime differs by more than 1ms', async () => {
mockStat(2000);
const result = await checkFileConflict('/test/file.ts', 1000);
expect(result.hasConflict).toBe(true);
expect(result.currentMtimeMs).toBe(2000);
expect(result.deleted).toBe(false);
});
it('detects deleted file (ENOENT)', async () => {
mockStatError('ENOENT');
const result = await checkFileConflict('/test/file.ts', 1000);
expect(result.hasConflict).toBe(true);
expect(result.currentMtimeMs).toBe(0);
expect(result.deleted).toBe(true);
});
it('re-throws non-ENOENT errors', async () => {
mockStatError('EPERM');
await expect(checkFileConflict('/test/file.ts', 1000)).rejects.toThrow('EPERM');
});
it('handles mtime slightly earlier than baseline (e.g. clock drift)', async () => {
mockStat(999);
const result = await checkFileConflict('/test/file.ts', 1000);
// |999 - 1000| = 1, which is <= 1ms tolerance
expect(result.hasConflict).toBe(false);
});
it('detects conflict for mtime 2ms earlier than baseline', async () => {
mockStat(998);
const result = await checkFileConflict('/test/file.ts', 1000);
// |998 - 1000| = 2, which is > 1ms tolerance
expect(result.hasConflict).toBe(true);
});
});