Files
vyndr/tests/unit/schemeClassifier.test.js
T

172 lines
6.0 KiB
JavaScript

const {
SCHEME_TYPES,
MIN_POSSESSIONS,
CACHE_TTL,
getCacheKey,
extractPnRPossessions,
classifyScheme,
} = require('../../src/services/schemeClassifier');
describe('Scheme Classifier', () => {
// --- Constants ---
test('SCHEME_TYPES contains all 5 classifications', () => {
expect(SCHEME_TYPES).toEqual(['DROP', 'SWITCH', 'HEDGE', 'MIXED', 'UNKNOWN']);
});
test('minimum possessions threshold is 8', () => {
expect(MIN_POSSESSIONS).toBe(8);
});
test('cache TTL is 6 hours (21600 seconds)', () => {
expect(CACHE_TTL).toBe(21600);
});
// --- Cache Key ---
test('cache key includes opponent and date', () => {
const key = getCacheKey('BOS', '2026-04-12');
expect(key).toBe('scheme:BOS:2026-04-12');
});
// --- PnR Extraction ---
test('extractPnRPossessions identifies pick-and-roll plays', () => {
const plays = [
{ description: 'LeBron pick and roll ball handler' },
{ description: 'AD catches lob pass' },
{ description: 'screen and roll coverage by Smart' },
{ description: 'transition fastbreak layup' },
{ description: 'ball screen switch by Tatum' },
];
const result = extractPnRPossessions(plays);
expect(result).toHaveLength(3);
});
test('extractPnRPossessions handles empty array', () => {
expect(extractPnRPossessions([])).toEqual([]);
});
test('extractPnRPossessions handles null/undefined', () => {
expect(extractPnRPossessions(null)).toEqual([]);
expect(extractPnRPossessions(undefined)).toEqual([]);
});
test('extractPnRPossessions handles play_description field', () => {
const plays = [
{ play_description: 'PnR ball handler drive' },
];
const result = extractPnRPossessions(plays);
expect(result).toHaveLength(1);
});
// --- Classification ---
test('classifyScheme returns UNKNOWN with fewer than 8 possessions', () => {
const possessions = Array.from({ length: 5 }, () => ({ description: 'drop coverage on screen' }));
const result = classifyScheme(possessions);
expect(result.scheme).toBe('UNKNOWN');
expect(result.reason).toBe('insufficient_data');
expect(result.possessions_analyzed).toBe(5);
});
test('classifyScheme returns DROP when drop coverage dominates', () => {
const possessions = [
...Array.from({ length: 7 }, () => ({ description: 'drop coverage on ball screen' })),
{ description: 'switch on screen action' },
{ description: 'hedge hard show on pick' },
];
const result = classifyScheme(possessions);
expect(result.scheme).toBe('DROP');
expect(result.confidence).toBeGreaterThanOrEqual(55);
expect(result.breakdown).toBeDefined();
expect(result.breakdown.DROP).toBe(7);
});
test('classifyScheme returns SWITCH when switch dominates', () => {
const possessions = [
...Array.from({ length: 8 }, () => ({ description: 'switch on screen swap defenders' })),
{ description: 'drop back on screen' },
{ description: 'drop sag coverage' },
];
const result = classifyScheme(possessions);
expect(result.scheme).toBe('SWITCH');
expect(result.confidence).toBeGreaterThanOrEqual(55);
});
test('classifyScheme returns HEDGE when hedge/blitz dominates', () => {
const possessions = [
...Array.from({ length: 8 }, () => ({ description: 'hedge hard show blitz screen' })),
{ description: 'switch on ball screen' },
];
const result = classifyScheme(possessions);
expect(result.scheme).toBe('HEDGE');
});
test('classifyScheme returns MIXED when no scheme exceeds 55%', () => {
const possessions = [
...Array.from({ length: 4 }, () => ({ description: 'drop coverage on screen' })),
...Array.from({ length: 3 }, () => ({ description: 'switch on screen' })),
...Array.from({ length: 3 }, () => ({ description: 'hedge trap on ball screen' })),
];
const result = classifyScheme(possessions);
expect(result.scheme).toBe('MIXED');
expect(result.breakdown).toBeDefined();
});
test('classifyScheme returns UNKNOWN when no classifiable actions found', () => {
const possessions = Array.from({ length: 10 }, () => ({ description: 'generic play action' }));
const result = classifyScheme(possessions);
expect(result.scheme).toBe('UNKNOWN');
expect(result.reason).toBe('no_classifiable_actions');
});
test('classifyScheme handles null possessions', () => {
const result = classifyScheme(null);
expect(result.scheme).toBe('UNKNOWN');
expect(result.reason).toBe('insufficient_data');
});
test('classifyScheme handles empty possessions', () => {
const result = classifyScheme([]);
expect(result.scheme).toBe('UNKNOWN');
expect(result.reason).toBe('insufficient_data');
});
// --- Confidence ---
test('confidence is a percentage rounded to integer', () => {
const possessions = Array.from({ length: 10 }, () => ({ description: 'drop coverage sag back' }));
const result = classifyScheme(possessions);
expect(Number.isInteger(result.confidence)).toBe(true);
expect(result.confidence).toBeGreaterThanOrEqual(0);
expect(result.confidence).toBeLessThanOrEqual(100);
});
// --- Breakdown ---
test('breakdown counts sum correctly', () => {
const possessions = [
...Array.from({ length: 5 }, () => ({ description: 'drop coverage on screen' })),
...Array.from({ length: 2 }, () => ({ description: 'switch on ball screen' })),
...Array.from({ length: 2 }, () => ({ description: 'hedge trap blitz' })),
];
const result = classifyScheme(possessions);
const { DROP, SWITCH, HEDGE } = result.breakdown;
expect(DROP).toBe(5);
expect(SWITCH).toBe(2);
expect(HEDGE).toBe(2);
});
// --- Graceful Degradation ---
test('scheme result always contains required fields', () => {
const result = classifyScheme(null);
expect(result).toHaveProperty('scheme');
expect(result).toHaveProperty('confidence');
expect(result).toHaveProperty('possessions_analyzed');
expect(SCHEME_TYPES).toContain(result.scheme);
});
});