Newer
Older
client_dwatch / lib / db / __tests__ / error-report-mapping.test.ts
/**
 * `lib/db/error-report-mapping.ts` の期待値表(指示書AW §3 / 段階1-1・段階3-6-1 / CLAUDE.md §9.4)
 *
 * ★表1 は監督確定(D-160 / 指示書AW §3)。変えないこと★
 * ★表3 は指示書AW で導いたもの(監督確認済み・正)★(ラベルのテストは `lib/error-reports/__tests__/label.test.ts`)
 *
 * 表1(★監督確定・正・変えない★)
 * | #  | 行                                                       | 既定 | 解消も表示 | 紐づく予定                 | 検知時刻 |
 * |----|----------------------------------------------------------|------|------------|----------------------------|----------|
 * | E1 | status 0・本清掃の予定に紐づく                           | 出る | 出る       | 本清掃                     | errored_at |
 * | E2 | status 1・事前チェック(precheck=30)に紐づく            | 出る | 出る       | 事前チェック(30分前)     | errored_at |
 * | E3 | status 2・本清掃                                         | 出ない | 出る     | 本清掃                     | errored_at |
 * | E4 | status 0・clean_timetable_id NULL                        | 出る | 出る       | 予定なし                   | errored_at |
 * | E5 | status 0・clean_timetable_id が両方の予定表に無い id     | 出る | 出る       | 予定不明                   | errored_at |
 * | E6 | status 0・予定が clean_timetable_histories にだけある    | 出る | 出る       | 本清掃(履歴の予定も引く) | errored_at |
 * | E7 | status 0・errored_at NULL                                | 出る | 出る       | (予定どおり)             | created_at |
 * | E8 | status 0・robot_id NULL・serial_number '0'・SECURITY_ON  | 出る | 出る       | (予定どおり)             | errored_at。シリアル「0」・種類「SECURITY_ON」のまま |
 * | E9 | status 0・precheck=10 の予定に紐づく                     | 出る | 出る       | 事前チェック(10分前)     | errored_at(設定の 60/30 に無い値でも生の分数) |
 * 並び順: 検知時刻の降順、同時刻は error_id の昇順。
 *
 * 表3(指示書AW で導いたもの。`errorScheduleLabel()`)
 * | #  | schedule_link / precheck_minutes | JP                     | EN(暫定) |
 * |----|----------------------------------|------------------------|------------|
 * | S1 | main / null                      | 本清掃                 | Main run |
 * | S2 | precheck / 30                    | 事前チェック(30分前) | Pre-check (30 min before) |
 * | S3 | precheck / 10                    | 事前チェック(10分前) | Pre-check (10 min before) |
 * | S4 | none / null                      | 予定なし               | No schedule |
 * | S5 | unknown / null                   | 予定不明               | Unknown schedule |
 * | S6 | precheck / null(契約では起きない)| 事前チェック          | Pre-check |
 *
 * ★DB 依存のテストは書かない★。すべて純粋関数への入出力。
 * ★E2 の 30 は `PRECHECK_MINUTES_BEFORE[1]` から読む。E9 の 10 は「設定に無い値」であることが本題なので直書きする★。
 * ★日時は `+09:00` 付きの ISO 文字列から `toDbJst(new Date(...))` で DB の壁時計値を作る★(Prisma が返す `Date` と同じ形)。
 */
import { describe, expect, it } from "vitest";
import { PRECHECK_MINUTES_BEFORE } from "@/lib/config";
import {
  errorReportItemFromRow,
  errorReportKeyFromRow,
  errorReportListWhere,
  errorScheduleLink,
  type ErrorReportRowSource,
  type ErrorTimetableRef,
} from "@/lib/db/error-report-mapping";
import { EMPTY_ERROR_LIST_INDEX, errorListIndex, type ErrorListRowSource } from "@/lib/db/error-list-source";
import { robotErrorStatusFromResolutionStatus } from "@/lib/db/precheck-mapping";
import { toDbJst } from "@/lib/format/date";
import type { ErrorReportDbQuery, ErrorScheduleLink } from "@/lib/types";

