import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, it } from "node:test"; import { alertSearchCursorsDb } from "../../src/db/alertSearchCursorsSqlite.js"; import { closeDatabase } from "../../src/db/sqlite.js"; import { partnerPageHasTiedBeforeDate, type FetchLike, } from "../../src/alertSearch/client.js"; import { runAlertSearchCycle } from "../../src/alertSearch/cycle.js"; import { ALERT_SEARCH_PAGE_SIZE } from "../../src/alertSearch/types.js"; const USER = "did:ethr:0xcycleuser"; const JWT = "delegated.jwt.token"; const ENDORSER_BASE = "https://api.endorser.ch"; const PARTNER_BASE = "https://partner-api.endorser.ch"; function ulid(n: number): string { return `01H${String(n).padStart(23, "0")}`; } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }); } function emptyEndorserBody(overrides?: Record) { return { data: { claims: [], personalPlanContributions: [], trackedPlanUpdates: [], trackedPlanClaims: [], plansNearby: [], ...overrides, }, }; } function emptyPartnerBody(overrides?: Record) { return { data: { profilesNearby: [], ...overrides } }; } function claimRows(from: number, count: number) { return Array.from({ length: count }, (_, i) => ({ id: ulid(from + i), issuedAt: "2026-01-01T00:00:00Z", issuer: "did:ethr:0x1", })); } function planRows(from: number, count: number) { return Array.from({ length: count }, (_, i) => ({ handleId: `plan-${from + i}`, jwtId: ulid(from + i), })); } function profileRows( count: number, updatedAt: (index: number) => string ) { return Array.from({ length: count }, (_, i) => ({ issuerDid: `did:ethr:0x${i}`, description: "p", updatedAt: updatedAt(i), rowId: i + 1, })); } function captureFetch( handler: (url: string) => Response | Promise ): { fetch: FetchLike; urls: string[] } { const urls: string[] = []; return { urls, fetch: async (url) => { urls.push(url); return handler(url); }, }; } const cycleOpts = { jwt: JWT, endorserBaseUrl: ENDORSER_BASE, partnerBaseUrl: PARTNER_BASE, }; describe("partnerPageHasTiedBeforeDate", () => { it("is false when the page is under the server limit", () => { assert.equal( partnerPageHasTiedBeforeDate({ profilesNearby: profileRows(10, () => "2026-01-01T00:00:00.000Z"), }), false ); }); it("is true when a full page shares the oldest updatedAt", () => { const tied = "2026-01-01T12:00:00.000Z"; assert.equal( partnerPageHasTiedBeforeDate({ profilesNearby: profileRows(ALERT_SEARCH_PAGE_SIZE, () => tied), }), true ); }); it("is true when two or more rows share the page minimum", () => { const rows = profileRows(ALERT_SEARCH_PAGE_SIZE, (i) => i < 2 ? "2026-01-01T00:00:00.000Z" : `2026-02-01T00:${String(i).padStart(2, "0")}:00.000Z` ); assert.equal(partnerPageHasTiedBeforeDate({ profilesNearby: rows }), true); }); it("is false when the full page has a unique oldest timestamp", () => { const rows = profileRows( ALERT_SEARCH_PAGE_SIZE, (i) => `2026-01-01T00:${String(i).padStart(2, "0")}:00.000Z` ); assert.equal(partnerPageHasTiedBeforeDate({ profilesNearby: rows }), false); }); }); describe("alertSearch cycle integration", () => { let dir: string; let previousDataDir: string | undefined; beforeEach(async () => { previousDataDir = process.env.NOTIFY_DATA_DIR; dir = await mkdtemp(path.join(tmpdir(), "alert-search-cycle-")); process.env.NOTIFY_DATA_DIR = dir; closeDatabase(); }); afterEach(async () => { closeDatabase(); if (previousDataDir === undefined) { delete process.env.NOTIFY_DATA_DIR; } else { process.env.NOTIFY_DATA_DIR = previousDataDir; } await rm(dir, { recursive: true, force: true }); }); it("first run omits cursors and persists after a complete non-empty retrieve", async () => { const cap = captureFetch((url) => { if (url.includes("/api/partner/")) { return jsonResponse( emptyPartnerBody({ profilesNearby: [ { issuerDid: "did:ethr:0x1", description: "p", updatedAt: "2026-03-01T00:00:00.000Z", rowId: 1, }, ], }) ); } return jsonResponse( emptyEndorserBody({ claims: claimRows(10, 1), trackedPlanUpdates: planRows(15, 1), }) ); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.loaded.endorserAfterId, undefined); assert.equal(cycle.loaded.partnerAfterDate, undefined); for (const url of cap.urls) { assert.equal(url.includes("afterId="), false); assert.equal(url.includes("afterDate="), false); } assert.equal(cycle.retrieved.endorser.outcome, "success"); assert.equal(cycle.retrieved.partner.outcome, "success"); assert.equal(cycle.advanced.endorserAdvanced, true); assert.equal(cycle.advanced.partnerAdvanced, true); assert.equal(cycle.advanced.endorserAfterId, ulid(15)); assert.equal(cycle.advanced.partnerAfterAt, "2026-03-01T00:00:00.000Z"); }); it("loads Endorser afterId, paginates with MAX per-bucket minima, then advances to max ULID", async () => { await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(1)); let endorserPages = 0; const cap = captureFetch((url) => { if (url.includes("/api/partner/")) { return jsonResponse(emptyPartnerBody()); } endorserPages += 1; if (endorserPages === 1) { assert.equal(url.includes(ulid(1)), true); assert.equal(url.includes("beforeId="), false); return jsonResponse( emptyEndorserBody({ claims: claimRows(200, ALERT_SEARCH_PAGE_SIZE), personalPlanContributions: claimRows(50, ALERT_SEARCH_PAGE_SIZE), }) ); } assert.equal(url.includes(`beforeId=${ulid(200)}`), true); assert.equal(url.includes(`beforeId=${ulid(50)}`), false); return jsonResponse( emptyEndorserBody({ claims: claimRows(180, 3), }) ); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.endorser.outcome, "success"); assert.equal(cycle.retrieved.endorser.pageCount, 2); assert.equal(cycle.advanced.endorserAdvanced, true); assert.equal(cycle.advanced.endorserAfterId, ulid(249)); assert.equal(cycle.advanced.partnerAdvanced, false); }); it("does not advance Endorser on empty retrieve", async () => { await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(9)); const cap = captureFetch((url) => { if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody()); return jsonResponse(emptyEndorserBody()); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.endorser.outcome, "empty"); assert.equal(cycle.advanced.endorserAdvanced, false); assert.equal(cycle.advanced.endorserAfterId, ulid(9)); }); it("does not advance Endorser when plansNearby hits 50", async () => { await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(4)); let endorserPages = 0; const cap = captureFetch((url) => { if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody()); endorserPages += 1; if (endorserPages === 1) { return jsonResponse( emptyEndorserBody({ plansNearby: planRows(300, ALERT_SEARCH_PAGE_SIZE), }) ); } return jsonResponse( emptyEndorserBody({ plansNearby: planRows(250, 2) }) ); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.endorser.outcome, "pagination"); assert.equal(cycle.retrieved.data.plansNearby.length, ALERT_SEARCH_PAGE_SIZE + 2); assert.equal(cycle.advanced.endorserAdvanced, false); assert.equal(cycle.advanced.endorserAfterId, ulid(4)); }); it("does not advance Endorser on HTTP auth failure", async () => { await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(4)); const cap = captureFetch((url) => { if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody()); return jsonResponse({ error: "Unauthorized" }, 401); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.endorser.outcome, "auth"); assert.equal(cycle.advanced.endorserAdvanced, false); assert.equal(cycle.advanced.endorserAfterId, ulid(4)); }); it("loads Partner afterDate, paginates with beforeDate, advances max updatedAt", async () => { await alertSearchCursorsDb.setPartnerAfterAt( USER, "2026-01-01T00:00:00.000Z" ); const page1 = profileRows( ALERT_SEARCH_PAGE_SIZE, (i) => `2026-02-01T00:${String(i).padStart(2, "0")}:00.000Z` ); const minTime = [...page1.map((p) => p.updatedAt)].sort()[0]; let partnerPages = 0; const cap = captureFetch((url) => { if (url.includes("/api/v2/report/")) { return jsonResponse(emptyEndorserBody()); } partnerPages += 1; if (partnerPages === 1) { assert.equal( decodeURIComponent(url).includes("afterDate=2026-01-01T00:00:00.000Z"), true ); return jsonResponse(emptyPartnerBody({ profilesNearby: page1 })); } assert.equal(decodeURIComponent(url).includes(`beforeDate=${minTime}`), true); return jsonResponse( emptyPartnerBody({ profilesNearby: [ { issuerDid: "did:ethr:0x99", description: "p", updatedAt: "2026-01-15T00:00:00.000Z", rowId: 99, }, ], }) ); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.partner.outcome, "success"); assert.equal(cycle.retrieved.partner.pageCount, 2); assert.equal(cycle.advanced.partnerAdvanced, true); assert.equal(cycle.advanced.partnerAfterAt, page1.map((p) => p.updatedAt).sort()[page1.length - 1]); }); it("does not advance Partner on empty retrieve", async () => { await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-01-01T00:00:00.000Z"); const cap = captureFetch((url) => { if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody()); return jsonResponse(emptyEndorserBody()); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.partner.outcome, "empty"); assert.equal(cycle.advanced.partnerAdvanced, false); assert.equal(cycle.advanced.partnerAfterAt, "2026-01-01T00:00:00.000Z"); }); it("does not advance Partner when a full page has tied oldest updatedAt", async () => { await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-01-01T00:00:00.000Z"); const tied = "2026-04-01T00:00:00.000Z"; const cap = captureFetch((url) => { if (url.includes("/api/v2/report/")) { return jsonResponse(emptyEndorserBody()); } return jsonResponse( emptyPartnerBody({ profilesNearby: profileRows(ALERT_SEARCH_PAGE_SIZE, () => tied), }) ); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.partner.outcome, "pagination"); assert.match(cycle.retrieved.partner.message ?? "", /updatedAt/); assert.equal( cycle.retrieved.data.profilesNearby.length, ALERT_SEARCH_PAGE_SIZE ); assert.equal(cycle.advanced.partnerAdvanced, false); assert.equal(cycle.advanced.partnerAfterAt, "2026-01-01T00:00:00.000Z"); }); it("A: both success → both advance", async () => { const cap = captureFetch((url) => { if (url.includes("/api/partner/")) { return jsonResponse( emptyPartnerBody({ profilesNearby: [ { issuerDid: "did:ethr:0x1", description: "p", updatedAt: "2026-05-01T00:00:00.000Z", }, ], }) ); } return jsonResponse(emptyEndorserBody({ claims: claimRows(20, 1) })); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.advanced.endorserAdvanced, true); assert.equal(cycle.advanced.partnerAdvanced, true); assert.equal(cycle.advanced.endorserAfterId, ulid(20)); assert.equal(cycle.advanced.partnerAfterAt, "2026-05-01T00:00:00.000Z"); }); it("B: Endorser success + Partner failure → Endorser only", async () => { const cap = captureFetch((url) => { if (url.includes("/api/partner/")) { return jsonResponse({ error: "Unauthorized" }, 401); } return jsonResponse(emptyEndorserBody({ claims: claimRows(21, 1) })); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.endorser.outcome, "success"); assert.equal(cycle.retrieved.partner.outcome, "auth"); assert.equal(cycle.advanced.endorserAdvanced, true); assert.equal(cycle.advanced.partnerAdvanced, false); assert.equal(cycle.advanced.endorserAfterId, ulid(21)); assert.equal(cycle.advanced.partnerAfterAt, null); }); it("C: Endorser failure + Partner success → Partner only", async () => { const cap = captureFetch((url) => { if (url.includes("/api/partner/")) { return jsonResponse( emptyPartnerBody({ profilesNearby: [ { issuerDid: "did:ethr:0x1", description: "p", updatedAt: "2026-06-01T00:00:00.000Z", }, ], }) ); } return jsonResponse({ error: "fail" }, 500); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.endorser.outcome, "http"); assert.equal(cycle.retrieved.partner.outcome, "success"); assert.equal(cycle.advanced.endorserAdvanced, false); assert.equal(cycle.advanced.partnerAdvanced, true); assert.equal(cycle.advanced.endorserAfterId, null); assert.equal(cycle.advanced.partnerAfterAt, "2026-06-01T00:00:00.000Z"); }); it("D: both fail → neither advances", async () => { await alertSearchCursorsDb.setEndorserAfterId(USER, ulid(3)); await alertSearchCursorsDb.setPartnerAfterAt(USER, "2026-01-01T00:00:00.000Z"); const cap = captureFetch(() => jsonResponse({ error: "down" }, 503)); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.advanced.endorserAdvanced, false); assert.equal(cycle.advanced.partnerAdvanced, false); assert.equal(cycle.advanced.endorserAfterId, ulid(3)); assert.equal(cycle.advanced.partnerAfterAt, "2026-01-01T00:00:00.000Z"); }); it("E: Partner incomplete + Endorser success → Endorser only", async () => { const tied = "2026-07-01T00:00:00.000Z"; const cap = captureFetch((url) => { if (url.includes("/api/partner/")) { return jsonResponse( emptyPartnerBody({ profilesNearby: profileRows(ALERT_SEARCH_PAGE_SIZE, () => tied), }) ); } return jsonResponse(emptyEndorserBody({ claims: claimRows(22, 1) })); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: cap.fetch }, }); assert.equal(cycle.retrieved.endorser.outcome, "success"); assert.equal(cycle.retrieved.partner.outcome, "pagination"); assert.equal(cycle.advanced.endorserAdvanced, true); assert.equal(cycle.advanced.partnerAdvanced, false); assert.equal(cycle.advanced.endorserAfterId, ulid(22)); }); it("persists cursors across close/reopen and sends them on the next retrieve", async () => { const first = captureFetch((url) => { if (url.includes("/api/partner/")) { return jsonResponse( emptyPartnerBody({ profilesNearby: [ { issuerDid: "did:ethr:0x1", description: "p", updatedAt: "2026-08-01T00:00:00.000Z", }, ], }) ); } return jsonResponse(emptyEndorserBody({ claims: claimRows(30, 1) })); }); await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: first.fetch }, }); closeDatabase(); const second = captureFetch((url) => { if (url.includes("/api/partner/")) return jsonResponse(emptyPartnerBody()); return jsonResponse(emptyEndorserBody()); }); const cycle = await runAlertSearchCycle(USER, { ...cycleOpts, config: { fetch: second.fetch }, }); assert.equal(cycle.loaded.endorserAfterId, ulid(30)); assert.equal(cycle.loaded.partnerAfterDate, "2026-08-01T00:00:00.000Z"); const endorserUrl = second.urls.find((u) => u.includes("/api/v2/report/")) ?? ""; const partnerUrl = second.urls.find((u) => u.includes("/api/partner/")) ?? ""; assert.equal(endorserUrl.includes(ulid(30)), true); assert.equal( decodeURIComponent(partnerUrl).includes("afterDate=2026-08-01T00:00:00.000Z"), true ); assert.equal(partnerUrl.includes("afterId=0"), false); }); });