/** * 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 = ``; // 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; 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; 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; 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); });