/** * Unit tests for MarketCallRepository * Each test gets its own temp file so tests are fully isolated. */ import { test, after } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { MarketCallRepository } from '../server/repositories/MarketCallRepository'; // ── Temp-file helpers ───────────────────────────────────────────────────────── const tmpDirs: string[] = []; function tempRepo(): MarketCallRepository { const dir = mkdtempSync(join(tmpdir(), 'mkt-calls-test-')); const path = join(dir, 'calls.json'); tmpDirs.push(dir); return new MarketCallRepository(path); } after(() => { for (const dir of tmpDirs) { rmSync(dir, { recursive: true, force: true }); } }); // ── Fixtures ────────────────────────────────────────────────────────────────── const CALL_INPUT = { title: 'Rate pivot play', quarter: 'Q3 2025', thesis: 'Fed cuts expected — rotate into duration and growth.', tickers: ['TLT', 'QQQ'], }; // ── Tests ───────────────────────────────────────────────────────────────────── test('list() returns empty array when file does not exist', () => { const repo = tempRepo(); assert.deepEqual(repo.list(), []); }); test('create() returns call with id, createdAt, and correct fields', () => { const repo = tempRepo(); const call = repo.create(CALL_INPUT); assert.ok(call.id, 'id should be set'); assert.ok(call.createdAt, 'createdAt should be set'); assert.equal(call.title, CALL_INPUT.title); assert.equal(call.quarter, CALL_INPUT.quarter); assert.equal(call.thesis, CALL_INPUT.thesis); assert.deepEqual(call.tickers, CALL_INPUT.tickers); }); test('create() persists to disk — list() returns the created call', () => { const repo = tempRepo(); repo.create(CALL_INPUT); assert.equal(repo.list().length, 1); assert.equal(repo.list()[0].title, CALL_INPUT.title); }); test('list() returns calls newest-first', () => { // Write two calls directly with distinct timestamps to guarantee stable ordering. const dir = mkdtempSync(join(tmpdir(), 'mkt-order-')); tmpDirs.push(dir); const path = join(dir, 'calls.json'); const older = { id: 'old-id', title: 'First', quarter: 'Q1', date: '2025-01-01', thesis: 'A', tickers: [], snapshot: {}, createdAt: '2025-01-01T00:00:00.000Z', }; const newer = { id: 'new-id', title: 'Second', quarter: 'Q1', date: '2025-01-02', thesis: 'B', tickers: [], snapshot: {}, createdAt: '2025-01-02T00:00:00.000Z', }; writeFileSync(path, JSON.stringify({ calls: [older, newer] }), 'utf8'); const repo = new MarketCallRepository(path); const list = repo.list(); assert.equal(list[0].id, 'new-id', 'newer call should be first'); assert.equal(list[1].id, 'old-id', 'older call should be second'); }); test('get() returns the call by id', () => { const repo = tempRepo(); const call = repo.create(CALL_INPUT); const found = repo.get(call.id); assert.ok(found, 'should find by id'); assert.equal(found!.id, call.id); }); test('get() returns null for unknown id', () => { const repo = tempRepo(); assert.equal(repo.get('nonexistent-id'), null); }); test('delete() removes the call and returns true', () => { const repo = tempRepo(); const call = repo.create(CALL_INPUT); const ok = repo.delete(call.id); assert.equal(ok, true); assert.equal(repo.list().length, 0); assert.equal(repo.get(call.id), null); }); test('delete() returns false for unknown id', () => { const repo = tempRepo(); assert.equal(repo.delete('no-such-id'), false); }); test('delete() only removes the targeted call, leaves others intact', () => { const repo = tempRepo(); const a = repo.create({ ...CALL_INPUT, title: 'Keep me' }); const b = repo.create({ ...CALL_INPUT, title: 'Delete me' }); repo.delete(b.id); const list = repo.list(); assert.equal(list.length, 1); assert.equal(list[0].id, a.id); }); test('create() stores snapshot when provided', () => { const repo = tempRepo(); const snapshot = { TLT: { price: 95.5, signal: '✅ Strong Buy' } }; const call = repo.create({ ...CALL_INPUT, snapshot } as any); const found = repo.get(call.id)!; assert.deepEqual(found.snapshot, snapshot); }); test('create() sets default date when not provided', () => { const repo = tempRepo(); const call = repo.create(CALL_INPUT); assert.match(call.date, /^\d{4}-\d{2}-\d{2}$/); }); test('create() uses provided date', () => { const repo = tempRepo(); const call = repo.create({ ...CALL_INPUT, date: '2025-03-15' }); assert.equal(call.date, '2025-03-15'); }); test('concurrent writes: two rapid creates do not lose data', async () => { const repo = tempRepo(); // Both writes happen synchronously (writeFileSync), so the second // always sees the first. This test documents the behaviour. const a = repo.create({ ...CALL_INPUT, title: 'A' }); const b = repo.create({ ...CALL_INPUT, title: 'B' }); const list = repo.list(); assert.equal(list.length, 2, 'both calls should be persisted'); const ids = new Set(list.map((c) => c.id)); assert.ok(ids.has(a.id), 'call A should be present'); assert.ok(ids.has(b.id), 'call B should be present'); });