/** `+09:00` 付きの ISO 文字列 → DB の壁時計値の `Date`(Prisma が返す形) */
const db = (iso: string): Date => toDbJst(new Date(iso));

const PRECHECK_30 = PRECHECK_MINUTES_BEFORE[1];
// ★10 は設定(PRECHECK_MINUTES_BEFORE = 60/30)に無い値であることが本題なので直書きする★
//   (`lib/fixtures/prechecks.ts` の `20` と同じ扱い。D-105 項1: 生の分数を出す)
const PRECHECK_10 = 10;

/** 本表(clean_timetables) */
const MAIN_TABLE: ErrorTimetableRef[] = [
  { id: BigInt(100), precheck: 0 }, // 本清掃
  { id: BigInt(101), precheck: PRECHECK_30 }, // 事前チェック(30分前)
  { id: BigInt(109), precheck: PRECHECK_10 }, // 事前チェック(10分前。設定に無い値)
];
/** 履歴(clean_timetable_histories)。★E6 の予定はここにだけある★ */
const HISTORY_TABLE: ErrorTimetableRef[] = [{ id: BigInt(106), precheck: 0 }];

/** 予定の引き当て(★同じ id は本表が勝つ★。`.toString()` をキーにする。指示書AW 4-9) */
function timetableFor(id: bigint | null): ErrorTimetableRef | undefined {
  if (id === null) return undefined;
  const byId = new Map<string, ErrorTimetableRef>();
  for (const t of HISTORY_TABLE) byId.set(t.id.toString(), t);
  for (const t of MAIN_TABLE) byId.set(t.id.toString(), t);
  return byId.get(id.toString());
}

/** ★施設・清掃エリアの `deleted_at` は既定 null(生きている)★(指示書AX U8。表1 の9行はすべて生きている) */
const FACILITY_B = { facility_name: "テスト施設B", deleted_at: null };
/** 削除の日時(★過去の日時★。指示書AX 表5 の注意) */
const DELETED_AT = db("2026-10-01T10:00:00+09:00");

function row(overrides: Partial<ErrorReportRowSource> & Pick<ErrorReportRowSource, "error_id">): ErrorReportRowSource {
  return {
    clean_timetable_id: BigInt(100),
    serial_number: "PD-CC1-0001",
    facility_id: BigInt(1),
    facility: { facility_name: "テスト施設A", deleted_at: null },
    section: { deleted_at: null },
    error_type: "EmergencyStop",
    error_level: "ERROR",
    errored_at: db("2026-10-05T19:00:00+09:00"),
    created_at: db("2026-10-05T19:01:00+09:00"),
    status: 0,
    responded_at: null,
    resolved_at: null,
    comment: null,
    error_list_id: null,
    ...overrides,
  };
}

/**
 * 表1 の9行。
 * ★E7 の `created_at`(10/6 08:00)は9行の中で一番新しい★ → 並べ替えで E7 が先頭に来る
 *   (`nulls: "last"` の並べ替えだと最後に来るので取り違えを検出できる)。
 * ★E4 = "E-10"・E5 = "E-9" は同じ検知時刻★ → 辞書順で "E-10" が先(数値順なら逆)。
 * E7・E8 の「(予定どおり)」は本清掃にした。
 */
