🐛 fix: normalize AI-generated dates to valid ISO offset datetimes
- add normalizeAiDateString() to coerce bare ISO datetimes, date-only strings, and fractional-second variants into offset-aware format - apply via z.preprocess in AiEventResponseItemSchema so Zod validation no longer rejects AI responses missing a timezone offset - fix system prompt to use toISOString() (unambiguous UTC) and clarify expected datetime format for the AI model - install bun-types and add to tsconfig so bun:test resolves cleanly - add 8 behaviour-driven tests covering all normalizer edge cases
This commit is contained in:
83
tests/date-normalizer.test.ts
Normal file
83
tests/date-normalizer.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { normalizeAiDateString } from "@/lib/date-normalizer";
|
||||
import { AiEventResponseSchema } from "@/lib/types";
|
||||
|
||||
describe("normalizeAiDateString", () => {
|
||||
test("converts bare ISO datetime (no offset) to valid offset datetime accepted by Zod", () => {
|
||||
const input = "2025-04-10T10:00:00";
|
||||
const result = normalizeAiDateString(input);
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/);
|
||||
expect(new Date(result).getHours()).toBe(10);
|
||||
});
|
||||
|
||||
test("converts date-only string to midnight datetime with offset", () => {
|
||||
const input = "2025-04-10";
|
||||
const result = normalizeAiDateString(input);
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T00:00:00(Z|[+-]\d{2}:\d{2})$/);
|
||||
});
|
||||
|
||||
test("passes through already-valid datetime with offset unchanged", () => {
|
||||
const input = "2025-04-10T10:00:00-04:00";
|
||||
const result = normalizeAiDateString(input);
|
||||
expect(result).toBe("2025-04-10T10:00:00-04:00");
|
||||
});
|
||||
|
||||
test("passes through datetime with Z suffix unchanged", () => {
|
||||
const input = "2025-04-10T10:00:00Z";
|
||||
const result = normalizeAiDateString(input);
|
||||
expect(result).toBe("2025-04-10T10:00:00Z");
|
||||
});
|
||||
|
||||
test("handles datetime with fractional seconds", () => {
|
||||
const input = "2025-04-10T10:00:00.000Z";
|
||||
const result = normalizeAiDateString(input);
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
expect(new Date(result).getHours()).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AiEventResponseSchema date normalization", () => {
|
||||
test("accepts event with bare ISO datetime (no offset) by normalizing it", () => {
|
||||
const raw = [
|
||||
{
|
||||
title: "Team Meeting",
|
||||
start: "2025-04-10T10:00:00",
|
||||
end: "2025-04-10T11:00:00",
|
||||
},
|
||||
];
|
||||
const result = AiEventResponseSchema.safeParse(raw);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data[0].start).toMatch(
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("accepts event with date-only start and no end", () => {
|
||||
const raw = [
|
||||
{
|
||||
title: "All-day workshop",
|
||||
start: "2025-07-20",
|
||||
allDay: true,
|
||||
},
|
||||
];
|
||||
const result = AiEventResponseSchema.safeParse(raw);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test("accepts event with already-valid offset datetime", () => {
|
||||
const raw = [
|
||||
{
|
||||
title: "Standup",
|
||||
start: "2025-04-10T09:00:00-05:00",
|
||||
end: "2025-04-10T09:30:00-05:00",
|
||||
},
|
||||
];
|
||||
const result = AiEventResponseSchema.safeParse(raw);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data[0].start).toBe("2025-04-10T09:00:00-05:00");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user