1
0
Fork 0
polaris-task-force/tests/int/evaluations.int.spec.ts
Z8MB1E a21770e54e feat(evaluations): add evaluation service
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-27 18:36:04 -04:00

476 lines
15 KiB
TypeScript

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, Role, User } from "@/payload-types";
import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions";
import { PERFORMANCE_LEVELS } from "@/lib/evaluations/levels";
import {
ANONYMITY_THRESHOLD,
evaluateEligibility,
getLatestSubordinateLevel,
getLeaderAggregate,
isMissionEvaluable,
upsertEvaluation,
} from "@/lib/evaluations";
let payload: Payload;
const RUN = `evl-${Date.now().toString(36)}`;
describe("Leadership evaluations", () => {
let mapId: number;
let campaignId: number;
let leaderA: User;
let subordinateB: User;
let participantC: User;
let participantE: User;
let noRsvpF: User;
let outsiderD: User;
let assignmentId: number;
let missionCompletedId: number;
let missionFutureId: number;
const userIds: number[] = [];
const attendanceIds: number[] = [];
const evaluationIds: number[] = [];
const makeUser = async (label: string): Promise<User> => {
const user = (await payload.create({
collection: "users",
data: {
username: `${RUN}-${label}`,
discordUsername: `${RUN}-${label}`,
displayName: label.toUpperCase(),
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userIds.push(user.id);
return user;
};
const makeMission = async (
label: string,
status: Mission["ownershipAndStatus"]["status"],
start: Date,
): Promise<Mission> => {
const mission = (await payload.create({
collection: "missions",
data: {
name: `${RUN} ${label}`,
codeName: `${RUN}-${label}`,
summary: "Leadership evaluation test mission",
operationType: "main",
classification: {
map: mapId,
missionType: "PvE",
campaign: campaignId,
startDateTime: start.toISOString(),
estimatedDuration: 60,
},
ownershipAndStatus: {
authors: [leaderA.id],
status,
visibility: "unit",
},
missionRoles: {
maxPlayers: 16,
},
gameDetails: {
serverDetails: {
serverIp: "127.0.0.1",
serverPort: 2302,
},
},
briefing: [],
},
overrideAccess: true,
depth: 0,
})) as unknown as Mission;
return mission;
};
const rsvp = async (user: User, missionId: number): Promise<void> => {
const attendance = await payload.create({
collection: "mission-attendances",
data: { mission: missionId, user: user.id, response: "yes" },
overrideAccess: true,
depth: 0,
});
attendanceIds.push(attendance.id);
};
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
const map = (await payload.create({
collection: "maps",
data: { name: `${RUN} Map` },
overrideAccess: true,
depth: 0,
})) as unknown as MissionMap;
mapId = map.id;
const campaign = (await payload.create({
collection: "campaigns",
data: { name: `${RUN} Campaign`, summary: "Evaluation test campaign", status: "concept" },
overrideAccess: true,
depth: 0,
})) as unknown as Campaign;
campaignId = campaign.id;
leaderA = await makeUser("leader-a");
subordinateB = await makeUser("subordinate-b");
participantC = await makeUser("participant-c");
participantE = await makeUser("participant-e");
noRsvpF = await makeUser("no-rsvp-f");
outsiderD = await makeUser("outsider-d");
const assignment = await payload.create({
collection: "assignments",
data: {
name: `${RUN} Squad`,
type: "squad",
leader: leaderA.id,
members: [subordinateB.id, noRsvpF.id],
},
overrideAccess: true,
depth: 0,
});
assignmentId = assignment.id;
const past = new Date();
past.setDate(past.getDate() - 7);
const future = new Date();
future.setDate(future.getDate() + 14);
const missionCompleted = await makeMission("completed", "Completed", past);
missionCompletedId = missionCompleted.id;
const missionFuture = await makeMission("future-scheduled", "Scheduled", future);
missionFutureId = missionFuture.id;
// Participation proxy: yes-RSVPs for B, C, E. F and D deliberately have none.
await rsvp(subordinateB, missionCompletedId);
await rsvp(participantC, missionCompletedId);
await rsvp(participantE, missionCompletedId);
});
afterAll(async () => {
const evaluations = await payload
.find({
collection: "evaluations",
where: { mission: { in: [missionCompletedId, missionFutureId] } },
limit: 100,
depth: 0,
overrideAccess: true,
})
.catch(() => null);
for (const doc of evaluations?.docs ?? []) {
await payload.delete({ collection: "evaluations", id: doc.id, overrideAccess: true }).catch(
() => {},
);
}
evaluationIds.length = 0;
for (const id of attendanceIds) {
await payload
.delete({ collection: "mission-attendances", id, overrideAccess: true })
.catch(() => {});
}
await payload.delete({ collection: "assignments", id: assignmentId, overrideAccess: true }).catch(
() => {},
);
for (const id of [missionCompletedId, missionFutureId]) {
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(() => {});
for (const userId of userIds) {
const accounts = await payload
.find({
collection: "bank-accounts",
where: { ownerUser: { equals: userId } },
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: userId } },
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: userId, overrideAccess: true }).catch(() => {});
}
});
it("classifies mission statuses for evaluability (pure)", () => {
const past = new Date(Date.now() - 86_400_000).toISOString();
const future = new Date(Date.now() + 86_400_000).toISOString();
expect(isMissionEvaluable("Completed")).toBe(true);
expect(isMissionEvaluable("Scheduled", past)).toBe(true);
expect(isMissionEvaluable("Active", past)).toBe(true);
expect(isMissionEvaluable("Scheduled", future)).toBe(false);
expect(isMissionEvaluable("Ready", past)).toBe(false);
expect(isMissionEvaluable("Cancelled", past)).toBe(false);
expect(isMissionEvaluable("Scheduled")).toBe(false);
expect(isMissionEvaluable("Scheduled", "not-a-date")).toBe(false);
});
it("derives the evaluation kind from the rater/ratee/mission relationship", async () => {
const cases = [
{ input: { raterId: subordinateB.id, rateeId: leaderA.id }, expectKind: "leader-direct" },
{ input: { raterId: participantC.id, rateeId: leaderA.id }, expectKind: "leader-indirect" },
{ input: { raterId: leaderA.id, rateeId: subordinateB.id }, expectKind: "subordinate" },
];
for (const { input, expectKind } of cases) {
const result = await evaluateEligibility(payload, { ...input, missionId: missionCompletedId });
expect(result.eligible).toBe(true);
expect(result.kind).toBe(expectKind);
}
// In A's assignment but never RSVPed yes — the participation proxy fails.
const noRsvp = await evaluateEligibility(payload, {
raterId: noRsvpF.id,
rateeId: leaderA.id,
missionId: missionCompletedId,
});
expect(noRsvp.eligible).toBe(false);
expect(noRsvp.reason).toBe("No attendance recorded for this mission.");
// No leadership relationship at all.
const outsider = await evaluateEligibility(payload, {
raterId: outsiderD.id,
rateeId: leaderA.id,
missionId: missionCompletedId,
});
expect(outsider.eligible).toBe(false);
// Self-rating is always rejected.
const selfRating = await evaluateEligibility(payload, {
raterId: leaderA.id,
rateeId: leaderA.id,
missionId: missionCompletedId,
});
expect(selfRating.eligible).toBe(false);
// A not-yet-past mission is not evaluable.
const tooEarly = await evaluateEligibility(payload, {
raterId: subordinateB.id,
rateeId: leaderA.id,
missionId: missionFutureId,
});
expect(tooEarly.eligible).toBe(false);
});
it("keeps one row per rater/ratee/mission/kind", async () => {
const first = await upsertEvaluation(payload, {
raterId: subordinateB.id,
rateeId: leaderA.id,
missionId: missionCompletedId,
kind: "leader-direct",
level: "excellent",
comment: "First pass",
});
evaluationIds.push(first.evaluation.id);
expect(first.created).toBe(true);
const second = await upsertEvaluation(payload, {
raterId: subordinateB.id,
rateeId: leaderA.id,
missionId: missionCompletedId,
kind: "leader-direct",
level: "proficient",
comment: "Revised",
});
expect(second.created).toBe(false);
expect(second.evaluation.id).toBe(first.evaluation.id);
expect(second.evaluation.level).toBe("proficient");
const rows = await payload.find({
collection: "evaluations",
where: {
and: [
{ rater: { equals: subordinateB.id } },
{ ratee: { equals: leaderA.id } },
{ mission: { equals: missionCompletedId } },
{ kind: { equals: "leader-direct" } },
],
},
limit: 10,
depth: 0,
overrideAccess: true,
});
expect(rows.docs).toHaveLength(1);
});
it("withholds the leader aggregate below the anonymity threshold", async () => {
const c = await upsertEvaluation(payload, {
raterId: participantC.id,
rateeId: leaderA.id,
missionId: missionCompletedId,
kind: "leader-indirect",
level: "excellent",
});
evaluationIds.push(c.evaluation.id);
const aggregate = await getLeaderAggregate(payload, leaderA.id);
expect(aggregate.totalRaters).toBe(2);
expect(ANONYMITY_THRESHOLD).toBe(3);
expect(aggregate.visible).toBe(false);
expect(aggregate.levels).toBeNull();
expect(aggregate.averageScore).toBeNull();
});
it("exposes the leader aggregate once the anonymity threshold is met", async () => {
const e = await upsertEvaluation(payload, {
raterId: participantE.id,
rateeId: leaderA.id,
missionId: missionCompletedId,
kind: "leader-indirect",
level: "exceptional",
});
evaluationIds.push(e.evaluation.id);
const aggregate = await getLeaderAggregate(payload, leaderA.id);
expect(aggregate.totalRaters).toBe(3);
expect(aggregate.visible).toBe(true);
expect(aggregate.levels).not.toBeNull();
expect(aggregate.averageScore).toBe(85); // (70 + 85 + 100) / 3
const byId = new Map((aggregate.levels ?? []).map((l) => [l.id, l]));
expect(byId.get("exceptional")?.count).toBe(1);
expect(byId.get("excellent")?.count).toBe(1);
expect(byId.get("proficient")?.count).toBe(1);
expect(byId.get("adequate")).toBeUndefined();
for (const level of aggregate.levels ?? []) {
expect(level.percentage).toBeCloseTo(33.33, 1);
expect(PERFORMANCE_LEVELS.some((p) => p.id === level.id)).toBe(true);
}
});
it("returns only the latest subordinate level, without rater data", async () => {
await upsertEvaluation(payload, {
raterId: leaderA.id,
rateeId: subordinateB.id,
missionId: missionCompletedId,
kind: "subordinate",
level: "adequate",
comment: "Manager note — must never leak",
});
const revised = await upsertEvaluation(payload, {
raterId: leaderA.id,
rateeId: subordinateB.id,
missionId: missionCompletedId,
kind: "subordinate",
level: "excellent",
});
evaluationIds.push(revised.evaluation.id);
const latest = await getLatestSubordinateLevel(payload, subordinateB.id);
expect(latest).not.toBeNull();
expect(latest?.level.id).toBe("excellent");
expect(latest?.missionId).toBe(missionCompletedId);
expect(typeof latest?.at).toBe("string");
expect(latest).not.toHaveProperty("rater");
expect(latest).not.toHaveProperty("comment");
const none = await getLatestSubordinateLevel(payload, participantC.id);
expect(none).toBeNull();
});
it("hides raw evaluation documents from users without the permission", async () => {
// Local ops bypass access control unless overrideAccess: false — this flag
// makes findOperation run the same requirePermission("evaluations:read")
// check that the REST endpoint runs. A user with no access gets a Forbidden
// error rather than an empty list.
await expect(
payload.find({
collection: "evaluations",
limit: 50,
depth: 0,
user: outsiderD,
overrideAccess: false,
}),
).rejects.toThrow(/not allowed/);
});
it("exposes raw evaluation documents to users holding evaluations:read", async () => {
const role = (await payload.create({
collection: "roles",
data: {
name: `${RUN} Evaluation Reader`,
slug: `${RUN}-evaluation-reader`,
permissions: ["evaluations:read"],
},
overrideAccess: true,
depth: 0,
})) as unknown as Role;
const reader = (await payload.create({
collection: "users",
data: {
username: `${RUN}-reader`,
discordUsername: `${RUN}-reader`,
displayName: "READER",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userIds.push(reader.id);
await payload.update({
collection: "users",
id: reader.id,
data: { roleDocs: [role.id] },
overrideAccess: true,
depth: 0,
});
invalidatePermissionCache();
try {
const result = await payload.find({
collection: "evaluations",
limit: 50,
depth: 0,
user: reader,
overrideAccess: false,
});
expect(result.docs.length).toBeGreaterThan(0);
const doc = result.docs[0] as unknown as Record<string, unknown>;
expect(typeof doc.rater).toBe("number");
expect(typeof doc.ratee).toBe("number");
expect(typeof doc.mission).toBe("number");
expect(["leader-direct", "leader-indirect", "subordinate"]).toContain(doc.kind);
} finally {
await payload.delete({ collection: "roles", id: role.id, overrideAccess: true }).catch(() => {});
}
});
});