mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
76a371028a
The magic-word @pxpipe directive fired unreliably and added per-request body scanning to the hot path, so drop it entirely: removed the directive module + its test and unwired it from transform.ts / proxy.ts / node.ts / index.ts. transformRequest now leaves message bodies byte-identical — no per-request directive parsing. In its place, `pxpipe export <paths>` renders code/text to dense PNG pages via the same SDK primitive the proxy uses, so exported pages are byte-identical to what ships to the model. Width is locked to proxy density (--cols rejected as an unknown option, guarded by tests) to prevent drift. Added --open to preview the rendered pages plus a result footer (artifact summary + token-savings line). Tests: export.test.ts 64 passed (incl. --open parse + default coverage); full suite 589 passed / 29 files; typecheck clean.
112 lines
5.3 KiB
TypeScript
112 lines
5.3 KiB
TypeScript
// Export ⇄ proxy render alignment.
|
|
//
|
|
// Goal (the whole point of routing `pxpipe export` through the SDK): a given text must
|
|
// render to the SAME PNG bytes whether it goes through the `pxpipe export` CLI or the
|
|
// live proxy's history-image compression. Both now share one renderer:
|
|
//
|
|
// export → renderTextToImages(text, { cols, shrink, multiCol }) [library.ts, public SDK]
|
|
// proxy → textToImageBlocks(text, cols, numCols, shrinkWidth) [transform.ts, internal]
|
|
//
|
|
// textToImageBlocks is the proxy's packaging wrapper (adds base64 ImageBlocks +
|
|
// droppedCodepoints); its column-selection rule is mirrored verbatim by renderTextToImages
|
|
// (`cols < maxCols ? 1 : requestedCols`). This test pins that parity against the REAL proxy
|
|
// function (not a replica), so any future drift in either path fails CI.
|
|
//
|
|
// Alignment is guaranteed at the proxy's operating width, DENSE_CONTENT_COLS (384) — the
|
|
// export CLI's default and the proxy's hardcoded dense cap. `pxpipe export --cols N` with
|
|
// N≠384 is an intentionally-wider export and is out of scope for proxy parity.
|
|
import { describe, expect, it } from 'vitest';
|
|
import { renderTextToImages } from '../src/core/library.js';
|
|
import { textToImageBlocks } from '../src/core/transform.js';
|
|
import { DENSE_CONTENT_COLS } from '../src/core/render.js';
|
|
|
|
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Render `text` through both paths and assert full parity: page count, per-page PNG bytes,
|
|
* pixel dimensions, dropped-glyph count, and total pixel area.
|
|
*
|
|
* `multiCol`/`shrink` map the export's SDK opts onto the proxy's positional args:
|
|
* export renderTextToImages(text, { cols, shrink, multiCol })
|
|
* proxy textToImageBlocks(text, cols, numCols=multiCol, shrinkWidth=shrink)
|
|
*/
|
|
async function expectIdenticalRender(
|
|
text: string,
|
|
{ multiCol = 1, shrink = true }: { multiCol?: number; shrink?: boolean } = {},
|
|
): Promise<number> {
|
|
const exported = await renderTextToImages(text, {
|
|
cols: DENSE_CONTENT_COLS,
|
|
shrink,
|
|
multiCol,
|
|
});
|
|
const proxied = await textToImageBlocks(text, DENSE_CONTENT_COLS, multiCol, shrink);
|
|
|
|
// Same number of pages.
|
|
expect(proxied.pngs.length).toBe(exported.pages.length);
|
|
|
|
// Byte-for-byte identical PNG per page, and matching pixel dimensions.
|
|
for (let i = 0; i < exported.pages.length; i++) {
|
|
const page = exported.pages[i];
|
|
expect(
|
|
bytesEqual(proxied.pngs[i], page.png),
|
|
`page ${i}: proxy PNG (${proxied.pngs[i].length}B) must equal export PNG (${page.png.length}B)`,
|
|
).toBe(true);
|
|
expect(proxied.dims[i]).toEqual({ width: page.width, height: page.height });
|
|
}
|
|
|
|
// Aggregate accounting must agree (drives px/token cost + dropped-glyph telemetry).
|
|
expect(proxied.droppedChars).toBe(exported.droppedChars);
|
|
expect(proxied.pixels).toBe(exported.pixels);
|
|
|
|
return exported.pages.length;
|
|
}
|
|
|
|
describe('export ⇄ proxy render alignment', () => {
|
|
it('narrow short-line code → byte-identical single-col PNG (shrink wins, no wasted width)', async () => {
|
|
const code = [
|
|
'function add(a, b) {',
|
|
' return a + b;',
|
|
'}',
|
|
'',
|
|
'const xs = [1, 2, 3].map((n) => n * 2);',
|
|
'export default add;',
|
|
].join('\n');
|
|
// export's real call is multiCol:'auto'; for shrunk narrow content the SDK collapses
|
|
// to single-col (cols < maxCols ⇒ numCols=1), which is what the proxy emits. multiCol:1
|
|
// here exercises that same single-col outcome against the real proxy function.
|
|
await expectIdenticalRender(code, { multiCol: 1, shrink: true });
|
|
});
|
|
|
|
it('full-width prose (lines at the 384-col cap) → byte-identical', async () => {
|
|
const line = 'lorem ipsum dolor sit amet '.repeat(20); // > 384 display cols → fills width
|
|
const prose = Array.from({ length: 12 }, () => line).join('\n');
|
|
await expectIdenticalRender(prose, { multiCol: 1, shrink: true });
|
|
});
|
|
|
|
it('large input spanning multiple pages → every page byte-identical', async () => {
|
|
// Enough lines to overflow MAX_HEIGHT_PX and force pagination, so we pin multi-PAGE
|
|
// parity (page-boundary slicing must match between the two paths).
|
|
const big = Array.from({ length: 900 }, (_, i) => `line ${i}: const value_${i} = compute(${i});`).join('\n');
|
|
const pages = await expectIdenticalRender(big, { multiCol: 1, shrink: true });
|
|
expect(pages).toBeGreaterThan(1);
|
|
});
|
|
|
|
it('multi-column slab (shrink:false, multiCol:3) → byte-identical packed columns', async () => {
|
|
// The proxy's slab combiner packs columns with shrinkWidth:false; mirror it with the
|
|
// SDK's shrink:false + explicit multiCol to pin renderTextToPngsMultiCol parity.
|
|
const snippet = Array.from({ length: 60 }, (_, i) => `row ${i} | ${'x'.repeat(20)}`).join('\n');
|
|
await expectIdenticalRender(snippet, { multiCol: 3, shrink: false });
|
|
});
|
|
|
|
it('non-atlas codepoints drop identically on both paths', async () => {
|
|
// Glyphs outside the atlas render as blank cells and bump droppedChars; both paths must
|
|
// count them the same or px/token accounting diverges.
|
|
const text = 'plain ascii line\n日本語 ☃ →★ exotic\nmore ascii';
|
|
await expectIdenticalRender(text, { multiCol: 1, shrink: true });
|
|
});
|
|
});
|