import { describe, it, expect } from 'vitest'; import { maximizeCount, priorityOrder, determineStatuses, optimize } from '../optimizer'; import { SPECIALIZATIONS } from '../../data/specializations'; const allSpecIds = SPECIALIZATIONS.map((s) => s.id); // A finance-heavy selection that should achieve finance specs const financeHeavyCourses = [ 'spr2-financial-services', // BNK CRF FIN FIM 'spr3-mergers-acquisitions', // CRF FIN LCM STR(S1) 'spr4-fintech', // FIN 'spr5-corporate-finance', // CRF FIN 'sum3-valuation', // BNK CRF FIN FIM 'fall1-private-equity', // BNK CRF FIN FIM STR(S2) 'fall2-behavioral-finance', // BNK CRF FIN FIM 'fall3-climate-finance', // BNK CRF FIN FIM GLB SBI 'fall4-financial-services', // BNK CRF FIN FIM ]; describe('maximizeCount', () => { it('returns empty achieved when no courses are selected', () => { const result = maximizeCount([], allSpecIds, []); expect(result.achieved).toEqual([]); }); it('achieves specs with enough qualifying courses', () => { const result = maximizeCount(financeHeavyCourses, allSpecIds, []); expect(result.achieved.length).toBeGreaterThan(0); // Should be able to achieve some combo of FIN, CRF, BNK, FIM for (const specId of result.achieved) { expect(['BNK', 'CRF', 'FIN', 'FIM', 'LCM', 'GLB', 'SBI', 'STR']).toContain(specId); } }); it('prefers higher-priority specs when breaking ties', () => { // Put FIM first in ranking const ranking = ['FIM', ...allSpecIds.filter((id) => id !== 'FIM')]; const result1 = maximizeCount(financeHeavyCourses, ranking, []); // Put BNK first const ranking2 = ['BNK', ...allSpecIds.filter((id) => id !== 'BNK')]; const result2 = maximizeCount(financeHeavyCourses, ranking2, []); // Both should achieve the same count expect(result1.achieved.length).toBe(result2.achieved.length); // If they achieve multiple, the first-ranked spec should appear when possible if (result1.achieved.length > 0) { // The highest-priority feasible spec should be in the result expect(result1.achieved).toContain('FIM'); } }); it('never exceeds 3 specializations', () => { const result = maximizeCount(financeHeavyCourses, allSpecIds, []); expect(result.achieved.length).toBeLessThanOrEqual(3); }); }); describe('priorityOrder', () => { it('guarantees the top-ranked feasible spec', () => { const ranking = ['FIN', ...allSpecIds.filter((id) => id !== 'FIN')]; const result = priorityOrder(financeHeavyCourses, ranking, []); expect(result.achieved[0]).toBe('FIN'); }); it('skips infeasible specs and continues', () => { // GLB has very few qualifying courses in this set const ranking = ['GLB', 'FIN', 'CRF', ...allSpecIds.filter((id) => !['GLB', 'FIN', 'CRF'].includes(id))]; const result = priorityOrder(financeHeavyCourses, ranking, []); // GLB might not be achievable on its own, but FIN should be expect(result.achieved).toContain('FIN'); }); it('returns empty when no courses are selected', () => { const result = priorityOrder([], allSpecIds, []); expect(result.achieved).toEqual([]); }); }); describe('determineStatuses', () => { it('marks achieved specs correctly', () => { const statuses = determineStatuses(financeHeavyCourses, [], ['FIN', 'CRF']); expect(statuses['FIN']).toBe('achieved'); expect(statuses['CRF']).toBe('achieved'); }); it('marks missing_required when required course set is pinned to different course', () => { // spr4-fintech selected (not sustainability or entrepreneurship) const statuses = determineStatuses(['spr4-fintech'], [], []); expect(statuses['SBI']).toBe('missing_required'); expect(statuses['ENT']).toBe('missing_required'); }); it('marks achievable when required course is in open set', () => { const statuses = determineStatuses( [], ['spr1', 'spr2', 'spr3', 'spr4', 'spr5', 'sum1', 'sum2', 'sum3', 'fall1', 'fall2', 'fall3', 'fall4'], [], ); // All specs with enough potential should be achievable expect(statuses['FIN']).toBe('achievable'); expect(statuses['MGT']).toBe('achievable'); }); it('marks unreachable when upper bound is below threshold', () => { // Only 1 set open, most specs won't have enough potential const statuses = determineStatuses([], ['spr1'], []); // Most specs only have 1 qualifying course in spr1 (2.5 credits < 9) expect(statuses['FIN']).toBe('unreachable'); }); it('marks spec as unreachable when infeasible alongside achieved specs due to credit sharing', () => { // Bug scenario: CRF+STR achieved, LCM has 10 credit upper bound but // shared courses (spr3, fall3) are consumed by CRF/STR const selectedCourses = [ 'spr1-collaboration', // LCM, MGT 'spr2-financial-services', // BNK, CRF, FIN, FIM 'spr3-mergers-acquisitions', // CRF, FIN, LCM, STR(S1) 'spr4-foundations-entrepreneurship', // ENT, MGT, STR(S1) 'spr5-corporate-finance', // CRF, FIN 'sum1-global-immersion', // GLB 'sum2-business-drivers', // STR(S1) 'sum3-valuation', // BNK, CRF, FIN, FIM 'fall1-managing-change', // LCM, MGT, STR(S2) 'fall2-decision-models', // MGT, MTO 'fall3-corporate-governance', // LCM, MGT, SBI, STR(S1) 'fall4-game-theory', // MGT, STR(S1) ]; // Baseline: LCM is achievable when no specs are achieved (upper bound alone) const statusesBaseline = determineStatuses(selectedCourses, [], []); expect(statusesBaseline['LCM']).toBe('achievable'); // Core bug scenario: CRF+STR achieved (without MGT), LCM should still be unreachable const statusesWithoutMgt = determineStatuses(selectedCourses, [], ['CRF', 'STR']); expect(statusesWithoutMgt['LCM']).toBe('unreachable'); // LCM upper bound is 10 (>= 9) but infeasible alongside CRF+STR+MGT const statusesWithMgt = determineStatuses(selectedCourses, [], ['CRF', 'STR', 'MGT']); expect(statusesWithMgt['LCM']).toBe('unreachable'); }); }); describe('optimize (integration)', () => { it('returns a complete AllocationResult', () => { const result = optimize(financeHeavyCourses, allSpecIds, [], 'maximize-count'); expect(result.achieved).toBeDefined(); expect(result.allocations).toBeDefined(); expect(result.statuses).toBeDefined(); expect(result.upperBounds).toBeDefined(); expect(Object.keys(result.statuses).length).toBe(SPECIALIZATIONS.length); }); it('modes can produce different results', () => { // Put a niche spec first in priority order const ranking = ['EMT', ...allSpecIds.filter((id) => id !== 'EMT')]; const maxResult = optimize(financeHeavyCourses, ranking, [], 'maximize-count'); const prioResult = optimize(financeHeavyCourses, ranking, [], 'priority-order'); // Both should produce valid results expect(maxResult.achieved.length).toBeGreaterThanOrEqual(0); expect(prioResult.achieved.length).toBeGreaterThanOrEqual(0); }); }); describe('externalCredits behavior', () => { it('hard 3-spec cap holds even with external credits', () => { // Even with 9 external HCR (a spec the courses don't otherwise support), // maximizeCount must never report more than 3 achieved. const result = maximizeCount( financeHeavyCourses, allSpecIds, [], undefined, { HCR: 9 }, ); expect(result.achieved.length).toBeLessThanOrEqual(3); }); it('external credits can substitute into the 3-spec set', () => { // External 9 in HCR makes HCR feasible for free; the optimizer can pick // HCR as one of the 3 achieved specs. const result = maximizeCount( financeHeavyCourses, allSpecIds, [], undefined, { HCR: 9 }, ); expect(result.achieved).toContain('HCR'); }); it('missing_required precedence: external alone cannot achieve a gated spec', () => { // BRM requires fall4-brand-strategy. Don't include it; pin fall4-game-theory instead. const noBrandStrategy = financeHeavyCourses.filter((c) => c !== 'fall4-financial-services') .concat(['fall4-game-theory']); const result = maximizeCount( noBrandStrategy, allSpecIds, [], undefined, { BRM: 9 }, ); expect(result.achieved).not.toContain('BRM'); const statuses = determineStatuses( noBrandStrategy, [], result.achieved, undefined, { BRM: 9 }, ); expect(statuses['BRM']).toBe('missing_required'); }); it('priorityOrder honors external credits in achievability', () => { // Rank HCR first; without external it would be skipped, with 9 external it's first achieved const ranking = ['HCR', ...allSpecIds.filter((id) => id !== 'HCR')]; const without = priorityOrder(financeHeavyCourses, ranking, []); expect(without.achieved).not.toContain('HCR'); const withExt = priorityOrder( financeHeavyCourses, ranking, [], undefined, { HCR: 9 }, ); expect(withExt.achieved[0]).toBe('HCR'); }); it('upper bounds reflect external credit additions', () => { const result = optimize( [], allSpecIds, [], 'maximize-count', undefined, { GLB: 5 }, ); expect(result.upperBounds['GLB']).toBe(5); }); });