import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { Campaign, Map as MissionMap, Mission, User } from "@/payload-types"; import { IMMINENT_REMINDER_GRACE_MS, IMMINENT_REMINDER_WINDOW_MS, getImminentReminderMissions, } from "@/lib/intelligence/imminentReminders"; let payload: Payload; const RUN = `imr-${Date.now().toString(36)}`; describe("Imminent-op reminder query", () => { let mapId: number; let campaignId: number; let authorId: number; const missionIds: number[] = []; beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); const map = (await payload.create({ collection: "maps", data: { name: `${RUN} Map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image" }, overrideAccess: true, depth: 0, })) as unknown as MissionMap; mapId = map.id; const campaign = (await payload.create({ collection: "campaigns", data: { name: `${RUN} Campaign`, summary: "Imminent reminder test campaign", status: "concept", campaignMode: "custom" }, overrideAccess: true, depth: 0, })) as unknown as Campaign; campaignId = campaign.id; const author = (await payload.create({ collection: "users", data: { username: `${RUN}-author`, discordUsername: `${RUN}-author`, displayName: "AUTHOR", steamId: `7656119${Math.floor(Math.random() * 1e9)}`, password: "Test123", roles: ["user"], }, overrideAccess: true, depth: 0, })) as unknown as User; authorId = author.id; }); afterAll(async () => { for (const id of missionIds) { await payload.delete({ collection: "missions", id, overrideAccess: true }).catch(() => {}); } await payload .delete({ collection: "campaigns", id: campaignId, overrideAccess: true }) .catch(() => {}); await payload.delete({ collection: "maps", id: mapId, overrideAccess: true }).catch(() => {}); // User deletion trips FK constraints unless the hook-provisioned // personal bank account and profile are removed first. const accounts = await payload .find({ collection: "bank-accounts", where: { ownerUser: { equals: authorId } }, limit: 5, depth: 0, overrideAccess: true, }) .catch(() => null); for (const account of accounts?.docs ?? []) { await payload .delete({ collection: "bank-accounts", id: account.id, overrideAccess: true }) .catch(() => {}); } const profiles = await payload .find({ collection: "profiles", where: { user: { equals: authorId } }, limit: 5, depth: 0, overrideAccess: true, }) .catch(() => null); for (const profile of profiles?.docs ?? []) { await payload .delete({ collection: "profiles", id: profile.id, overrideAccess: true }) .catch(() => {}); } await payload .delete({ collection: "users", id: authorId, overrideAccess: true }) .catch(() => {}); }); const makeMission = async ( label: string, overrides: { status?: Mission["ownershipAndStatus"]["status"]; visibility?: Mission["ownershipAndStatus"]["visibility"]; start: Date; withRollCall?: boolean; marked?: boolean; }, ): Promise => { const mission = (await payload.create({ collection: "missions", data: { name: `${RUN} ${label}`, codeName: `${RUN}-${label}`, summary: "Imminent reminder boundary test mission", operationType: "main", classification: { map: mapId, missionType: "PvE", campaign: campaignId, startDateTime: overrides.start.toISOString(), estimatedDuration: 60, }, ownershipAndStatus: { authors: [authorId], status: overrides.status ?? "Scheduled", visibility: overrides.visibility ?? "unit", }, missionRoles: { maxPlayers: 16, }, gameDetails: { serverDetails: { serverIp: "127.0.0.1", serverPort: 2302, }, }, briefing: [], ...(overrides.withRollCall === false ? {} : { discordMessageId: `${RUN}-${label}-message` }), ...(overrides.marked ? { imminentReminderSentAt: new Date().toISOString() } : {}), }, overrideAccess: true, depth: 0, })) as unknown as Mission; missionIds.push(mission.id); return mission; }; it("finds posted Ready/Scheduled ops starting inside the 15 minute window", async () => { const now = new Date(); const start = new Date(now.getTime() + IMMINENT_REMINDER_WINDOW_MS - 60_000); const ready = await makeMission("in-window-ready", { status: "Ready", start }); const scheduled = await makeMission("in-window-scheduled", { status: "Scheduled", start }); const found = await getImminentReminderMissions(payload, now); expect(found.map((m) => m.id)).toContain(ready.id); expect(found.map((m) => m.id)).toContain(scheduled.id); }); it("includes the exact window edge and the grace zone just past the start", async () => { const now = new Date(); const edge = await makeMission("window-edge", { start: new Date(now.getTime() + IMMINENT_REMINDER_WINDOW_MS), }); const justStarted = await makeMission("grace-zone", { start: new Date(now.getTime() - 5 * 60_000), }); const found = await getImminentReminderMissions(payload, now); expect(found.map((m) => m.id)).toContain(edge.id); expect(found.map((m) => m.id)).toContain(justStarted.id); }); it("excludes ops outside the window, past the grace bound, marked, without a roll-call, or not unit-visible", async () => { const now = new Date(); const tooEarly = await makeMission("too-early", { start: new Date(now.getTime() + IMMINENT_REMINDER_WINDOW_MS + 60_000), }); const tooLate = await makeMission("too-late", { start: new Date(now.getTime() - IMMINENT_REMINDER_GRACE_MS - 60_000), }); const alreadyReminded = await makeMission("already-reminded", { start: new Date(now.getTime() + 10 * 60_000), marked: true, }); const noRollCall = await makeMission("no-roll-call", { start: new Date(now.getTime() + 10 * 60_000), withRollCall: false, }); const notUnitVisible = await makeMission("not-unit-visible", { start: new Date(now.getTime() + 10 * 60_000), visibility: "leadership", }); const draftStatus = await makeMission("draft-status", { status: "Planning", start: new Date(now.getTime() + 10 * 60_000), }); const foundIds = (await getImminentReminderMissions(payload, now)).map((m) => m.id); for (const excluded of [ tooEarly, tooLate, alreadyReminded, noRollCall, notUnitVisible, draftStatus, ]) { expect(foundIds).not.toContain(excluded.id); } }); });