const E1 = row({ error_id: "E-1", errored_at: db("2026-10-05T19:05:00+09:00"), created_at: db("2026-10-05T19:06:00+09:00") });
const E2 = row({
  error_id: "E-2",
  clean_timetable_id: BigInt(101),
  error_type: "LowBattery",
  error_level: "WARNING",
  errored_at: db("2026-10-05T19:30:00+09:00"),
  created_at: db("2026-10-05T19:31:00+09:00"),
  status: robotErrorStatusFromResolutionStatus("in_progress"),
  responded_at: db("2026-10-06T09:00:00+09:00"),
});
const E3 = row({
  error_id: "E-3",
  errored_at: db("2026-10-05T20:00:00+09:00"),
  created_at: db("2026-10-05T20:01:00+09:00"),
  status: robotErrorStatusFromResolutionStatus("resolved"),
  responded_at: db("2026-10-06T09:00:00+09:00"),
  resolved_at: db("2026-10-06T10:00:00+09:00"),
  comment: "ブラシを清掃した",
});
const E4 = row({
  error_id: "E-10",
  clean_timetable_id: null,
  errored_at: db("2026-10-05T21:00:00+09:00"),
  created_at: db("2026-10-05T21:01:00+09:00"),
});
const E5 = row({
  error_id: "E-9",
  clean_timetable_id: BigInt(999), // 本表にも履歴にも無い
  errored_at: db("2026-10-05T21:00:00+09:00"),
  created_at: db("2026-10-05T21:02:00+09:00"),
});
const E6 = row({
  error_id: "E-6",
  clean_timetable_id: BigInt(106), // 履歴にだけある
  facility_id: BigInt(2),
  facility: FACILITY_B,
  errored_at: db("2026-10-05T20:30:00+09:00"),
  created_at: db("2026-10-05T20:31:00+09:00"),
});
const E7 = row({
  error_id: "E-7",
  facility_id: BigInt(2),
  facility: FACILITY_B,
  errored_at: null,
  created_at: db("2026-10-06T08:00:00+09:00"),
});
const E8 = row({
  error_id: "E-8",
  facility_id: BigInt(2),
  facility: FACILITY_B,
  serial_number: "0",
  error_type: "SECURITY_ON",
  error_level: "CRITICAL",
  errored_at: db("2026-10-05T21:30:00+09:00"),
  created_at: db("2026-10-05T21:31:00+09:00"),
});
const E9 = row({
  error_id: "E-11",
  clean_timetable_id: BigInt(109),
  facility_id: BigInt(2),
  facility: FACILITY_B,
  errored_at: db("2026-10-05T22:00:00+09:00"),
  created_at: db("2026-10-05T22:01:00+09:00"),
});

const TABLE1: [string, ErrorReportRowSource, ErrorScheduleLink, number | null, string, string, string, string][] = [
  // [#, 行, schedule_link, precheck_minutes, detected_at, serial_number, error_type, error_level]
  ["E1", E1, "main", null, "2026-10-05T19:05:00+09:00", "PD-CC1-0001", "EmergencyStop", "ERROR"],
  ["E2", E2, "precheck", PRECHECK_30, "2026-10-05T19:30:00+09:00", "PD-CC1-0001", "LowBattery", "WARNING"],
  ["E3", E3, "main", null, "2026-10-05T20:00:00+09:00", "PD-CC1-0001", "EmergencyStop", "ERROR"],
  ["E4", E4, "none", null, "2026-10-05T21:00:00+09:00", "PD-CC1-0001", "EmergencyStop", "ERROR"],
  ["E5", E5, "unknown", null, "2026-10-05T21:00:00+09:00", "PD-CC1-0001", "EmergencyStop", "ERROR"],
  ["E6", E6, "main", null, "2026-10-05T20:30:00+09:00", "PD-CC1-0001", "EmergencyStop", "ERROR"],
  ["E7", E7, "main", null, "2026-10-06T08:00:00+09:00", "PD-CC1-0001", "EmergencyStop", "ERROR"],
  ["E8", E8, "main", null, "2026-10-05T21:30:00+09:00", "0", "SECURITY_ON", "CRITICAL"],
  ["E9", E9, "precheck", PRECHECK_10, "2026-10-05T22:00:00+09:00", "PD-CC1-0001", "EmergencyStop", "ERROR"],
];

