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 { getFridayClaimant, hasMissionOnFriday, isNoticeDue, noticeToken, upcomingFridayRange, } from "@/lib/intelligence/fridayOpsNotice"; let payload: Payload; const RUN = `fon-${Date.now().toString(36)}`; // Hermetic window: 2027-01-13 is a Wednesday far enough out that the shared // dev database holds no real missions on its Friday (2027-01-15). const WEDNESDAY = new Date(2027, 0, 13, 12, 0, 0, 0); const TUESDAY = new Date(2027, 0, 12, 12, 0, 0, 0); const FRIDAY = new Date(2027, 0, 15, 12, 0, 0, 0); const FRIDAY_EVENING = new Date(2027, 0, 15, 20, 0, 0, 0); const SATURDAY = new Date(2027, 0, 16, 20, 0, 0, 0); describe("Friday open-slot notice", () => { let mapId: number; let campaignId: number; let authorId: number; let zeusId: number; let superuserId: number; const missionIds: number[] = []; const userIds: number[] = []; const roleIds: number[] = []; beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); // Sweep leftovers from crashed earlier runs: a stray Concept mission in // the fixture window would flip the first hasMissionOnFriday assertion. const stale = await payload.find({ collection: "missions", overrideAccess: true, depth: 0, limit: 100, where: { codeName: { like: "fon-" } }, }); for (const doc of stale.docs) { await payload.delete({ collection: "missions", id: doc.id, overrideAccess: true }).catch(() => {}); } 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: "Friday notice 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; userIds.push(authorId); const zeus = (await payload.create({ collection: "users", data: { username: `${RUN}-zeus`, discordUsername: `${RUN}-zeus`, displayName: "ZEUS", steamId: `7656119${Math.floor(Math.random() * 1e9)}`, password: "Test123", roles: ["user"], }, overrideAccess: true, depth: 0, })) as unknown as User; zeusId = zeus.id; userIds.push(zeusId); const superRole = (await payload.create({ collection: "roles", data: { name: `FON Superuser ${RUN}`, slug: "", isSuperuser: true }, overrideAccess: true, depth: 0, })) as unknown as { id: number }; roleIds.push(superRole.id); const superuser = (await payload.create({ collection: "users", data: { username: `${RUN}-su`, discordUsername: `${RUN}-su`, displayName: "SU", steamId: `7656119${Math.floor(Math.random() * 1e9)}`, password: "Test123", roles: ["user"], roleDocs: [superRole.id], }, overrideAccess: true, depth: 0, })) as unknown as User; superuserId = superuser.id; userIds.push(superuserId); }); 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. for (const uid of userIds) { const accounts = await payload .find({ collection: "bank-accounts", where: { ownerUser: { equals: uid } }, 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: uid } }, 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: uid, overrideAccess: true }).catch(() => {}); } for (const rid of roleIds) { await payload.delete({ collection: "roles", id: rid, overrideAccess: true }).catch(() => {}); } }); const makeMission = async ( label: string, start: Date, status: Mission["ownershipAndStatus"]["status"] = "Scheduled", creatorId?: number, ): Promise => { const mission = (await payload.create({ collection: "missions", data: { name: `${RUN} ${label}`, codeName: `${RUN}-${label}`, summary: "Friday notice test mission", operationType: "side", classification: { map: mapId, missionType: "PvE", campaign: campaignId, startDateTime: start.toISOString(), estimatedDuration: 60, }, ownershipAndStatus: { authors: [authorId], status, visibility: "unit", }, missionRoles: { maxPlayers: 16, }, gameDetails: { serverDetails: { serverIp: "127.0.0.1", serverPort: 2302, }, }, briefing: [], ...(creatorId ? { createdBy: creatorId } : {}), }, overrideAccess: true, depth: 0, })) as unknown as Mission; missionIds.push(mission.id); return mission; }; it("is due on Wednesdays at 8 PM Eastern, until the ET day rolls over", () => { // 2026-09-16 is a Wednesday in EDT (UTC-4); January is EST (UTC-5). expect(isNoticeDue(new Date("2026-09-17T00:30:00Z"))).toBe(true); // Wed 20:30 ET expect(isNoticeDue(new Date("2026-09-17T03:59:00Z"))).toBe(true); // Wed 23:59 ET expect(isNoticeDue(new Date("2026-09-15T20:30:00Z"))).toBe(false); // Tue 16:30 ET expect(isNoticeDue(new Date("2026-09-16T16:00:00Z"))).toBe(false); // Wed 12:00 ET expect(isNoticeDue(new Date("2026-09-16T23:59:00Z"))).toBe(false); // Wed 19:59 ET expect(isNoticeDue(new Date("2026-09-17T00:00:00Z"))).toBe(true); // Wed 20:00 ET sharp expect(isNoticeDue(new Date("2026-09-17T04:00:00Z"))).toBe(false); // Thu 00:00 ET expect(isNoticeDue(new Date("2026-01-15T01:00:00Z"))).toBe(true); // Wed 20:00 ET (EST) }); it("targets the upcoming Friday, strictly in the future", () => { const fromWednesday = upcomingFridayRange(WEDNESDAY); expect(fromWednesday.start).toEqual(new Date(2027, 0, 15, 0, 0, 0, 0)); expect(fromWednesday.end).toEqual(new Date(2027, 0, 15, 23, 59, 59, 999)); expect(fromWednesday.epoch).toBe(Math.floor(fromWednesday.start.getTime() / 1000)); // Earlier in the week still lands on the same Friday. expect(upcomingFridayRange(new Date(2027, 0, 11, 8, 0, 0, 0)).start).toEqual( fromWednesday.start, ); // On a Friday itself, the next Friday is targeted instead. expect(upcomingFridayRange(FRIDAY).start).toEqual(new Date(2027, 0, 22, 0, 0, 0, 0)); }); it("builds a stable per-Friday dedup token", () => { expect(noticeToken(WEDNESDAY)).toBe(noticeToken(TUESDAY)); expect(noticeToken(WEDNESDAY)).toBe(``); expect(noticeToken(FRIDAY)).not.toBe(noticeToken(WEDNESDAY)); }); it("treats the Friday slot as open only when a non-cancelled mission starts that Friday", async () => { await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(false); const concept = await makeMission("concept-friday", FRIDAY_EVENING, "Concept"); await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(true); await makeMission("cancelled-friday", FRIDAY_EVENING, "Cancelled"); await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(true); await payload.delete({ collection: "missions", id: concept.id, overrideAccess: true }); await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(false); const saturday = await makeMission("saturday-mission", SATURDAY); await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(false); await payload.delete({ collection: "missions", id: saturday.id, overrideAccess: true }); }); it("credits the earliest non-superuser creator as the claimant", async () => { await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toBeNull(); const zeusMission = await makeMission("zeus-claim", FRIDAY_EVENING, "Scheduled", zeusId); await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" }); await makeMission("su-claim", FRIDAY_EVENING, "Scheduled", superuserId); await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" }); await payload.delete({ collection: "missions", id: zeusMission.id, overrideAccess: true }); await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toBeNull(); const first = await makeMission("zeus-first", FRIDAY_EVENING, "Scheduled", zeusId); const second = await makeMission("zeus-second", FRIDAY_EVENING, "Scheduled", zeusId); await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" }); expect(first.createdAt <= second.createdAt).toBe(true); await payload.delete({ collection: "missions", id: first.id, overrideAccess: true }); await payload.delete({ collection: "missions", id: second.id, overrideAccess: true }); const claimer = await makeMission("zeus-cancelled", FRIDAY_EVENING, "Scheduled", zeusId); await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" }); await payload.update({ collection: "missions", id: claimer.id, data: { ownershipAndStatus: { status: "Cancelled" } }, overrideAccess: true, }); await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toBeNull(); }); });