import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { createTestState, createTestTrainingPipeline, createTestBaseModel, createTestModelFamily, createSeededRNG, } from '../__test-utils__'; import { processModels } from './modelSystem'; import { POINT_RELEASE_CAPABILITY_GAIN, VRAM_REQUIREMENTS_BY_GENERATION, } from '@token-empire/shared'; import type { ResearchBonuses } from './researchBonuses'; const rng = createSeededRNG(42); beforeEach(() => rng.install()); afterEach(() => rng.uninstall()); /** Helper: build state with a single active pipeline, talent, and compute */ function stateWithPipeline( pipelineOverrides?: Parameters[0], familyOverrides?: Parameters[0], stateOverrides?: Parameters[0], ) { const family = createTestModelFamily({ id: 'fam-1', name: 'Test', baseModelIds: [], variants: [], generation: 1, createdAtTick: 0, ...familyOverrides, }); const pipeline = createTestTrainingPipeline({ familyId: 'fam-1', ...pipelineOverrides, }); return createTestState({ models: { activeTrainingPipelines: [pipeline], families: [family], }, compute: { totalTrainingFlops: 1e15, trainingAllocation: 0.5, totalVramGB: 256 }, talent: { departments: { research: { headcount: 5, effectiveness: 0.5 }, engineering: { headcount: 5, effectiveness: 0.5 }, }, }, ...stateOverrides, }); } function defaultResearchBonuses(overrides?: Partial): ResearchBonuses { return { energyCostReduction: 0, pipelineSpeedBonus: 0, trainingSpeedBonus: 0, inferenceEfficiencyBonus: 0, tokensPerFlopBonus: 0, dataQualityBonus: 0, sdkCoverageBonus: 0, globalCapabilityBonus: 0, reasoningBonus: 0, codingBonus: 0, creativeBonus: 0, multimodalBonus: 0, agentsBonus: 0, reputationBonus: 0, safetyBonus: 0, autoScalingBonus: 0, ...overrides, }; } // ---------- Pipeline stage progression ---------- describe('processModels', () => { describe('pipeline stage progression', () => { it('increments pretraining progressTicks each tick', () => { const state = stateWithPipeline({ currentStage: 'pretraining', stages: { pretraining: { progressTicks: 0, totalTicks: 1000 }, }, }); const result = processModels(state); const pre = result.modelsState.activeTrainingPipelines[0].stages.pretraining; expect(pre.progressTicks).toBeGreaterThan(0); }); it('applies speed formula: (1 + talent*0.15) * (1 + trainingSpeedBonus)', () => { // researcher boost = 5*0.5 = 2.5, engineer boost = 5*0.5 = 2.5 // speedMultiplier = (1 + (2.5+2.5)*0.15) * (1 + 0.2) = (1 + 0.75) * 1.2 = 1.75 * 1.2 = 2.1 const state = stateWithPipeline({ currentStage: 'pretraining', stages: { pretraining: { progressTicks: 0, totalTicks: 10000 }, }, }); const bonuses = defaultResearchBonuses({ trainingSpeedBonus: 0.2 }); const result = processModels(state, bonuses); const pre = result.modelsState.activeTrainingPipelines[0].stages.pretraining; // With seeded RNG, events may or may not occur. The base progress without events = 2.1 // We check it's approximately right (events can add delays). // Use a range to account for possible training events with the seeded rng. expect(pre.progressTicks).toBeGreaterThan(0); expect(pre.progressTicks).toBeLessThanOrEqual(2.1); }); it('applies MoE 0.8 speed penalty during pretraining', () => { const denseState = stateWithPipeline({ currentStage: 'pretraining', architecture: { type: 'dense', totalParameters: 7e9, activeParameters: 7e9, contextWindow: 8192, vocabularySize: 32000 }, stages: { pretraining: { progressTicks: 0, totalTicks: 10000 } }, }); const moeState = stateWithPipeline({ currentStage: 'pretraining', architecture: { type: 'moe', totalParameters: 7e9, activeParameters: 2e9, contextWindow: 8192, vocabularySize: 32000, expertCount: 8, expertTopK: 2 }, stages: { pretraining: { progressTicks: 0, totalTicks: 10000 } }, }); const denseResult = processModels(denseState); const moeResult = processModels(moeState); const densePre = denseResult.modelsState.activeTrainingPipelines[0].stages.pretraining; const moePre = moeResult.modelsState.activeTrainingPipelines[0].stages.pretraining; // MoE should be slower (0.8x of dense progress, modulo random events) // Both might have events; we just check MoE is not faster than dense without events // dense speed = 1.75, moe speed = 1.75 * 0.8 = 1.4 // Use a threshold: moe progress should be less than dense progress (approximately) expect(moePre.progressTicks).toBeLessThanOrEqual(densePre.progressTicks); }); it('transitions from pretraining to sft when progressTicks >= totalTicks', () => { const state = stateWithPipeline({ currentStage: 'pretraining', stages: { pretraining: { progressTicks: 999, totalTicks: 1000 }, }, }); const result = processModels(state); const pipeline = result.modelsState.activeTrainingPipelines[0]; expect(pipeline.stages.pretraining.isComplete).toBe(true); expect(pipeline.currentStage).toBe('sft'); }); it('generates "Pre-training Complete" notification on pretraining completion', () => { const state = stateWithPipeline({ currentStage: 'pretraining', stages: { pretraining: { progressTicks: 999, totalTicks: 1000 }, }, }); const result = processModels(state); const pretrainNotif = result.notifications.find(n => n.title === 'Pre-training Complete'); expect(pretrainNotif).toBeDefined(); expect(pretrainNotif!.type).toBe('info'); }); it('transitions from sft to alignment when progressTicks >= totalTicks', () => { const state = stateWithPipeline({ currentStage: 'sft', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { progressTicks: 99, totalTicks: 100 }, }, }); const result = processModels(state); const pipeline = result.modelsState.activeTrainingPipelines[0]; expect(pipeline.stages.sft.isComplete).toBe(true); expect(pipeline.currentStage).toBe('alignment'); }); it('generates "SFT Complete" notification on sft completion', () => { const state = stateWithPipeline({ currentStage: 'sft', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { progressTicks: 99, totalTicks: 100 }, }, }); const result = processModels(state); const sftNotif = result.notifications.find(n => n.title === 'SFT Complete'); expect(sftNotif).toBeDefined(); expect(sftNotif!.type).toBe('info'); }); it('creates a BaseModel when alignment completes', () => { const state = stateWithPipeline({ currentStage: 'alignment', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { specializations: ['general'], progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 79, totalTicks: 80 }, }, }); const result = processModels(state); const pipeline = result.modelsState.activeTrainingPipelines[0]; expect(pipeline.stages.alignment.isComplete).toBe(true); expect(pipeline.status).toBe('completed'); expect(result.completedModels).toHaveLength(1); expect(result.modelsState.baseModels.length).toBeGreaterThanOrEqual(1); }); it('adds completed model to the family baseModelIds', () => { const state = stateWithPipeline({ currentStage: 'alignment', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { specializations: ['general'], progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 79, totalTicks: 80 }, }, }); const result = processModels(state); const family = result.modelsState.families.find(f => f.id === 'fam-1'); expect(family).toBeDefined(); expect(family!.baseModelIds.length).toBe(1); expect(family!.baseModelIds[0]).toBe(result.completedModels[0].id); }); it('progresses SFT stage without MoE penalty', () => { // SFT uses plain speedMultiplier without MoE penalty const state = stateWithPipeline({ currentStage: 'sft', architecture: { type: 'moe', totalParameters: 7e9, activeParameters: 2e9, contextWindow: 8192, vocabularySize: 32000, expertCount: 8, expertTopK: 2 }, stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { progressTicks: 0, totalTicks: 1000 }, }, }); const result = processModels(state); const sft = result.modelsState.activeTrainingPipelines[0].stages.sft; // speedMultiplier = (1 + (2.5+2.5)*0.15) * 1 = 1.75 // SFT does not apply MoE penalty expect(sft.progressTicks).toBeCloseTo(1.75, 1); }); }); // ---------- VRAM stalling ---------- describe('VRAM stalling', () => { it('stalls pipeline when VRAM is insufficient for generation', () => { const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[1]; // 48 GB const state = stateWithPipeline( { currentStage: 'pretraining', stages: { pretraining: { progressTicks: 0, totalTicks: 1000 } } }, { generation: 1 }, { compute: { totalVramGB: requiredVram - 1 } }, ); const result = processModels(state); const pipeline = result.modelsState.activeTrainingPipelines[0]; expect(pipeline.status).toBe('stalled'); }); it('does not stall when VRAM is sufficient', () => { const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[1]; // 48 GB const state = stateWithPipeline( { currentStage: 'pretraining', stages: { pretraining: { progressTicks: 0, totalTicks: 1000 } } }, { generation: 1 }, { compute: { totalVramGB: requiredVram + 100 } }, ); const result = processModels(state); const pipeline = result.modelsState.activeTrainingPipelines[0]; expect(pipeline.status).toBe('active'); }); it('resumes stalled pipeline once VRAM becomes available', () => { const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[1]; // 48 GB // First: not enough VRAM -> stall const stalledState = stateWithPipeline( { currentStage: 'pretraining', status: 'stalled', stages: { pretraining: { progressTicks: 50, totalTicks: 1000 } }, }, { generation: 1 }, { compute: { totalVramGB: requiredVram + 100 } }, ); const result = processModels(stalledState); const pipeline = result.modelsState.activeTrainingPipelines[0]; expect(pipeline.status).toBe('active'); expect(pipeline.stages.pretraining.progressTicks).toBeGreaterThan(50); }); it('applies MoE VRAM multiplier (1.5x) when checking VRAM', () => { const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[1]; // 48 GB // MoE needs 48 * 1.5 = 72 GB const state = stateWithPipeline( { currentStage: 'pretraining', architecture: { type: 'moe', totalParameters: 7e9, activeParameters: 2e9, contextWindow: 8192, vocabularySize: 32000, expertCount: 8, expertTopK: 2 }, stages: { pretraining: { progressTicks: 0, totalTicks: 1000 } }, }, { generation: 1 }, { compute: { totalVramGB: 60 } }, // 60 < 72 ); const result = processModels(state); const pipeline = result.modelsState.activeTrainingPipelines[0]; expect(pipeline.status).toBe('stalled'); }); }); // ---------- Best deployed model tracking ---------- describe('best deployed model tracking', () => { it('tracks highest rawCapability among deployed base models', () => { const model1 = createTestBaseModel({ id: 'model-1', familyId: 'fam-1', rawCapability: 60, isDeployed: true, capabilities: { reasoning: 60, coding: 50, creative: 40, math: 45, knowledge: 55, multimodal: 20, agents: 30, speed: 80, contextUtilization: 50, }, safetyProfile: { overallSafety: 70, refusalRate: 0.1, harmAvoidance: 70, instructionFollowing: 50, honesty: 60 }, }); const model2 = createTestBaseModel({ id: 'model-2', familyId: 'fam-1', rawCapability: 80, isDeployed: true, capabilities: { reasoning: 80, coding: 70, creative: 60, math: 65, knowledge: 75, multimodal: 40, agents: 50, speed: 70, contextUtilization: 60, }, safetyProfile: { overallSafety: 85, refusalRate: 0.1, harmAvoidance: 85, instructionFollowing: 70, honesty: 80 }, }); const family = createTestModelFamily({ id: 'fam-1', name: 'Test', baseModelIds: ['model-1', 'model-2'], variants: [], generation: 1, createdAtTick: 0 }); const state = createTestState({ models: { baseModels: [model1, model2], families: [family], activeTrainingPipelines: [], }, }); const result = processModels(state); expect(result.modelsState.bestDeployedModelScore).toBe(80); }); it('tracks highest overallSafety among deployed base models', () => { const model = createTestBaseModel({ id: 'model-1', familyId: 'fam-1', rawCapability: 50, isDeployed: true, capabilities: { reasoning: 50, coding: 40, creative: 30, math: 35, knowledge: 45, multimodal: 10, agents: 20, speed: 80, contextUtilization: 50, }, safetyProfile: { overallSafety: 92, refusalRate: 0.1, harmAvoidance: 90, instructionFollowing: 60, honesty: 85 }, }); const family = createTestModelFamily({ id: 'fam-1', name: 'Test', baseModelIds: ['model-1'], variants: [], generation: 1, createdAtTick: 0 }); const state = createTestState({ models: { baseModels: [model], families: [family], activeTrainingPipelines: [], }, }); const result = processModels(state); expect(result.modelsState.bestDeployedSafetyScore).toBe(92); }); it('returns 0 for best scores when no models are deployed', () => { const model = createTestBaseModel({ id: 'model-1', familyId: 'fam-1', rawCapability: 50, isDeployed: false, capabilities: { reasoning: 50, coding: 40, creative: 30, math: 35, knowledge: 45, multimodal: 10, agents: 20, speed: 80, contextUtilization: 50, }, safetyProfile: { overallSafety: 92, refusalRate: 0.1, harmAvoidance: 90, instructionFollowing: 60, honesty: 85 }, }); const family = createTestModelFamily({ id: 'fam-1', name: 'Test', baseModelIds: ['model-1'], variants: [], generation: 1, createdAtTick: 0 }); const state = createTestState({ models: { baseModels: [model], families: [family], activeTrainingPipelines: [], }, }); const result = processModels(state); expect(result.modelsState.bestDeployedModelScore).toBe(0); expect(result.modelsState.bestDeployedSafetyScore).toBe(0); }); }); // ---------- Point releases ---------- describe('point releases', () => { it('creates a point release model with boosted capability from source', () => { const sourceModel = createTestBaseModel({ id: 'source-model', familyId: 'fam-1', rawCapability: 60, version: 1.0, capabilities: { reasoning: 60, coding: 50, creative: 40, math: 45, knowledge: 55, multimodal: 20, agents: 30, speed: 80, contextUtilization: 50, }, safetyProfile: { overallSafety: 70, refusalRate: 0.1, harmAvoidance: 70, instructionFollowing: 50, honesty: 60 }, }); const family = createTestModelFamily({ id: 'fam-1', name: 'Test', baseModelIds: ['source-model'], variants: [], generation: 1, createdAtTick: 0 }); const pipeline = createTestTrainingPipeline({ familyId: 'fam-1', isPointRelease: true, sourceModelId: 'source-model', currentStage: 'alignment', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 79, totalTicks: 80 }, }, }); const state = createTestState({ models: { baseModels: [sourceModel], activeTrainingPipelines: [pipeline], families: [family], }, compute: { totalTrainingFlops: 1e15, trainingAllocation: 0.5, totalVramGB: 256 }, talent: { departments: { research: { headcount: 5, effectiveness: 0.5 }, engineering: { headcount: 5, effectiveness: 0.5 }, }, }, }); const result = processModels(state); expect(result.completedModels).toHaveLength(1); const newModel = result.completedModels[0]; const expectedCapability = Math.min(98, 60 * (1 + POINT_RELEASE_CAPABILITY_GAIN)); expect(newModel.rawCapability).toBeCloseTo(expectedCapability, 2); }); it('caps point release rawCapability at 98', () => { const sourceModel = createTestBaseModel({ id: 'source-model', familyId: 'fam-1', rawCapability: 96, version: 1.0, capabilities: { reasoning: 96, coding: 90, creative: 85, math: 88, knowledge: 92, multimodal: 60, agents: 70, speed: 70, contextUtilization: 60, }, safetyProfile: { overallSafety: 80, refusalRate: 0.1, harmAvoidance: 80, instructionFollowing: 70, honesty: 75 }, }); const family = createTestModelFamily({ id: 'fam-1', name: 'Test', baseModelIds: ['source-model'], variants: [], generation: 1, createdAtTick: 0 }); const pipeline = createTestTrainingPipeline({ familyId: 'fam-1', isPointRelease: true, sourceModelId: 'source-model', currentStage: 'alignment', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 79, totalTicks: 80 }, }, }); const state = createTestState({ models: { baseModels: [sourceModel], activeTrainingPipelines: [pipeline], families: [family], }, compute: { totalTrainingFlops: 1e15, trainingAllocation: 0.5, totalVramGB: 256 }, talent: { departments: { research: { headcount: 5, effectiveness: 0.5 }, engineering: { headcount: 5, effectiveness: 0.5 }, }, }, }); const result = processModels(state); const newModel = result.completedModels[0]; // 96 * 1.08 = 103.68 -> capped at 98 expect(newModel.rawCapability).toBe(98); }); }); // ---------- No active pipelines ---------- describe('no active pipelines', () => { it('returns quickly with no changes when no pipelines exist', () => { const state = createTestState({ models: { activeTrainingPipelines: [], families: [] }, }); const result = processModels(state); expect(result.completedModels).toHaveLength(0); expect(result.notifications).toHaveLength(0); expect(result.reputationHit).toBe(0); expect(result.legalCosts).toBe(0); expect(result.modelsState.activeTrainingPipelines).toHaveLength(0); }); it('skips completed pipelines without modifying them', () => { const pipeline = createTestTrainingPipeline({ familyId: 'fam-1', status: 'completed', currentStage: 'alignment', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 80, totalTicks: 80, isComplete: true }, }, }); const family = createTestModelFamily({ id: 'fam-1', name: 'Test', baseModelIds: [], variants: [], generation: 1, createdAtTick: 0 }); const state = createTestState({ models: { activeTrainingPipelines: [pipeline], families: [family] }, compute: { totalTrainingFlops: 1e15, trainingAllocation: 0.5, totalVramGB: 256 }, }); const result = processModels(state); expect(result.completedModels).toHaveLength(0); expect(result.modelsState.activeTrainingPipelines[0].status).toBe('completed'); }); }); // ---------- Completed model properties ---------- describe('completed model properties', () => { it('creates a model with correct version 1.0 for non-point-release', () => { const state = stateWithPipeline({ currentStage: 'alignment', isPointRelease: false, stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { specializations: ['general'], progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 79, totalTicks: 80 }, }, }); const result = processModels(state); const model = result.completedModels[0]; expect(model.version).toBe(1.0); expect(model.isDeployed).toBe(false); expect(model.familyId).toBe('fam-1'); }); it('records trainingStagesCompleted including previously completed stages', () => { // The model is created from the pipeline state before the alignment stage // update is written back, so alignment.isComplete must be true in the input // for it to appear in trainingStagesCompleted. const state = stateWithPipeline({ currentStage: 'alignment', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { specializations: ['general'], progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 79, totalTicks: 80, isComplete: false }, }, }); const result = processModels(state); const model = result.completedModels[0]; // pretraining and sft were already complete when the model was created expect(model.trainingStagesCompleted).toContain('pretraining'); expect(model.trainingStagesCompleted).toContain('sft'); // alignment isComplete is set on the local copy after createBaseModel is called // with the old pipeline state, so alignment is NOT included expect(model.trainingStagesCompleted).not.toContain('alignment'); }); it('uses family name in model auto-name', () => { const state = stateWithPipeline( { currentStage: 'alignment', sizeTier: 'small', stages: { pretraining: { progressTicks: 1000, totalTicks: 1000, isComplete: true }, sft: { specializations: ['general'], progressTicks: 100, totalTicks: 100, isComplete: true }, alignment: { progressTicks: 79, totalTicks: 80 }, }, }, { name: 'Nexus' }, ); const result = processModels(state); const model = result.completedModels[0]; expect(model.name).toContain('Nexus'); expect(model.name).toContain('Small'); expect(model.name).toContain('v1.0'); }); }); });