dayBoundary.test.ts4.2 KBView on GitHub
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import {
  localDayKey,
  msUntilNextLocalMidnight,
} from '@/modules/userTasks/hooks/use-day-boundary';
import { endOfToday } from '@/modules/userTasks/utils/task-filters';

/**
 * Every task surface splits "due now" from "upcoming" at `endOfToday()`, which reads the clock at
 * call time — inside a `useMemo` whose dependencies are all DATA. Nothing in those arrays moves
 * when the day does, so a tab left open holds the boundary it was opened with. Past midnight,
 * everything due TODAY reads as `> cutoff` and files under Upcoming, out of its own group column,
 * which then collapses into the hidden-columns rail for being empty.
 *
 * `useDayKey` is the missing dependency. Its two pure halves are tested here; that the surfaces
 * actually depend on it is asserted structurally below, because a dependency array is exactly the
 * kind of thing that gets dropped in a refactor and cannot fail a behavioural test.
 */
describe('local day boundary', () => {
  describe('localDayKey', () => {
    it('is the LOCAL calendar day, not the UTC one', () => {
      // 2026-09-01T02:30:00Z is still 31 August in every US timezone. Reading the UTC day here
      // would disagree with endOfToday(), which is local — and a boundary that two functions
      // disagree about is the whole bug class this hook exists for.
      const evening = new Date(2026, 7, 31, 20, 30, 0); // 31 Aug, 20:30 local
      expect(localDayKey(evening)).toBe('2026-08-31');
    });

    it('zero-pads, so keys compare as strings in calendar order', () => {
      expect(localDayKey(new Date(2026, 0, 5, 12, 0, 0))).toBe('2026-01-05');
    });

    it('changes exactly once across midnight', () => {
      const before = new Date(2026, 8, 1, 23, 59, 59);
      const after = new Date(2026, 8, 2, 0, 0, 1);
      expect(localDayKey(before)).toBe('2026-09-01');
      expect(localDayKey(after)).toBe('2026-09-02');
    });

    it('names the same day endOfToday() bounds', () => {
      const cutoff = new Date(endOfToday());
      expect(localDayKey(cutoff)).toBe(localDayKey());
    });
  });

  describe('msUntilNextLocalMidnight', () => {
    it('lands just AFTER the boundary, never on it', () => {
      // Firing exactly on midnight can round a millisecond early, recompute the same day, and
      // re-arm for ~0ms — a tight loop for the rest of the second. The slack is what prevents it.
      const now = new Date(2026, 8, 1, 23, 59, 59, 0);
      const delay = msUntilNextLocalMidnight(now);
      const firesAt = new Date(now.getTime() + delay);
      expect(firesAt.getTime()).toBeGreaterThan(new Date(2026, 8, 2, 0, 0, 0, 0).getTime());
      expect(localDayKey(firesAt)).toBe('2026-09-02');
    });

    it('is always positive, including at the first millisecond of a day', () => {
      expect(msUntilNextLocalMidnight(new Date(2026, 8, 1, 0, 0, 0, 0))).toBeGreaterThan(0);
      expect(msUntilNextLocalMidnight(new Date(2026, 8, 1, 12, 0, 0, 0))).toBeGreaterThan(0);
      expect(msUntilNextLocalMidnight(new Date(2026, 8, 1, 23, 59, 59, 999))).toBeGreaterThan(0);
    });

    it('never schedules more than a day and a half out', () => {
      for (let hour = 0; hour < 24; hour++) {
        const delay = msUntilNextLocalMidnight(new Date(2026, 8, 1, hour, 30, 0));
        expect(delay).toBeLessThan(36 * 60 * 60 * 1000);
      }
    });
  });

  /**
   * Structural, deliberately. `[..., dayKey]` is a dependency array — dropping it changes no type,
   * fails no render test, and silently restores a bug that only appears if you leave a tab open
   * overnight, which no test suite does.
   */
  describe('the surfaces that bucket by today depend on it', () => {
    const read = (relative: string) =>
      readFileSync(join(__dirname, '..', '..', '..', 'modules', 'userTasks', relative), 'utf8');

    const SURFACES = [
      'components/TaskKanbanBoard.tsx',
      'components/TaskListView.tsx',
      'hooks/use-task-group-buckets.ts',
    ];

    it.each(SURFACES)('%s calls useDayKey and puts it in a dependency array', (relative) => {
      const source = read(relative);
      expect(source).toContain('useDayKey');
      expect(source).toMatch(/dayKey\]/);
    });
  });
});