"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { createOpenAIClient, extractFlightSearchRequest, } = require("../src/llmParameterExtractor"); test("uses LLM output when client is provided", async () => { const llmClient = async () => ({ departureDateWindow: { from: "2026-11-21", to: "2026-12-10", }, stayDurationDays: { minDays: 12, maxDays: 14, }, segments: [ { from: "ICN", to: "MAD" }, { from: "MAD", to: "ICN" }, ], passengers: { total: 2, byCabin: { economy: 0, premium_economy: 0, business: 2, first: 0, }, }, constraints: { sameFlightForAllPassengers: true, itineraryCount: 1, maxStops: 0, maxJourneyHours: { hours: 20, operator: "<=", }, }, tripType: "round_trip", warnings: [], missingFields: [], }); const result = await extractFlightSearchRequest("임의 입력", { now: new Date("2026-02-19T00:00:00Z"), llmClient, }); assert.equal(result.source, "llm"); assert.equal(result.params.tripType, "round_trip"); assert.equal(result.params.departureDateWindow.from, "2026-11-21"); assert.equal(result.params.constraints.maxJourneyHours.hours, 20); assert.deepEqual(result.params.missingFields, []); }); test("falls back to rule parser when LLM client fails", async () => { const result = await extractFlightSearchRequest("인천->마드리드 20시간 이하", { llmClient: async () => { throw new Error("intentional failure"); }, }); assert.equal(result.source, "rule_parser"); assert.equal(result.params.constraints.maxJourneyHours.hours, 20); assert.equal( result.params.warnings.some((warning) => warning.includes("LLM extraction fallback triggered")), true ); }); test("corrects LLM round_trip to multi_city when segments have different cities", async () => { // LLM incorrectly says round_trip for ICN→MAD / BCN→ICN (MAD ≠ BCN) const llmClient = async () => ({ departureDateWindow: { from: "2026-11-25", to: "2026-11-25" }, stayDurationDays: { minDays: 12, maxDays: 12 }, segments: [ { from: "ICN", to: "MAD" }, { from: "BCN", to: "ICN" }, ], passengers: { total: 3, byCabin: { economy: 1, premium_economy: 0, business: 2, first: 0 } }, constraints: { sameFlightForAllPassengers: true, itineraryCount: null, maxStops: 1, maxJourneyHours: { hours: 42, operator: "<" } }, tripType: "round_trip", warnings: [], missingFields: [], }); const result = await extractFlightSearchRequest("인천-마드리드, 바르셀로나-인천", { now: new Date("2026-02-20T00:00:00Z"), llmClient, }); assert.equal(result.source, "llm"); // Must be corrected to multi_city, not round_trip assert.equal(result.params.tripType, "multi_city"); assert.equal(result.params.segments.length, 2); assert.equal(result.params.segments[0].from, "ICN"); assert.equal(result.params.segments[1].from, "BCN"); }); test("keeps round_trip when segments are genuinely reversed (A→B / B→A)", async () => { const llmClient = async () => ({ departureDateWindow: { from: "2026-06-01", to: "2026-06-01" }, stayDurationDays: { minDays: 7, maxDays: 7 }, segments: [ { from: "ICN", to: "NRT" }, { from: "NRT", to: "ICN" }, ], passengers: { total: 1, byCabin: { economy: 1, premium_economy: 0, business: 0, first: 0 } }, constraints: { sameFlightForAllPassengers: true, itineraryCount: null, maxStops: null, maxJourneyHours: null }, tripType: "round_trip", warnings: [], missingFields: [], }); const result = await extractFlightSearchRequest("인천-도쿄 왕복", { now: new Date("2026-02-20T00:00:00Z"), llmClient, }); assert.equal(result.params.tripType, "round_trip"); }); test("corrects open_jaw to multi_city", async () => { const llmClient = async () => ({ departureDateWindow: { from: "2026-06-01", to: "2026-06-01" }, stayDurationDays: { minDays: 10, maxDays: 10 }, segments: [ { from: "ICN", to: "MAD" }, { from: "BCN", to: "ICN" }, ], passengers: { total: 1, byCabin: { economy: 1, premium_economy: 0, business: 0, first: 0 } }, constraints: { sameFlightForAllPassengers: true, itineraryCount: null, maxStops: null, maxJourneyHours: null }, tripType: "open_jaw", warnings: [], missingFields: [], }); const result = await extractFlightSearchRequest("인천-마드리드, 바르셀로나-인천", { now: new Date("2026-02-20T00:00:00Z"), llmClient, }); assert.equal(result.params.tripType, "multi_city"); }); test("createOpenAIClient aborts timed out requests", async () => { const llmClient = createOpenAIClient({ apiKey: "dummy-key", timeoutMs: 10, fetch: async (_url, options) => new Promise((_resolve, reject) => { options.signal.addEventListener("abort", () => { const abortError = new Error("aborted"); abortError.name = "AbortError"; reject(abortError); }); }), }); await assert.rejects( () => llmClient({ input: "임의 입력", now: new Date("2026-02-19T00:00:00Z"), }), /timed out/ ); });