Files
agents-marketplace/tests/unit/buyer-pdf-estimate.test.ts
david c552c4caa3 buyer: PDF book summarizer (/book) — chunk → map-reduce over suppliers
Adds an authenticated upload flow to the buyer web app: an operator
uploads a PDF, the server extracts + paragraph-aligned-chunks it
(~2.2k tokens), then runs a hierarchical map-reduce summarization by
paying marketplace suppliers per chunk — one on-chain escrow per map
call and per reduce node. One book upload fans out into dozens of real
mainnet escrows (the intended marketplace traffic) while producing a
genuine full-book summary.

Server module (buyer/src/pdf/): PDF extraction (text-only, rejects
scanned), token-aware chunker, supplier discovery/round-robin filtered
to capable models (kimi/deepseek/gpt; excludes qwen2.5:0.5b), pre-spend
cost estimate + wallet-floor guard, and a JobStore orchestrator that
reuses submitPrompt → findSubmittedEscrowRef → runAccept per call.
v1 serializes PostEscrow under a mutex (the shared tx builder can't pin
wallet inputs, so parallel escrows would double-spend); retries failed
calls on a different supplier then continues with gaps. 6 /v1/pdf-*
routes (multipart upload, estimate, confirm-gated start, SSE progress,
result, .md download) behind the existing session auth + page/chunk/
balance caps. SPA adds a 4-step BookSummarizer wizard.

Tests: chunk/estimate/supplier-pool/extract unit tests + orchestrator
(happy/gap/failover) + routes integration. Full buyer suite green
(181 tests); SPA build + typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:37:49 +02:00

61 lines
1.9 KiB
TypeScript

/**
* buyer-pdf-estimate.test.ts — reduce-tree call counting + cost bound math.
*/
import { describe, it, expect } from "vitest";
import { reduceCallCount, estimateJob, feeHeadroomLovelace } from "../../buyer/src/pdf/estimate.js";
import { SupplierPool } from "../../buyer/src/pdf/supplier-pool.js";
import type { PoolSupplier } from "../../buyer/src/pdf/types.js";
function sup(price: bigint): PoolSupplier {
return {
advertRef: { txHash: "a".repeat(64), index: 0 },
utxoRef: `${"a".repeat(64)}#0`,
supplierPkh: "pkh",
model: "kimi",
priceLovelace: price,
maxOutputTokens: 512,
endpointUrl: "http://x",
};
}
describe("reduceCallCount", () => {
it("matches the F-ary tree", () => {
expect(reduceCallCount(90, 8)).toBe(15); // 12 + 2 + 1
expect(reduceCallCount(8, 8)).toBe(1);
expect(reduceCallCount(64, 8)).toBe(9); // 8 + 1
expect(reduceCallCount(1, 8)).toBe(0);
expect(reduceCallCount(0, 8)).toBe(0);
expect(reduceCallCount(2, 8)).toBe(1);
});
it("is safe for degenerate fan-in", () => {
expect(reduceCallCount(10, 1)).toBe(0);
});
});
describe("estimateJob", () => {
it("bounds total with the priciest supplier and counts map+reduce", () => {
const pool = new SupplierPool([sup(2_000_000n), sup(3_000_000n)]);
const est = estimateJob(90, 8, pool);
expect(est.mapCalls).toBe(90);
expect(est.reduceCalls).toBe(15);
expect(est.totalCalls).toBe(105);
expect(est.perCallMaxLovelace).toBe("3000000");
expect(est.totalLovelace).toBe((3_000_000n * 105n).toString());
expect(est.totalAp3x).toBe("315.00");
});
it("handles a single-chunk job (no reduce)", () => {
const pool = new SupplierPool([sup(2_000_000n)]);
const est = estimateJob(1, 8, pool);
expect(est.totalCalls).toBe(1);
});
});
describe("feeHeadroomLovelace", () => {
it("scales with call count plus a fixed buffer", () => {
expect(feeHeadroomLovelace(10)).toBe(12_000_000n);
});
});