Files
pxpipe/tests/export.test.ts
T
Danil Silantyev 028f6a7cd8 fix(vision): patch-based Anthropic image-token model + doc sync (#27)
Anthropic bills images by a 28-px patch grid (⌈w/28⌉×⌈h/28⌉ after tier downscale), not (w·h)/750 — the old fit overcharged large images ~4–5%.

- new src/core/anthropic-vision.ts: exact patch + downscale model matching
  Anthropic's reference (incl. extreme aspect ratios); wired into the unified
  visionTokensForModel router.
- gate counts real 28-px patches; 10% bias renamed ANTHROPIC_GATE_MARGIN, gate-only.
- removed ANTHROPIC_PIXELS_PER_TOKEN / IMAGE_COST_SAFETY_MARGIN; cols 313 → 312
  (slab = 1568 px cap).
- docs (README / RENDER_SIZING / TRANSFORM_INFO / audit) synced to shipped
  geometry (1568×728, 312 cols) and the patch billing model.
2026-07-18 19:55:30 -04:00

655 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import {
matchGlob,
shouldIncludeFile,
parseExportArgv,
computeTokenReport,
buildPromptText,
sourceShortHash,
runExportCore,
exportImageTokens,
DEFAULT_EXPORT_MODEL,
DEFAULT_EXPORT_COLS,
} from '../src/core/export.js';
import { extractFactSheetTokensAllPages, extractFactSheetTokens } from '../src/core/factsheet.js';
import { DENSE_CONTENT_CHARS_PER_IMAGE } from '../src/core/render.js';
import { patchTokens } from '../src/core/anthropic-vision.js';
// ---------------------------------------------------------------------------
// Temp-dir helpers
// ---------------------------------------------------------------------------
let tmpDir = '';
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pxpipe-export-test-'));
});
afterEach(() => {
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
});
// ---------------------------------------------------------------------------
// matchGlob
// ---------------------------------------------------------------------------
describe('matchGlob', () => {
it('matches a bare extension glob against basename', () => {
expect(matchGlob('*.ts', 'src/foo.ts')).toBe(true);
expect(matchGlob('*.ts', 'src/foo.js')).toBe(false);
expect(matchGlob('*.ts', 'foo.ts')).toBe(true);
});
it('handles ** for any-depth matching', () => {
expect(matchGlob('**/*.ts', 'src/core/foo.ts')).toBe(true);
expect(matchGlob('**/*.ts', 'foo.ts')).toBe(true);
expect(matchGlob('**/*.ts', 'src/core/foo.js')).toBe(false);
});
it('matches a directory prefix pattern', () => {
expect(matchGlob('src/**', 'src/core/foo.ts')).toBe(true);
expect(matchGlob('src/**', 'tests/foo.ts')).toBe(false);
});
it('matches literal basename (no separator = basename match anywhere)', () => {
expect(matchGlob('README.md', 'README.md')).toBe(true);
// no separator in pattern → matches the basename at any depth
expect(matchGlob('README.md', 'docs/README.md')).toBe(true);
});
it('literal path with separator matches only that path', () => {
expect(matchGlob('docs/README.md', 'docs/README.md')).toBe(true);
expect(matchGlob('docs/README.md', 'other/README.md')).toBe(false);
});
it('handles ? wildcard', () => {
expect(matchGlob('foo?.ts', 'foo1.ts')).toBe(true);
expect(matchGlob('foo?.ts', 'foo.ts')).toBe(false);
expect(matchGlob('foo?.ts', 'foo12.ts')).toBe(false);
});
it('handles node_modules exclude pattern', () => {
expect(matchGlob('node_modules/**', 'node_modules/x/y.js')).toBe(true);
expect(matchGlob('node_modules/**', 'src/foo.ts')).toBe(false);
});
it('escapes regex special chars in patterns', () => {
// dot in extension should match literal dot, not any char
expect(matchGlob('*.ts', 'foots')).toBe(false);
});
});
// ---------------------------------------------------------------------------
// shouldIncludeFile
// ---------------------------------------------------------------------------
describe('shouldIncludeFile', () => {
it('includes everything when no patterns', () => {
expect(shouldIncludeFile('src/foo.ts', [], [])).toBe(true);
expect(shouldIncludeFile('README.md', [], [])).toBe(true);
});
it('excludes files matching an exclude pattern', () => {
expect(shouldIncludeFile('node_modules/x.js', [], ['node_modules/**'])).toBe(false);
expect(shouldIncludeFile('src/x.js', [], ['node_modules/**'])).toBe(true);
});
it('includes only files matching an include pattern', () => {
expect(shouldIncludeFile('src/foo.ts', ['*.ts'], [])).toBe(true);
expect(shouldIncludeFile('src/foo.js', ['*.ts'], [])).toBe(false);
});
it('exclude takes priority over include', () => {
expect(shouldIncludeFile('dist/foo.ts', ['*.ts'], ['dist/**'])).toBe(false);
});
it('multiple include patterns — any match passes', () => {
expect(shouldIncludeFile('README.md', ['*.ts', '*.md'], [])).toBe(true);
expect(shouldIncludeFile('foo.txt', ['*.ts', '*.md'], [])).toBe(false);
});
});
// ---------------------------------------------------------------------------
// parseExportArgv
// ---------------------------------------------------------------------------
describe('parseExportArgv', () => {
it('returns help when -h is passed', () => {
expect(parseExportArgv(['-h'])).toEqual({ kind: 'help' });
expect(parseExportArgv(['--help'])).toEqual({ kind: 'help' });
});
it('returns defaults when argv is empty', () => {
const result = parseExportArgv([], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
const p = result.parsed;
expect(p.targets).toEqual([]);
expect(p.include).toEqual([]);
expect(p.exclude).toEqual([]);
expect(p.git).toBe(false);
expect(p.diff).toBeUndefined();
expect(p.stdin).toBe(false);
expect(p.cols).toBe(DEFAULT_EXPORT_COLS);
expect(p.out).toBe('/tmp');
expect(p.model).toBe(DEFAULT_EXPORT_MODEL);
expect(p.json).toBe(false);
expect(p.open).toBe(false);
});
it('parses --include and --exclude (repeatable)', () => {
const result = parseExportArgv(
['--include', '*.ts', '--include', '*.md', '--exclude', 'node_modules/**'],
'/tmp',
);
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.include).toEqual(['*.ts', '*.md']);
expect(result.parsed.exclude).toEqual(['node_modules/**']);
});
it('parses --include= and --exclude= forms', () => {
const result = parseExportArgv(['--include=*.ts', '--exclude=dist/**'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.include).toEqual(['*.ts']);
expect(result.parsed.exclude).toEqual(['dist/**']);
});
it('parses --git flag', () => {
const result = parseExportArgv(['--git'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.git).toBe(true);
});
it('parses --diff <ref>', () => {
const result = parseExportArgv(['--diff', 'HEAD~3'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.diff).toBe('HEAD~3');
});
it('parses --stdin flag', () => {
const result = parseExportArgv(['--stdin'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.stdin).toBe(true);
});
// Export has NO width knob: cols is locked to the proxy's density (DENSE_CONTENT_COLS)
// so the rendered pages are byte-identical to what the proxy ships to the model.
// --cols (in either form) is therefore rejected as an unknown option.
it('rejects --cols <n> (no width knob; locked to proxy density)', () => {
const result = parseExportArgv(['--cols', '200'], '/tmp');
expect(result.kind).toBe('error');
if (result.kind !== 'error') return;
expect(result.message).toContain('unknown option');
});
it('rejects --cols=<n> form', () => {
const result = parseExportArgv(['--cols=128'], '/tmp');
expect(result.kind).toBe('error');
if (result.kind !== 'error') return;
expect(result.message).toContain('unknown option');
});
it('parses --out', () => {
const result = parseExportArgv(['--out', '/custom/dir'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.out).toBe('/custom/dir');
});
it('parses --model', () => {
const result = parseExportArgv(['--model', 'gpt-4o'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.model).toBe('gpt-4o');
});
it('parses --json flag', () => {
const result = parseExportArgv(['--json'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.json).toBe(true);
});
it('parses --open flag', () => {
const result = parseExportArgv(['--open'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.open).toBe(true);
});
it('collects positional targets', () => {
const result = parseExportArgv(['src/', 'README.md'], '/tmp');
expect(result.kind).toBe('opts');
if (result.kind !== 'opts') return;
expect(result.parsed.targets).toEqual(['src/', 'README.md']);
});
it('returns error for unknown flag', () => {
const result = parseExportArgv(['--unknown-flag'], '/tmp');
expect(result.kind).toBe('error');
if (result.kind !== 'error') return;
expect(result.message).toContain('unknown option');
});
});
// ---------------------------------------------------------------------------
// computeTokenReport
// ---------------------------------------------------------------------------
describe('computeTokenReport', () => {
it('computes text tokens as sourceChars / REPORT_CHARS_PER_TOKEN', () => {
const text = 'a'.repeat(3700);
const report = computeTokenReport(text, DEFAULT_EXPORT_COLS, DEFAULT_EXPORT_MODEL);
// 3700 / 3.7 = 1000 tokens
expect(report.textTokens).toBe(1000);
});
it('returns positive imageTokens', () => {
const text = 'hello world\n'.repeat(100);
const report = computeTokenReport(text, DEFAULT_EXPORT_COLS, DEFAULT_EXPORT_MODEL);
expect(report.imageTokens).toBeGreaterThan(0);
});
it('images are cheaper than text for dense code content', () => {
// Dense code: many short lines, high chars/token ratio
const text = 'const x = 1;\n'.repeat(3000); // ~42k chars
const report = computeTokenReport(text, DEFAULT_EXPORT_COLS, DEFAULT_EXPORT_MODEL);
// For dense code, image should be cheaper (positive % saved)
expect(report.textTokens).toBeGreaterThan(0);
expect(report.imageTokens).toBeGreaterThan(0);
});
it('percentSaved is (textTokens - imageTokens) / textTokens * 100 (rounded to 1 dp)', () => {
const text = 'x'.repeat(37000); // 37000 chars = ~10000 text tokens
const report = computeTokenReport(text, DEFAULT_EXPORT_COLS, DEFAULT_EXPORT_MODEL);
const expected =
Math.round(((report.textTokens - report.imageTokens) / report.textTokens) * 1000) / 10;
expect(report.percentSaved).toBe(expected);
});
it('factsheetItemCount is >= 0', () => {
const report = computeTokenReport('hello world', DEFAULT_EXPORT_COLS, DEFAULT_EXPORT_MODEL);
expect(report.factsheetItemCount).toBeGreaterThanOrEqual(0);
});
it('factsheetDropped is 0 for short inputs with few identifiers', () => {
const report = computeTokenReport('hello world const x = 1;', DEFAULT_EXPORT_COLS, DEFAULT_EXPORT_MODEL);
expect(report.factsheetDropped).toBe(0);
});
});
// ---------------------------------------------------------------------------
// buildPromptText
// ---------------------------------------------------------------------------
describe('buildPromptText', () => {
it('mentions the correct page count', () => {
const text = buildPromptText(3, '', []);
expect(text).toContain('3 images');
expect(text).toContain('page-001.png');
expect(text).toContain('page-003.png');
});
it('singular for 1 page', () => {
const text = buildPromptText(1, '', []);
expect(text).toContain('1 image');
expect(text).not.toContain('1 images');
});
it('includes the factsheet content', () => {
const factsheet = '[Exact identifiers … foo/bar.ts · abc123]';
const text = buildPromptText(2, factsheet, []);
expect(text).toContain(factsheet);
});
it('includes file list when files are provided', () => {
const text = buildPromptText(2, '', ['src/foo.ts', 'src/bar.ts']);
expect(text).toContain('src/foo.ts');
expect(text).toContain('src/bar.ts');
});
it('shows (none) when factsheet is empty', () => {
const text = buildPromptText(1, '', []);
expect(text).toContain('(none)');
});
});
// ---------------------------------------------------------------------------
// sourceShortHash
// ---------------------------------------------------------------------------
describe('sourceShortHash', () => {
it('returns an 8-char hex string', () => {
const h = sourceShortHash('hello world');
expect(h).toMatch(/^[0-9a-f]{8}$/);
});
it('is deterministic', () => {
const h1 = sourceShortHash('some text');
const h2 = sourceShortHash('some text');
expect(h1).toBe(h2);
});
it('differs for different content', () => {
const h1 = sourceShortHash('text a');
const h2 = sourceShortHash('text b');
expect(h1).not.toBe(h2);
});
});
// ---------------------------------------------------------------------------
// runExportCore — integration: writes expected files to temp dir
// ---------------------------------------------------------------------------
describe('runExportCore integration', () => {
it('produces page-001.png, factsheet.txt, manifest.json, prompt.txt for short text', async () => {
const sourceText = 'const answer = 42;\n// hello world\n'.repeat(20);
const result = await runExportCore(sourceText, {
sourceFiles: ['test.ts'],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
const filenames = result.artifacts.map((a) => a.filename);
expect(filenames).toContain('page-001.png');
expect(filenames).toContain('factsheet.txt');
expect(filenames).toContain('manifest.json');
expect(filenames).toContain('prompt.txt');
// Write artifacts and verify they exist on disk
for (const artifact of result.artifacts) {
fs.writeFileSync(path.join(tmpDir, artifact.filename), artifact.data);
}
expect(fs.existsSync(path.join(tmpDir, 'page-001.png'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'factsheet.txt'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'manifest.json'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'prompt.txt'))).toBe(true);
});
it('manifest.json has expected structure', async () => {
const sourceText = 'const x = 1;\n'.repeat(50);
const result = await runExportCore(sourceText, {
sourceFiles: ['src/x.ts'],
cols: 200,
model: 'gpt-4o',
});
const manifestArtifact = result.artifacts.find((a) => a.filename === 'manifest.json');
expect(manifestArtifact).toBeDefined();
if (!manifestArtifact) return;
const manifest = JSON.parse(new TextDecoder().decode(manifestArtifact.data)) as Record<string, unknown>;
expect(manifest['sourceChars']).toBe(sourceText.length);
expect(manifest['cols']).toBe(200);
expect(manifest['model']).toBe('gpt-4o');
expect(Array.isArray(manifest['files'])).toBe(true);
expect(Array.isArray(manifest['pages'])).toBe(true);
expect(typeof manifest['generatedAt']).toBe('string');
expect(typeof manifest['tokenReport']).toBe('object');
const tokenReport = manifest['tokenReport'] as Record<string, unknown>;
expect(typeof tokenReport['textTokens']).toBe('number');
expect(typeof tokenReport['imageTokens']).toBe('number');
expect(typeof tokenReport['percentSaved']).toBe('number');
expect(typeof tokenReport['factsheetItemCount']).toBe('number');
expect(typeof tokenReport['factsheetDropped']).toBe('number');
});
it('manifest.pages has correct shape for each rendered page', async () => {
const sourceText = 'hello pxpipe export\n'.repeat(30);
const result = await runExportCore(sourceText, {
sourceFiles: [],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
for (const page of result.manifest.pages) {
expect(page.filename).toMatch(/^page-\d{3}\.png$/);
expect(page.bytes).toBeGreaterThan(0);
expect(page.width).toBeGreaterThan(0);
expect(page.height).toBeGreaterThan(0);
}
});
it('PNG artifacts are valid PNG files (start with PNG magic bytes)', async () => {
const sourceText = 'export function foo() { return 1; }\n'.repeat(10);
const result = await runExportCore(sourceText, {
sourceFiles: [],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
const pngs = result.artifacts.filter((a) => a.filename.endsWith('.png'));
expect(pngs.length).toBeGreaterThan(0);
for (const png of pngs) {
// PNG magic bytes: 0x89 0x50 0x4E 0x47
expect(png.data[0]).toBe(0x89);
expect(png.data[1]).toBe(0x50); // 'P'
expect(png.data[2]).toBe(0x4e); // 'N'
expect(png.data[3]).toBe(0x47); // 'G'
}
});
it('empty source text still produces factsheet.txt, manifest.json, prompt.txt', async () => {
const result = await runExportCore('', {
sourceFiles: [],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
// The renderer may produce 0 or 1 page for empty text; we do not constrain it.
const filenames = result.artifacts.map((a) => a.filename);
expect(filenames).toContain('factsheet.txt');
expect(filenames).toContain('manifest.json');
expect(filenames).toContain('prompt.txt');
});
it('tokenReport.textTokens is approximately sourceChars / REPORT_CHARS_PER_TOKEN', async () => {
const sourceText = 'x'.repeat(3700);
const result = await runExportCore(sourceText, {
sourceFiles: [],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
// 3700 / 3.7 = 1000
expect(result.manifest.tokenReport.textTokens).toBe(1000);
});
it('includes source file list in manifest and prompt', async () => {
const files = ['src/foo.ts', 'src/bar.ts'];
const result = await runExportCore('const x = 1;\n', {
sourceFiles: files,
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
expect(result.manifest.files).toEqual(files);
const promptArtifact = result.artifacts.find((a) => a.filename === 'prompt.txt');
const promptText = new TextDecoder().decode(promptArtifact?.data ?? new Uint8Array());
expect(promptText).toContain('src/foo.ts');
expect(promptText).toContain('src/bar.ts');
});
it('manifest.tokenReport uses factsheetItemCount and factsheetDropped (not factsheetTokenCount)', async () => {
const result = await runExportCore('const x = 1;\n'.repeat(20), {
sourceFiles: [],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
const tr = result.manifest.tokenReport;
expect(typeof tr.factsheetItemCount).toBe('number');
expect(typeof tr.factsheetDropped).toBe('number');
expect(tr.factsheetItemCount).toBeGreaterThanOrEqual(0);
expect(tr.factsheetDropped).toBeGreaterThanOrEqual(0);
// Old field must not exist
expect((tr as Record<string, unknown>)['factsheetTokenCount']).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// BLOCKING 1 — Claude vs GPT image token routing
// ---------------------------------------------------------------------------
describe('exportImageTokens model routing', () => {
// Current full dense export page: width = 2*PAD_X + 312*CELL_W = 1568 px,
// height = MAX_HEIGHT_PX = 728 px. Fits both Anthropic tiers unchanged, so the
// cost is the raw 28-px patch count: ⌈1568/28⌉×⌈728/28⌉ = 56×26 = 1456.
const W = 1568;
const H = 728;
it('returns Anthropic-formula tokens for claude-sonnet-4-5', () => {
expect(exportImageTokens('claude-sonnet-4-5', W, H)).toBe(patchTokens(W, H));
expect(exportImageTokens('claude-sonnet-4-5', W, H)).toBe(1456);
});
it('returns Anthropic-formula tokens for any claude-* model', () => {
expect(exportImageTokens('claude-opus-4', W, H)).toBe(patchTokens(W, H));
expect(exportImageTokens('claude-haiku-3-5', W, H)).toBe(patchTokens(W, H));
});
it('returns Anthropic-formula tokens when model includes "anthropic"', () => {
expect(exportImageTokens('anthropic/claude-3-5-sonnet', W, H)).toBe(patchTokens(W, H));
});
it('routes gpt-4o to the OpenAI tile formula, not the Anthropic patch formula', () => {
const gpTokens = exportImageTokens('gpt-4o', W, H);
const claudeTokens = exportImageTokens('claude-sonnet-4-5', W, H);
expect(gpTokens).toBeGreaterThan(0);
// Different pricing model → different number (they happen to be close at this
// size, but must not be computed by the same formula).
expect(gpTokens).not.toBe(claudeTokens);
});
it('uses measured Grok pixel pricing instead of the GPT fallback', () => {
expect(exportImageTokens('grok-4.5', 768, 360)).toBe(Math.ceil((768 * 360) / 1000));
expect(exportImageTokens('grok-4.5', 768, 360)).not.toBe(
exportImageTokens('gpt-4o', 768, 360),
);
});
it('caps a huge Claude image at the tier visual-token budget (patch downscale)', () => {
// The old ceil(w*h/750*1.10) formula ignored Anthropic's downscale and grossly
// overcharged large images (a 1928² page "cost" ~5464 when the API bills ≤1568
// on the standard tier / 4761 on high-res). The patch model honors the cap.
expect(exportImageTokens('claude-sonnet-4-5', 4000, 4000)).toBeLessThanOrEqual(1568);
expect(exportImageTokens('claude-fable-5', 4000, 4000)).toBeLessThanOrEqual(4784);
});
it('computeTokenReport uses Anthropic formula for default claude model', () => {
// A single dense page: text so long that 1 page is estimated.
// The report should reflect the higher Anthropic cost.
const text = 'const x = 1;\n'.repeat(100);
const claudeReport = computeTokenReport(text, DEFAULT_EXPORT_COLS, 'claude-sonnet-4-5');
const gptReport = computeTokenReport(text, DEFAULT_EXPORT_COLS, 'gpt-4o');
// Claude reports more image tokens than GPT for the same content
expect(claudeReport.imageTokens).toBeGreaterThan(gptReport.imageTokens);
});
});
// ---------------------------------------------------------------------------
// BLOCKING 2 — Factsheet coverage across pages (>262,144-char input)
// ---------------------------------------------------------------------------
describe('extractFactSheetTokensAllPages multi-page coverage', () => {
it('covers a unique identifier that appears only after 262,144 chars', () => {
// Build a text where the interesting identifier lives past MAX_SCAN (262,144)
const BEFORE = 'x'.repeat(270_000); // plain chars, no extractable tokens
const AFTER = '/very/deep/unique/path/that/only/lives/in/later/page.ts';
const fullText = BEFORE + '\n' + AFTER;
expect(fullText.length).toBeGreaterThan(262_144);
const { kept } = extractFactSheetTokensAllPages(fullText, DENSE_CONTENT_CHARS_PER_IMAGE);
// The path should be present because it lives on a later page
expect(kept.some((t) => t.includes('unique/path') || t.includes('later/page.ts'))).toBe(true);
});
it('returns the same result as extractFactSheetTokens for short text (< MAX_SCAN)', () => {
const text = 'const x = require("/some/path/foo.ts"); // v1.2.3 sha=abc1234def';
const single = extractFactSheetTokens(text);
const { kept } = extractFactSheetTokensAllPages(text, DENSE_CONTENT_CHARS_PER_IMAGE);
// The kept set should be a subset of (or equal to) the single-call result
// (page-by-page dedup is deterministic, though ordering/budget may differ)
expect(kept.length).toBeGreaterThan(0);
for (const t of kept) {
expect(single.includes(t) || text.includes(t)).toBe(true);
}
});
it('droppedItems in runExportCore is zero for short inputs', async () => {
const result = await runExportCore('const x = 1;\n'.repeat(5), {
sourceFiles: [],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
expect(result.manifest.tokenReport.factsheetDropped).toBe(0);
});
});
// ---------------------------------------------------------------------------
// BLOCKING 3 — Dropped identifier count is surfaced in the report
// ---------------------------------------------------------------------------
describe('factsheet dropped count surfacing', () => {
it('buildPromptText mentions dropped identifiers when droppedItems > 0', () => {
const text = buildPromptText(1, '[factsheet content]', [], 7);
expect(text).toContain('7 identifier(s)');
// Should NOT claim factsheet is authoritative for ALL strings when items were dropped
expect(text).not.toContain('authoritative source of truth for all exact strings');
});
it('buildPromptText does NOT mention drop count when droppedItems is 0', () => {
const text = buildPromptText(1, '[factsheet content]', [], 0);
// With no drops, factsheet IS the authoritative source of truth
expect(text).toContain('authoritative source of truth');
expect(text).not.toContain('identifier(s) were extracted');
});
it('buildPromptText defaults to droppedItems=0 (no extra note in normal case)', () => {
const text = buildPromptText(1, '[factsheet content]', []);
expect(text).not.toContain('identifier(s) were extracted');
expect(text).toContain('authoritative source of truth');
});
it('factsheetDropped is a non-negative number in runExportCore output', async () => {
// Verify the field exists and is well-typed; small inputs have 0 drops.
const result = await runExportCore('const x = 1;\nconst y = 2;\n'.repeat(10), {
sourceFiles: [],
cols: DEFAULT_EXPORT_COLS,
model: DEFAULT_EXPORT_MODEL,
});
const tr = result.manifest.tokenReport;
expect(tr.factsheetDropped).toBeGreaterThanOrEqual(0);
expect(tr.factsheetItemCount + tr.factsheetDropped).toBeGreaterThanOrEqual(tr.factsheetItemCount);
});
it('extractFactSheetTokensAllPages reports dropped > 0 when unique identifiers across pages exceed the factsheet budget', () => {
// Place 60 unique paths on "page 1" and 60 different unique paths on "page 2".
// Each page is < 92,160 chars so extractFactSheetTokens won't hit MAX_SCAN.
// Each page contributes up to 64 tokens; cross-page merge gives >64 → some dropped.
const page1 = Array.from({ length: 60 }, (_, i) =>
`/alpha/module-${i}/lib/component-${i}.ts`,
).join('\n');
// Pad page 1 to exactly one page boundary (DENSE_CONTENT_CHARS_PER_IMAGE chars)
const pad1 = ' '.repeat(Math.max(0, DENSE_CONTENT_CHARS_PER_IMAGE - page1.length));
const page2 = Array.from({ length: 60 }, (_, i) =>
`/beta/service-${i}/util/helper-${i}.ts`,
).join('\n');
const fullText = page1 + pad1 + page2;
const { kept, dropped } = extractFactSheetTokensAllPages(fullText, DENSE_CONTENT_CHARS_PER_IMAGE);
// At most MAX_TOKENS kept; with many from page 1 + some from page 2 the total should exceed the factsheet budget
expect(kept.length).toBeLessThanOrEqual(96);
expect(dropped).toBeGreaterThan(0);
});
});