Files
local-cal/tests/date-normalizer.test.ts
Dmytro Stanchiev cbd2559169 🐛 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
2026-04-07 23:01:42 -04:00

84 lines
2.6 KiB
TypeScript

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");
}
});
});