Files
pxpipe/tests/tracker.test.ts
T
teamchong f199640bdc ship Unifont @ 10px multilingual rendering + 400 diagnosis fixes
- Switch font: JetBrains Mono @ 15px → Unifont 16.0.04 @ 10px. Sparse
  atlas with double-width EAW dispatch for CJK. Full BMP profile
  (35,501 glyphs) covers Latin/Cyrillic/Greek/Hebrew/Arabic/CJK/Hangul
  plus Letterlike, Misc Technical, Block Elements, Geometric Shapes,
  Misc Symbols, Dingbats, Enclosed Alphanumerics, all box-drawing.
- Fix cell-height bug (was hardcoded 9; now probed from font metrics
  via Math.ceil(maxAscent+maxDescent)). Previously clipped the top
  2 rows off every CJK glyph since the original Unifont ship.
- Expand tabs to spaces with 4-column tab stops, EAW-aware. Was the
  silent-drop source for 99.6% of dropped chars in production.
- Add dropped_codepoints_top telemetry (top 20 per event, only emitted
  when drops > 0) so future coverage gaps surface in events.jsonl.
- Raise minToolResultChars 2000→5000 and minReminderChars 1000→2000
  to stop net-losing tokens on small compressions, per ~2,500 tok/img
  empirical measurement (N=33 cold-miss events).
- Fix dashboard per-image-cost formula: was pngBytes/375 (~190 tok),
  now imageCount * 2500 (the measured empirical rate). Reduces the
  inflated "tokens saved" headline to honest numbers.
- Schema-stub fix in compressSchemas path: preserveSchemaShell()
  retains type / properties / required / enum / oneOf / anyOf / $ref /
  numeric constraints recursively (depth 20), strips only descriptions
  and metadata. Prevents 400 errors on first-tool-call requests.
- Capture full gzipped 4xx body + per-event req_body_sha8 for upstream
  error diagnosis. Sidecar to ~/.pixelpipe/4xx-bodies/ when over 32 KB
  inline cap. Workers-safe via CompressionStream + Web Crypto.
- scripts/restart.sh: pnpm-aware one-command kill+rebuild+start with
  graceful SIGTERM→SIGKILL escalation, build-failure abort, port-in-use
  precheck, --no-build flag for fast iteration. Shell-tested.
- Bundle 1.58 MB gzipped on Node. 91/91 vitest + 4/4 shell tests.
2026-05-18 21:28:25 -04:00

173 lines
5.8 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();
});
});
describe('toTrackEvent body-sample mapping', () => {
const baseEv = {
method: 'POST',
path: '/v1/messages',
status: 400,
durationMs: 100,
};
it('inlines small gzipped bodies as req_body_sample_b64', () => {
const small = new Uint8Array(64).fill(0x1f);
const out = toTrackEvent({
...baseEv,
reqBodySha8: 'deadbeef',
reqBodyGz: small,
} as ProxyEvent);
expect(out.req_body_sha8).toBe('deadbeef');
expect(out.req_body_sample_b64).toBeDefined();
expect(out.req_body_sample_b64!.length).toBeLessThanOrEqual(128);
expect(out.req_body_sample_path).toBeUndefined();
});
it('drops oversized gzipped bodies that lack a sidecar path', () => {
// 40 KiB of gz bytes → ~53 KiB base64 → exceeds the 32 KiB cap, and we
// didn't pre-write a sidecar → must silently drop the inline body
// (Workers path). req_body_sha8 still lands.
const big = new Uint8Array(40 * 1024).fill(0x42);
const out = toTrackEvent({
...baseEv,
reqBodySha8: 'cafef00d',
reqBodyGz: big,
} as ProxyEvent);
expect(out.req_body_sha8).toBe('cafef00d');
expect(out.req_body_sample_b64).toBeUndefined();
expect(out.req_body_sample_path).toBeUndefined();
});
it('prefers reqBodySamplePath over inlining when both are set', () => {
const someGz = new Uint8Array(64).fill(0x1f);
const out = toTrackEvent({
...baseEv,
reqBodySha8: 'feedface',
reqBodyGz: someGz,
reqBodySamplePath: '/tmp/4xx-bodies/x.json.gz',
} as ProxyEvent);
// Sidecar path wins; we don't double-encode inline.
expect(out.req_body_sample_path).toBe('/tmp/4xx-bodies/x.json.gz');
expect(out.req_body_sample_b64).toBeUndefined();
});
it('sets req_body_sha8 on 2xx events too (correlation across statuses)', () => {
const out = toTrackEvent({
...baseEv,
status: 200,
reqBodySha8: '01234567',
} as ProxyEvent);
expect(out.req_body_sha8).toBe('01234567');
// No body sample fields for 2xx.
expect(out.req_body_sample_b64).toBeUndefined();
expect(out.req_body_sample_path).toBeUndefined();
});
});