186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
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,
|
|
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"
|
|
};
|
|
}
|
|
|
|
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 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
|
|
}
|
|
]);
|
|
});
|