import { describe, expect, test } from "bun:test"; import { UiConfigSchema } from "../config/schemas/platform.js"; import { canonicalizeTimeZone, extractUserTimeZoneFromRecall, formatLocalTimestamp, formatTurnTimestamp, resolveTurnTimezoneContext, } from "../daemon/date-context.js"; // --------------------------------------------------------------------------- // extractUserTimeZoneFromRecall // --------------------------------------------------------------------------- describe("extractUserTimeZoneFromRecall", () => { test("returns null for empty input", () => { expect(extractUserTimeZoneFromRecall("")).toBeNull(); expect(extractUserTimeZoneFromRecall(" ")).toBeNull(); }); test("extracts IANA timezone from identity item", () => { const text = ` User's timezone is America/New_York User works as a software engineer `; expect(extractUserTimeZoneFromRecall(text)).toBe("America/New_York"); }); test("extracts timezone from 'timezone: ...' in identity item", () => { const text = ` timezone: Europe/London name: Alice `; expect(extractUserTimeZoneFromRecall(text)).toBe("Europe/London"); }); test("extracts UTC offset timezone", () => { const text = ` User's time zone is UTC+5:30 `; const result = extractUserTimeZoneFromRecall(text); expect(result).not.toBeNull(); expect(result).toBe("+05:30"); }); test("falls back to scanning full text when no identity items", () => { const text = ` User mentioned their timezone is Asia/Tokyo `; expect(extractUserTimeZoneFromRecall(text)).toBe("Asia/Tokyo"); }); test("returns null when no timezone info present", () => { const text = ` User's name is Bob User works at Acme Corp `; expect(extractUserTimeZoneFromRecall(text)).toBeNull(); }); test("prefers identity items over other recalled content", () => { const text = ` User's timezone is America/Chicago Discussed timezone America/Los_Angeles for the deployment `; expect(extractUserTimeZoneFromRecall(text)).toBe("America/Chicago"); }); test("extracts timezone from identity item without timezone keyword via second pass", () => { const text = ` America/Denver `; expect(extractUserTimeZoneFromRecall(text)).toBe("America/Denver"); }); }); // --------------------------------------------------------------------------- // UiConfigSchema timezone fields // --------------------------------------------------------------------------- describe("UiConfigSchema timezone fields", () => { test("accepts canonicalizable IANA timezone identifiers", () => { const result = UiConfigSchema.parse({ userTimezone: "america/new_york", detectedTimezone: "america/los_angeles", }); expect(result.userTimezone).toBe("America/New_York"); expect(result.detectedTimezone).toBe("America/Los_Angeles"); expect(UiConfigSchema.parse({ userTimezone: "UTC" }).userTimezone).toBe( "UTC", ); }); test("accepts empty-string clearing sentinels", () => { const result = UiConfigSchema.parse({ userTimezone: "", detectedTimezone: "", }); expect(result.userTimezone).toBe(""); expect(result.detectedTimezone).toBe(""); }); test("rejects invalid non-empty userTimezone and detectedTimezone values", () => { expect(() => UiConfigSchema.parse({ userTimezone: "not-a-timezone" }), ).toThrow("ui.userTimezone must be a valid IANA timezone identifier"); expect(() => UiConfigSchema.parse({ detectedTimezone: "Mars/Olympus_Mons" }), ).toThrow("ui.detectedTimezone must be a valid IANA timezone identifier"); expect(() => UiConfigSchema.parse({ userTimezone: "+05:30" })).toThrow( "ui.userTimezone must be a valid IANA timezone identifier", ); }); test("rejects ambiguous abbreviations and offset strings", () => { for (const value of ["EST", "PST", "UTC+5:30", "GMT-0800", "+05:30"]) { expect(() => UiConfigSchema.parse({ userTimezone: value })).toThrow( "ui.userTimezone must be a valid IANA timezone identifier", ); expect(() => UiConfigSchema.parse({ detectedTimezone: value })).toThrow( "ui.detectedTimezone must be a valid IANA timezone identifier", ); } }); }); // --------------------------------------------------------------------------- // canonicalizeTimeZone // --------------------------------------------------------------------------- describe("canonicalizeTimeZone", () => { test("returns canonical timezone identifiers and ignores empty values", () => { expect(canonicalizeTimeZone("america/new_york")).toBe("America/New_York"); expect(canonicalizeTimeZone("")).toBeNull(); expect(canonicalizeTimeZone(null)).toBeNull(); expect(canonicalizeTimeZone("not-a-timezone")).toBeNull(); }); }); // --------------------------------------------------------------------------- // resolveTurnTimezoneContext // --------------------------------------------------------------------------- describe("resolveTurnTimezoneContext", () => { test("prefers configured user timezone over automatic sources", () => { const context = resolveTurnTimezoneContext({ configuredUserTimeZone: "America/New_York", clientTimezone: "America/Chicago", detectedTimezone: "Asia/Tokyo", hostTimeZone: "Europe/London", }); expect(context.effectiveTimezone).toBe("America/New_York"); expect(context.source).toBe("configuredUserTimezone"); }); test("prefers client timezone over detected and host timezones", () => { const context = resolveTurnTimezoneContext({ clientTimezone: "America/Chicago", detectedTimezone: "Asia/Tokyo", hostTimeZone: "Europe/London", }); expect(context.effectiveTimezone).toBe("America/Chicago"); expect(context.source).toBe("clientTimezone"); }); test("prefers detected timezone over host timezone", () => { const context = resolveTurnTimezoneContext({ detectedTimezone: "Asia/Tokyo", hostTimeZone: "Europe/London", }); expect(context.effectiveTimezone).toBe("Asia/Tokyo"); expect(context.source).toBe("detectedTimezone"); }); test("uses host timezone before UTC fallback", () => { const context = resolveTurnTimezoneContext({ hostTimeZone: "Europe/London", }); expect(context.effectiveTimezone).toBe("Europe/London"); expect(context.source).toBe("hostTimezone"); }); test("falls back to UTC when no timezone resolves", () => { const context = resolveTurnTimezoneContext({ configuredUserTimeZone: "not-a-timezone", clientTimezone: "also-invalid", detectedTimezone: "", hostTimeZone: "still-invalid", }); expect(context.effectiveTimezone).toBe("UTC"); expect(context.source).toBe("utcFallback"); }); test("ignores empty strings during runtime resolution", () => { const context = resolveTurnTimezoneContext({ configuredUserTimeZone: "", clientTimezone: "", detectedTimezone: "", hostTimeZone: "UTC", }); expect(context.configuredUserTimezone).toBeNull(); expect(context.clientTimezone).toBeNull(); expect(context.detectedTimezone).toBeNull(); expect(context.effectiveTimezone).toBe("UTC"); expect(context.source).toBe("hostTimezone"); }); test("does not use recalled profile timezone in normal turn precedence", () => { const context = resolveTurnTimezoneContext({ userTimeZone: "Asia/Tokyo", hostTimeZone: "UTC", }); expect(context.effectiveTimezone).toBe("UTC"); expect(context.source).toBe("hostTimezone"); }); }); // --------------------------------------------------------------------------- // formatTurnTimestamp // --------------------------------------------------------------------------- describe("formatTurnTimestamp", () => { /** 2026-04-02 06:52:33 UTC (Thursday) */ const THU_APR_02_0652 = Date.UTC(2026, 3, 2, 6, 52, 33); test("includes seconds in the timestamp", () => { const result = formatTurnTimestamp({ nowMs: THU_APR_02_0652, timeZone: "America/Chicago", }); expect(result).toContain("01:52:33"); }); test("timezone name appears in parentheses", () => { const result = formatTurnTimestamp({ nowMs: THU_APR_02_0652, timeZone: "America/Chicago", }); expect(result).toContain("(America/Chicago)"); }); test("produces expected full format", () => { const result = formatTurnTimestamp({ nowMs: THU_APR_02_0652, timeZone: "America/Chicago", }); expect(result).toBe( "2026-04-02 (Thursday) 01:52:33 -05:00 (America/Chicago)", ); }); test("handles UTC fallback when no timezone provided", () => { const result = formatTurnTimestamp({ nowMs: THU_APR_02_0652, hostTimeZone: "UTC", }); expect(result).toBe("2026-04-02 (Thursday) 06:52:33 +00:00 (UTC)"); }); test("handles configured user timezone override", () => { const result = formatTurnTimestamp({ nowMs: THU_APR_02_0652, hostTimeZone: "UTC", configuredUserTimeZone: "Asia/Tokyo", }); expect(result).toBe("2026-04-02 (Thursday) 15:52:33 +09:00 (Asia/Tokyo)"); }); test("uses client timezone when no configured override exists", () => { const result = formatTurnTimestamp({ nowMs: THU_APR_02_0652, hostTimeZone: "UTC", clientTimezone: "Asia/Tokyo", }); expect(result).toBe("2026-04-02 (Thursday) 15:52:33 +09:00 (Asia/Tokyo)"); }); test("handles DST correctly", () => { // Jul 1 12:00:30 UTC = Jul 1 08:00:30 EDT (Eastern Daylight Time, -04:00) const summerWithSeconds = Date.UTC(2026, 6, 1, 12, 0, 30); const result = formatTurnTimestamp({ nowMs: summerWithSeconds, timeZone: "America/New_York", }); expect(result).toBe( "2026-07-01 (Wednesday) 08:00:30 -04:00 (America/New_York)", ); }); test("formats midnight as 00", () => { // 2026-02-19 00:00:15 UTC const justAfterMidnight = Date.UTC(2026, 1, 19, 0, 0, 15); const result = formatTurnTimestamp({ nowMs: justAfterMidnight, timeZone: "UTC", }); expect(result).toContain("00:00:15"); expect(result).not.toContain("24:"); }); }); // --------------------------------------------------------------------------- // formatLocalTimestamp // --------------------------------------------------------------------------- describe("formatLocalTimestamp", () => { // 2026-05-11T17:30:00Z is 10:30 PDT (UTC-7, DST in effect). const utcMidday = Date.parse("2026-05-11T17:30:00Z"); test("defaults to UTC when no timeZone is provided", () => { expect(formatLocalTimestamp(utcMidday)).toBe("2026-05-11 17:30:00"); }); test("explicit UTC matches the no-timezone default", () => { expect(formatLocalTimestamp(utcMidday, "UTC")).toBe("2026-05-11 17:30:00"); }); test("renders in America/Los_Angeles when configured", () => { expect(formatLocalTimestamp(utcMidday, "America/Los_Angeles")).toBe( "2026-05-11 10:30:00", ); }); test("renders in Asia/Tokyo when configured", () => { // UTC+9 — 17:30 UTC is 02:30 next-day local. expect(formatLocalTimestamp(utcMidday, "Asia/Tokyo")).toBe( "2026-05-12 02:30:00", ); }); test("handles a DST spring-forward boundary correctly", () => { // 2026-03-08 06:30:00Z is 01:30 EST. The 02:00 → 03:00 spring-forward // in America/New_York happens at 07:00 UTC. 07:30 UTC is 03:30 EDT. const beforeJump = Date.parse("2026-03-08T06:30:00Z"); const afterJump = Date.parse("2026-03-08T07:30:00Z"); expect(formatLocalTimestamp(beforeJump, "America/New_York")).toBe( "2026-03-08 01:30:00", ); expect(formatLocalTimestamp(afterJump, "America/New_York")).toBe( "2026-03-08 03:30:00", ); }); });