返回 CodeWhale
pet-runtime-incremental.test.mjs
根目录 / pet / tests / pet-runtime-incremental.test.mjs
1 import test from 'node:test';
2 import assert from 'node:assert/strict';
3 import { importTrace, privacyEvent, redact } from '../dist/core/ingest.js';
4 import { CodewhaleRuntimeTrace, observeRuntimeRequests } from '../dist/core/codewhale.js';
5 import { compilePetTelemetry } from '../dist/core/pet-telemetry.js';
6
7 const epoch = Date.parse('2026-09-12T00:00:00Z');
8 const stamp = ms => new Date(epoch + ms).toISOString();
9 const row = (seq, event, ms, payload, turn_id = 'turn-a') => ({ seq, event, thread_id: 'fixture', turn_id, timestamp: stamp(ms), payload });
10 const read = rows => importTrace(JSON.stringify(rows), 'fixture', { privacy: 'metadata' })[0];
11 const incremental = (maxEvents = 250_000, maxBytes = 64 * 1024 * 1024) => new CodewhaleRuntimeTrace('fixture', maxEvents, event => privacyEvent(event, 'metadata'), maxBytes);
12 const recent = (trace, now) => {
13 const observed = observeRuntimeRequests(trace, epoch + now);
14 return compilePetTelemetry(observed.events, observed.duration, Math.floor(now / 400) - 6, epoch - Date.parse(observed.originTime));
15 };
16
17 test('pruned incremental Runtime input matches full imports through origin changes, open requests, automatic consent and late failures', () => {
18 const source = incremental(), all = [];
19 const steps = [
20 [12_000, [row(1, 'thread.updated', 10_000, {}), row(2, 'item.completed', 5000, { item: { id: 'old', kind: 'tool_call', started_at: stamp(1000), ended_at: stamp(3000), status: 'failed' }, tool: 'bash' })]],
21 [24_000, [row(3, 'user_input.required', 20_000, { id: 'human', request: { questions: ['fixture-private-question'] } }), row(4, 'item.started', 22_000, { item: { id: 'long', kind: 'tool_call', started_at: stamp(0), status: 'running' }, tool: 'bash' })]],
22 [40_000, [row(5, 'thread.updated', 39_000, {})]],
23 [60_000, [row(6, 'item.completed', 59_000, { item: { id: 'long', status: 'failed', ended_at: stamp(10_000), detail: 'fixture-private-result' } })]],
24 [72_000, [row(7, 'user_input.answered', 70_000, { input_id: 'human', answers: ['fixture-private-answer'] }), row(8, 'approval.required', 71_000, { approval_id: 'automatic' }), row(9, 'approval.decided', 71_100, { approval_id: 'automatic', auto: true })]],
25 [96_000, [row(10, 'user_input.required', 94_000, { id: 'turn-close' }), row(11, 'turn.completed', 95_000, { turn: { status: 'completed' } })]],
26 ];
27 let prior, priorText;
28 for (const [now, rows] of steps) {
29 all.push(...rows); source.append(redact(rows)); source.prune(epoch + now - 16_000);
30 if (prior) assert.equal(JSON.stringify(prior), priorText, 'A later receipt cannot mutate an already captured snapshot');
31 const snapshot = source.snapshot();
32 assert.deepEqual(recent(snapshot, now), recent(read(all), now));
33 assert.doesNotMatch(JSON.stringify(snapshot), /fixture-private/);
34 if (now === 12_000) assert.equal(snapshot.events.find(e => e.id === 'old').attributes['whalesong.error_onset_ms'], 4000, 'An older negative relative error timestamp is normalized, not dropped');
35 if (now === 40_000) assert.ok(snapshot.events.some(e => e.id === 'long' && e.openEnded));
36 prior = snapshot; priorText = JSON.stringify(snapshot);
37 }
38 source.prune(epoch + 120_000);
39 assert.equal(source.retainedEvents, 0); assert.equal(source.retainedBytes, 0);
40 });
41
42 test('retained Runtime limits reject excess unfinished work and count terminal mutations without retaining payloads', () => {
43 const source = incremental(2, 8192);
44 source.append([row(1, 'user_input.required', 0, { id: 'a' }), row(2, 'user_input.required', 1000, { id: 'b' })]);
45 source.prune(epoch + 100_000); assert.equal(source.retainedEvents, 2);
46 const before = source.retainedBytes;
47 // Completion must remain possible at the event limit; it updates the existing
48 // lifetime rather than consuming another event slot.
49 source.append([row(3, 'user_input.answered', 100_000, { input_id: 'a', answers: ['private'.repeat(10_000)] })]);
50 source.prune(epoch + 120_000); assert.equal(source.retainedEvents, 1);
51 assert.ok(source.retainedBytes > 0 && source.retainedBytes < before);
52 source.append([row(4, 'user_input.required', 120_000, { id: 'c' })]);
53 assert.throws(() => source.append([row(5, 'user_input.required', 120_001, { id: 'd' })]), /event limit/);
54 assert.equal(source.retainedEvents, 2);
55 const bounded = incremental(100, 128);
56 assert.throws(() => bounded.append([row(1, 'thread.updated', 0, {})]), /retained input limit/);
57 assert.equal(bounded.retainedEvents, 0); assert.equal(bounded.retainedBytes, 0);
58 });
59
59 lines Plain Text