返回 CodeWhale
pet-engine.test.mjs
根目录 / pet / tests / pet-engine.test.mjs
1 import test from 'node:test';
2 import assert from 'node:assert/strict';
3 import { readFileSync } from 'node:fs';
4 import { PetEngineTelemetry } from '../dist/core/pet-engine.js';
5 import { PetNative } from '../dist/core/pet-native.js';
6 import { compilePetTelemetry, encodePetJSONL } from '../dist/core/pet-telemetry.js';
7 import { petDemoEvents } from '../dist/core/pet-demo.js';
8
9 const points = JSON.stringify(readFileSync(new URL('../public/whale-points.tsv', import.meta.url), 'utf8').trim().split('\n').map(row => row.trim().split(/\s+/).map(Number)));
10 test('typed operation activity carries a kind, never a tool name, and goes stale', () => {
11 const e = new PetEngineTelemetry();
12 for (const [i, kind] of ['reading','searching','editing','executing','testing','browsing','computer'].entries()) {
13 const at = i * 20_000;
14 e.observe({event:'turn_started',turn_id:'turn-'+i},at);
15 e.observe({event:'operation_activity_started',span_id:'private-span-'+i,activity_kind:kind},at);
16 const activity=e.activity(at+100);
17 assert.equal(activity.activityKind,kind); assert.equal(activity.authoritativePresence,'working');
18 assert.equal(activity.freshness,'fresh'); assert.equal(activity.turnId,'turn-'+i);
19 assert.deepEqual(Object.keys(activity).sort(),['activeSpans','activityKind','authoritativePresence','cursor','doneEffectId',
20 'failedToolAge','freshness','observed','observedAtMs','parallelAgentCount','schemaVersion','sessionId','turnId','turnOutcome']);
21 assert.equal(JSON.stringify(activity).includes('private-span'),false);
22 const stale=e.activity(at+12_001);
23 assert.equal(stale.freshness,'stale'); assert.equal(stale.activityKind,null); assert.deepEqual(stale.activeSpans,[]);
24 }
25 // The pre-contract events carried tool names; the boundary refuses them.
26 assert.throws(() => e.observe({event:'tool_call_started',tool_call_id:'a',tool_name:'read_file'},200_000),/Invalid Engine pet metadata fields/);
27 assert.throws(() => e.observe({event:'turn_started'},200_000),/Missing Engine turn_id/);
28 assert.throws(() => e.observe({event:'operation_activity_started',span_id:'a',activity_kind:'shell'},200_000));
29 });
30 test('only a completed turn is Done; interrupted and failed turns are idle', () => {
31 for (const [outcome, presence] of [['completed','done'],['interrupted','idle'],['failed','idle']]) {
32 const e = new PetEngineTelemetry();
33 e.observe({event:'turn_started',turn_id:'t'},0);
34 e.observe({event:'operation_activity_started',span_id:'s',activity_kind:'editing'},10);
35 e.observe({event:'turn_complete',turn_id:'t',turn_outcome:outcome},20);
36 const activity=e.activity(30);
37 assert.equal(activity.authoritativePresence,presence,outcome);
38 assert.equal(activity.doneEffectId,outcome==='completed'?'t':null);
39 assert.equal(activity.activityKind,null); assert.deepEqual(activity.activeSpans,[]);
40 }
41 const untracked = new PetEngineTelemetry();
42 untracked.observe({event:'turn_complete',turn_outcome:'completed'},0);
43 assert.equal(untracked.activity(10).authoritativePresence,'idle');
44 assert.throws(() => untracked.observe({event:'turn_complete',turn_id:null,turn_outcome:'completed'},20));
45 // `/purge`, an edit rejection or a mid-turn session switch complete with no
46 // turn id. An outcome with no turn would fail the Rust projection
47 // invariant (`turn_outcome` requires `turn_id`) and drop the shared frame.
48 for (const outcome of ['completed','interrupted','failed']) {
49 const e = new PetEngineTelemetry();
50 e.observe({event:'turn_started',turn_id:'t'},0);
51 e.observe({event:'turn_complete',turn_outcome:outcome},20);
52 const activity=e.activity(30);
53 assert.equal(activity.turnId,null,outcome); assert.equal(activity.turnOutcome,null,outcome);
54 assert.equal(activity.authoritativePresence,'idle',outcome); assert.equal(activity.doneEffectId,null,outcome);
55 }
56 });
57 test('a new turn drops spans left open by the previous turn', () => {
58 const e = new PetEngineTelemetry();
59 e.observe({event:'turn_started',turn_id:'t1'},0);
60 e.observe({event:'operation_activity_started',span_id:'orphan',activity_kind:'editing'},10);
61 e.observe({event:'turn_started',turn_id:'t2'},20);
62 const activity=e.activity(30);
63 assert.equal(activity.turnId,'t2'); assert.equal(activity.activityKind,null); assert.deepEqual(activity.activeSpans,[]);
64 });
65 test('waiting and concurrent activity stay bounded and clear after disconnect', () => {
66 const pet=new PetNative(points,'','[]',true);
67 pet.observeEngineBatch(JSON.stringify([{event:'turn_started',turn_id:'t'},{event:'operation_activity_started',span_id:'a',activity_kind:'computer'},
68 ...Array.from({length:3},(_,i)=>({event:'agent_spawned',id:'private-'+i}))]),0);
69 pet.advanceEngine(200,true,false);
70 const frame=JSON.parse(pet.presentation());
71 assert.equal(frame.activity.activityKind,'computer');assert.equal(frame.activity.parallelAgentCount,3);
72 assert.equal(JSON.stringify(frame.activity).includes('private-'),false);
73 pet.observeEngine(JSON.stringify({event:'approval_required',id:'private'}),200);
74 pet.advanceEngine(600,true,true);
75 const waiting=JSON.parse(pet.presentation()).activity;
76 assert.equal(waiting.authoritativePresence,'needs_you');assert.equal(waiting.activityKind,null);
77 pet.observeEngine(JSON.stringify({event:'approval_resolved',id:'private',outcome:'denied'}),700);
78 pet.advanceEngine(800,true,false);
79 assert.equal(JSON.parse(pet.presentation()).activity.authoritativePresence,'working');
80 const before=pet.recording(true);pet.presentation();assert.equal(pet.recording(true),before);
81 pet.disconnectEngine();
82 const gone=JSON.parse(pet.presentation()).activity;
83 assert.equal(gone.observed,false);assert.equal(gone.freshness,'missing');
84 });
85 test('the incremental bucket range uses the same measured projection as full replay', () => {
86 const events = petDemoEvents(), full = compilePetTelemetry(events, 80_000);
87 for (let i = 0; i < full.length; i++) assert.deepEqual(compilePetTelemetry(events, 80_000, i)[0], full[i]);
88 });
89 test('Engine pulses expire; a late failed completion tears at receipt time without rewriting history', () => {
90 const engine = new PetEngineTelemetry();
91 engine.observe({ event: 'operation_activity_started', span_id: 'a', activity_kind: 'executing' }, 0);
92 engine.observe({ event: 'tool_call_heartbeat' }, 300);
93 const first = engine.bucket(0);
94 assert.equal(first.channel, 'code'); assert.equal(first.activeMs[3], 300);
95 assert.equal(engine.bucket(3).observed, 0);
96 engine.observe({ event: 'operation_activity_completed', span_id: 'a', activity_kind: 'executing', outcome: 'failed' }, 5900);
97 assert.equal(engine.bucket(13).observed, 0);
98 assert.equal(engine.bucket(14).channel, 'error'); assert.equal(engine.bucket(14).errors, 1);
99 assert.deepEqual(engine.bucket(0), first);
100 });
101 test('an authoritative waiting request escalates, accepted tape replays, and silence stays unknown', () => {
102 const pet = new PetNative(points, '', '[]', true);
103 pet.observeEngine(JSON.stringify({ event: 'approval_required', id: 'permission' }), 100);
104 for (let ms = 400; ms <= 30_000; ms += 400) pet.advanceEngine(ms, true, true);
105 assert.equal(JSON.parse(pet.snapshot()).needs, 'call');
106 assert.equal(JSON.parse(pet.snapshot()).state.channel, 'human');
107 const saved = JSON.parse(pet.recording()), replay = new PetNative(points, encodePetJSONL(saved.tape), JSON.stringify(saved.interactions));
108 for (let i = 0; i < 900; i++) replay.step(1 / 30, true);
109 assert.deepEqual(JSON.parse(replay.snapshot()).state, JSON.parse(pet.snapshot()).state);
110 assert.equal(JSON.parse(replay.snapshot()).digest, JSON.parse(pet.snapshot()).digest);
111 pet.observeEngine(JSON.stringify({ event: 'turn_complete', turn_outcome: 'interrupted' }), 30_100);
112 pet.advanceEngine(32_000, true, false);
113 assert.equal(JSON.parse(pet.snapshot()).state.observed, 0);
114 });
115 test('Engine worker boundary rejects payloads, invalid clocks and oversized active sets', () => {
116 const e = new PetEngineTelemetry();
117 assert.throws(() => e.observe({ event: 'response_delta', index: 0, delta: 'private' }, 0));
118 assert.throws(() => e.observe({ event: 'response_delta', index: 0, channel: 'fabricated' }, 0));
119 e.observe({ event: 'thinking_started', index: 0 }, 100);
120 assert.throws(() => e.observe({ event: 'thinking_complete', index: 0 }, 90));
121 for (let i = 1; i < 256; i++) e.observe({ event: 'thinking_started', index: i }, 100);
122 assert.throws(() => e.observe({ event: 'thinking_started', index: 257 }, 100));
123 });
124
125 test('failed shared metadata batches accept no partial spans or private input', () => {
126 const control = new PetNative(points, '', '[]', true), pet = new PetNative(points, '', '[]', true);
127 assert.throws(() => pet.observeEngineBatch(JSON.stringify([{event:'thinking_started',index:1},{event:'response_delta',index:1,content:'PRIVATE'}]),0));
128 pet.advanceEngine(400,true,false); control.advanceEngine(400,true,false);
129 assert.deepEqual(JSON.parse(pet.recording(true)),JSON.parse(control.recording(true)));
130 pet.observeEngineBatch(JSON.stringify([{event:'thinking_started',index:1},{event:'response_delta',index:1,channel:'reasoning'}]),400);
131 pet.advanceEngine(800,true,false);
132 assert.equal(JSON.parse(pet.snapshot()).state.channel,'reasoning');
133 });
134
135 test('successive shared batches preserve active and waiting coverage exactly', () => {
136 let shared = new PetEngineTelemetry();
137 const direct = new PetEngineTelemetry();
138 for (const [at, event, waiting] of [
139 [0, {event:'operation_activity_started', span_id:'build', activity_kind:'executing'}, false],
140 [300, {event:'tool_call_heartbeat'}, false],
141 [600, {event:'tool_call_heartbeat'}, false],
142 [800, {event:'operation_activity_completed', span_id:'build', activity_kind:'executing', outcome:'succeeded'}, false],
143 [900, {event:'approval_required', id:'permission'}, true],
144 [1200, {event:'agent_spawned', id:'worker'}, true],
145 [1500, {event:'agent_progress', id:'worker', worker_status:'running'}, true],
146 ]) {
147 shared = shared.clone();
148 for (const engine of [shared, direct]) {
149 engine.observe(event, at);
150 engine.confirmWaiting(at, waiting);
151 }
152 for (let bucket=0; bucket<=Math.floor(at/400); bucket++)
153 assert.deepEqual(shared.bucket(bucket), direct.bucket(bucket));
154 }
155 });
156
157 test('shared presentation and still projections cannot change physics, clock, journal or score', () => {
158 const pet = new PetNative(points, '', '[]', true), control = new PetNative(points, '', '[]', true);
159 for(let i=1;i<=120;i++) {
160 pet.advanceEngine(i*1000/30,true,false); control.advanceEngine(i*1000/30,true,false);
161 const before=pet.recording(true), frame=JSON.parse(pet.presentation());
162 assert.equal(frame.points.length,980);assert.equal(frame.still.points.length,980);
163 for(let view=0;view<3;view++) pet.presentation();
164 assert.equal(pet.recording(true),before);
165 }
166 assert.deepEqual(JSON.parse(pet.recording(true)),JSON.parse(control.recording(true)));
167 });
168
168 lines Plain Text