test(int): cover imminent reminder query boundaries
Window edges, post-start grace zone, and exclusions for marked missions, missing roll-call, non-unit visibility, and draft status.
This commit is contained in:
parent
35769ea0f6
commit
8f7573f742
1 changed files with 214 additions and 0 deletions
214
tests/int/imminent-reminders.int.spec.ts
Normal file
214
tests/int/imminent-reminders.int.spec.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
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<Mission> => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue