// tests/presets/schema.test.ts
import { describe, expect, it } from 'vitest';
import { PresetSchema, validatePreset } from '~/presets/schema';
const species = {
color: [0.5, 0.5, 0.5],
populationShare: 0.33,
sensorAngle: 0.5,
sensorDistance: 24,
turnSpeed: 0.4,
moveSpeed: 1,
depositAmount: 20,
};
const valid = {
id: 'test',
name: 'Test',
builtIn: true,
tags: ['minimal'],
tagSignal: 'pearl',
params: {
agentCount: 1000,
spawnPattern: 'random',
species: [species, species, species],
interactionMatrix: [
[1, -0.6, -0.6],
[-0.6, 1, -0.6],
[-0.6, -0.6, 1],
],
decayRate: 0.97,
diffusionFreq: 3,
fadeStrength: 0.1,
trailSaturation: 255,
chaosInterval: 0,
chaosStrength: 0,
rhythmAmplitude: 0.05,
backgroundColor: [0, 0, 0],
exposure: 2.5,
feedbackAmount: 0.2,
bloomThreshold: 0.25,
bloomIntensity: 0.9,
vignette: 0.3,
saturation: 1.1,
contrast: 1.05,
audioReactive: { enabled: false, bassMod: 0, midMod: 0, highMod: 0 },
},
};
describe('PresetSchema', () => {
it('accepts valid preset', () => {
expect(() => PresetSchema.parse(valid)).not.toThrow();
});
it('defaults simSpeed to 1 for pre-1.2 presets that lack the field', () => {
const parsed = PresetSchema.parse(valid);
expect(parsed.params.simSpeed).toBe(1);
});
it('defaults blendMode to additive for pre-1.3 presets that lack the field', () => {
const parsed = PresetSchema.parse(valid);
expect(parsed.params.blendMode).toBe('additive');
});
it('preserves an explicit simSpeed value', () => {
const withSpeed = { ...valid, params: { ...valid.params, simSpeed: 4 } };
const parsed = PresetSchema.parse(withSpeed);
expect(parsed.params.simSpeed).toBe(4);
});
it('rejects simSpeed outside 1..8', () => {
const tooHigh = { ...valid, params: { ...valid.params, simSpeed: 9 } };
expect(() => PresetSchema.parse(tooHigh)).toThrow();
const tooLow = { ...valid, params: { ...valid.params, simSpeed: 0 } };
expect(() => PresetSchema.parse(tooLow)).toThrow();
});
it('folds pre-1.2 attractionStrength/repulsionStrength into the matrix', () => {
// Old shape: identity-ish matrix + non-trivial strengths
const legacy = {
...valid,
params: {
...valid.params,
interactionMatrix: [
[1, -0.5, -0.5],
[-0.5, 1, -0.5],
[-0.5, -0.5, 1],
],
attractionStrength: 1.5,
repulsionStrength: -0.4,
},
};
const parsed = PresetSchema.parse(legacy);
// Expected folded values:
// diagonal: 1 * 1.5 = 1.5
// off-diagonal: -0.5 * -0.4 = 0.2
expect(parsed.params.interactionMatrix[0][0]).toBeCloseTo(1.5);
expect(parsed.params.interactionMatrix[1][1]).toBeCloseTo(1.5);
expect(parsed.params.interactionMatrix[2][2]).toBeCloseTo(1.5);
expect(parsed.params.interactionMatrix[0][1]).toBeCloseTo(0.2);
expect(parsed.params.interactionMatrix[1][0]).toBeCloseTo(0.2);
expect(parsed.params.interactionMatrix[2][0]).toBeCloseTo(0.2);
// The legacy fields must not survive on the parsed shape
expect('attractionStrength' in parsed.params).toBe(false);
expect('repulsionStrength' in parsed.params).toBe(false);
});
it('passes through a 1.2+ preset (no strengths) unchanged', () => {
const parsed = PresetSchema.parse(valid);
expect(parsed.params.interactionMatrix[0][0]).toBeCloseTo(1);
expect(parsed.params.interactionMatrix[0][1]).toBeCloseTo(-0.6);
});
it('rejects negative agentCount', () => {
const bad = { ...valid, params: { ...valid.params, agentCount: -5 } };
expect(() => PresetSchema.parse(bad)).toThrow();
});
it('rejects sensorDistance out of range', () => {
const bad = {
...valid,
params: {
...valid.params,
species: [{ ...species, sensorDistance: 999 }, species, species],
},
};
expect(() => PresetSchema.parse(bad)).toThrow();
});
it('rejects decayRate out of range', () => {
const bad = { ...valid, params: { ...valid.params, decayRate: 1.5 } };
expect(() => PresetSchema.parse(bad)).toThrow();
});
it('rejects matrix value out of range', () => {
const bad = {
...valid,
params: {
...valid.params,
interactionMatrix: [
[3, 0, 0],
[0, 1, 0],
[0, 0, 1],
],
},
};
expect(() => PresetSchema.parse(bad)).toThrow();
});
it('rejects invalid spawnPattern', () => {
const bad = { ...valid, params: { ...valid.params, spawnPattern: 'spiral' } };
expect(() => PresetSchema.parse(bad)).toThrow();
});
it('rejects invalid tagSignal', () => {
const bad = { ...valid, tagSignal: 'magenta' };
expect(() => PresetSchema.parse(bad)).toThrow();
});
it('accepts populationShare above 1 (relative weights)', () => {
const ok = {
...valid,
params: {
...valid.params,
species: [{ ...species, populationShare: 3 }, species, species],
},
};
expect(() => PresetSchema.parse(ok)).not.toThrow();
});
it('rejects all-zero population shares', () => {
const bad = {
...valid,
params: {
...valid.params,
species: [
{ ...species, populationShare: 0 },
{ ...species, populationShare: 0 },
{ ...species, populationShare: 0 },
],
},
};
expect(() => PresetSchema.parse(bad)).toThrow();
});
});
describe('validatePreset', () => {
it('returns Preset on success', () => {
expect(validatePreset(valid).kind).toBe('ok');
});
it('returns error on failure', () => {
expect(validatePreset({ id: 'x' }).kind).toBe('error');
});
});