mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
f199640bdc
- 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.
173 lines
5.8 KiB
TypeScript
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();
|
|
});
|
|
});
|