Files
pxpipe/tests/tracker.test.ts
T
teamchong b9c438b275 host: wire FileTracker (Node) + JsonLogTracker (Worker) into per-request flow
Node host now appends one JSONL event per request to
~/.pixelpipe/events.jsonl (override with --events-file or PIXELPIPE_LOG).
- Size-based rotation at 100 MB (old file → events.jsonl.1).
- Sync writes — overhead is ~1ms, dwarfed by upstream latency.
- Failures degrade silently: a broken disk disables the tracker but
  never breaks a request.
- Flushed and fsync'd on SIGINT/SIGTERM so the last batch isn't lost.
- Opt out via --no-track or PIXELPIPE_TRACK=0.

Worker host uses JsonLogTracker with console.log; Cloudflare ingests
this as Workers Logs, and Logpush can ship it to R2/S3 as JSONL —
same shape Node writes locally, so analysis tooling works on either.

Console log line on both hosts now also includes tokens/cache_read
inline (e.g. 'compressed 4799ch → 2img/12343B tokens=1234+56
cache_read=800') so cache effectiveness is visible at a glance even
without parsing the JSONL.

Unknown-tag canary fires a stderr warning when info.unknownStaticTags
is non-empty — surfaces Claude Code releases that introduce new
dynamic tags before they tank the cache hit rate.

Help text and HANDOFF will be updated in the next commit; for now the
new flag drops the need for --exclude-dynamic-system-prompt-sections.

Tests: +5 (toTrackEvent flattening, JsonLogTracker sink behavior, error
swallowing, minimal-event handling, noop).
2026-05-18 16:10:07 -04:00

111 lines
3.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { toTrackEvent, JsonLogTracker, noopTracker, type TrackEvent } from '../src/core/tracker.js';
import type { ProxyEvent } from '../src/core/proxy.js';
describe('toTrackEvent', () => {
it('flattens ProxyEvent + TransformInfo + Usage into a single record', () => {
const ev: ProxyEvent = {
method: 'POST',
path: '/v1/messages',
status: 200,
durationMs: 1234,
firstByteMs: 200,
info: {
compressed: true,
origChars: 16000,
imageCount: 1,
imageBytes: 2103,
staticChars: 14000,
dynamicChars: 500,
dynamicBlockCount: 2,
systemSha8: 'a1b2c3d4',
claudeMdSha8: 'cafebabe',
firstUserSha8: 'deadbeef',
unknownStaticTags: ['recent_files'],
env: {
cwd: '/Users/me/code/pp',
isGitRepo: true,
gitBranch: 'main',
platform: 'darwin',
osVersion: 'Darwin 25.0.0',
today: '2026-05-18',
},
},
usage: {
input_tokens: 42,
output_tokens: 7,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 100,
},
};
const out = toTrackEvent(ev);
// Spot-check every category of field made it across with the right
// snake_case names.
expect(out.method).toBe('POST');
expect(out.path).toBe('/v1/messages');
expect(out.status).toBe(200);
expect(out.duration_ms).toBe(1234);
expect(out.first_byte_ms).toBe(200);
expect(out.compressed).toBe(true);
expect(out.orig_chars).toBe(16000);
expect(out.static_chars).toBe(14000);
expect(out.dynamic_chars).toBe(500);
expect(out.dynamic_block_count).toBe(2);
expect(out.system_sha8).toBe('a1b2c3d4');
expect(out.claude_md_sha8).toBe('cafebabe');
expect(out.first_user_sha8).toBe('deadbeef');
expect(out.unknown_static_tags).toEqual(['recent_files']);
expect(out.cwd).toBe('/Users/me/code/pp');
expect(out.git_branch).toBe('main');
expect(out.is_git_repo).toBe(true);
expect(out.input_tokens).toBe(42);
expect(out.cache_read_tokens).toBe(100);
expect(out.cache_create_tokens).toBe(0);
// ts is ISO8601
expect(out.ts).toMatch(/^\d{4}-\d{2}-\d{2}T/);
});
it('handles a minimal ProxyEvent (no info, no usage) without throwing', () => {
const out = toTrackEvent({
method: 'GET',
path: '/health',
status: 200,
durationMs: 4,
});
expect(out.method).toBe('GET');
expect(out.compressed).toBeUndefined();
expect(out.cwd).toBeUndefined();
expect(out.input_tokens).toBeUndefined();
});
});
describe('JsonLogTracker', () => {
it('emits one JSON line per event to the sink', () => {
const lines: string[] = [];
const t = new JsonLogTracker((s) => lines.push(s));
t.emit({ ts: '2026-05-18T00:00:00Z', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 1 } as TrackEvent);
t.emit({ ts: '2026-05-18T00:00:01Z', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 2 } as TrackEvent);
expect(lines).toHaveLength(2);
const parsed = lines.map((l) => JSON.parse(l));
expect(parsed[0].duration_ms).toBe(1);
expect(parsed[1].duration_ms).toBe(2);
});
it('swallows sink errors — tracker must never break a request', () => {
const t = new JsonLogTracker(() => {
throw new Error('disk full');
});
expect(() =>
t.emit({ ts: 'x', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 1 } as TrackEvent),
).not.toThrow();
});
});
describe('noopTracker', () => {
it('discards events silently', () => {
expect(() =>
noopTracker.emit({ ts: 'x', method: 'POST', path: '/v1/messages', status: 200, duration_ms: 1 } as TrackEvent),
).not.toThrow();
});
});