Add company overview skeleton and cache
This commit is contained in:
251
lib/server/company-analysis.test.ts
Normal file
251
lib/server/company-analysis.test.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
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<Parameters<typeof __companyAnalysisInternals.buildCompanyAnalysisSourceSignature>[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: 100,
|
||||
position: null,
|
||||
priceHistory: [],
|
||||
benchmarkHistory: [],
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user