describe("errorReportItemFromRow(表1 E1〜E9)", () => {
  it.each(TABLE1)(
    "%s",
    (_no, source, link, minutes, detectedAt, serial, errorType, errorLevel) => {
      const item = errorReportItemFromRow(source, timetableFor(source.clean_timetable_id), EMPTY_ERROR_LIST_INDEX);
      expect(item.schedule_link).toBe(link);
      expect(item.precheck_minutes).toBe(minutes);
      expect(item.detected_at).toBe(detectedAt);
      expect(item.serial_number).toBe(serial);
      expect(item.error_type).toBe(errorType);
      expect(item.error_level).toBe(errorLevel);
    }
  );

  it("id は error_id の文字列のまま(Number() を通さない。D-105 項2)", () => {
    expect(errorReportItemFromRow(E4, undefined, EMPTY_ERROR_LIST_INDEX).id).toBe("E-10");
  });

  it("★BigInt を残さない★(facility_id / clean_timetable_id は number。JSON にできる)", () => {
    const item = errorReportItemFromRow(E1, timetableFor(E1.clean_timetable_id), EMPTY_ERROR_LIST_INDEX);
    expect(item.facility_id).toBe(1);
    expect(item.clean_timetable_id).toBe(100);
    expect(() => JSON.stringify(item)).not.toThrow();
    expect(errorReportItemFromRow(E4, undefined, EMPTY_ERROR_LIST_INDEX).clean_timetable_id).toBeNull();
  });

  it("★clean_timetable_id が 0 → 予定なし(none)・API の clean_timetable_id は null★(TeamWorks 回答 2026-10-27。NULL と同じ)", () => {
    const zero = { ...E4, clean_timetable_id: BigInt(0) };
    const item = errorReportItemFromRow(zero, undefined, EMPTY_ERROR_LIST_INDEX);
    expect(item.schedule_link).toBe("none");
    expect(item.precheck_minutes).toBeNull();
    expect(item.clean_timetable_id).toBeNull();
    // ★E4(NULL)と同じ行になる★
    expect(item).toEqual(errorReportItemFromRow(E4, undefined, EMPTY_ERROR_LIST_INDEX));
  });

  it("E5(予定不明)でも clean_timetable_id は載せる(調査用。D18)", () => {
    expect(errorReportItemFromRow(E5, undefined, EMPTY_ERROR_LIST_INDEX).clean_timetable_id).toBe(999);
  });

  it("施設名・対応状況・日時・備考を写す(E3: 解消済み)", () => {
    const item = errorReportItemFromRow(E3, timetableFor(E3.clean_timetable_id), EMPTY_ERROR_LIST_INDEX);
    expect(item.facility_name).toBe("テスト施設A");
    expect(item.resolution_status).toBe("resolved");
    expect(item.responded_at).toBe("2026-10-06T09:00:00+09:00");
    expect(item.resolved_at).toBe("2026-10-06T10:00:00+09:00");
    expect(item.note).toBe("ブラシを清掃した");
  });

  it("E2: 対応中・responded_at だけある", () => {
    const item = errorReportItemFromRow(E2, timetableFor(E2.clean_timetable_id), EMPTY_ERROR_LIST_INDEX);
    expect(item.resolution_status).toBe("in_progress");
    expect(item.responded_at).toBe("2026-10-06T09:00:00+09:00");
    expect(item.resolved_at).toBeNull();
    expect(item.note).toBeNull();
  });

  it("★status が 0/1/2 以外なら例外(fail-loud。一覧では1段目で外れるので来ない)★", () => {
    expect(() => errorReportItemFromRow({ ...E1, status: 3 }, timetableFor(E1.clean_timetable_id), EMPTY_ERROR_LIST_INDEX)).toThrow();
  });
});

// ---------------------------------------------------------------- エラー内容(指示書BD §3-2。D-184)

/**
 * 表 L(エラー内容の3項目。指示書BD §3-2 から導いた期待値)
 * | #  | 行の error_type        | error_list_id | 索引                      | error_list_id(出力) | error_list | error_list_selectable |
 * |----|------------------------|---------------|---------------------------|------------------------|------------|-----------------------|
 * | L1 | EmergencyStop          | null          | —                         | null                   | null       | true  |
 * | L2 | EmergencyStop          | 40            | 40 あり(有効)           | 40                     | 40 の行    | true  |
 * | L3 | EmergencyStop          | 9999          | 9999 なし                 | 9999                   | null(不明)| true |
 * | L4 | EmergencyStop          | 7             | 7 あり(★inactive=1★)   | 7                      | 7 の行(★絞らない★) | true |
 * | L5 | EmergencyStop          | 8             | 8 あり(★deleted_at あり★)| 8                    | 8 の行(★絞らない★) | true |
 * | L6 | NOT_STARTED            | 131           | 131 あり                  | 131                    | 131 の行・note_required true | true |
 * | L7 | D-S_CONTROL_FAILURE    | null          | —                         | null                   | null       | false |
 * | L8 | SECURITY_ON            | null          | —                         | null                   | null       | false |
 */
