WFDASH-WEBAPP/scripts/smoke/upload-attachment.ts
SJCoetz 7df7a8850f feat(item-history): add getItemHistory service with 5 PB queries
Co-authored-by: Orca <help@stably.ai>
2026-07-29 14:51:58 +02:00

117 lines
5 KiB
TypeScript

/**
* Smoke test for the new `attachments.upload_for_quote` MCP tool.
*
* Verifies:
* 1. We can create a quote, then upload 3 attachments of different types
* (PDF stub, CSV, plain text) to it via the new MCP tool.
* 2. The `quote_attachments` collection has 3 rows linked to the new
* quote, with the right kinds (pdf / other / other) and content_types.
* 3. The quote's `has_attachments` flag is set to true.
* 4. The PB file URL for each attachment is reachable.
*
* Run with: tsx scripts/smoke/upload-attachment.ts
*/
import { createQuoteFromEmail, findQuoteByMessageId } from "../../src/lib/server/quotes-service";
import { getAdminPocketBase } from "../../src/lib/server/pocketbase-admin";
import { uploadAttachmentForQuote } from "../../src/lib/server/quotes-service";
const RUN_ID = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const MSG_ID = `<attach-smoke-${RUN_ID}@example.com>`;
// Tiny valid PDF (1 page, "Hello"). Generated by hand.
const PDF_BYTES = Buffer.from(
"%PDF-1.1\n1 0 obj<<>>endobj\nxref\n0 1\n0000000000 65535 f\ntrailer<<>>\n%%EOF\n",
"utf-8"
);
const PDF_B64 = PDF_BYTES.toString("base64");
const CSV_TEXT = "sku,description,qty\nBOLT-M20,M20 bolt,50\nNUT-M12,M12 nut,7\n";
const CSV_B64 = Buffer.from(CSV_TEXT, "utf-8").toString("base64");
const TXT_TEXT = "Plain text note from the smoke test.\nLine 2.\n";
const TXT_B64 = Buffer.from(TXT_TEXT, "utf-8").toString("base64");
async function main() {
// ── 1. Create a quote (no inline attachments — they'll go via the new tool)
const r = await createQuoteFromEmail({
from_email: `attach-test-${RUN_ID}@example.com`,
from_name: "Attachment Test",
subject: `Testing attachments ${RUN_ID}`,
body_plain: "Please find 3 attachments: a PDF, a CSV, and a text note.",
message_id: MSG_ID,
received_at: new Date().toISOString(),
extracted_items: [{ description: "Test line item", quantity: 1 }],
// attachments field accepted for back-compat but ignored
attachments: [],
});
if (r.deduped) throw new Error("FAIL — first call should not be deduped");
console.log(`Created ${r.quote_number} (id=${r.quote.id}) with ${r.items} item(s).`);
if (r.attachments !== 0) throw new Error(`FAIL — base create should report 0 attachments, got ${r.attachments}`);
// ── 2. Upload 3 attachments via the new MCP-path function.
const u1 = (await uploadAttachmentForQuote({
quoteId: r.quote.id,
filename: "rfq.pdf",
contentType: "application/pdf",
size: PDF_BYTES.length,
base64: PDF_B64,
messageId: MSG_ID,
})) as Record<string, string>;
console.log(` + ${u1.filename} kind=${u1.kind} size=${u1.size_bytes} id=${u1.id}`);
const u2 = (await uploadAttachmentForQuote({
quoteId: r.quote.id,
filename: "line-items.csv",
contentType: "text/csv",
base64: CSV_B64,
messageId: MSG_ID,
})) as Record<string, string>;
console.log(` + ${u2.filename} kind=${u2.kind} size=${u2.size_bytes} id=${u2.id}`);
const u3 = (await uploadAttachmentForQuote({
quoteId: r.quote.id,
filename: "notes.txt",
contentType: "text/plain",
base64: TXT_B64,
messageId: MSG_ID,
})) as Record<string, string>;
console.log(` + ${u3.filename} kind=${u3.kind} size=${u3.size_bytes} id=${u3.id}`);
// ── 3. Verify the rows are linked to the quote with the right kinds.
if (u1.kind !== "pdf") throw new Error(`FAIL — u1.kind should be 'pdf', got '${u1.kind}'`);
if (u2.kind !== "other") throw new Error(`FAIL — u2.kind should be 'other' (csv), got '${u2.kind}'`);
if (u3.kind !== "other") throw new Error(`FAIL — u3.kind should be 'other' (txt), got '${u3.kind}'`);
if (u1.quote !== r.quote.id) throw new Error(`FAIL — u1.quote should be ${r.quote.id}, got ${u1.quote}`);
// ── 4. Verify the quote's has_attachments flag is set.
const pb = await getAdminPocketBase();
const refetched = await pb.collection("quotes").getOne(r.quote.id);
if (!refetched.has_attachments) {
throw new Error("FAIL — quote.has_attachments should be true after uploads");
}
// ── 5. Verify the PB file URLs are reachable.
const baseUrl =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io";
for (const a of [u1, u2, u3]) {
const url = `${baseUrl.replace(/\/$/, "")}/api/files/quote_attachments/${a.id}/${a.file}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`FAIL — attachment ${a.filename} fetch returned ${res.status}`);
}
console.log(` ✓ ${a.filename} HTTP ${res.status} ${(await res.arrayBuffer()).byteLength} bytes`);
}
// ── 6. Verify dedup still works for the same message_id
const looked = await findQuoteByMessageId(MSG_ID);
if (!looked || looked.id !== r.quote.id) {
throw new Error("FAIL — message_id lookup should still return the same quote");
}
console.log(`\nALL ATTACHMENT SMOKE CHECKS PASSED.`);
console.log(`Quote ${r.quote_number} (id=${r.quote.id}) has 3 attachments.`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});