Files
multica/packages/core/issues/date.test.ts
Bohan Jiang 5900d8b637 fix(issues): make start_date/due_date timezone-stable calendar days (#3618) (#3692)
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925)

These fields are calendar days (the pickers offer no time-of-day), but were
stored as TIMESTAMPTZ. A client serializing local midnight via toISOString()
folded its timezone into the instant, so the day shifted by the local offset
(GH #3618). Migrate the columns to DATE and parse/serialize date-only
"YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the
UTC day) so older clients keep working.

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925)

Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of
toISOString(), and every read formats via the shared @multica/core/issues/date
helpers with timeZone:"UTC" so the day never shifts with the viewer's offset.
The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers,
quick-set menu, list/board/detail/activity, and the mobile due-date picker.

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925)

Review follow-ups on #3692:
- ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339
  to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight
  instant and rejects ambiguous ones loudly. Adds util unit tests.
- migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via
  AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too.
- Convert remaining date-change display sites to formatDateOnly: inbox detail
  label (web) and mobile activity + inbox labels (were new Date()+local format).
- CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 14:34:01 +08:00

67 lines
2.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
toDateOnly,
todayDateOnly,
addDaysDateOnly,
dateOnlyToUTCDate,
dateOnlyToLocalDate,
formatDateOnly,
isPastDateOnly,
} from "./date";
describe("issue date-only helpers", () => {
it("serializes a picked local day to YYYY-MM-DD with the local calendar", () => {
// A calendar picker hands back local midnight of the clicked day.
expect(toDateOnly(new Date(2026, 2, 1))).toBe("2026-03-01");
expect(toDateOnly(new Date(2026, 0, 5))).toBe("2026-01-05"); // zero-padded
});
it("formats a date-only string timezone-safely (no day shift)", () => {
// The bug: a calendar day must render as the same day in every timezone.
expect(
formatDateOnly("2026-03-01", { month: "short", day: "numeric" }, "en-US"),
).toBe("Mar 1");
expect(formatDateOnly("2026-03-01", undefined, "en-US")).toBe("Mar 1");
expect(formatDateOnly(null)).toBe("");
expect(formatDateOnly("")).toBe("");
});
it("round-trips a picked day back to the same displayed day", () => {
const picked = new Date(2026, 2, 1); // user clicks March 1 locally
const stored = toDateOnly(picked);
expect(stored).toBe("2026-03-01");
expect(formatDateOnly(stored, { month: "short", day: "numeric" }, "en-US")).toBe(
"Mar 1",
);
});
it("anchors a date-only value at UTC midnight", () => {
expect(dateOnlyToUTCDate("2026-03-01")?.toISOString()).toBe(
"2026-03-01T00:00:00.000Z",
);
expect(dateOnlyToUTCDate(null)).toBeNull();
});
it("tolerates a legacy RFC3339 instant by reading its UTC day", () => {
// Old clients stored local-midnight-as-UTC; read the stored UTC calendar day.
expect(dateOnlyToUTCDate("2026-02-28T16:00:00Z")?.toISOString()).toBe(
"2026-02-28T00:00:00.000Z",
);
});
it("builds a local-midnight Date for the picker's selected day", () => {
const d = dateOnlyToLocalDate("2026-03-01");
expect(d?.getFullYear()).toBe(2026);
expect(d?.getMonth()).toBe(2);
expect(d?.getDate()).toBe(1);
expect(dateOnlyToLocalDate(null)).toBeUndefined();
});
it("detects past calendar days relative to today", () => {
expect(isPastDateOnly(addDaysDateOnly(-1))).toBe(true);
expect(isPastDateOnly(todayDateOnly())).toBe(false);
expect(isPastDateOnly(addDaysDateOnly(1))).toBe(false);
expect(isPastDateOnly(null)).toBe(false);
});
});