glytch/fktry/src/ui/voice.test.ts
type-two ea17e95318 [ui] conform to contracts v3: queue truth, scram latch, chassis vs accent
NEXT IN TRAY now reads snap.commissionQueue[1]. SIM publishes the field, so
the round-2 data-order inference is deleted rather than kept as a fallback —
it went wrong the moment a standing order was re-queued to the back, and a fax
that lies quietly is worse than one that says nothing. No queue, no peek.

Inspector reads EntityState.scrammed instead of inferring scram from heat >= 1.
A scrammed machine cools back below 1.0 while still offline, so the threshold
was wrong in exactly the window that matters; re-deriving SIM's hysteresis here
would also desync silently the moment they retune HEAT_RESTART.

Build-bar icons follow the codex's two-material rule, prompted by the v3 ruling
that color is chassis, not accent: grimy body from data, plus one emissive
element derived from the machine's first output item — the same derivation
RENDER uses, so the bar and the world agree. DATA's chassis values are all
desaturated neutrals (correct per §8), and 21 icons in those colours is an
unreadable grey wall; the accent is what tells a quantizer from a demuxer.
The round-1 id/kind tables were never wrong, only mislabeled — they're the
accent fallback now, for machines that output nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:34:22 +10:00

127 lines
4.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { GameData, MachineDef, MachineKind, SimEvent } from '../contracts';
import { createAccentLookup, machineChassis } from './palette';
import { eventToast, jamText, machineFlavor } from './voice';
function def(over: Partial<MachineDef> = {}): MachineDef {
return {
id: 'quantizer', name: 'QUANTIZER', codex: '', kind: 'crafter',
footprint: { x: 2, y: 2 }, recipes: [], powerDraw: 0, asset: 'quantizer', ...over,
};
}
const named = (id: number) => (id === 5 ? 'MOSH REACTOR' : `UNIT ${id}`);
describe('machineFlavor', () => {
it('prefers the codex line from data once LANE-DATA fills it', () => {
expect(machineFlavor(def({ flavor: 'The dial goes to CRIME.' }), 'quantizer'))
.toBe('The dial goes to CRIME.');
});
it('falls back to the hand-transcribed line while data is empty', () => {
expect(machineFlavor(def(), 'quantizer')).toContain('Rounds coefficients to zero');
});
it('gives an unknown machine generic copy rather than nothing', () => {
expect(machineFlavor(def({ id: 'wormhole' }), 'wormhole')).toContain('Industrial equipment');
});
it('survives a missing def', () => {
expect(machineFlavor(undefined, 'belt')).toContain('Moving things is the entire job');
});
});
describe('jamText', () => {
it('distinguishes backed-up from starved', () => {
expect(jamText('output full')).toBe('OUTPUT BUFFER AT CAPACITY. THIS IS A YOU PROBLEM.');
expect(jamText('starved')).toBe('INTAKE STARVED. NOTHING IS ARRIVING.');
});
it('passes an unknown reason through uppercased rather than swallowing it', () => {
expect(jamText('belt on fire')).toBe('BELT ON FIRE');
});
});
describe('eventToast', () => {
it('names the machine that jammed and says why', () => {
const e: SimEvent = { kind: 'jammed', entity: 5, reason: 'starved', tick: 1 };
expect(eventToast(e, named)).toBe('MOSH REACTOR: INTAKE STARVED. NOTHING IS ARRIVING.');
});
it('has different copy for a brownout arriving and lifting', () => {
const on = eventToast({ kind: 'brownout', on: true, tick: 1 }, named);
const off = eventToast({ kind: 'brownout', on: false, tick: 2 }, named);
expect(on).not.toBe(off);
expect(on).toContain('STILL LIFE');
});
it('reports a thermal scram against the machine that scrammed', () => {
const e: SimEvent = { kind: 'scram', entity: 5, on: true, tick: 9 };
expect(eventToast(e, named)).toContain('MOSH REACTOR');
expect(eventToast(e, named)).toContain('THERMAL SCRAM');
});
it('confirms a removal, because demolition should feel acknowledged', () => {
const e: SimEvent = { kind: 'removed', entity: 1, def: 'belt', tick: 1 };
expect(eventToast(e, named)).toBe('UNIT RECLAIMED. THE FLOOR REMEMBERS.');
});
it('stays quiet for the high-frequency events', () => {
for (const e of [
{ kind: 'shipped', item: 'melt', count: 1, tick: 1 },
{ kind: 'crafted', entity: 1, recipe: 'mosh', tick: 1 },
{ kind: 'placed', entity: 1, def: 'belt', tick: 1 },
] as SimEvent[]) {
expect(eventToast(e, named)).toBeNull();
}
});
});
describe('machineChassis', () => {
it('uses the art-directed body colour from data', () => {
expect(machineChassis(def({ color: '#4e5a62' }))).toBe('#4e5a62');
});
it('falls back to a neutral for a machine DATA has not art-directed', () => {
expect(machineChassis(def({ color: undefined }))).toMatch(/^#/);
});
});
describe('createAccentLookup', () => {
// contracts v3: color is the chassis; the accent derives from what the machine makes.
const data: GameData = {
items: [
{ id: 'melt', name: 'MELT', codex: '', tier: 2, color: '#ff7a3f' },
{ id: 'hf-dust', name: 'HF DUST', codex: '', tier: 1, color: '#e8e0ff' },
],
machines: [],
recipes: [
{ id: 'mosh', machine: 'mosh-reactor', inputs: {}, outputs: { melt: 1 }, ticks: 90, bandwidth: 10 },
],
tech: [],
commissions: [],
};
const accentOf = createAccentLookup(data);
it('derives the accent from the first thing the machine outputs', () => {
expect(accentOf(def({ id: 'mosh-reactor', recipes: ['mosh'] }))).toBe('#ff7a3f');
});
it('falls back to the codex glow for machines that output nothing', () => {
// Belts carry; they do not produce. Their accent has to come from the codex table.
expect(accentOf(def({ id: 'belt', kind: 'belt', recipes: [] }))).toBe('#5a7a5a');
});
it('falls back by kind for an unknown machine with no recipes', () => {
expect(accentOf(def({ id: 'brand-new-thing', kind: 'power', recipes: [] }))).toBe('#3fffe0');
});
it('ignores a recipe id that does not resolve rather than throwing', () => {
expect(accentOf(def({ id: 'x', kind: 'crafter', recipes: ['ghost-recipe'] }))).toBe('#3fffe0');
});
it('always returns something for an unheard-of kind', () => {
expect(accentOf(def({ id: 'x', kind: 'teleporter' as MachineKind, recipes: [] }))).toMatch(/^#/);
});
});