phase-8g: add sqllite.
This commit is contained in:
@@ -1,32 +1,31 @@
|
||||
/**
|
||||
* Unit tests for MarketCallRepository
|
||||
* Each test gets its own temp file so tests are fully isolated.
|
||||
* Unit tests for MarketCallRepository (SQLite-backed).
|
||||
* Each test gets its own in-memory database so tests are fully isolated.
|
||||
*/
|
||||
|
||||
import { test, after } from 'node:test';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import BetterSqlite3 from 'better-sqlite3';
|
||||
import { MarketCallRepository } from '../server/repositories/MarketCallRepository';
|
||||
|
||||
// ── Temp-file helpers ─────────────────────────────────────────────────────────
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const tmpDirs: string[] = [];
|
||||
const DDL = `
|
||||
CREATE TABLE IF NOT EXISTS market_calls (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL, quarter TEXT NOT NULL, date TEXT NOT NULL,
|
||||
thesis TEXT NOT NULL, tickers TEXT NOT NULL, snapshot TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
function tempRepo(): MarketCallRepository {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'mkt-calls-test-'));
|
||||
const path = join(dir, 'calls.json');
|
||||
tmpDirs.push(dir);
|
||||
return new MarketCallRepository(path);
|
||||
function makeRepo(): MarketCallRepository {
|
||||
const db = new BetterSqlite3(':memory:');
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.exec(DDL);
|
||||
return new MarketCallRepository(db);
|
||||
}
|
||||
|
||||
after(() => {
|
||||
for (const dir of tmpDirs) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const CALL_INPUT = {
|
||||
@@ -38,14 +37,12 @@ const CALL_INPUT = {
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
test('list() returns empty array when file does not exist', () => {
|
||||
const repo = tempRepo();
|
||||
assert.deepEqual(repo.list(), []);
|
||||
test('list() returns empty array on fresh db', () => {
|
||||
assert.deepEqual(makeRepo().list(), []);
|
||||
});
|
||||
|
||||
test('create() returns call with id, createdAt, and correct fields', () => {
|
||||
const repo = tempRepo();
|
||||
const call = repo.create(CALL_INPUT);
|
||||
const call = makeRepo().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);
|
||||
@@ -54,76 +51,58 @@ test('create() returns call with id, createdAt, and correct fields', () => {
|
||||
assert.deepEqual(call.tickers, CALL_INPUT.tickers);
|
||||
});
|
||||
|
||||
test('create() persists to disk — list() returns the created call', () => {
|
||||
const repo = tempRepo();
|
||||
test('create() persists — list() returns the created call', () => {
|
||||
const repo = makeRepo();
|
||||
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 repo = makeRepo();
|
||||
const db = (repo as any).db as BetterSqlite3.Database;
|
||||
|
||||
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');
|
||||
// Insert two rows with distinct created_at values directly
|
||||
db.prepare(
|
||||
`INSERT INTO market_calls (id, title, quarter, date, thesis, tickers, snapshot, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?)`,
|
||||
).run('old-id', 'First', 'Q1', '2025-01-01', 'A', '[]', '{}', '2025-01-01T00:00:00.000Z');
|
||||
db.prepare(
|
||||
`INSERT INTO market_calls (id, title, quarter, date, thesis, tickers, snapshot, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?)`,
|
||||
).run('new-id', 'Second', 'Q1', '2025-01-02', 'B', '[]', '{}', '2025-01-02T00:00:00.000Z');
|
||||
|
||||
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 repo = makeRepo();
|
||||
const call = repo.create(CALL_INPUT);
|
||||
const found = repo.get(call.id);
|
||||
assert.ok(found, 'should find by id');
|
||||
assert.ok(found);
|
||||
assert.equal(found!.id, call.id);
|
||||
});
|
||||
|
||||
test('get() returns null for unknown id', () => {
|
||||
const repo = tempRepo();
|
||||
assert.equal(repo.get('nonexistent-id'), null);
|
||||
assert.equal(makeRepo().get('no-such-id'), null);
|
||||
});
|
||||
|
||||
test('delete() removes the call and returns true', () => {
|
||||
const repo = tempRepo();
|
||||
const repo = makeRepo();
|
||||
const call = repo.create(CALL_INPUT);
|
||||
const ok = repo.delete(call.id);
|
||||
assert.equal(ok, true);
|
||||
assert.equal(repo.delete(call.id), 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);
|
||||
assert.equal(makeRepo().delete('no-such-id'), false);
|
||||
});
|
||||
|
||||
test('delete() only removes the targeted call, leaves others intact', () => {
|
||||
const repo = tempRepo();
|
||||
test('delete() only removes the targeted call', () => {
|
||||
const repo = makeRepo();
|
||||
const a = repo.create({ ...CALL_INPUT, title: 'Keep me' });
|
||||
const b = repo.create({ ...CALL_INPUT, title: 'Delete me' });
|
||||
repo.delete(b.id);
|
||||
@@ -133,34 +112,29 @@ test('delete() only removes the targeted call, leaves others intact', () => {
|
||||
});
|
||||
|
||||
test('create() stores snapshot when provided', () => {
|
||||
const repo = tempRepo();
|
||||
const repo = makeRepo();
|
||||
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);
|
||||
assert.deepEqual(repo.get(call.id)!.snapshot, snapshot);
|
||||
});
|
||||
|
||||
test('create() sets default date when not provided', () => {
|
||||
const repo = tempRepo();
|
||||
const call = repo.create(CALL_INPUT);
|
||||
const call = makeRepo().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' });
|
||||
const call = makeRepo().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.
|
||||
test('concurrent writes: two rapid creates both persist (SQLite WAL is concurrency-safe)', () => {
|
||||
const repo = makeRepo();
|
||||
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');
|
||||
assert.equal(list.length, 2);
|
||||
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');
|
||||
assert.ok(ids.has(a.id));
|
||||
assert.ok(ids.has(b.id));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user