import { describe, expect, it, mock } from 'bun:test'; import type { CompanyAnalysis, Filing, ResearchMemo } from '@/lib/types'; import { __companyAnalysisInternals, getCompanyAnalysisPayload } from './company-analysis'; function buildFiling(updatedAt = '2026-03-10T00:00:00.000Z'): Filing { return { id: 1, ticker: 'MSFT', filing_type: '10-K', filing_date: '2026-02-01', accession_number: '0000000000-26-000001', cik: '0000789019', company_name: 'Microsoft Corporation', filing_url: 'https://www.sec.gov/Archives/test.htm', submission_url: 'https://www.sec.gov/submissions/test.json', primary_document: 'test.htm', metrics: null, analysis: null, created_at: updatedAt, updated_at: updatedAt }; } function buildMemo(updatedAt = '2026-03-10T00:00:00.000Z', thesis = 'Azure remains durable.'): ResearchMemo { return { id: 1, user_id: 'user-1', organization_id: null, ticker: 'MSFT', rating: 'buy', conviction: 'high', time_horizon_months: 24, packet_title: null, packet_subtitle: null, thesis_markdown: thesis, variant_view_markdown: '', catalysts_markdown: '', risks_markdown: '', disconfirming_evidence_markdown: '', next_actions_markdown: '', created_at: updatedAt, updated_at: updatedAt }; } function buildLocalInputs(overrides: Partial[0]['localInputs']> = {}) { return { filings: [buildFiling()], holding: null, watchlistItem: null, journalPreview: [], memo: null, ...overrides }; } 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 } }; } function buildCacheRecord(payload: CompanyAnalysis, sourceSignature: string) { return { id: 1, user_id: 'user-1', ticker: 'MSFT', cache_version: 1, source_signature: sourceSignature, payload, created_at: '2026-03-13T11:55:00.000Z', updated_at: '2026-03-13T11:55:00.000Z' }; } describe('company analysis cache orchestration', () => { it('changes the source signature when local inputs change', () => { const base = __companyAnalysisInternals.buildCompanyAnalysisSourceSignature({ ticker: 'MSFT', localInputs: buildLocalInputs() }); const memoChanged = __companyAnalysisInternals.buildCompanyAnalysisSourceSignature({ ticker: 'MSFT', localInputs: buildLocalInputs({ memo: buildMemo('2026-03-11T00:00:00.000Z') }) }); const filingChanged = __companyAnalysisInternals.buildCompanyAnalysisSourceSignature({ ticker: 'MSFT', localInputs: buildLocalInputs({ filings: [buildFiling('2026-03-11T00:00:00.000Z')] }) }); const journalChanged = __companyAnalysisInternals.buildCompanyAnalysisSourceSignature({ ticker: 'MSFT', localInputs: buildLocalInputs({ journalPreview: [{ id: 7, user_id: 'user-1', ticker: 'MSFT', accession_number: null, entry_type: 'note', title: 'Updated note', body_markdown: 'Body', metadata: null, created_at: '2026-03-01T00:00:00.000Z', updated_at: '2026-03-11T00:00:00.000Z' }] }) }); expect(memoChanged).not.toBe(base); expect(filingChanged).not.toBe(base); expect(journalChanged).not.toBe(base); }); it('returns a fresh cached payload when signature and ttl match', async () => { const localInputs = buildLocalInputs(); const sourceSignature = __companyAnalysisInternals.buildCompanyAnalysisSourceSignature({ ticker: 'MSFT', localInputs }); const buildOverview = mock(async () => buildAnalysisPayload('Built Corp')); const upsertCachedOverview = mock(async () => buildCacheRecord(buildAnalysisPayload('Cached Corp'), sourceSignature)); const analysis = await getCompanyAnalysisPayload({ userId: 'user-1', ticker: 'MSFT', now: new Date('2026-03-13T12:00:00.000Z') }, { getLocalInputs: async () => localInputs, getCachedOverview: async () => buildCacheRecord(buildAnalysisPayload('Cached Corp'), sourceSignature), buildOverview, upsertCachedOverview }); expect(analysis.company.companyName).toBe('Cached Corp'); expect(buildOverview).not.toHaveBeenCalled(); expect(upsertCachedOverview).not.toHaveBeenCalled(); }); it('bypasses the cache when refresh is requested', async () => { const localInputs = buildLocalInputs(); const sourceSignature = __companyAnalysisInternals.buildCompanyAnalysisSourceSignature({ ticker: 'MSFT', localInputs }); const buildOverview = mock(async () => buildAnalysisPayload('Refreshed Corp')); const upsertCachedOverview = mock(async () => buildCacheRecord(buildAnalysisPayload('Refreshed Corp'), sourceSignature)); const analysis = await getCompanyAnalysisPayload({ userId: 'user-1', ticker: 'MSFT', refresh: true, now: new Date('2026-03-13T12:00:00.000Z') }, { getLocalInputs: async () => localInputs, getCachedOverview: async () => buildCacheRecord(buildAnalysisPayload('Cached Corp'), sourceSignature), buildOverview, upsertCachedOverview }); expect(analysis.company.companyName).toBe('Refreshed Corp'); expect(buildOverview).toHaveBeenCalledTimes(1); expect(upsertCachedOverview).toHaveBeenCalledTimes(1); }); it('rebuilds when local inputs invalidate the cached signature', async () => { const cachedInputs = buildLocalInputs({ memo: buildMemo('2026-03-10T00:00:00.000Z', 'Old memo') }); const freshInputs = buildLocalInputs({ memo: buildMemo('2026-03-11T00:00:00.000Z', 'New memo') }); const staleSignature = __companyAnalysisInternals.buildCompanyAnalysisSourceSignature({ ticker: 'MSFT', localInputs: cachedInputs }); const buildOverview = mock(async () => buildAnalysisPayload('Rebuilt Corp')); const analysis = await getCompanyAnalysisPayload({ userId: 'user-1', ticker: 'MSFT', now: new Date('2026-03-13T12:00:00.000Z') }, { getLocalInputs: async () => freshInputs, getCachedOverview: async () => buildCacheRecord(buildAnalysisPayload('Cached Corp'), staleSignature), buildOverview, upsertCachedOverview: async () => buildCacheRecord(buildAnalysisPayload('Rebuilt Corp'), staleSignature) }); expect(analysis.company.companyName).toBe('Rebuilt Corp'); expect(buildOverview).toHaveBeenCalledTimes(1); }); });