import { strict as assert } from "node:assert"; import { test } from "node:test"; import { loadWithCurrentTrainFallback, mergeCurrentTrainFallback, RequestCoordinator, } from "../lib/requestCoordinator"; import { carryForwardOfficialPositionMetadata } from "../lib/currentTrainMetadata"; const deferred = () => { let resolve!: (value: T) => void; let reject!: (error: unknown) => void; const promise = new Promise((promiseResolve, promiseReject) => { resolve = promiseResolve; reject = promiseReject; }); return { promise, resolve, reject }; }; test("same-source polling skips a second in-flight request", async () => { const coordinator = new RequestCoordinator(); const firstResult = deferred(); let calls = 0; const first = coordinator.start("primary", () => { calls += 1; return firstResult.promise; }); if (!first) throw new Error("first request was not started"); const duplicate = coordinator.start("primary", () => { calls += 1; return "duplicate"; }); assert.equal(duplicate, null); assert.equal(calls, 0, "operations start on the next microtask"); firstResult.resolve("first"); assert.equal(await first.promise, "first"); await Promise.resolve(); assert.equal(calls, 1); const next = coordinator.start("primary", () => { calls += 1; return "next"; }); if (!next) throw new Error("next polling request was not started"); assert.equal(await next.promise, "next"); assert.equal(calls, 2); }); test("source switch aborts the old request and only the newest response commits", async () => { const coordinator = new RequestCoordinator(); const oldResult = deferred(); const commits: string[] = []; const oldRequest = coordinator.start("primary", () => oldResult.promise); if (!oldRequest) throw new Error("old request was not started"); const newRequest = coordinator.start("mock", () => "new"); if (!newRequest) throw new Error("new request was not started"); assert.equal(oldRequest.signal.aborted, true); const newResult = await newRequest.promise; if (coordinator.isCurrent(newRequest.token)) commits.push(newResult); oldResult.resolve("old"); const oldValue = await oldRequest.promise; if (coordinator.isCurrent(oldRequest.token)) commits.push(oldValue); assert.deepEqual(commits, ["new"]); assert.equal(coordinator.isCurrent(oldRequest.token), false); }); test("dispose aborts an in-flight request and rejects late commits", async () => { const coordinator = new RequestCoordinator(); const result = deferred(); const request = coordinator.start("primary", () => result.promise); if (!request) throw new Error("request was not started"); coordinator.dispose(); assert.equal(request.signal.aborted, true); result.resolve("late"); await request.promise; assert.equal(coordinator.isCurrent(request.token), false); }); test("cancel aborts polling without disposing the coordinator", async () => { const coordinator = new RequestCoordinator(); const result = deferred(); const request = coordinator.start("primary", () => result.promise); if (!request) throw new Error("request was not started"); coordinator.cancel(); assert.equal(request.signal.aborted, true); result.resolve("late"); await request.promise; assert.equal(coordinator.isCurrent(request.token), false); const next = coordinator.start("primary", () => "next"); if (!next) throw new Error("next request was not started"); assert.equal(await next.promise, "next"); }); test("primary success does not call the fallback", async () => { let fallbackCalls = 0; const result = await loadWithCurrentTrainFallback({ signal: new AbortController().signal, primary: async () => "primary", fallback: async () => { fallbackCalls += 1; return "fallback"; }, }); assert.deepEqual(result, { data: "primary", source: "primary" }); assert.equal(fallbackCalls, 0); }); test("primary failure keeps the GAS fallback path", async () => { let fallbackCalls = 0; const result = await loadWithCurrentTrainFallback({ signal: new AbortController().signal, primary: async () => { throw new Error("primary unavailable"); }, fallback: async () => { fallbackCalls += 1; return "fallback"; }, }); assert.deepEqual(result, { data: "fallback", source: "gas_fallback" }); assert.equal(fallbackCalls, 1); }); test("fallback failure remains an error", async () => { await assert.rejects( loadWithCurrentTrainFallback({ signal: new AbortController().signal, primary: async () => { throw new Error("primary unavailable"); }, fallback: async () => { throw new Error("fallback unavailable"); }, }), /fallback unavailable/, ); }); test("aborted primary does not start the fallback", async () => { const controller = new AbortController(); controller.abort(); let fallbackCalls = 0; await assert.rejects( loadWithCurrentTrainFallback({ signal: controller.signal, primary: async () => { throw new Error("aborted"); }, fallback: async () => { fallbackCalls += 1; return "fallback"; }, }), /aborted/, ); assert.equal(fallbackCalls, 0); }); test("partial fallback preserves fields from the last complete response", () => { const previous = [ { num: "D01", Pos: "高松", Direction: 1, Line: "dosan" }, { num: "D02", Pos: "坂出", Direction: 0, Line: "yosan" }, ]; const fallback = [{ num: "D01", Pos: "多度津" }, { num: "D03", Pos: "琴平" }]; assert.deepEqual(mergeCurrentTrainFallback(fallback, previous), [ { num: "D01", Pos: "多度津", Direction: 1, Line: "dosan" }, { num: "D03", Pos: "琴平" }, ]); }); test("recording and mock requests remain separate sources", async () => { const coordinator = new RequestCoordinator(); const recording = coordinator.start("recording", () => "recorded"); if (!recording) throw new Error("recording request was not started"); assert.equal(await recording.promise, "recorded"); const mock = coordinator.start("mock", () => "mocked"); if (!mock) throw new Error("mock request was not started"); assert.equal(await mock.promise, "mocked"); }); test("ambiguous fallback keeps every previous line-specific row", () => { const previous = [ { num: "363D", Pos: "高松", PosNum: 279, Line: "yosan" }, { num: "363D", Pos: "高松", PosNum: 279, Line: "koutoku" }, ]; const fallback = [ { num: "363D", Pos: "高松" }, { num: "363D", Pos: "高松" }, ]; assert.deepEqual(mergeCurrentTrainFallback(fallback, previous), previous); }); test("line-aware fallback merges only the matching previous row", () => { const previous = [ { num: "363D", Pos: "高松", PosNum: 279, Line: "yosan", delay: 0 }, { num: "363D", Pos: "高松", PosNum: 279, Line: "koutoku", delay: 0 }, ]; const fallback = [{ num: "363D", Pos: "高松", Line: "koutoku", delay: 3 }]; assert.deepEqual(mergeCurrentTrainFallback(fallback, previous), [ { num: "363D", Pos: "高松", PosNum: 279, Line: "koutoku", delay: 3 }, ]); }); test("refresh keeps official position metadata for the unchanged position tuple", () => { const officialPosition = { positionId: "position-yosan-takamatsu", pos: "高松", line: "yosan", posNum: "279", positionType: "station", platform: "4", track: "4", note: null, enrichmentStatus: "reviewed", firstSeenAt: null, lastSeenAt: null, revision: 1, } as const; const previous = [ { num: "1007M", Pos: "高松", PosNum: "279", Line: "yosan", officialPosition, }, { num: "1007M", Pos: "高松", PosNum: "279", Line: "koutoku", }, ]; const next = [ { num: "1007M", Pos: "高松", PosNum: "279", Line: "yosan" }, { num: "1007M", Pos: "高松", PosNum: "279", Line: "koutoku" }, { num: "1007M", Pos: "坂出", PosNum: "280", Line: "yosan" }, ]; const result = carryForwardOfficialPositionMetadata(next, previous); assert.equal(result[0]?.officialPosition, officialPosition); assert.equal(result[1]?.officialPosition, undefined); assert.equal(result[2]?.officialPosition, undefined); });