Files
agents-marketplace/tests/unit/buyer-pdf-supplier-pool.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

109 lines
3.7 KiB
TypeScript

/**
* buyer-pdf-supplier-pool.test.ts — capability filtering (must drop the weak
* qwen model and non-parseable demo refs) + round-robin with failover.
*/
import { describe, it, expect } from "vitest";
import {
isCapableModel,
filterSuppliers,
SupplierPool,
} from "../../buyer/src/pdf/supplier-pool.js";
import { loadPdfCaps } from "../../buyer/src/pdf/caps.js";
import type { SupplierView } from "../../buyer/src/sdk/types.js";
const caps = loadPdfCaps({});
function view(p: Partial<SupplierView>): SupplierView {
return {
utxo_ref: `${"a".repeat(64)}#0`,
supplier_pkh: "pkh",
capability_id: "llm.text.generate.v1",
model: "kimi-k2",
max_output_tokens: 512,
max_processing_ms: 60000,
price_lovelace: "2000000",
supplier_bond_lovelace: "1000000",
buyer_bond_lovelace: "1000000",
endpoint_url: "http://supplier",
detail_uri: "",
detail_hash: "",
advertised_at: 0,
status: "active",
advert_status: "Active",
current_escrow_ref: null,
last_seen_iso: null,
created_slot: 0,
...p,
};
}
describe("isCapableModel", () => {
it("excludes deny-listed models, allows allow-listed", () => {
expect(isCapableModel("kimi-k2.6", ["kimi", "deepseek", "gpt"], ["qwen2.5:0.5b"])).toBe(true);
expect(isCapableModel("deepseek-chat", ["kimi", "deepseek", "gpt"], ["qwen2.5:0.5b"])).toBe(true);
expect(isCapableModel("qwen2.5:0.5b", ["kimi", "deepseek", "gpt"], ["qwen2.5:0.5b"])).toBe(false);
expect(isCapableModel("llama3", ["kimi", "deepseek", "gpt"], [])).toBe(false);
});
it("deny wins over allow", () => {
expect(isCapableModel("gpt-qwen2.5:0.5b", ["gpt"], ["qwen2.5:0.5b"])).toBe(false);
});
it("empty allowlist allows anything not denied", () => {
expect(isCapableModel("anything", [], ["bad"])).toBe(true);
});
});
describe("filterSuppliers", () => {
it("keeps capable suppliers and drops qwen + demo + wrong-capability", () => {
const views = [
view({ model: "kimi-k2.6", utxo_ref: `${"a".repeat(64)}#0` }),
view({ model: "deepseek-chat", utxo_ref: `${"b".repeat(64)}#1` }),
view({ model: "qwen2.5:0.5b", utxo_ref: `${"c".repeat(64)}#0` }),
view({ model: "kimi", utxo_ref: "demo:piper-tts" }), // unparseable ref
view({ model: "gpt-4o", capability_id: "audio.synthesize.piper.v1", utxo_ref: `${"d".repeat(64)}#0` }),
];
const out = filterSuppliers(views, caps);
expect(out.map((s) => s.model).sort()).toEqual(["deepseek-chat", "kimi-k2.6"]);
expect(out[0].priceLovelace).toBe(2_000_000n);
});
it("dedups by utxo_ref", () => {
const ref = `${"a".repeat(64)}#0`;
const out = filterSuppliers([view({ utxo_ref: ref }), view({ utxo_ref: ref })], caps);
expect(out).toHaveLength(1);
});
});
describe("SupplierPool.next", () => {
function pool(): SupplierPool {
return new SupplierPool(
filterSuppliers(
[
view({ model: "kimi", utxo_ref: `${"a".repeat(64)}#0` }),
view({ model: "deepseek", utxo_ref: `${"b".repeat(64)}#0` }),
view({ model: "gpt-4o", utxo_ref: `${"c".repeat(64)}#0` }),
],
caps,
),
);
}
it("round-robins through all suppliers", () => {
const p = pool();
const seen = [p.next()?.model, p.next()?.model, p.next()?.model, p.next()?.model];
expect(seen.slice(0, 3).sort()).toEqual(["deepseek", "gpt-4o", "kimi"]);
expect(seen[3]).toBe(seen[0]); // wrapped around
});
it("skips excluded suppliers and returns null when all excluded", () => {
const p = pool();
const all = p.all().map((s) => s.utxoRef);
const exclude = new Set(all);
expect(p.next(exclude)).toBeNull();
const excludeTwo = new Set(all.slice(0, 2));
expect(p.next(excludeTwo)?.utxoRef).toBe(all[2]);
});
});