test: complete test suite — 166 new tests (stores + hardening + agent)

@agor/stores (37 tests):
- theme: 6 (17 themes, 3 groups, no duplicates)
- notifications: 11 (types, rate limiter, window expiry)
- health: 20 (scoring, burn rate, context pressure, tool tracking)

Electrobun stores (90 tests):
- agent-store: 27 (seqId, dedup, double-start guard, persistence)
- workspace-store: 17 (CRUD, derived state, aggregates)
- plugin-store: 14 (commands, events, permissions, meta)
- keybinding-store: 18 (defaults, chords, conflicts, capture)

Hardening (39 tests):
- durable-sequencing: 10 (monotonic, dedup, restore)
- file-conflict: 10 (mtime, atomic write, workflows)
- backpressure: 7 (paste 64KB, buffer 50MB, line 10MB)
- retention: 7 (count, age, running protected)
- channel-acl: 9 (join/leave, rejection, isolation)

Total across all suites: 1,020+ tests
This commit is contained in:
Hibryda 2026-03-22 05:07:40 +01:00
parent c0eca4964a
commit 1995f03682
7 changed files with 911 additions and 10 deletions

View file

@ -0,0 +1,127 @@
// Tests for file conflict detection via mtime comparison.
// Uses bun:test. Tests the mtime-based conflict detection and atomic write logic
// from ui-electrobun/src/bun/handlers/files-handlers.ts and FileBrowser.svelte.
import { describe, it, expect } from 'bun:test';
// ── Replicated conflict detection logic ──────────────────────────────────────
interface FileStat {
mtimeMs: number;
size: number;
error?: string;
}
/**
* Check if the file was modified since we last read it.
* Returns true if conflict detected (mtime differs).
*/
function hasConflict(readMtimeMs: number, currentStat: FileStat): boolean {
if (readMtimeMs <= 0) return false; // No baseline — skip check
if (currentStat.error) return false; // Can't stat — skip check
return currentStat.mtimeMs > readMtimeMs; // Modified since read
}
/**
* Simulate atomic write: write to temp file, then rename.
* Returns the operations performed for verification.
*/
function atomicWriteOps(filePath: string, _content: string): { tmpPath: string; finalPath: string } {
const tmpPath = filePath + '.agor-tmp';
return { tmpPath, finalPath: filePath };
}
// ── Tests ───────────────────────────────────────────────────────────────────
describe('mtime conflict detection', () => {
it('no conflict when mtime matches', () => {
const readTime = 1700000000000;
const stat: FileStat = { mtimeMs: readTime, size: 100 };
expect(hasConflict(readTime, stat)).toBe(false);
});
it('conflict detected when mtime is newer', () => {
const readTime = 1700000000000;
const stat: FileStat = { mtimeMs: readTime + 5000, size: 120 };
expect(hasConflict(readTime, stat)).toBe(true);
});
it('no conflict when readMtimeMs is 0 (first write)', () => {
const stat: FileStat = { mtimeMs: 1700000005000, size: 120 };
expect(hasConflict(0, stat)).toBe(false);
});
it('no conflict when stat returns error', () => {
const stat: FileStat = { mtimeMs: 0, size: 0, error: 'ENOENT: no such file' };
expect(hasConflict(1700000000000, stat)).toBe(false);
});
it('no conflict when file is older than read (edge case)', () => {
const readTime = 1700000005000;
const stat: FileStat = { mtimeMs: 1700000000000, size: 100 };
expect(hasConflict(readTime, stat)).toBe(false);
});
it('detects tiny mtime difference (1ms)', () => {
const readTime = 1700000000000;
const stat: FileStat = { mtimeMs: readTime + 1, size: 100 };
expect(hasConflict(readTime, stat)).toBe(true);
});
});
describe('atomic write', () => {
it('uses .agor-tmp suffix for temp file', () => {
const ops = atomicWriteOps('/home/user/project/main.ts', 'content');
expect(ops.tmpPath).toBe('/home/user/project/main.ts.agor-tmp');
expect(ops.finalPath).toBe('/home/user/project/main.ts');
});
it('temp file path differs from final path', () => {
const ops = atomicWriteOps('/test/file.txt', 'data');
expect(ops.tmpPath).not.toBe(ops.finalPath);
});
it('handles paths with special characters', () => {
const ops = atomicWriteOps('/path/with spaces/file.ts', 'data');
expect(ops.tmpPath).toBe('/path/with spaces/file.ts.agor-tmp');
});
});
describe('conflict workflow', () => {
it('full read-modify-check-write cycle — no conflict', () => {
// 1. Read file, record mtime
const readStat: FileStat = { mtimeMs: 1700000000000, size: 50 };
const readMtimeMs = readStat.mtimeMs;
// 2. User edits in editor
// 3. Before save, stat again
const preSaveStat: FileStat = { mtimeMs: 1700000000000, size: 50 }; // unchanged
expect(hasConflict(readMtimeMs, preSaveStat)).toBe(false);
// 4. Write via atomic
const ops = atomicWriteOps('/test/file.ts', 'new content');
expect(ops.tmpPath).toContain('.agor-tmp');
});
it('full read-modify-check-write cycle — conflict detected', () => {
// 1. Read file, record mtime
const readMtimeMs = 1700000000000;
// 2. External process modifies the file
const preSaveStat: FileStat = { mtimeMs: 1700000002000, size: 80 };
// 3. Conflict detected — should warn user
expect(hasConflict(readMtimeMs, preSaveStat)).toBe(true);
});
it('after successful save, update readMtimeMs', () => {
let readMtimeMs = 1700000000000;
// Save succeeds, stat again to get new mtime
const postSaveStat: FileStat = { mtimeMs: 1700000003000, size: 120 };
readMtimeMs = postSaveStat.mtimeMs;
// No conflict on subsequent check
expect(hasConflict(readMtimeMs, postSaveStat)).toBe(false);
});
});