phase 16: vitest + MSW frontend tests, CI integration

This commit is contained in:
Celes Renata
2026-04-11 19:28:38 -07:00
parent 5758a704ec
commit 4d0c38bba7
9 changed files with 1939 additions and 3 deletions
+72
View File
@@ -0,0 +1,72 @@
import { http, HttpResponse } from 'msw';
// Seed data for deterministic tests
export const mockCompanies = [
{ id: '1', ticker: 'AAPL', legal_name: 'Apple Inc.', exchange: 'NASDAQ', sector: 'Technology', industry: 'Consumer Electronics', market_cap_bucket: 'mega', active: true, active_source_count: 3 },
{ id: '2', ticker: 'MSFT', legal_name: 'Microsoft Corporation', exchange: 'NASDAQ', sector: 'Technology', industry: 'Software', market_cap_bucket: 'mega', active: true, active_source_count: 2 },
];
export const mockDocuments = [
{ id: 'd1', document_type: 'news', source_type: 'news_api', publisher: 'Reuters', url: null, title: 'Apple Q4 Earnings Beat', published_at: '2026-04-10T12:00:00Z', retrieved_at: '2026-04-10T12:05:00Z', language: 'en', content_hash: 'abc123', parse_quality_score: 0.92, parse_confidence: 'high', status: 'extracted', created_at: '2026-04-10T12:05:00Z' },
];
export const mockTrends = [
{ id: 't1', entity_type: 'company', entity_id: 'AAPL', window: '7d', trend_direction: 'bullish', trend_strength: 0.75, confidence: 0.82, top_supporting_evidence: ['Strong earnings'], top_opposing_evidence: [], dominant_catalysts: ['earnings'], material_risks: [], contradiction_score: 0.1, market_context: null, generated_at: '2026-04-10T18:00:00Z' },
];
export const mockRecommendations = [
{ id: 'r1', ticker: 'AAPL', action: 'buy', mode: 'paper', confidence: 0.78, time_horizon: '7d', thesis: 'Strong earnings momentum', invalidation_conditions: null, portfolio_pct: 0.05, max_loss_pct: 0.02, model_version: 'v1', risk_classification: 'moderate', generated_at: '2026-04-10T19:00:00Z' },
];
export const mockOrders = [
{ id: 'o1', recommendation_id: 'r1', broker_account_id: null, ticker: 'AAPL', side: 'buy', order_type: 'market', quantity: 10, limit_price: null, stop_price: null, status: 'filled', broker_order_id: null, submitted_at: '2026-04-10T19:01:00Z', fill_price: 185.50, fill_quantity: 10, created_at: '2026-04-10T19:01:00Z' },
];
export const mockPositions = [
{ id: 'p1', broker_account_id: null, ticker: 'AAPL', quantity: 10, avg_entry_price: 185.50, current_price: 188.20, unrealized_pnl: 27.00, realized_pnl: 0, updated_at: '2026-04-11T12:00:00Z' },
];
export const handlers = [
// Query API (proxied at /api/)
http.get('/api/companies', () => HttpResponse.json(mockCompanies)),
http.get('/api/companies/:id', ({ params }) => {
const c = mockCompanies.find((c) => c.id === params.id);
return c ? HttpResponse.json({ ...c, aliases: [], active_source_count: c.active_source_count }) : new HttpResponse(null, { status: 404 });
}),
http.get('/api/companies/:id/sources', () => HttpResponse.json([])),
http.get('/api/documents', () => HttpResponse.json(mockDocuments)),
http.get('/api/documents/:id', () => HttpResponse.json({ ...mockDocuments[0], canonical_url: null, raw_storage_ref: null, normalized_storage_ref: null, company_mentions: [], intelligence: null })),
http.get('/api/trends', () => HttpResponse.json(mockTrends)),
http.get('/api/trends/:id', () => HttpResponse.json(mockTrends[0])),
http.get('/api/trends/:id/evidence', () => HttpResponse.json({ trend: mockTrends[0], evidence: [] })),
http.get('/api/recommendations', () => HttpResponse.json(mockRecommendations)),
http.get('/api/recommendations/:id', () => HttpResponse.json({ ...mockRecommendations[0], company_id: '1', evidence: [], risk_evaluation: null })),
http.get('/api/orders', () => HttpResponse.json(mockOrders)),
http.get('/api/orders/:id', () => HttpResponse.json({ ...mockOrders[0], idempotency_key: null, decision_trace: null, events: [], audit_trail: [] })),
http.get('/api/positions', () => HttpResponse.json(mockPositions)),
http.get('/api/admin/trading/config', () => HttpResponse.json({ trading_mode: 'paper', config: {} })),
http.get('/api/admin/trading/approvals', () => HttpResponse.json([])),
http.get('/api/admin/trading/lockouts', () => HttpResponse.json([])),
http.get('/api/ops/pipeline/health', () => HttpResponse.json({ hours: 24, document_stages: [{ status: 'extracted', doc_count: 5 }], parsing: {}, extraction: {}, aggregation: {} })),
http.get('/api/ops/ingestion/summary', () => HttpResponse.json({ total_runs: 10, completed: 8, failed: 2, total_items_fetched: 50, total_items_new: 12, by_source_type: [] })),
http.get('/api/ops/ingestion/throughput', () => HttpResponse.json([])),
http.get('/api/ops/model/performance', () => HttpResponse.json({ total_extractions: 20, success_rate: 0.9, avg_duration_ms: 1500, retry_rate: 0.05, avg_confidence: 0.8 })),
http.get('/api/ops/model/failures', () => HttpResponse.json([])),
http.get('/api/ops/sources/coverage-gaps', () => HttpResponse.json({ missing_source_types: [], stale_sources: [] })),
http.get('/api/admin/companies/coverage', () => HttpResponse.json([])),
// Symbol Registry (proxied at /registry/)
http.get('/registry/companies', () => HttpResponse.json(mockCompanies)),
http.post('/registry/companies', async ({ request }) => {
const body = await request.json() as Record<string, string>;
return HttpResponse.json({ id: '99', ticker: body.ticker, legal_name: body.legal_name, exchange: body.exchange ?? null, sector: body.sector ?? null, industry: null, market_cap_bucket: null, active: true }, { status: 201 });
}),
http.get('/registry/watchlists', () => HttpResponse.json([])),
http.post('/registry/watchlists', async ({ request }) => {
const body = await request.json() as Record<string, string>;
return HttpResponse.json({ id: 'w1', name: body.name, description: body.description ?? null, active: true }, { status: 201 });
}),
// Health
http.get('/api/health', () => HttpResponse.json({ status: 'ok' })),
];