import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Database } from 'bun:sqlite'; import type { CompanyAnalysis } from '@/lib/types'; const TEST_USER_ID = 'overview-cache-user'; let tempDir: string | null = null; let sqliteClient: Database | null = null; let overviewCacheRepo: typeof import('./company-overview-cache') | null = null; function resetDbSingletons() { const globalState = globalThis as typeof globalThis & { __fiscalSqliteClient?: Database; __fiscalDrizzleDb?: unknown; }; globalState.__fiscalSqliteClient?.close(); globalState.__fiscalSqliteClient = undefined; globalState.__fiscalDrizzleDb = undefined; } function applyMigration(client: Database, fileName: string) { const sql = readFileSync(join(process.cwd(), 'drizzle', fileName), 'utf8'); client.exec(sql); } function ensureUser(client: Database) { const now = Date.now(); client.exec(` INSERT OR REPLACE INTO user (id, name, email, emailVerified, image, createdAt, updatedAt, role, banned, banReason, banExpires) VALUES ('${TEST_USER_ID}', 'Overview Cache User', 'overview-cache@example.com', 1, NULL, ${now}, ${now}, NULL, 0, NULL, NULL); `); } function clearCache(client: Database) { client.exec('DELETE FROM company_overview_cache;'); } function buildAnalysisPayload(companyName: string): CompanyAnalysis { return { company: { ticker: 'MSFT', companyName, sector: null, category: null, tags: [], cik: null }, quote: { value: 100, stale: false }, position: null, priceHistory: { value: [], stale: false }, benchmarkHistory: { value: [], stale: false }, financials: [], filings: [], aiReports: [], coverage: null, journalPreview: [], recentAiReports: [], latestFilingSummary: null, keyMetrics: { referenceDate: null, revenue: null, netIncome: null, totalAssets: null, cash: null, debt: null, netMargin: null }, companyProfile: { description: null, exchange: null, industry: null, country: null, website: null, fiscalYearEnd: null, employeeCount: null, source: 'unavailable' }, valuationSnapshot: { sharesOutstanding: null, marketCap: null, enterpriseValue: null, trailingPe: null, evToRevenue: null, evToEbitda: null, source: 'unavailable' }, bullBear: { source: 'unavailable', bull: [], bear: [], updatedAt: null }, recentDevelopments: { status: 'unavailable', items: [], weeklySnapshot: null } }; } describe('company overview cache repo', () => { beforeAll(async () => { tempDir = mkdtempSync(join(tmpdir(), 'fiscal-overview-cache-')); const env = process.env as Record; env.DATABASE_URL = `file:${join(tempDir, 'repo.sqlite')}`; env.NODE_ENV = 'test'; resetDbSingletons(); sqliteClient = new Database(join(tempDir, 'repo.sqlite'), { create: true }); sqliteClient.exec('PRAGMA foreign_keys = ON;'); applyMigration(sqliteClient, '0000_cold_silver_centurion.sql'); applyMigration(sqliteClient, '0012_company_overview_cache.sql'); ensureUser(sqliteClient); const globalState = globalThis as typeof globalThis & { __fiscalSqliteClient?: Database; __fiscalDrizzleDb?: unknown; }; globalState.__fiscalSqliteClient = sqliteClient; globalState.__fiscalDrizzleDb = undefined; overviewCacheRepo = await import('./company-overview-cache'); }); afterAll(() => { sqliteClient?.close(); resetDbSingletons(); if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); } }); beforeEach(() => { if (!sqliteClient) { throw new Error('sqlite client not initialized'); } clearCache(sqliteClient); }); it('upserts and reloads cached overview payloads', async () => { if (!overviewCacheRepo) { throw new Error('overview cache repo not initialized'); } const first = await overviewCacheRepo.upsertCompanyOverviewCache({ userId: TEST_USER_ID, ticker: 'msft', sourceSignature: 'sig-1', payload: buildAnalysisPayload('Microsoft Corporation') }); const loaded = await overviewCacheRepo.getCompanyOverviewCache({ userId: TEST_USER_ID, ticker: 'MSFT' }); expect(first.ticker).toBe('MSFT'); expect(loaded?.source_signature).toBe('sig-1'); expect(loaded?.payload.company.companyName).toBe('Microsoft Corporation'); }); it('updates existing cached rows in place', async () => { if (!overviewCacheRepo) { throw new Error('overview cache repo not initialized'); } const first = await overviewCacheRepo.upsertCompanyOverviewCache({ userId: TEST_USER_ID, ticker: 'MSFT', sourceSignature: 'sig-1', payload: buildAnalysisPayload('Old Name') }); const second = await overviewCacheRepo.upsertCompanyOverviewCache({ userId: TEST_USER_ID, ticker: 'MSFT', sourceSignature: 'sig-2', payload: buildAnalysisPayload('New Name') }); const loaded = await overviewCacheRepo.getCompanyOverviewCache({ userId: TEST_USER_ID, ticker: 'MSFT' }); expect(second.id).toBe(first.id); expect(loaded?.source_signature).toBe('sig-2'); expect(loaded?.payload.company.companyName).toBe('New Name'); }); });