Add period prediction accuracy feedback (P4.5 complete)
All checks were successful
Deploy / deploy (push) Successful in 1m36s
All checks were successful
Deploy / deploy (push) Successful in 1m36s
Implements visual feedback for cycle prediction accuracy in ICS calendar feeds: - Add predictedDate field to PeriodLog type for tracking predicted vs actual dates - POST /api/cycle/period now calculates and stores predictedDate based on previous lastPeriodDate + cycleLength, returns daysEarly/daysLate in response - ICS feed generates "(Predicted)" events when actual period start differs from predicted, with descriptions like "period arrived 2 days early" - Calendar route fetches period logs and passes them to ICS generator This creates an accuracy feedback loop helping users understand their cycle variability over time per calendar.md spec. 807 tests passing across 43 test files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,32 +4,51 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { User } from "@/types";
|
||||
import type { PeriodLog, User } from "@/types";
|
||||
|
||||
// Module-level variable to control mock user lookup
|
||||
let mockUsers: Map<string, User> = new Map();
|
||||
let mockPeriodLogs: PeriodLog[] = [];
|
||||
|
||||
// Mock PocketBase
|
||||
vi.mock("@/lib/pocketbase", () => ({
|
||||
createPocketBaseClient: vi.fn(() => ({
|
||||
collection: vi.fn(() => ({
|
||||
getOne: vi.fn((userId: string) => {
|
||||
const user = mockUsers.get(userId);
|
||||
if (!user) {
|
||||
const error = new Error("Not found");
|
||||
(error as unknown as { status: number }).status = 404;
|
||||
throw error;
|
||||
}
|
||||
collection: vi.fn((name: string) => {
|
||||
if (name === "users") {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
calendarToken: user.calendarToken,
|
||||
lastPeriodDate: user.lastPeriodDate.toISOString(),
|
||||
cycleLength: user.cycleLength,
|
||||
garminConnected: user.garminConnected,
|
||||
getOne: vi.fn((userId: string) => {
|
||||
const user = mockUsers.get(userId);
|
||||
if (!user) {
|
||||
const error = new Error("Not found");
|
||||
(error as unknown as { status: number }).status = 404;
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
calendarToken: user.calendarToken,
|
||||
lastPeriodDate: user.lastPeriodDate.toISOString(),
|
||||
cycleLength: user.cycleLength,
|
||||
garminConnected: user.garminConnected,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
})),
|
||||
}
|
||||
if (name === "period_logs") {
|
||||
return {
|
||||
getFullList: vi.fn(() =>
|
||||
mockPeriodLogs.map((log) => ({
|
||||
id: log.id,
|
||||
user: log.user,
|
||||
startDate: log.startDate.toISOString(),
|
||||
predictedDate: log.predictedDate?.toISOString() ?? null,
|
||||
created: log.created.toISOString(),
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -73,6 +92,7 @@ describe("GET /api/calendar/[userId]/[token].ics", () => {
|
||||
vi.clearAllMocks();
|
||||
mockUsers = new Map();
|
||||
mockUsers.set("user123", mockUser);
|
||||
mockPeriodLogs = [];
|
||||
});
|
||||
|
||||
// Helper to create route context with params
|
||||
@@ -228,4 +248,47 @@ describe("GET /api/calendar/[userId]/[token].ics", () => {
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it("passes period logs to ICS generator for prediction accuracy", async () => {
|
||||
mockPeriodLogs = [
|
||||
{
|
||||
id: "log1",
|
||||
user: "user123",
|
||||
startDate: new Date("2025-01-10"),
|
||||
predictedDate: new Date("2025-01-12"), // 2 days early
|
||||
created: new Date("2025-01-10"),
|
||||
},
|
||||
{
|
||||
id: "log2",
|
||||
user: "user123",
|
||||
startDate: new Date("2024-12-15"),
|
||||
predictedDate: null, // First log, no prediction
|
||||
created: new Date("2024-12-15"),
|
||||
},
|
||||
];
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const context = createRouteContext(
|
||||
"user123",
|
||||
"valid-calendar-token-abc123def",
|
||||
);
|
||||
|
||||
await GET(mockRequest, context);
|
||||
|
||||
expect(mockGenerateIcsFeed).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
periodLogs: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "log1",
|
||||
startDate: expect.any(Date),
|
||||
predictedDate: expect.any(Date),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "log2",
|
||||
predictedDate: null,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user