d09a06c054
Product overhaul core — the two transformative, differentiated systems: Self-learning loop (Phase 2): outcomeService settles locked snapshot grades against real MLB Stats API results → hit/miss/push, rolling accuracy by grade tier (30d window). Idempotent, injectable, unit-tested. New GET /api/accuracy + /api/ledger/accuracy + internal settle triggers + cron hook. AccuracyBadge (dashboard/scan/landing) is honest — "LEARNING" below MIN_SAMPLE, never a fake number. Settled HIT/MISS chips overlay the live slate. Real-time layer (Phase 1): Slate silent 60s auto-refresh (no flash, no wipe on transient blips) + "SIGNAL LIVE · UPDATED Xs ago" freshness strip; Ticker LIVE badge that flashes on fresh events. Landing (Phase 3): TopSignals shows tonight's real top-3 A-rated grades + live accuracy — the product shown, not described. Founder pricing: FOUNDER_CODE_EXPIRY default 2026-06-30 → 2026-12-31 (had lapsed, disabling every founder code + the ClaimMeter pitch). That expiry — not a tier change — was the real cause of the 4 stripe test failures. Backend 2255 (4 failing) → 2274 (all green; +19 new, +4 fixed). Web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.5 KiB
JavaScript
68 lines
2.5 KiB
JavaScript
'use strict';
|
|
|
|
// Session 55 — the self-learning loop's public read endpoints. Redis is mocked
|
|
// so these run offline; the store is seeded per-test via cacheGet.
|
|
|
|
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,
|
|
}));
|
|
|
|
const app = require('../../src/app');
|
|
|
|
beforeEach(() => { mockStore = {}; });
|
|
|
|
describe('GET /api/accuracy', () => {
|
|
test('cold cache → valid empty-safe shape', async () => {
|
|
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 persisted record when present', async () => {
|
|
mockStore['accuracy:overall'] = {
|
|
sport: 'overall', updated_at: '2026-07-10T00:00:00Z', window_days: 30, 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 } },
|
|
};
|
|
mockStore['accuracy:mlb'] = mockStore['accuracy:overall'];
|
|
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 accuracy record', async () => {
|
|
mockStore['accuracy:overall'] = {
|
|
sport: 'overall', updated_at: '2026-07-10T00:00:00Z', window_days: 30, sample: 15,
|
|
overall: { hits: 10, misses: 5, pushes: 0, total: 15, pct: 67 },
|
|
byGrade: {
|
|
'A+': { hits: 3, misses: 0, pushes: 0, total: 3, pct: 100 },
|
|
'A': { hits: 5, misses: 2, pushes: 0, total: 7, pct: 71 },
|
|
'B': { hits: 2, misses: 3, pushes: 0, total: 5, pct: 40 },
|
|
},
|
|
};
|
|
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+');
|
|
expect(grades).toContain('A');
|
|
});
|
|
|
|
test('cold cache → empty buckets, never 500', async () => {
|
|
const res = await request(app).get('/api/ledger/accuracy');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.buckets).toEqual([]);
|
|
});
|
|
});
|