118 lines
5 KiB
TypeScript
118 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);
|
||
|
|
});
|