c1cc70eeb9
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>
642 lines
23 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|