function listRow(id: number, overrides: Partial<ErrorListRowSource> = {}): ErrorListRowSource {
  return {
    id: BigInt(id),
    error_category_main_en: `MAIN-EN-${id}`,
    error_category_main_ja: `分類${id}`,
    error_category_sub_en: `sub-en-${id}`,
    error_category_sub_ja: `小分類${id}`,
    error_detail_en: `detail-en-${id}`,
    error_detail_ja: `詳細${id}`,
    inactive: 0,
    deleted_at: null,
    ...overrides,
  };
}

const LIST_INDEX = errorListIndex([
  listRow(40),
  listRow(7, { inactive: 1 }),
  listRow(8, { deleted_at: DELETED_AT }),
  listRow(131),
]);

describe("errorReportItemFromRow — エラー内容(表 L。指示書BD §3-2)", () => {
  const pick = (source: ErrorReportRowSource) => {
    const item = errorReportItemFromRow(source, timetableFor(source.clean_timetable_id), LIST_INDEX);
    return {
      error_list_id: item.error_list_id,
      error_list_resolved_id: item.error_list?.id ?? null,
      error_list_selectable: item.error_list_selectable,
    };
  };

  it.each([
    ["L1", row({ error_id: "L-1" }), null, null, true],
    ["L2", row({ error_id: "L-2", error_list_id: BigInt(40) }), 40, 40, true],
    ["L3", row({ error_id: "L-3", error_list_id: BigInt(9999) }), 9999, null, true],
    ["L4", row({ error_id: "L-4", error_list_id: BigInt(7) }), 7, 7, true],
    ["L5", row({ error_id: "L-5", error_list_id: BigInt(8) }), 8, 8, true],
    ["L6", row({ error_id: "L-6", error_type: "NOT_STARTED", error_list_id: BigInt(131) }), 131, 131, true],
    ["L7", row({ error_id: "L-7", error_type: "D-S_CONTROL_FAILURE" }), null, null, false],
    ["L8", row({ error_id: "L-8", error_type: "SECURITY_ON" }), null, null, false],
  ] as const)("%s", (_no, source, listId, resolvedId, selectable) => {
    expect(pick(source)).toEqual({
      error_list_id: listId,
      error_list_resolved_id: resolvedId,
      error_list_selectable: selectable,
    });
  });

  it("L2: error_list は索引の行をそのまま(文字列は変換しない・note_required false)・BigInt を残さない", () => {
    const item = errorReportItemFromRow(row({ error_id: "L-2", error_list_id: BigInt(40) }), undefined, LIST_INDEX);
    expect(item.error_list).toEqual({
      id: 40,
      error_category_main_en: "MAIN-EN-40",
      error_category_main_ja: "分類40",
      error_category_sub_en: "sub-en-40",
      error_category_sub_ja: "小分類40",
      error_detail_en: "detail-en-40",
      error_detail_ja: "詳細40",
      note_required: false,
    });
    expect(typeof item.error_list_id).toBe("number");
    expect(() => JSON.stringify(item)).not.toThrow();
  });

  it("L6: 131 は note_required true", () => {
    const item = errorReportItemFromRow(row({ error_id: "L-6", error_list_id: BigInt(131) }), undefined, LIST_INDEX);
    expect(item.error_list?.note_required).toBe(true);
  });

  it("★空の索引なら、有効な id でも error_list は null(索引を渡し忘れると「不明」に化ける — 呼び出し側の責務)★", () => {
    const item = errorReportItemFromRow(row({ error_id: "L-2", error_list_id: BigInt(40) }), undefined, EMPTY_ERROR_LIST_INDEX);
    expect(item).toMatchObject({ error_list_id: 40, error_list: null });
  });
});

