/**
* buyer-archive.test.ts — coverage for the response archive (SQLite +
* filesystem) added in M2-AUDIT.
*
* Two layers:
* 1. ResponseArchive in isolation (persistChat / persistTts / list / get
* / readRequest / readResponse, plus filesystem layout).
* 2. The /v1/responses* HTTP endpoints exposed by createApp when an
* archive is injected (and the 503 fallback when it isn't).
*
* Uses a per-test temp dir so tests are independent and don't pollute each
* other's SQLite WAL. better-sqlite3 is synchronous — no flakey waits.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync, existsSync, readFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { ResponseArchive } from "../../buyer/src/db/archive.js";
import { createApp } from "../../buyer/src/server.js";
let dir: string;
let archive: ResponseArchive;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "archive-test-"));
archive = new ResponseArchive(dir);
});
afterEach(() => {
archive.close();
rmSync(dir, { recursive: true, force: true });
});
const SAMPLE_REF = `${"a".repeat(64)}#0`;
function sampleReceipt() {
return {
prompt_hash: "b".repeat(64),
response_hash: "c".repeat(64),
model: "qwen2.5:0.5b",
prompt_tokens: 12,
completion_tokens: 48,
wallclock_ms: 3200,
supplier_pkh: "d".repeat(56),
escrow_ref: SAMPLE_REF,
};
}
// ─── ResponseArchive in isolation ────────────────────────────────────────
describe("ResponseArchive — persistChat", () => {
it("writes request.json + response.json + DB row", () => {
const row = archive.persistChat({
escrow_ref: SAMPLE_REF,
posted_at: 1_700_000_000_000,
capability_id: "llm.text.generate.v1",
supplier_pkh: "d".repeat(56),
model: "qwen2.5:0.5b",
payment_lovelace: "2000000",
request_messages: [{ role: "user", content: "hi" }],
response_canonical: '{"role":"assistant","content":"hello there"}',
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
expect(row.escrow_ref).toBe(SAMPLE_REF);
expect(row.response_content_type).toBe("application/json");
expect(row.response_byte_length).toBe(44);
// Filesystem:
/_0/{request.json,response.json}
const recDir = join(dir, `${"a".repeat(64)}_0`);
expect(existsSync(join(recDir, "request.json"))).toBe(true);
expect(existsSync(join(recDir, "response.json"))).toBe(true);
// Response file matches what we passed in (byte-exact — important for
// sha256 verification against receipt.response_hash).
expect(readFileSync(join(recDir, "response.json"), "utf8"))
.toBe('{"role":"assistant","content":"hello there"}');
});
it("INSERT OR REPLACE on duplicate escrow_ref", () => {
const baseParams = {
escrow_ref: SAMPLE_REF,
posted_at: 1_700_000_000_000,
capability_id: "llm.text.generate.v1",
supplier_pkh: "d".repeat(56),
model: "qwen2.5:0.5b",
payment_lovelace: "2000000",
request_messages: [{ role: "user", content: "first" }],
response_canonical: '{"role":"assistant","content":"first"}',
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
};
archive.persistChat(baseParams);
archive.persistChat({
...baseParams,
response_canonical: '{"role":"assistant","content":"second"}',
});
expect(archive.list(10).length).toBe(1);
const r = archive.readResponse(SAMPLE_REF);
expect(r?.bytes.toString("utf8")).toBe('{"role":"assistant","content":"second"}');
});
});
describe("ResponseArchive — persistTts", () => {
it("writes audio bytes verbatim under response. + DB row", () => {
const audio = new Uint8Array([0xff, 0xfb, 0x90, 0x44, 0x00, 0x00]);
const row = archive.persistTts({
escrow_ref: SAMPLE_REF,
posted_at: 1_700_000_000_000,
capability_id: "audio.synthesize.piper.v1",
supplier_pkh: "d".repeat(56),
model: "piper-en-us-multi",
payment_lovelace: "2000000",
request_envelope: { text: "hello", voice: "nova", format: "mp3", speed: 1.0 },
response_audio: audio,
response_content_type: "audio/mpeg",
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
expect(row.response_filename).toBe("response.mp3");
expect(row.response_content_type).toBe("audio/mpeg");
expect(row.response_byte_length).toBe(6);
const r = archive.readResponse(SAMPLE_REF);
expect(r?.bytes).toEqual(Buffer.from(audio));
expect(r?.contentType).toBe("audio/mpeg");
expect(r?.filename).toBe("response.mp3");
});
it("picks the right extension for various audio content-types", () => {
const cases: Array<[string, string]> = [
["audio/mpeg", "response.mp3"],
["audio/wav", "response.wav"],
["audio/x-wav", "response.wav"],
["audio/opus", "response.opus"],
["audio/flac", "response.flac"],
["audio/aac", "response.aac"],
];
for (const [ct, expectedFilename] of cases) {
const ref = `${ct.replace(/\W/g, "0").padEnd(64, "0").slice(0, 64)}#0`;
const row = archive.persistTts({
escrow_ref: ref,
posted_at: 0,
capability_id: "audio.synthesize.piper.v1",
supplier_pkh: "d".repeat(56),
model: "x",
payment_lovelace: "1",
request_envelope: {},
response_audio: new Uint8Array([1, 2, 3, 4]),
response_content_type: ct,
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
expect(row.response_filename).toBe(expectedFilename);
}
});
});
describe("ResponseArchive — read helpers", () => {
it("get() returns null on miss", () => {
expect(archive.get("not-a-real-ref")).toBeNull();
expect(archive.readRequest("not-a-real-ref")).toBeNull();
expect(archive.readResponse("not-a-real-ref")).toBeNull();
});
it("list() returns newest first", async () => {
archive.persistChat({
escrow_ref: `${"a".repeat(64)}#0`,
posted_at: 0,
capability_id: "llm.text.generate.v1",
supplier_pkh: "d".repeat(56),
model: "x",
payment_lovelace: "1",
request_messages: [],
response_canonical: "first",
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
// Force a different completed_at (better-sqlite3 + Date.now gives ms
// resolution; sleep a tick).
await new Promise((r) => setTimeout(r, 5));
archive.persistChat({
escrow_ref: `${"b".repeat(64)}#0`,
posted_at: 0,
capability_id: "llm.text.generate.v1",
supplier_pkh: "d".repeat(56),
model: "x",
payment_lovelace: "1",
request_messages: [],
response_canonical: "second",
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
const rows = archive.list(10);
expect(rows.length).toBe(2);
expect(rows[0].escrow_ref.startsWith("b")).toBe(true);
expect(rows[1].escrow_ref.startsWith("a")).toBe(true);
});
it("respects limit", () => {
for (let i = 0; i < 5; i++) {
archive.persistChat({
escrow_ref: `${i.toString().repeat(64).slice(0, 64)}#0`,
posted_at: 0,
capability_id: "llm.text.generate.v1",
supplier_pkh: "d".repeat(56),
model: "x",
payment_lovelace: "1",
request_messages: [],
response_canonical: "x",
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
}
expect(archive.list(3).length).toBe(3);
});
});
// ─── HTTP endpoints (server.ts wiring) ───────────────────────────────────
describe("GET /v1/responses* — disabled (no archive injected)", () => {
it("503 service_unavailable on list endpoint", async () => {
const app = createApp({});
const { default: request } = await import("supertest");
const res = await request(app).get("/v1/responses");
expect(res.status).toBe(503);
expect(res.body.error).toBe("service_unavailable");
});
it("503 also on detail / artefact endpoints", async () => {
const app = createApp({});
const { default: request } = await import("supertest");
const ref = SAMPLE_REF;
for (const path of [
`/v1/responses/${ref}`,
`/v1/responses/${ref}/request`,
`/v1/responses/${ref}/response`,
]) {
const res = await request(app).get(path);
expect(res.status).toBe(503);
}
});
});
describe("GET /v1/responses* — happy paths", () => {
it("list returns persisted rows newest-first with parsed receipt", async () => {
archive.persistChat({
escrow_ref: SAMPLE_REF,
posted_at: 1_700_000_000_000,
capability_id: "llm.text.generate.v1",
supplier_pkh: "d".repeat(56),
model: "qwen2.5:0.5b",
payment_lovelace: "2000000",
request_messages: [{ role: "user", content: "hi" }],
response_canonical: '{"role":"assistant","content":"hello"}',
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
const app = createApp({ archive });
const { default: request } = await import("supertest");
const res = await request(app).get("/v1/responses");
expect(res.status).toBe(200);
expect(Array.isArray(res.body.responses)).toBe(true);
expect(res.body.responses.length).toBe(1);
const r = res.body.responses[0];
expect(r.escrow_ref).toBe(SAMPLE_REF);
expect(r.receipt).toMatchObject({ model: "qwen2.5:0.5b", supplier_pkh: "d".repeat(56) });
expect(r.receipt_signature).toBe("f".repeat(128));
});
it("detail returns metadata + receipt for a known escrow", async () => {
archive.persistTts({
escrow_ref: SAMPLE_REF,
posted_at: 1_700_000_000_000,
capability_id: "audio.synthesize.piper.v1",
supplier_pkh: "d".repeat(56),
model: "piper-en-us-multi",
payment_lovelace: "2000000",
request_envelope: { text: "hi", voice: "nova", format: "mp3", speed: 1.0 },
response_audio: new Uint8Array([1, 2, 3, 4]),
response_content_type: "audio/mpeg",
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
const app = createApp({ archive });
const { default: request } = await import("supertest");
const res = await request(app).get(`/v1/responses/${SAMPLE_REF.replace("#", "_")}`);
expect(res.status).toBe(200);
expect(res.body.escrow_ref).toBe(SAMPLE_REF);
expect(res.body.response_filename).toBe("response.mp3");
expect(res.body.response_content_type).toBe("audio/mpeg");
expect(res.body.response_byte_length).toBe(4);
});
it("404 on unknown escrow_ref", async () => {
const app = createApp({ archive });
const { default: request } = await import("supertest");
const ghost = `${"e".repeat(64)}#3`;
const res = await request(app).get(`/v1/responses/${ghost.replace("#", "_")}`);
expect(res.status).toBe(404);
expect(res.body.error).toBe("not_found");
});
it("/request returns the JSON envelope as application/json", async () => {
archive.persistChat({
escrow_ref: SAMPLE_REF,
posted_at: 0,
capability_id: "llm.text.generate.v1",
supplier_pkh: "d".repeat(56),
model: "x",
payment_lovelace: "1",
request_messages: [{ role: "user", content: "hello world" }],
response_canonical: '{"role":"assistant","content":"hi"}',
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
const app = createApp({ archive });
const { default: request } = await import("supertest");
const res = await request(app).get(`/v1/responses/${SAMPLE_REF.replace("#", "_")}/request`);
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toMatch(/application\/json/);
expect(JSON.parse(res.text).messages[0].content).toBe("hello world");
});
it("/response returns audio bytes with upstream Content-Type", async () => {
const audio = new Uint8Array([0xff, 0xfb, 0x90, 0x44, 0x00, 0x00, 0x00, 0x01]);
archive.persistTts({
escrow_ref: SAMPLE_REF,
posted_at: 0,
capability_id: "audio.synthesize.piper.v1",
supplier_pkh: "d".repeat(56),
model: "x",
payment_lovelace: "1",
request_envelope: {},
response_audio: audio,
response_content_type: "audio/mpeg",
receipt: sampleReceipt(),
receipt_signature: "f".repeat(128),
});
const app = createApp({ archive });
const { default: request } = await import("supertest");
const res = await request(app).get(`/v1/responses/${SAMPLE_REF.replace("#", "_")}/response`);
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toMatch(/audio\/mpeg/);
expect(res.body).toEqual(Buffer.from(audio));
expect(res.headers["content-disposition"]).toContain("response.mp3");
});
});