'use strict'; // Truth-Everywhere Part 2 (item 7) — the public accuracy endpoints now read the // CLEAN Postgres ledger aggregate (getModelAggregate / getAccuracyView), NOT the // Redis outcome log. We partial-mock ledgerService so these run offline. const request = require('supertest'); let mockStore = {}; jest.mock('../../src/utils/redis', () => ({ getRedisClient: () => ({}), cacheGet: async (k) => (k in mockStore ? mockStore[k] : null), cacheSet: async (k, v) => { mockStore[k] = v; return true; }, cacheDel: async () => true, isDegraded: () => false, })); jest.mock('../../src/services/ledgerService', () => { const actual = jest.requireActual('../../src/services/ledgerService'); return { ...actual, getAccuracyView: jest.fn(), getModelAggregate: jest.fn(), accuracyBucketsFromAgg: actual.accuracyBucketsFromAgg, // keep the real bucketer }; }); const ledgerService = require('../../src/services/ledgerService'); const app = require('../../src/app'); const EMPTY_AGG = { window_days: 30, min_sample: 20, settled: 0, hits: 0, misses: 0, pushes: 0, hit_pct: null, beat_close_pct: null, clv_distribution: null, by_tier: {}, pending: 0, }; beforeEach(() => { mockStore = {}; ledgerService.getAccuracyView.mockReset(); ledgerService.getModelAggregate.mockReset(); }); describe('GET /api/accuracy', () => { test('cold (no data) → valid empty-safe shape from the ledger view', async () => { ledgerService.getAccuracyView.mockResolvedValue({ overall: { sport: 'overall', window_days: 30, sample: 0, min_sample: 20, overall: { hits: 0, misses: 0, pushes: 0, total: 0, pct: null }, byGrade: {} }, sports: {}, min_sample: 20, updated_at: null, }); const res = await request(app).get('/api/accuracy'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('overall'); expect(res.body).toHaveProperty('sports'); expect(res.body.min_sample).toBeGreaterThan(0); }); test('returns the clean ledger record when present', async () => { ledgerService.getAccuracyView.mockResolvedValue({ overall: { sport: 'overall', window_days: 30, sample: 20, min_sample: 20, overall: { hits: 14, misses: 6, pushes: 0, total: 20, pct: 70 }, byGrade: { A: { hits: 8, misses: 2, pushes: 0, total: 10, pct: 80 } } }, sports: { mlb: { sport: 'mlb', overall: { hits: 14, misses: 6, pushes: 0, total: 20, pct: 70 }, byGrade: {} } }, min_sample: 20, updated_at: null, }); const res = await request(app).get('/api/accuracy'); expect(res.body.overall.overall.pct).toBe(70); expect(res.body.sports.mlb).toBeTruthy(); }); }); describe('GET /api/ledger/accuracy', () => { test('returns grade-tier buckets from the clean ledger aggregate', async () => { ledgerService.getModelAggregate.mockResolvedValue({ ...EMPTY_AGG, settled: 30, hits: 20, misses: 10, hit_pct: 67, by_tier: { 'A+': { settled: 3, hits: 3, misses: 0, pushes: 0, hit_pct: null }, 'A': { settled: 22, hits: 15, misses: 7, pushes: 0, hit_pct: 68 }, 'B': { settled: 5, hits: 2, misses: 3, pushes: 0, hit_pct: null }, }, }); const res = await request(app).get('/api/ledger/accuracy'); expect(res.status).toBe(200); expect(Array.isArray(res.body.buckets)).toBe(true); const grades = res.body.buckets.map((b) => b.grade); expect(grades).toContain('A'); // A+ folds into A in the public strip expect(res.body.overall.pct).toBe(67); }); test('empty aggregate → empty buckets, never 500', async () => { ledgerService.getModelAggregate.mockResolvedValue({ ...EMPTY_AGG }); const res = await request(app).get('/api/ledger/accuracy'); expect(res.status).toBe(200); expect(res.body.buckets).toEqual([]); }); });