Zum Inhalt

tests/presets/schema.test.ts

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