describe("errorScheduleLink(指示書AW D4)", () => {
  it("clean_timetable_id が NULL → none", () => {
    expect(errorScheduleLink(null, undefined)).toEqual({ schedule_link: "none", precheck_minutes: null });
  });

  it("★clean_timetable_id が 0 → none★(予定外。TeamWorks 回答 2026-10-27。unknown にしない)", () => {
    expect(errorScheduleLink(BigInt(0), undefined)).toEqual({ schedule_link: "none", precheck_minutes: null });
    // ★id=0 の予定が渡されても予定外のまま★(本清掃・事前チェックとして扱わない。事前チェックのアラートにもならない)
    expect(errorScheduleLink(BigInt(0), { id: BigInt(0), precheck: 0 })).toEqual({
      schedule_link: "none",
      precheck_minutes: null,
    });
    expect(errorScheduleLink(BigInt(0), { id: BigInt(0), precheck: PRECHECK_30 })).toEqual({
      schedule_link: "none",
      precheck_minutes: null,
    });
  });

  it("予定が見つからない(undefined)→ unknown", () => {
    expect(errorScheduleLink(BigInt(999), undefined)).toEqual({ schedule_link: "unknown", precheck_minutes: null });
  });

  it("本清掃 → main", () => {
    expect(errorScheduleLink(BigInt(100), { id: BigInt(100), precheck: 0 })).toEqual({
      schedule_link: "main",
      precheck_minutes: null,
    });
  });

  it("PRECHECK_MINUTES_BEFORE[1] → precheck・その分数", () => {
    expect(errorScheduleLink(BigInt(101), { id: BigInt(101), precheck: PRECHECK_30 })).toEqual({
      schedule_link: "precheck",
      precheck_minutes: PRECHECK_30,
    });
  });

  it("★設定に無い 10 → precheck・10(生の分数)★", () => {
    expect(errorScheduleLink(BigInt(109), { id: BigInt(109), precheck: PRECHECK_10 })).toEqual({
      schedule_link: "precheck",
      precheck_minutes: 10,
    });
  });

  it("★予定の id と clean_timetable_id が違う → 例外(呼び出しの誤り)★", () => {
    expect(() => errorScheduleLink(BigInt(100), { id: BigInt(101), precheck: 0 })).toThrow();
  });
});

describe("errorReportKeyFromRow", () => {
  it("detected_at は errored_at(壁時計値のまま +09:00 の ISO)", () => {
    const key = errorReportKeyFromRow({
      ...E1,
      error_id: "K-1",
      errored_at: toDbJst(new Date("2026-10-05T19:05:00+09:00")),
      created_at: toDbJst(new Date("2026-10-05T19:06:00+09:00")),
    });
    expect(key.detected_at).toBe("2026-10-05T19:05:00+09:00");
  });

  it.each(TABLE1)("%s: detected_at が表1 どおり(E7 だけ created_at)", (_no, source, _l, _m, detectedAt) => {
    expect(errorReportKeyFromRow(source).detected_at).toBe(detectedAt);
  });

  it("キーの形(facility_id は number・error_id は string のまま)", () => {
    expect(errorReportKeyFromRow(E8)).toEqual({
      error_id: "E-8",
      facility_id: 2,
      serial_number: "0",
      error_type: "SECURITY_ON",
      error_level: "CRITICAL",
      detected_at: "2026-10-05T21:30:00+09:00",
      status: 0,
      note: null,
      scheduled: true,
      visible: true,
    });
  });

  it("★status に 3 が入った行でも例外にならない(キーは生の数値を持つ。表2 F12)★", () => {
    const key = errorReportKeyFromRow({ ...E1, status: 3 });
    expect(key.status).toBe(3);
  });

  // ★指示書AX 表5 の可視性(U8・D12)。外すのは `filterSortErrorReportKeys()`★
  it.each([
    ["V1: 施設・清掃エリアとも生きている", null, null, true],
    ["V2: 施設が削除済み", DELETED_AT, null, false],
    ["V3: 清掃エリアが削除済み", null, DELETED_AT, false],
    ["V7: 施設・清掃エリアとも削除済み", DELETED_AT, DELETED_AT, false],
  ] as const)("%s → visible %s", (_label, facilityDeleted, sectionDeleted, expected) => {
    const key = errorReportKeyFromRow({
      ...E1,
      facility: { ...E1.facility, deleted_at: facilityDeleted },
      section: { deleted_at: sectionDeleted },
    });
    expect(key.visible).toBe(expected);
  });
});

