Files
emba-course-solver/app/src/solver/__tests__/optimizer.test.ts
T
Bill 3a5ebaa17a v1.5.0: External credits per specialization
Students can now record credits earned in courses taken outside the J27
program via an inline editable amber chip on each spec card. Values flow
through the LP (per-spec demand reduces by external amount), upper-bound
math, decision-tree search, and the credit bar visualization. The 9-credit
threshold and the 3-spec achievement cap are unchanged; required-course
gates remain authoritative — external credits never satisfy them.
2026-05-10 11:47:22 -04:00

246 lines
9.2 KiB
TypeScript

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);
});
});