Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions __tests__/datalib/judgeToTeam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { db } from '../../jest.setup';
import { GetJudgeToTeamPairings } from '@datalib/judgeToTeam/getJudgeToTeamPairings';
import { ObjectId } from 'mongodb';
import JudgeToTeam from '@typeDefs/judgeToTeam';

beforeEach(async () => {
await db.collection('submissions').deleteMany({});
});

// Helper to create valid submission documents
function createSubmission(judgeId: ObjectId, teamId: ObjectId) {
return {
judge_id: judgeId,
team_id: teamId,
social_good: null,
creativity: null,
presentation: null,
scores: [],
is_scored: false,
queuePosition: null,
};
}

describe('GetJudgeToTeamPairings', () => {
it('should return an empty array when no submissions exist', async () => {
const result = await GetJudgeToTeamPairings();
expect(result.ok).toBe(true);
expect(result.body).toEqual([]);
expect(result.error).toBe(null);
});

it('should return pairings with string IDs converted from ObjectIds', async () => {
const judgeId = new ObjectId();
const teamId = new ObjectId();

await db
.collection('submissions')
.insertOne(createSubmission(judgeId, teamId));

const result = await GetJudgeToTeamPairings();
expect(result.ok).toBe(true);
expect(result.body).toHaveLength(1);
expect(result.error).toBe(null);

const pairing = result.body?.[0];
expect(pairing).toEqual({
judge_id: judgeId.toString(),
team_id: teamId.toString(),
});
});

it('should return multiple pairings correctly', async () => {
const judgeId1 = new ObjectId();
const judgeId2 = new ObjectId();
const teamId1 = new ObjectId();
const teamId2 = new ObjectId();
const teamId3 = new ObjectId();

await db
.collection('submissions')
.insertMany([
createSubmission(judgeId1, teamId1),
createSubmission(judgeId1, teamId2),
createSubmission(judgeId2, teamId3),
]);

const result = await GetJudgeToTeamPairings();
expect(result.ok).toBe(true);
expect(result.body).toHaveLength(3);
expect(result.error).toBe(null);

const pairings = result.body as JudgeToTeam[];
expect(pairings[0]).toEqual({
judge_id: judgeId1.toString(),
team_id: teamId1.toString(),
});
expect(pairings[1]).toEqual({
judge_id: judgeId1.toString(),
team_id: teamId2.toString(),
});
expect(pairings[2]).toEqual({
judge_id: judgeId2.toString(),
team_id: teamId3.toString(),
});
});

it('should handle duplicate judge-team pairings', async () => {
const judgeId = new ObjectId();
const teamId = new ObjectId();

await db
.collection('submissions')
.insertMany([
createSubmission(judgeId, teamId),
createSubmission(judgeId, teamId),
]);

const result = await GetJudgeToTeamPairings();
expect(result.ok).toBe(true);
expect(result.body).toHaveLength(2);

const pairings = result.body as JudgeToTeam[];
expect(pairings[0]).toEqual(pairings[1]);
});

it('should convert ObjectIds to strings for duplicate prevention comparison', async () => {
const judgeId = new ObjectId();
const teamId = new ObjectId();
const judgeIdString = judgeId.toString();
const teamIdString = teamId.toString();

await db
.collection('submissions')
.insertOne(createSubmission(judgeId, teamId));

const result = await GetJudgeToTeamPairings();
const pairings = result.body as JudgeToTeam[];

// This test verifies that String() conversion is necessary for comparison
// in algorithms like judgesToTeamsAlgorithm.ts
expect(String(pairings[0].judge_id)).toBe(judgeIdString);
expect(String(pairings[0].team_id)).toBe(teamIdString);

// Simulate the duplicate check from judgesToTeamsAlgorithm.ts (lines 183-189)
const duplicateExists = pairings.some(
(entry) =>
String(entry.judge_id) === judgeIdString &&
String(entry.team_id) === teamIdString
);
expect(duplicateExists).toBe(true);
});
});
35 changes: 26 additions & 9 deletions app/(api)/_actions/logic/applyDiagnosticAlpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,35 @@ import checkMatches from '@actions/logic/checkMatches';
export default async function applyDiagnosticAlpha(options: {
alpha: number;
judgeToTeam: JudgeToTeam[];
}): Promise<{ ok: boolean; body: JudgeToTeam[] | null; error: string | null }> {
}): Promise<{
ok: boolean;
body: JudgeToTeam[] | null;
error: string | null;
message?: string;
}> {
const existing = await GetManySubmissions();
if (existing.ok && existing.body && existing.body.length > 0) {
const teamsRes = await GetManyTeams();
if (!teamsRes.ok) {
return {
ok: false,
body: null,
error:
'Submissions collection is not empty. Please clear before applying diagnostics.',
error: `GetManyTeams error: ${teamsRes.error}`,
};
}
const teamsRes = await GetManyTeams();
if (!teamsRes.ok) {
const teams = teamsRes.body;
const existingCount = existing.ok && existing.body ? existing.body.length : 0;
const maxTotalAssignmentsPerTeam = 4;
const maxSubmissions = teams.length * maxTotalAssignmentsPerTeam;
const isSecondRound = existingCount > 0;

if (existingCount >= maxSubmissions) {
return {
ok: false,
body: null,
error: `GetManyTeams error: ${teamsRes.error}`,
error:
'Maximum judge assignments reached (4 judges per team). Clear submissions to rerun.',
};
}
const teams = teamsRes.body;
const parsedSubmissions = await parseAndReplace(options.judgeToTeam);
if (!checkMatches(parsedSubmissions, teams.length)) {
return {
Expand Down Expand Up @@ -62,5 +72,12 @@ export default async function applyDiagnosticAlpha(options: {
// return { ok: false, body: null, error: res.error };
// }

return { ok: true, body: options.judgeToTeam, error: null };
return {
ok: true,
body: options.judgeToTeam,
error: null,
message: isSecondRound
? 'Second round detected: new pairings were added on top of existing submissions.'
: undefined,
};
}
4 changes: 2 additions & 2 deletions app/(api)/_actions/logic/checkMatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function checkMatches(
matches: Submission[],
teamsLength: number
) {
if (matches.length < 3 * teamsLength) return false;
if (matches.length < 2 * teamsLength) return false;

let valid = true;
const mp: Map<string, number> = new Map();
Expand All @@ -18,7 +18,7 @@ export default function checkMatches(
}

mp.forEach((count) => {
if (count !== 3) valid = false;
if (count !== 2) valid = false;
});

return valid;
Expand Down
13 changes: 8 additions & 5 deletions app/(api)/_actions/logic/matchTeams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import matchAllTeams from '@utils/matching/judgesToTeamsAlgorithm';
import parseAndReplace from '@utils/request/parseAndReplace';
import { GetManyTeams } from '@datalib/teams/getTeam';
import { CreateManySubmissions } from '@datalib/submissions/createSubmission';
import { GetManySubmissions } from '@datalib/submissions/getSubmissions';
//import { GetManySubmissions } from '@datalib/submissions/getSubmissions';
import checkMatches from '@actions/logic/checkMatches';

export default async function matchTeams(
options: { alpha: number } = { alpha: 4 }
) {
const submissionsResponse = await GetManySubmissions();
/*const submissionsResponse = await GetManySubmissions();
if (
submissionsResponse.ok &&
submissionsResponse.body &&
Expand All @@ -23,7 +23,7 @@ export default async function matchTeams(
error:
'Submissions collection is not empty. Please clear submissions before matching teams.',
};
}
}*/

// Generate submissions based on judge-team assignments.
const teamsRes = await GetManyTeams();
Expand All @@ -46,10 +46,13 @@ export default async function matchTeams(
}
const res = await CreateManySubmissions(parsedJudgeToTeam);
if (!res.ok) {
console.log(`${res.error}`);
return {
ok: false,
body: null,
error: 'Invalid submissions.',
error: `Failed to create submissions in database: ${
res.error ?? 'Unknown error'
}`,
};
}
// for (const submission of parsedJudgeToTeam) {
Expand All @@ -65,7 +68,7 @@ export default async function matchTeams(
return {
ok: false,
body: null,
error: 'Invalid submissions.',
error: 'Invalid submissions: assignment validation failed.',
};
}
return {
Expand Down
28 changes: 28 additions & 0 deletions app/(api)/_datalib/judgeToTeam/getJudgeToTeamPairings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import JudgeToTeam from '@typeDefs/judgeToTeam';
import { getDatabase } from '@utils/mongodb/mongoClient.mjs';
import { HttpError } from '@utils/response/Errors';
import Submission from '@typeDefs/submission';
import { ObjectId, Db } from 'mongodb';

type MongoSubmission = Omit<Submission, 'judge_id' | 'team_id'> & {
judge_id: ObjectId;
team_id: ObjectId;
};

export const GetJudgeToTeamPairings = async () => {
try {
const db = (await getDatabase()) as Db;
const submissions = await db
.collection<MongoSubmission>('submissions')
.find()
.toArray();
const pairings = submissions.map((submission) => ({
judge_id: String(submission.judge_id),
team_id: String(submission.team_id),
}));
return { ok: true, body: pairings as JudgeToTeam[], error: null };
} catch (e) {
const error = e as HttpError;
return { ok: false, body: null, error: error.message };
}
};
43 changes: 40 additions & 3 deletions app/(api)/_utils/matching/judgesToTeamsAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { optedHDTracks, nonHDTracks } from '@data/tracks';

import { GetManyUsers } from '@datalib/users/getUser';
import { GetManyTeams } from '@datalib/teams/getTeam';
import { GetJudgeToTeamPairings } from '@datalib/judgeToTeam/getJudgeToTeamPairings';

interface Judge {
user: User;
Expand Down Expand Up @@ -66,7 +67,7 @@ export default async function matchAllTeams(options?: { alpha?: number }) {
const teamMatchQualities: { [teamId: string]: number[] } = {};
const teamJudgeDomainTypes: { [teamId: string]: string[] } = {};

const rounds = 3;
const rounds = 2;
const ALPHA = options?.alpha ?? 4;
// Fetch all checked in judges.
const judgesResponse = await GetManyUsers({
Expand Down Expand Up @@ -157,6 +158,19 @@ export default async function matchAllTeams(options?: { alpha?: number }) {
.filter((team) => team.tracks.length < rounds)
.map((team) => [team._id ?? '', rounds - team.tracks.length])
);

// Get previous pairings and push it to the judgeToTeam array (so that !duplicateExists is true)
const previousPairings = await GetJudgeToTeamPairings();
if (!previousPairings.ok || !previousPairings.body) {
throw new Error(
`Failed to load existing judge-to-team pairings: ${
previousPairings.error ?? 'Unknown error'
}`
);
}
Comment on lines +164 to +170
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for !previousPairings.body is redundant and potentially confusing. When previousPairings.ok is true, the body will always be an array (empty or populated), never null. The current condition correctly handles the empty array case (no previous pairings) since an empty array is truthy. However, for clarity and type safety, consider removing the !previousPairings.body check and relying solely on !previousPairings.ok, or explicitly check for null if that's the intended behavior.

Copilot uses AI. Check for mistakes.
const previousPairingsBody = previousPairings.body;
judgeToTeam.push(...previousPairingsBody);

// Main loop: process each team for each round.
for (let domainIndex = 0; domainIndex < rounds; domainIndex++) {
for (const team of modifiedTeams) {
Expand All @@ -168,10 +182,14 @@ export default async function matchAllTeams(options?: { alpha?: number }) {

let selectedJudge: Judge | undefined = undefined;
for (const judge of judgesQueue) {
// String() conversion is necessary because:
// - previousPairings from GetJudgeToTeamPairings converts ObjectIds to strings
// - judge.user._id and team._id are ObjectIds that need .toString()
// - Comparing without String() would cause false negatives in duplicate detection
const duplicateExists = judgeToTeam.some(
(entry) =>
entry.judge_id === judge.user._id?.toString() &&
entry.team_id === team._id?.toString()
String(entry.judge_id) === judge.user._id?.toString() &&
String(entry.team_id) === team._id?.toString()
);
if (!duplicateExists) {
selectedJudge = judge;
Expand Down Expand Up @@ -212,6 +230,25 @@ export default async function matchAllTeams(options?: { alpha?: number }) {
shuffleArray(modifiedTeams);
}

// Remove the previous pairings without relying on array insertion order.
if (previousPairingsBody.length > 0) {
const previousPairingKeySet = new Set(
previousPairingsBody.map((pairing) => {
const judgeId = String(pairing.judge_id);
const teamId = String(pairing.team_id);
return `${judgeId}::${teamId}`;
})
);
const filteredJudgeToTeam = judgeToTeam.filter((entry) => {
const judgeId = String(entry.judge_id);
const teamId = String(entry.team_id);
const key = `${judgeId}::${teamId}`;
return !previousPairingKeySet.has(key);
});
judgeToTeam.length = 0;
judgeToTeam.push(...filteredJudgeToTeam);
}

console.log('No. of judgeToTeam:', judgeToTeam.length);

const judgeAssignments = judgesQueue.map((judge) => judge.teamsAssigned);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ export default function JudgeTeamGrouping() {
setShowSubmissions(true);
setApplyAlphaSuccess(true);
setError('');
if (res.message) {
alert(res.message);
}
} else {
setError(res.error!);
setShowMatching(false);
Expand Down Expand Up @@ -326,14 +329,14 @@ export default function JudgeTeamGrouping() {

return (
<div className={styles.body}>
<div className="flex items-center gap-4">
<h4 style={{ cursor: 'pointer' }}>Error</h4>
{error && (
{error && (
<div className="flex items-center gap-4">
<h4 style={{ cursor: 'pointer' }}>Error</h4>
<div>
<pre style={{ color: 'red' }}>{error}</pre>
</div>
)}
</div>
</div>
)}

{/* Diagnostics inputs */}
<div className="mt-6">
Expand Down