Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
const {
|
||||
ROLE_TAXONOMY,
|
||||
calculateRoleVariance,
|
||||
getDominantRole,
|
||||
detectRoleElevation,
|
||||
getConditionalProfile,
|
||||
calculateRoleProfile,
|
||||
} = require('../../src/services/roleProfileEngine');
|
||||
|
||||
const {
|
||||
calculateStability,
|
||||
applyDecayWeights,
|
||||
} = require('../../src/services/roleStabilityEngine');
|
||||
|
||||
describe('roleProfileEngine', () => {
|
||||
// ─── calculateRoleVariance ──────────────────────────────────
|
||||
|
||||
test('returns 0 for a single-role profile', () => {
|
||||
const profile = { PRIMARY_BALL_HANDLER: 1.0 };
|
||||
expect(calculateRoleVariance(profile)).toBe(0);
|
||||
});
|
||||
|
||||
test('returns ~1.0 for perfectly equal distribution', () => {
|
||||
const profile = {};
|
||||
for (const role of ROLE_TAXONOMY) {
|
||||
profile[role] = 1 / ROLE_TAXONOMY.length;
|
||||
}
|
||||
const variance = calculateRoleVariance(profile);
|
||||
expect(variance).toBeGreaterThan(0.99);
|
||||
expect(variance).toBeLessThanOrEqual(1.0);
|
||||
});
|
||||
|
||||
test('returns value strictly between 0 and 1 for mixed profile', () => {
|
||||
const profile = {
|
||||
PRIMARY_BALL_HANDLER: 0.50,
|
||||
SECONDARY_PLAYMAKER: 0.30,
|
||||
CONNECTOR: 0.20,
|
||||
};
|
||||
const variance = calculateRoleVariance(profile);
|
||||
expect(variance).toBeGreaterThan(0);
|
||||
expect(variance).toBeLessThan(1);
|
||||
});
|
||||
|
||||
test('handles empty profile gracefully', () => {
|
||||
expect(calculateRoleVariance({})).toBe(0);
|
||||
});
|
||||
|
||||
test('ignores zero-weight roles in entropy calculation', () => {
|
||||
const profileA = { PRIMARY_BALL_HANDLER: 0.5, CONNECTOR: 0.5 };
|
||||
const profileB = {
|
||||
PRIMARY_BALL_HANDLER: 0.5,
|
||||
CONNECTOR: 0.5,
|
||||
FLOOR_RAISER: 0,
|
||||
PAINT_PRESENCE: 0,
|
||||
};
|
||||
// Both should give same result since zeros are filtered
|
||||
expect(calculateRoleVariance(profileA)).toBeCloseTo(calculateRoleVariance(profileB), 5);
|
||||
});
|
||||
|
||||
// ─── getDominantRole ────────────────────────────────────────
|
||||
|
||||
test('returns the role with the highest weight', () => {
|
||||
const profile = {
|
||||
PRIMARY_BALL_HANDLER: 0.15,
|
||||
FLOOR_RAISER: 0.55,
|
||||
CONNECTOR: 0.30,
|
||||
};
|
||||
expect(getDominantRole(profile)).toBe('FLOOR_RAISER');
|
||||
});
|
||||
|
||||
test('returns null for empty profile', () => {
|
||||
expect(getDominantRole({})).toBeNull();
|
||||
});
|
||||
|
||||
// ─── detectRoleElevation ────────────────────────────────────
|
||||
|
||||
test('detects elevation when delta exceeds threshold', () => {
|
||||
const base = {
|
||||
PRIMARY_BALL_HANDLER: 0.20,
|
||||
SECONDARY_PLAYMAKER: 0.50,
|
||||
CONNECTOR: 0.30,
|
||||
};
|
||||
const tonight = {
|
||||
PRIMARY_BALL_HANDLER: 0.60,
|
||||
SECONDARY_PLAYMAKER: 0.25,
|
||||
CONNECTOR: 0.15,
|
||||
};
|
||||
const result = detectRoleElevation(base, tonight, 0.20);
|
||||
expect(result.elevated).toBe(true);
|
||||
expect(result.elevatedRole).toBe('PRIMARY_BALL_HANDLER');
|
||||
expect(result.delta).toBeGreaterThan(0.20);
|
||||
});
|
||||
|
||||
test('does not flag elevation when delta is below threshold', () => {
|
||||
const base = {
|
||||
PRIMARY_BALL_HANDLER: 0.40,
|
||||
SECONDARY_PLAYMAKER: 0.35,
|
||||
CONNECTOR: 0.25,
|
||||
};
|
||||
const tonight = {
|
||||
PRIMARY_BALL_HANDLER: 0.45,
|
||||
SECONDARY_PLAYMAKER: 0.30,
|
||||
CONNECTOR: 0.25,
|
||||
};
|
||||
const result = detectRoleElevation(base, tonight, 0.20);
|
||||
expect(result.elevated).toBe(false);
|
||||
expect(result.elevatedRole).toBeNull();
|
||||
});
|
||||
|
||||
// ─── getConditionalProfile ──────────────────────────────────
|
||||
|
||||
test('returns conditional profile for valid condition', () => {
|
||||
const conditionals = {
|
||||
star_out: { PRIMARY_BALL_HANDLER: 0.70, FLOOR_RAISER: 0.30 },
|
||||
closing_lineup: { SWITCHABLE_DEFENDER: 0.60, CONNECTOR: 0.40 },
|
||||
};
|
||||
const result = getConditionalProfile(conditionals, 'star_out');
|
||||
expect(result).toEqual({ PRIMARY_BALL_HANDLER: 0.70, FLOOR_RAISER: 0.30 });
|
||||
});
|
||||
|
||||
test('returns null for invalid condition key', () => {
|
||||
const conditionals = { star_out: { PRIMARY_BALL_HANDLER: 1.0 } };
|
||||
expect(getConditionalProfile(conditionals, 'garbage_condition')).toBeNull();
|
||||
});
|
||||
|
||||
// ─── calculateRoleProfile ──────────────────────────────────
|
||||
|
||||
test('produces distribution that sums to approximately 1.0', () => {
|
||||
const gameLogStats = [
|
||||
{
|
||||
usage_rate: 28,
|
||||
assist_rate: 32,
|
||||
three_point_share: 40,
|
||||
off_ball_movement: 30,
|
||||
paint_touches: 4,
|
||||
rebounds_per_game: 5,
|
||||
defensive_versatility: 45,
|
||||
screen_assists: 2,
|
||||
},
|
||||
{
|
||||
usage_rate: 30,
|
||||
assist_rate: 35,
|
||||
three_point_share: 38,
|
||||
off_ball_movement: 25,
|
||||
paint_touches: 3,
|
||||
rebounds_per_game: 4.5,
|
||||
defensive_versatility: 50,
|
||||
screen_assists: 3,
|
||||
},
|
||||
];
|
||||
const profile = calculateRoleProfile(gameLogStats);
|
||||
const total = Object.values(profile).reduce((s, v) => s + v, 0);
|
||||
expect(total).toBeGreaterThan(0.95);
|
||||
expect(total).toBeLessThanOrEqual(1.05);
|
||||
});
|
||||
|
||||
test('returns empty object for empty game logs', () => {
|
||||
expect(calculateRoleProfile([])).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('roleStabilityEngine', () => {
|
||||
// ─── calculateStability ─────────────────────────────────────
|
||||
|
||||
test('low variance player gets no decay (all weights 1.0)', () => {
|
||||
const profile = { PRIMARY_BALL_HANDLER: 0.85, CONNECTOR: 0.15 };
|
||||
const varianceScore = 0.15; // below 0.2 threshold
|
||||
const history = [
|
||||
{ date: '2026-01-01', roleProfile: { PRIMARY_BALL_HANDLER: 0.85, CONNECTOR: 0.15 } },
|
||||
{ date: '2026-01-05', roleProfile: { PRIMARY_BALL_HANDLER: 0.80, CONNECTOR: 0.20 } },
|
||||
{ date: '2026-01-10', roleProfile: { PRIMARY_BALL_HANDLER: 0.85, CONNECTOR: 0.15 } },
|
||||
];
|
||||
const result = calculateStability(profile, varianceScore, history);
|
||||
// All decay weights should be 1.0
|
||||
expect(result.decay_weights_by_period.every((w) => w === 1.0)).toBe(true);
|
||||
expect(result.stability_score).toBeGreaterThan(0.7);
|
||||
});
|
||||
|
||||
test('high variance player gets recency decay', () => {
|
||||
const profile = {
|
||||
PRIMARY_BALL_HANDLER: 0.30,
|
||||
FLOOR_RAISER: 0.30,
|
||||
CONNECTOR: 0.20,
|
||||
SWITCHABLE_DEFENDER: 0.20,
|
||||
};
|
||||
const varianceScore = 0.65; // above 0.5 threshold
|
||||
const history = [
|
||||
{ date: '2026-01-01', roleProfile: { FLOOR_RAISER: 0.60, CONNECTOR: 0.40 } },
|
||||
{ date: '2026-01-05', roleProfile: { PRIMARY_BALL_HANDLER: 0.50, CONNECTOR: 0.50 } },
|
||||
{ date: '2026-01-10', roleProfile: { SWITCHABLE_DEFENDER: 0.55, FLOOR_RAISER: 0.45 } },
|
||||
{ date: '2026-01-15', roleProfile: { PRIMARY_BALL_HANDLER: 0.30, FLOOR_RAISER: 0.30, CONNECTOR: 0.20, SWITCHABLE_DEFENDER: 0.20 } },
|
||||
];
|
||||
const result = calculateStability(profile, varianceScore, history);
|
||||
// Older entries should have lower weights than newer
|
||||
const weights = result.decay_weights_by_period;
|
||||
expect(weights[weights.length - 1]).toBeGreaterThan(weights[0]);
|
||||
// Should detect role changes
|
||||
expect(result.role_change_events).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('stability score is between 0 and 1', () => {
|
||||
const profile = { PAINT_PRESENCE: 0.70, SWITCHABLE_DEFENDER: 0.30 };
|
||||
const history = [
|
||||
{ date: '2026-01-01', roleProfile: { PAINT_PRESENCE: 0.65, SWITCHABLE_DEFENDER: 0.35 } },
|
||||
{ date: '2026-01-10', roleProfile: { PAINT_PRESENCE: 0.70, SWITCHABLE_DEFENDER: 0.30 } },
|
||||
];
|
||||
const result = calculateStability(profile, 0.35, history);
|
||||
expect(result.stability_score).toBeGreaterThanOrEqual(0);
|
||||
expect(result.stability_score).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
// ─── applyDecayWeights ──────────────────────────────────────
|
||||
|
||||
test('all weights are 1.0 when variance is below 0.2', () => {
|
||||
const instances = [{}, {}, {}, {}, {}];
|
||||
const weights = applyDecayWeights(instances, 0.10);
|
||||
expect(weights).toEqual([1.0, 1.0, 1.0, 1.0, 1.0]);
|
||||
});
|
||||
|
||||
test('returns empty array for empty instances', () => {
|
||||
expect(applyDecayWeights([], 0.6)).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user