Files
AIHostingTycoon/packages/game-engine/src/systems/modelSystem.test.ts
T
josh c1cc70eeb9
Balance Check / balance-simulation (pull_request) Successful in 38s
Balance Check / multi-run-balance (pull_request) Successful in 13m44s
Rename AI Tycoon to Token Empire across entire codebase
Full rebrand: UI display text, package scope (@ai-tycoon/* -> @token-empire/*),
localStorage keys, Docker/CI image paths, database names, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 21:04:07 -04:00

642 lines
23 KiB
TypeScript

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<typeof createTestTrainingPipeline>[0],
familyOverrides?: Parameters<typeof createTestModelFamily>[0],
stateOverrides?: Parameters<typeof createTestState>[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>): 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');
});
});
});