const mockState = { resolutionCount: 100, weightRows: [], // engine1_weights rows inserts: [], }; jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => ({ from(table) { const ctx = { table, filters: {} }; const proxy = { select(_cols, opts) { ctx.head = !!opts?.head; ctx.countMode = opts?.count; return proxy; }, eq(col, val) { ctx.filters[col] = val; return proxy; }, order() { return proxy; }, limit() { return proxy; }, maybeSingle() { const match = mockState.weightRows.find( (r) => r.sport === ctx.filters.sport && r.stat_type === ctx.filters.stat_type && r.factor_name === ctx.filters.factor_name && r.version === ctx.filters.version ); return Promise.resolve({ data: match || null, error: null }); }, insert(row) { mockState.inserts.push(row); mockState.weightRows.push(row); return Promise.resolve({ error: null }); }, then(resolve) { if (ctx.table === 'resolution_results' && ctx.countMode === 'exact') { return resolve({ count: mockState.resolutionCount, error: null }); } // List of engine1_weights matching filters. const matches = mockState.weightRows.filter((r) => { for (const [k, v] of Object.entries(ctx.filters)) { if (r[k] !== v) return false; } return true; }); matches.sort((a, b) => b.version - a.version); return resolve({ data: matches, error: null }); }, }; return proxy; }, }), })); const wa = require('../../src/services/intelligence/weightAdjuster'); beforeEach(() => { mockState.resolutionCount = 100; mockState.weightRows = []; mockState.inserts.length = 0; }); describe('weightAdjuster — skip conditions', () => { test('skips when sample too thin', async () => { mockState.resolutionCount = 5; const r = await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'A', result: 'hit', factors: ['l5_avg'], grade_id: 'g', }); expect(r.skipped).toBe(true); expect(r.reason).toBe('thin_sample'); }); test('skips on push / void', async () => { const r = await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'A', result: 'push', factors: ['l5_avg'], grade_id: 'g', }); expect(r.skipped).toBe(true); expect(r.reason).toBe('non_decisive_result'); }); test('skips on incomplete input', async () => { const r = await wa.adjustWeights({ sport: 'nba', grade: 'A', result: 'hit', factors: [] }); expect(r.skipped).toBe(true); }); }); describe('weightAdjuster — adjustments', () => { test('A+ hit nudges factor up by at most 0.5%', async () => { const r = await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'A+', result: 'hit', factors: ['l5_hot_vs_line'], grade_id: 'g1', }); expect(r.skipped).toBe(false); const adj = r.adjustments[0]; expect(adj.previous).toBe(1.0); expect(adj.next).toBeGreaterThan(1.0); // confidence of A+ = 1.0, LR = 0.005 → multiplier = 1.005 → next = 1.005 expect(adj.next).toBeCloseTo(1.005, 5); }); test('A+ miss nudges factor down by at most 0.5%', async () => { const r = await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'A+', result: 'miss', factors: ['l5_hot_vs_line'], grade_id: 'g2', }); expect(r.adjustments[0].next).toBeCloseTo(0.995, 5); }); test('low-confidence grade produces smaller nudge', async () => { const high = await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'A+', result: 'hit', factors: ['x'], grade_id: 'h', }); const low = await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'C', result: 'hit', factors: ['y'], grade_id: 'l', }); expect(Math.abs(high.adjustments[0].next - 1.0)) .toBeGreaterThan(Math.abs(low.adjustments[0].next - 1.0)); }); test('weights clamp at MIN_WEIGHT and MAX_WEIGHT', () => { const c = wa.__internals.clamp; expect(c(0.05)).toBe(wa.MIN_WEIGHT); expect(c(99)).toBe(wa.MAX_WEIGHT); expect(c(2.5)).toBe(2.5); }); test('repeated adjustments stack into incrementing versions', async () => { await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'A', result: 'hit', factors: ['l5_hot_vs_line'], grade_id: 'g1', }); await wa.adjustWeights({ sport: 'nba', stat_type: 'points', grade: 'A', result: 'hit', factors: ['l5_hot_vs_line'], grade_id: 'g2', }); const history = await wa.getWeightHistory('nba', 'points', 'l5_hot_vs_line', 10); expect(history.length).toBe(2); expect(history[0].version).toBe(2); expect(history[1].version).toBe(1); }); }); describe('weightAdjuster — rollback', () => { test('rollback inserts a new row whose weight equals the target version', async () => { // Seed three versions. mockState.weightRows.push( { sport: 'nba', stat_type: 'points', factor_name: 'f', weight: 1.0, version: 1 }, { sport: 'nba', stat_type: 'points', factor_name: 'f', weight: 1.1, version: 2 }, { sport: 'nba', stat_type: 'points', factor_name: 'f', weight: 1.2, version: 3 }, ); const ok = await wa.rollbackToVersion('nba', 'points', 'f', 1); expect(ok).toBe(true); const history = await wa.getWeightHistory('nba', 'points', 'f', 10); expect(history[0].weight).toBe(1.0); expect(history[0].version).toBe(4); }); });