Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 14 additions & 10 deletions __tests__/booking-algorithm/allocationAlgorithms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Covers:
* - manualAllocate (validation, business rules, error handling)
* - autoAllocate (all strategies, preference filtering, error handling)
* - preAllocate (validation, delegation)
* - allocateRequestedSlots (validation, delegation; formerly preAllocate)
* - filterSlotsByPreferences (private, tested via autoAllocate)
* - allocateConsultationSlots (session duration fix verification)
* - allocateWebinarSlots (consecutive slot finding)
Expand All @@ -20,7 +20,7 @@
type AllocationOptions,
} from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/allocationAlgorithms";
import { AllocationService } from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/allocationService";
import {

Check failure on line 23 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Mocks should not be manually imported from a __mocks__ directory. Instead use `jest.mock` and import from the original module path
makeTimeSlot,
makeConsecutiveTimeSlots,
makeWeekOfAvailability,
Expand Down Expand Up @@ -398,8 +398,8 @@

if (result.success) {
// Should have distributed across weeks
expect(result.selectedSlots.length).toBeGreaterThan(0);

Check failure on line 401 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
expect(result.strategy).toBe("optimal-distribution");

Check failure on line 402 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
}
});

Expand Down Expand Up @@ -533,8 +533,8 @@
// Morning preference should only include 9-12 slots
result.selectedSlots.forEach((s) => {
const hour = s.startTime.getHours();
expect(hour).toBeGreaterThanOrEqual(9);

Check failure on line 536 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
expect(hour).toBeLessThan(12);

Check failure on line 537 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
});
}
});
Expand Down Expand Up @@ -606,11 +606,11 @@
});
});

// ─── preAllocate ────────────────────────────────────────────────────────────
// ─── allocateRequestedSlots ─────────────────────────────────────────────────

describe("AllocationAlgorithms.preAllocate", () => {
describe("AllocationAlgorithms.allocateRequestedSlots", () => {
it("should reject when no requested slots provided", async () => {
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -620,7 +620,7 @@
});

it("should reject when requested slots is empty array", async () => {
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -631,7 +631,7 @@

it("should reject wrong number of requested slots", async () => {
const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 1);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -643,7 +643,7 @@

it("should succeed with correct number of slots", async () => {
const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 2);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -655,7 +655,11 @@
"consultation",
"event-1",
slots,
{ useRequestedSlots: true },
{
useRequestedSlots: true,
idempotencyKey: undefined,
initialAllocation: undefined,
},
);
});

Expand All @@ -666,7 +670,7 @@
});

const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 2);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -680,7 +684,7 @@
mockAllocateSlots.mockRejectedValue(new Error("Connection failed"));

const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 2);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand Down
3 changes: 2 additions & 1 deletion __tests__/booking-algorithm/calendarUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
type Appointment,
} from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/calendarUtils";
import { ScheduleType, DayOfWeek, AppointmentsType } from "@prisma/client";
import {

Check failure on line 43 in __tests__/booking-algorithm/calendarUtils.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Mocks should not be manually imported from a __mocks__ directory. Instead use `jest.mock` and import from the original module path
makeTimeSlot,
makeConsecutiveTimeSlots,
makeWeeklyAvailabilitySlot,
Expand Down Expand Up @@ -97,7 +97,7 @@
const slots = mapWeeklySlots(data as any, new Date("2025-01-06"), "week");
const mondaySlots = slots.filter((s) => s.startTime.getUTCDay() === 1);
if (mondaySlots.length > 0) {
expect(mondaySlots[0].startTime.getUTCHours()).toBe(14);

Check failure on line 100 in __tests__/booking-algorithm/calendarUtils.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
}
});

Expand Down Expand Up @@ -438,7 +438,8 @@

it("startOfWeekSunday should return Sunday", () => {
const result = startOfWeekSunday(new Date("2025-01-08")); // Wednesday
expect(result.getDay()).toBe(0);
// UTC weekday — this helper is UTC-based; local getDay() shifts by machine TZ
expect(result.getUTCDay()).toBe(0);
});
});

Expand Down
91 changes: 91 additions & 0 deletions __tests__/booking-algorithm/idempotency-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Idempotency-Key lifecycle for allocation attempts. A retry of the SAME
* payload must reuse the key (the server replays the original batch, #837);
* any change to mode, event, or slots must mint a fresh key.
*/

import "./setup";

import {
computeAttemptFingerprint,
resolveAttemptKey,
} from "@/app/dashboard/consultant/[consultantId]/(features)/shared/hooks/useSlotAllocation";
// eslint-disable-next-line jest/no-mocks-import -- shared fixture builders, not module mocks (suite-wide pattern)
import { makeConsecutiveTimeSlots } from "./__mocks__/booking.mockData";
import type { TimeSlot } from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/calendarUtils";

const slots = makeConsecutiveTimeSlots(
"2026-08-03T09:00:00.000Z",
2,
) as TimeSlot[];

describe("computeAttemptFingerprint", () => {
it("is stable regardless of slot order", () => {
const reversed = [...slots].reverse();
expect(computeAttemptFingerprint("manual", "e1", slots)).toBe(
computeAttemptFingerprint("manual", "e1", reversed),
);
});

it("differs across modes, events, and slot sets", () => {
const fp = computeAttemptFingerprint("manual", "e1", slots);
expect(computeAttemptFingerprint("auto", "e1", slots)).not.toBe(fp);
expect(computeAttemptFingerprint("manual", "e2", slots)).not.toBe(fp);
expect(
computeAttemptFingerprint(
"manual",
"e1",
makeConsecutiveTimeSlots("2026-08-04T09:00:00.000Z", 2) as TimeSlot[],
),
).not.toBe(fp);
});
});

describe("resolveAttemptKey", () => {
it("reuses the key for an identical retry", () => {
const fp = computeAttemptFingerprint("manual", "e1", slots);
const first = resolveAttemptKey(null, fp);
const retry = resolveAttemptKey(first, fp);
expect(retry.key).toBe(first.key);
});

it("mints a new key when the payload changes", () => {
const first = resolveAttemptKey(
null,
computeAttemptFingerprint("manual", "e1", slots),
);
const changed = resolveAttemptKey(
first,
computeAttemptFingerprint("manual", "e1", [
...slots,
...(makeConsecutiveTimeSlots(
"2026-08-05T09:00:00.000Z",
2,
) as TimeSlot[]),
]),
);
expect(changed.key).not.toBe(first.key);
});

it("mints a new key when the mode changes", () => {
const manual = resolveAttemptKey(
null,
computeAttemptFingerprint("manual", "e1", slots),
);
const auto = resolveAttemptKey(
manual,
computeAttemptFingerprint("auto", "e1", []),
);
expect(auto.key).not.toBe(manual.key);
});

it("keys look like UUIDs", () => {
const attempt = resolveAttemptKey(
null,
computeAttemptFingerprint("auto", "e1", []),
);
expect(attempt.key).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
);
});
});
Loading
Loading