import test from "node:test"; import assert from "node:assert/strict"; import os from "node:os"; import path from "node:path"; import fs from "node:fs"; import { describeQuoteBookingLinks, formatArtifactsRoot, renderFlightReportPdf, ReportValidationError } from "../src/report-pdf.js"; import type { FlightReportPayload } from "../src/types.js"; import { DFW_BLQ_2026_PROMPT_DRAFT, normalizeFlightReportRequest } from "../src/request-normalizer.js"; function samplePayload(): FlightReportPayload { return { request: normalizeFlightReportRequest(DFW_BLQ_2026_PROMPT_DRAFT).request, searchExecution: { freshSearch: true, startedAt: "2026-03-30T20:45:00Z", completedAt: "2026-03-30T21:00:00Z", artifactsRoot: "/Users/stefano/.openclaw/workspace/reports/dfw-blq-flight-report-2026-03-30-exec", notes: ["Fresh bounded search executed for this report."] }, sourceFindings: [ { source: "kayak", status: "viable", checkedAt: "2026-03-30T21:00:00Z", notes: ["Returned usable options."] } ], quotes: [ { id: "quote-1", source: "kayak", legId: "outbound", passengerGroupIds: ["pair", "solo"], bookingLink: "https://example.com/quote-1", itinerarySummary: "DFW -> BLQ via LHR", totalPriceUsd: 2847, displayPriceUsd: "$2,847" } ], rankedOptions: [ { id: "primary", title: "Best overall itinerary", quoteIds: ["quote-1"], totalPriceUsd: 2847, rationale: "Best price-to-convenience tradeoff." } ], executiveSummary: ["Best fare currently found is $2,847 total for 3 adults."], reportWarnings: [], degradedReasons: [], comparisonCurrency: "USD", generatedAt: "2026-03-30T21:00:00Z", lastCompletedPhase: "ranking" }; } function countPdfPages(outputPath: string): number { return fs.readFileSync(outputPath, "latin1").split("/Type /Page").length - 1; } test("renderFlightReportPdf writes a non-empty PDF", async () => { const outputPath = path.join(os.tmpdir(), `flight-finder-${Date.now()}.pdf`); await renderFlightReportPdf(samplePayload(), outputPath); assert.ok(fs.existsSync(outputPath)); assert.ok(fs.statSync(outputPath).size > 0); }); test("renderFlightReportPdf embeds booking links and only includes direct-airline URLs when captured", async () => { const outputPath = path.join(os.tmpdir(), `flight-finder-links-${Date.now()}.pdf`); const payload = samplePayload(); payload.quotes = [ { ...payload.quotes[0], bookingLink: "https://example.com/quote-1", directBookingUrl: null }, { id: "quote-2", source: "airline-direct", legId: "return-pair", passengerGroupIds: ["pair"], bookingLink: "https://example.com/quote-2", directBookingUrl: "https://www.britishairways.com/", airlineName: "British Airways", itinerarySummary: "BLQ -> DFW via LHR", totalPriceUsd: 1100, displayPriceUsd: "$1,100" } ]; payload.rankedOptions = [ { id: "primary", title: "Best overall itinerary", quoteIds: ["quote-1", "quote-2"], totalPriceUsd: 3947, rationale: "Best price-to-convenience tradeoff." } ]; await renderFlightReportPdf(payload, outputPath); const pdfContents = fs.readFileSync(outputPath, "latin1"); assert.match(pdfContents, /https:\/\/example\.com\/quote-1/); assert.match(pdfContents, /https:\/\/example\.com\/quote-2/); assert.match(pdfContents, /https:\/\/www\.britishairways\.com\//); assert.doesNotMatch(pdfContents, /Direct airline booking: not captured in this run[\s\S]*https:\/\/example\.com\/quote-1/); }); test("renderFlightReportPdf avoids raw absolute artifact paths and keeps page count bounded for a long report", async () => { const outputPath = path.join(os.tmpdir(), `flight-finder-long-${Date.now()}.pdf`); const payload = samplePayload(); payload.searchExecution.artifactsRoot = "/Users/stefano/.openclaw/workspace/reports/dfw-blq-flight-report-2026-03-30-fresh-185742"; payload.quotes = [ { ...payload.quotes[0], id: "quote-1", notes: [ "Observed on fresh KAYAK sweep at $794 per traveler for 3 adults.", "Skyscanner TH/USD corroborated the same Madrid pattern.", "Expedia also found a nearby fare band." ] }, { id: "quote-2", source: "kayak", legId: "return-pair", passengerGroupIds: ["pair"], bookingLink: "https://example.com/quote-2", itinerarySummary: "BLQ -> DFW via LHR", totalPriceUsd: 1316, displayPriceUsd: "$1,316 total ($658 pp x 2)", notes: [ "Cheapest valid paired return captured in the fresh KAYAK sweep.", "Skyscanner did not reproduce the exact low Jun 8 return cleanly.", "Expedia hit a bot challenge on the Jun 8 paired-return check." ] }, { id: "quote-3", source: "kayak", legId: "return-solo", passengerGroupIds: ["solo"], bookingLink: "https://example.com/quote-3", itinerarySummary: "BLQ -> DFW via MAD", totalPriceUsd: 722, displayPriceUsd: "$722 total", notes: [ "Lowest valid solo-return fare in the fresh Jul window capture.", "Skyscanner TH/USD showed the same Madrid connection at about $742.", "Expedia surfaced a cheaper Turkish option, but it was excluded." ] } ]; payload.rankedOptions = [ { id: "primary", title: "Best overall itinerary", quoteIds: ["quote-1", "quote-2", "quote-3"], totalPriceUsd: 4420, rationale: "Best price-to-convenience tradeoff." }, { id: "backup", title: "Backup itinerary", quoteIds: ["quote-1", "quote-2", "quote-3"], totalPriceUsd: 4515, rationale: "Slightly higher price but still viable." } ]; payload.executiveSummary = [ "Fresh Thailand-market sweeps shifted the outbound slightly earlier.", "Lowest valid total observed was $4,420.", "Cross-source corroboration is strongest for the outbound and solo return." ]; await renderFlightReportPdf(payload, outputPath); const pdfContents = fs.readFileSync(outputPath, "latin1"); assert.doesNotMatch(pdfContents, /\/Users\/stefano\/\.openclaw\/workspace\/reports\//); assert.ok(countPdfPages(outputPath) <= 4); }); test("renderFlightReportPdf rejects incomplete report payloads", async () => { const outputPath = path.join(os.tmpdir(), `flight-finder-incomplete-${Date.now()}.pdf`); await assert.rejects( () => renderFlightReportPdf( { ...samplePayload(), rankedOptions: [], executiveSummary: [] }, outputPath ), ReportValidationError ); }); test("renderFlightReportPdf rejects payloads that are not marked as fresh-search output", async () => { const outputPath = path.join(os.tmpdir(), `flight-finder-stale-${Date.now()}.pdf`); await assert.rejects( () => renderFlightReportPdf( { ...samplePayload(), searchExecution: { ...samplePayload().searchExecution, freshSearch: false } }, outputPath ), ReportValidationError ); }); test("describeQuoteBookingLinks includes both aggregator and airline-direct links when captured", () => { const payload = samplePayload(); const quote = { ...payload.quotes[0], airlineName: "British Airways", directBookingUrl: "https://www.britishairways.com/" }; assert.deepEqual(describeQuoteBookingLinks(quote), [ { label: "Search / book this fare", text: "Search / book this fare: $2,847 via kayak", url: "https://example.com/quote-1" }, { label: "Direct airline booking", text: "Direct airline booking: British Airways", url: "https://www.britishairways.com/" } ]); }); test("describeQuoteBookingLinks calls out missing direct-airline links explicitly", () => { const payload = samplePayload(); assert.deepEqual(describeQuoteBookingLinks(payload.quotes[0]), [ { label: "Search / book this fare", text: "Search / book this fare: $2,847 via kayak", url: "https://example.com/quote-1" }, { label: "Direct airline booking", text: "Direct airline booking: not captured in this run", url: null } ]); }); test("formatArtifactsRoot only shortens known workspace report paths", () => { assert.equal( formatArtifactsRoot("/Users/stefano/.openclaw/workspace/reports/dfw-blq-flight-report-2026-03-30-fresh-185742"), "reports/dfw-blq-flight-report-2026-03-30-fresh-185742" ); assert.equal(formatArtifactsRoot("/tmp/run-123"), "run-123"); });