Newer
Older
client_dwatch / lib / db / __tests__ / date-range.test.ts
import { describe, expect, it } from "vitest";

import { dbDateRange } from "@/lib/db/date-range";
import { fromDbJst, jstIsoFromDb, toDbJst } from "@/lib/format/date";
import { parseDate } from "@/lib/clean-logs/query";

/**
 * 指示書AC 段階2 ★D-2 / C-6★ — 日付境界の補正。
 *
 * ------------------------------------------------------------------------
 * ★★このテストがある理由 — ローカルDBでは間違いを検出できないから★★
 * ------------------------------------------------------------------------
 * `clean_timetables.start_at` は ★日本時間の壁時計値★ が入った DATETIME(D-115)。
 * 一方 `parseDate("2026-09-08")` が返すのは ★真の瞬間★(= `2026-09-07T15:00:00Z`)。
 * ★そのまま `where` に渡すと9時間ずれます★。
 *
 * ★★ところがローカルDB(`dwatch_local`)の `start_at`(壁時計)は
 *   0時:155 / 1時:185 / 2時:168 / 3時:30 で、★15時以降が0件★★★
 *   (2026-09-13 本作業の実測。指示書AC C-6 の実測とも一致)。
 *   ★9時間ずらしても日付が変わらない時間帯にしかデータが無い★ため、
 *   ★★補正を忘れてもローカルでは件数が1件も変わりません★★。
 *
 * ★★だから「壁時計 20:00 の清掃」を合成データで必ず通すこと★★(期待値表 #7)。
 * ★このテストを消さないこと★。
 */

/**
 * DB に入っているのと同じ「壁時計値としての `Date`」を作る
 * (`lib/db/__tests__/consecutive-source.test.ts` と同じヘルパー)。
 * ★`fromDbJst()` を通すと元の ISO に戻る★のが正しい状態。
 */
function dbWallClock(jstIso: string): Date {
  return new Date(jstIso.replace("+09:00", "Z"));
}

/** Prisma の `start_at: { gte, lt }` と同じ判定(★DBがやることの写し★) */
function matchesRange(
  startAt: Date,
  range: { gte?: Date; lt?: Date }
): boolean {
  if (range.gte && startAt.getTime() < range.gte.getTime()) return false;
  if (range.lt && startAt.getTime() >= range.lt.getTime()) return false;
  return true;
}

describe("dbDateRange(指示書AC 段階2 D-2 / C-6)", () => {
  it("下限は `toDbJst()` を通した壁時計値になる(★補正を忘れると9時間ずれる★)", () => {
    const range = dbDateRange("2026-09-08", null);

    // ★指示書AC C-6 の実測値をそのまま固定する★
    expect(parseDate("2026-09-08")?.toISOString()).toBe("2026-09-07T15:00:00.000Z");
    expect(range.gte?.toISOString()).toBe("2026-09-08T00:00:00.000Z");
    // ★★`parseDate()` の結果をそのまま渡していないこと★★
    expect(range.gte?.getTime()).not.toBe(parseDate("2026-09-08")?.getTime());
    expect(range.lt).toBeUndefined();
  });

  it("上限はその日を**含む**(=翌日 00:00 未満)", () => {
    const range = dbDateRange(null, "2026-09-10");
    expect(range.gte).toBeUndefined();
    expect(range.lt?.toISOString()).toBe("2026-09-11T00:00:00.000Z");
  });

  it("どちらも `null` なら空オブジェクト(★呼び出し側は `start_at` のキーごと付けない★)", () => {
    expect(dbDateRange(null, null)).toEqual({});
  });

  it("形式が不正な文字列は「指定なし」として扱う(★検証は `parseCleanLogQuery()` の仕事★)", () => {
    // ★ここで422を返さないこと★ — 2箇所で検証すると片方だけ緩めたときに気づけない
    expect(dbDateRange("2026-9-8", "")).toEqual({});
    expect(dbDateRange("not-a-date", "2026-13-45")).toEqual({});
  });

  it("月をまたぐ範囲でも境界がずれない", () => {
    const range = dbDateRange("2026-08-30", "2026-09-01");
    expect(range.gte?.toISOString()).toBe("2026-08-30T00:00:00.000Z");
    expect(range.lt?.toISOString()).toBe("2026-09-02T00:00:00.000Z");
  });

  describe("★#7 壁時計 20:00 の清掃★(★ローカルDBには1件も無い時間帯★)", () => {
    /** ★9時間ずらすと日付が翌日に化ける行★ */
    const evening = dbWallClock("2026-09-10T20:00:00+09:00");

    it("その行が JST の 9/10 であることを前提として確認する", () => {
      expect(jstIsoFromDb(evening)).toBe("2026-09-10T20:00:00+09:00");
    });

    it("`date_from = date_to = 2026-09-10` の範囲に**含まれる**", () => {
      expect(matchesRange(evening, dbDateRange("2026-09-10", "2026-09-10"))).toBe(true);
    });

    it("前日・翌日だけを指定した範囲には**含まれない**", () => {
      expect(matchesRange(evening, dbDateRange("2026-09-09", "2026-09-09"))).toBe(false);
      expect(matchesRange(evening, dbDateRange("2026-09-11", "2026-09-11"))).toBe(false);
    });

    it("★★`toDbJst()` を忘れた実装なら、この行は 9/10 の範囲から落ちる★★(=検出できる)", () => {
      // ★補正を忘れた実装★(`parseDate()` の結果をそのまま `where` に渡す)
      const buggy = {
        gte: parseDate("2026-09-10") as Date,
        lt: new Date((parseDate("2026-09-10") as Date).getTime() + 86_400_000),
      };
      // ★9時間ずれるので 20:00 の行は範囲外になる(=画面から静かに消える)★
      expect(matchesRange(evening, buggy)).toBe(false);
      // ★深夜2時の行なら補正を忘れても落ちない=ローカルDBでは気づけない★
      const midnight = dbWallClock("2026-09-10T02:00:00+09:00");
      expect(matchesRange(midnight, buggy)).toBe(true);
      expect(matchesRange(midnight, dbDateRange("2026-09-10", "2026-09-10"))).toBe(true);
    });
  });

  it("境界ちょうど(00:00:00 と 23:59:59)が両端とも含まれる", () => {
    const range = dbDateRange("2026-09-10", "2026-09-10");
    expect(matchesRange(dbWallClock("2026-09-10T00:00:00+09:00"), range)).toBe(true);
    expect(matchesRange(dbWallClock("2026-09-10T23:59:59+09:00"), range)).toBe(true);
    expect(matchesRange(dbWallClock("2026-09-09T23:59:59+09:00"), range)).toBe(false);
    expect(matchesRange(dbWallClock("2026-09-11T00:00:00+09:00"), range)).toBe(false);
  });

  it("`toDbJst()` / `fromDbJst()` の往復で元に戻る(★新しい変換を書いていないことの確認★)", () => {
    const from = parseDate("2026-09-08") as Date;
    expect(fromDbJst(toDbJst(from)).getTime()).toBe(from.getTime());
    expect(dbDateRange("2026-09-08", null).gte?.getTime()).toBe(toDbJst(from).getTime());
  });
});