describe("errorReportListWhere(SQL 側の写し。指示書AW 段階3-6-1)", () => {
  const base: ErrorReportDbQuery = {
    include_resolved: false,
    facility_ids: null,
    date_from: null,
    date_to: null,
    error_type: "",
    sort: null,
    order: "asc",
    schedule: null,
    page: 1,
    per_page: 50,
  };
  const OPEN_CODES = (["unresolved", "in_progress"] as const).map(robotErrorStatusFromResolutionStatus);
  const ALL_CODES = (["unresolved", "in_progress", "resolved"] as const).map(robotErrorStatusFromResolutionStatus);
  /**
   * ★施設・清掃エリアの可視性(指示書AX U8。`ERROR_ROW_VISIBLE_WHERE` の2キー)は、どのケースでも入る★。
   * ★表1 E1〜E9 の期待値は変えない★(全行が生きている)
   */
  const VISIBLE = { facility: { deleted_at: null }, section: { deleted_at: null } };

  it("既定 → status: { in: [未解消, 対応中] } と施設・清掃エリアの可視性", () => {
    expect(errorReportListWhere(base)).toEqual({ status: { in: OPEN_CODES }, ...VISIBLE });
  });

  it("include_resolved=true → 解消のコードも入る", () => {
    expect(errorReportListWhere({ ...base, include_resolved: true })).toEqual({ status: { in: ALL_CODES }, ...VISIBLE });
  });

  it("★facility_ids: [] → facility_id: { in: [] }(キーが消えない=0件。罠1)★", () => {
    expect(errorReportListWhere({ ...base, facility_ids: [] })).toEqual({
      status: { in: OPEN_CODES },
      ...VISIBLE,
      facility_id: { in: [] },
    });
  });

  it("facility_ids: [1, 2] → BigInt で in", () => {
    expect(errorReportListWhere({ ...base, facility_ids: [1, 2] })).toEqual({
      status: { in: OPEN_CODES },
      ...VISIBLE,
      facility_id: { in: [BigInt(1), BigInt(2)] },
    });
  });

  it("facility_ids: null → facility_id のキーが無い", () => {
    expect(errorReportListWhere(base)).not.toHaveProperty("facility_id");
  });

  it("★期間あり → OR の2要素(errored_at の範囲 / errored_at が NULL なら created_at の範囲)。値は壁時計値★", () => {
    const range = {
      gte: new Date("2026-10-05T00:00:00.000Z"),
      lt: new Date("2026-10-06T00:00:00.000Z"),
    };
    expect(errorReportListWhere({ ...base, date_from: "2026-10-05", date_to: "2026-10-05" })).toEqual({
      status: { in: OPEN_CODES },
      ...VISIBLE,
      OR: [{ errored_at: range }, { errored_at: null, created_at: range }],
    });
  });

  it("下限だけ → OR の範囲は gte だけ", () => {
    const range = { gte: new Date("2026-10-05T00:00:00.000Z") };
    expect(errorReportListWhere({ ...base, date_from: "2026-10-05" })).toEqual({
      status: { in: OPEN_CODES },
      ...VISIBLE,
      OR: [{ errored_at: range }, { errored_at: null, created_at: range }],
    });
  });

  it("期間なし → OR のキーが無い", () => {
    expect(errorReportListWhere(base)).not.toHaveProperty("OR");
  });

  it("★error_type・page・per_page は SQL に降ろさない(D8 / D9)★", () => {
    expect(errorReportListWhere({ ...base, error_type: "secur", page: 3, per_page: 10 })).toEqual({
      status: { in: OPEN_CODES },
      ...VISIBLE,
    });
  });
});