fix: backfill symbol_demand for sidebar-added symbols + analyst ratings schema fix

- Add await ctx.cache.subscribe() to addSymbol mutation so symbols
  added via the sidebar get registered in symbol_demand and yfinance
  jobs are queued immediately
- Backfill PEP, WYNN, STZ, CELH into symbol_demand + adapter_queue
- Upgrade yahoo-finance2 3.15.3 -> 3.15.4 and pass validateResult:false
  to quoteSummary() to handle Yahoo schema drift
- Add error detail logging for analyst ratings schema failures
- Update .gitignore with common ignores
This commit is contained in:
Investor Flow Build
2026-07-23 18:02:24 -04:00
parent 5ef2b2f060
commit e262187c3c
204 changed files with 25014 additions and 2934 deletions
@@ -0,0 +1,65 @@
// Tests — ReportRunner (Slice 22). Pure, no network.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { generateReport } from '../ReportRunner.ts';
test('generateReport produces valid HTML for a symbol report', () => {
const result = generateReport({ scope: 'symbol', symbol: 'NVDA', data: { overview: 'NVDA overview text.' } });
assert.ok(result.html.startsWith('<!DOCTYPE html>'));
assert.ok(result.html.includes('NVDA'));
assert.ok(result.html.includes('Overview'));
assert.ok(result.html.includes('Educational analysis'));
assert.ok(result.html.includes('</html>'));
assert.equal(result.title, 'Research Note: NVDA');
});
test('generateReport produces valid HTML for portfolio report', () => {
const result = generateReport({
scope: 'portfolio',
data: {
holdings: [
{ symbol: 'NVDA', shares: 10, avgCost: 250 },
{ symbol: 'AAPL', shares: 5, avgCost: 180 },
],
},
});
assert.ok(result.html.includes('Holdings'));
assert.ok(result.html.includes('NVDA'));
assert.ok(result.html.includes('AAPL'));
assert.ok(result.html.includes('$2500')); // 10 * 250
assert.equal(result.title, 'Research Note: Portfolio Analysis');
});
test('generateReport includes recommended actions when provided', () => {
const result = generateReport({
scope: 'risk_posture',
data: {
recommendedActions: [
{ id: 'consider_reducing_position', tradeOff: 'Test trade-off', explanation: 'Test explanation.' },
],
},
});
assert.ok(result.html.includes('Considerations'));
assert.ok(result.html.includes('Test trade-off'));
assert.ok(result.html.includes('Test explanation'));
});
test('generateReport includes ADR-0007 footer', () => {
const result = generateReport({ scope: 'symbol', symbol: 'TEST', data: {} });
assert.ok(result.html.includes('not investment advice'));
assert.ok(result.html.includes('responsible for your own decisions'));
});
test('generateReport outputs deterministic content given same inputs', () => {
const input = { scope: 'symbol' as const, symbol: 'NVDA', data: { overview: 'test' } };
const a = generateReport(input);
const b = generateReport(input);
assert.equal(a.html, b.html);
assert.equal(a.title, b.title);
});
test('generateReport escapes HTML in user data', () => {
const result = generateReport({ scope: 'symbol', symbol: '<script>', data: { overview: '<script>alert("xss")</script>' } });
assert.ok(!result.html.includes('<script>'));
assert.ok(result.html.includes('&lt;script&gt;'));
});