返回 CodeWhale
pet.test.mjs
根目录 / pet / tests / pet.test.mjs
1 import test from 'node:test';
2 import assert from 'node:assert/strict';
3 import { readFileSync } from 'node:fs';
4 import { PetSim, REST_STATE, digest, CHANNELS } from '../dist/core/pet-sim.js';
5 import { compilePetTelemetry, encodePetJSONL, decodePetJSONL } from '../dist/core/pet-telemetry.js';
6 import { PetWorld } from '../dist/core/pet-world.js';
7 import { renderPetPCM } from '../dist/core/pet-audio.js';
8 import { petDemoEvents } from '../dist/core/pet-demo.js';
9 import { importTrace } from '../dist/core/ingest.js';
10
11 const points = readFileSync(new URL('../public/whale-points.tsv', import.meta.url), 'utf8').trim().split('\n').map(row => row.trim().split(/\s+/).map(Number));
12 const event = (overrides = {}) => ({ schemaVersion: 1, id: 'e', traceId: 't', startTime: 0, endTime: 400,
13 agentId: 'parent', category: 'tool', name: 'work', status: 'success', attributes: {}, ...overrides });
14
15 test('span time beats onset count; long tools remain measured; containers are excluded', () => {
16 const e = [event({ id: 'root', category: 'orchestration', endTime: 4000 }),
17 event({ id: 'long', parentId: 'root', category: 'reasoning', endTime: 4000 }),
18 ...Array.from({ length: 8 }, (_, i) => event({ id: String(i), startTime: i * 10, endTime: i * 10 }))];
19 const b = compilePetTelemetry(e);
20 assert.equal(b[0].channel, 'reasoning'); assert.equal(b[9].channel, 'reasoning');
21 assert.equal(b[0].activeMs[9], 0); assert.equal(b[0].activeMs[0], 400);
22 });
23 test('half-open bins retain endpoint onsets and distinguish unknown duration from idle', () => {
24 const b = compilePetTelemetry([event({ endTime: 800 }), event({ id: 'last', startTime: 800, endTime: 800, category: 'human' })], 1600);
25 assert.equal(b[2].channel, 'human'); assert.equal(b[2].onsets[11], 1); assert.equal(b[3].observed, 0); assert.equal(b[3].channel, 'other');
26 const open = compilePetTelemetry([event({ openEnded: true, endTime: 3000 })], 1200);
27 assert.equal(open[0].onsets[1], 1); assert.equal(open[0].activeMs[1], 0); assert.equal(open[1].observed, 0);
28 });
29 test('repeat density is causal; independent future events do not rewrite past buckets', () => {
30 const first = [event({ endTime: 80 })], future = Array.from({ length: 7 }, (_, i) => event({ id: `f${i}`, startTime: 400 + i * 400, endTime: 480 + i * 400 }));
31 const a = compilePetTelemetry(first), b = compilePetTelemetry([...first, ...future]);
32 assert.deepEqual(a[0], b[0]); assert.ok(b[4].coherence < b[0].coherence);
33 });
34 test('errors, human spans and concurrent peers select semantic gaits', () => {
35 assert.equal(compilePetTelemetry([event({ category: 'reasoning', status: 'error' })])[0].channel, 'error');
36 const wait = compilePetTelemetry([event({ category: 'human', status: 'pending' })])[0];
37 assert.equal(wait.channel, 'human'); assert.equal(wait.waiting, true);
38 const peers = compilePetTelemetry(['a', 'b', 'c'].map(id => event({ id, agentId: id })))[0];
39 assert.equal(peers.channel, 'agent'); assert.deepEqual(peers.agentIds, ['a', 'b', 'c']);
40 });
41 test('updates deduplicate, multiple traces reject, wire rejects corrupt values', () => {
42 assert.equal(compilePetTelemetry([event(), event({ endTime: 800 })])[0].onsets[1], 1);
43 assert.throws(() => compilePetTelemetry([event(), event({ traceId: 'another' })]), /one trace/);
44 const b = compilePetTelemetry([event()]); assert.deepEqual(decodePetJSONL(encodePetJSONL(b)), b);
45 for (const edit of [{ activity: null }, { observed: 2 }, { sequence: 8 }, { channel: 'fake' }, { onsets: [1] }])
46 assert.throws(() => decodePetJSONL(JSON.stringify({ ...b[0], ...edit })));
47 assert.throws(() => compilePetTelemetry([event({ endTime: Infinity })]));
48 });
49 test('reduced motion has no settling, jitter, colour fade or tear decay under constant input', () => {
50 for (const ch of CHANNELS) {
51 const sim = new PetSim(points), state = { ...REST_STATE, channel: ch.key, activity: .8, coherence: .4 };
52 sim.step(1 / 30, state, { motion: false, sensitivity: 1 }); const first = digest(sim);
53 for (let i = 0; i < 90; i++) sim.step(1 / 30, state, { motion: false, sensitivity: 1 });
54 assert.equal(digest(sim), first, ch.key);
55 }
56 });
57 test('world and score are invariant to display cadence and repeat replay exactly', () => {
58 const tape = compilePetTelemetry(petDemoEvents(), 80_000);
59 const run = dt => {
60 const world = new PetWorld(points, tape), voices = [...world.voices];
61 for (let i = 0; i < Math.round(36 / dt); i++) { world.step(dt); voices.push(...world.voices); }
62 return { frame: world.frame, digest: digest(world.sim), voices };
63 };
64 const a = run(1 / 30); assert.deepEqual(run(1 / 60), a); assert.deepEqual(run(1 / 10), a);
65 assert.equal(a.frame.behaviour === 'doze', false);
66 assert.ok(a.voices.some(v => v.id.startsWith('tear:'))); assert.ok(a.voices.some(v => v.id.startsWith('address:')));
67 });
68 test('sleep remains observed in wild mode; recorded gaps stay hollow; touch wakes without inventing coverage', () => {
69 const world = new PetWorld(points, [], [{ timeMs: 80_000, kind: 'attention', x: .2, y: 0 }]);
70 for (let i = 0; i < 79 * 30; i++) world.step(1 / 30);
71 assert.equal(world.frame.behaviour, 'doze'); assert.equal(world.frame.state.observed, 1); assert.ok(world.frame.state.lit < .3);
72 for (let i = 0; i < 30; i++) world.step(1 / 30);
73 assert.equal(world.frame.behaviour, 'wake'); assert.equal(world.frame.state.channel, 'human');
74 const unknown = new PetWorld(points, compilePetTelemetry([], 90_000), [{ timeMs: 1000, kind: 'attention', x: 0, y: 0 }]);
75 unknown.step(1); assert.equal(unknown.frame.state.channel, 'human'); assert.equal(unknown.sim.frame.hollow, true);
76 });
77 test('pod identity survives membership changes; needs escalate on actual pending spans', () => {
78 const events = ['c', 'a', 'b'].map(agentId => event({ id: agentId, agentId, endTime: 800 }));
79 events.push(event({ id: 'b2', agentId: 'b', startTime: 1200, endTime: 1600 }));
80 const world = new PetWorld(points, compilePetTelemetry(events, 2000));
81 const slots = new Map(world.frame.pod.map(m => [m.id, m.slot])); world.step(1.3);
82 assert.equal(world.frame.pod.find(m => m.id === 'b').slot, slots.get('b'));
83 assert.equal(world.frame.pod.find(m => m.id === 'a').present, false);
84 const waiting = new PetWorld(points, compilePetTelemetry([event({ category: 'human', status: 'pending', endTime: 30_000 })]));
85 waiting.step(9); assert.equal(waiting.frame.needs, 'approach'); waiting.step(10); waiting.step(8); assert.equal(waiting.frame.needs, 'call');
86 });
87 test('PCM absolute samples match split chunks including tear noise and remain bounded', () => {
88 const voices = [{ id: 'tear', start: .05, duration: .3, frequency: 185, gain: .1, pan: -.2, kind: 'noise' },
89 { id: 'tone', start: 0, duration: .44, frequency: 130.81, gain: .08, pan: .3, kind: 'tone' }];
90 const whole = renderPetPCM(voices, 0, 24_000), a = renderPetPCM(voices, 0, 9123), b = renderPetPCM(voices, 9123, 14_877);
91 assert.deepEqual([...a.left, ...b.left], [...whole.left]); assert.deepEqual([...a.right, ...b.right], [...whole.right]);
92 assert.ok(whole.left.some(n => n !== 0)); assert.ok(whole.left.every(n => Number.isFinite(n) && Math.abs(n) <= 1));
93 assert.throws(() => renderPetPCM(voices, -1, 4));
94 });
95
96 test('live accepted packets and interactions replay the same frames and audio including expired gaps', () => {
97 const live = new PetWorld(points, compilePetTelemetry([])), voices = [...live.voices], frames = [];
98 const packets = compilePetTelemetry(petDemoEvents(), 80_000);
99 for (let tick = 0; tick < 180; tick++) {
100 if ([4, 29, 92].includes(tick)) live.acceptTelemetry(packets[tick]);
101 if (tick === 73) live.interact('attention', .4, -.2);
102 live.step(1 / 30); voices.push(...live.voices); frames.push(digest(live.sim));
103 }
104 const replay = new PetWorld(points, live.tape, live.interactions), replayVoices = [...replay.voices];
105 for (let tick = 0; tick < 180; tick++) {
106 replay.step(1 / 30); replayVoices.push(...replay.voices); assert.equal(digest(replay.sim), frames[tick], `tick ${tick}`);
107 }
108 assert.deepEqual(replayVoices, voices); assert.equal(live.frame.state.observed, 0);
109 });
110 test('metadata privacy preserves only boolean container markers needed by every live and replay driver', () => {
111 const events = [event({ id: 'container', category: 'reasoning', attributes: { 'whalesong.container': true, 'codewhale.container': 'private text' } }), event({ id: 'work' })];
112 const [trace] = importTrace(JSON.stringify(events), 'fixture', { privacy: 'metadata' });
113 assert.deepEqual(trace.events[0].attributes, { 'whalesong.container': true });
114 assert.equal(compilePetTelemetry(trace.events)[0].channel, 'tool');
115 });
116
117 test('successive peer groups reuse vacant slots while continuously present identities keep their slot and phase', () => {
118 const events = ['a', 'b', 'c', 'd', 'e', 'f'].map(id => event({ id, agentId: id, endTime: 800 }));
119 events.push(...['b', 'g', 'h', 'i', 'j', 'k', 'l'].map(id => event({ id: `next-${id}`, agentId: id, startTime: 800, endTime: 1600 })));
120 const world = new PetWorld(points, compilePetTelemetry(events)), before = world.frame.pod.find(p => p.id === 'b');
121 world.step(1);
122 assert.deepEqual(world.frame.pod.find(p => p.id === 'b'), before);
123 assert.equal(world.frame.pod.filter(p => p.present).length, 6);
124 assert.ok(world.frame.pod.some(p => p.id === 'k')); assert.ok(!world.frame.pod.some(p => p.id === 'a'));
125 assert.equal(new Set(world.frame.pod.map(p => p.slot)).size, 6);
126 });
127 test('multiple inputs before one fixed tick are all journalled and replay together', () => {
128 const world = new PetWorld(points);
129 world.interact('food', -.4, .3); world.interact('attention', .2, .1); world.step(1 / 30);
130 assert.equal(world.interactions.length, 2); assert.equal(world.frame.food.x, -.4);
131 const replay = new PetWorld(points, [], world.interactions); replay.step(1 / 30);
132 assert.deepEqual(replay.frame, world.frame); assert.equal(digest(replay.sim), digest(world.sim));
133 });
134
134 lines Plain Text