import { describe, expect, it } from "vitest";
import { filterFixturePrechecks } from "@/lib/db/fixture-prechecks";
import { prechecks } from "@/lib/fixtures";
import { filterPrechecksByDate, type PrecheckQuery } from "@/lib/prechecks/query";
import type { PrecheckDbQuery } from "@/lib/types";
/**
* 指示書AC ★段階4★ — fixtures 版も同じ引数を受け取り、同じ結果を返す(段階2 D-5 の作法)。
*
* ★★`USE_FIXTURES` の true/false で件数が変わってはいけない★★(指示書AC 禁止事項6)。
* ★絞り込み規則の「正」は `filterPrechecksByDate()`★ — ここはその写し(C-5)。
*
* ★fixtures の**既存の値**は変更していない★(禁止事項5)— ここは読むだけ。
*/
const ALL: PrecheckDbQuery = { date_from: null, date_to: null, facility_ids: null };
function memoryQuery(partial: Partial<PrecheckQuery>): PrecheckQuery {
return {
dateFrom: null,
dateTo: null,
status: "all",
// ★絞り込まない(指示書AK 段階2 で足った2つ。`null` = 条件なし)★
groupId: null,
customerId: null,
page: 1,
perPage: 50,
...partial,
};
}
/** fixtures の中でデータが載っている日(`at()` が相対日で作るので実行日から決める) */
const days = [...new Set(prechecks.map((r) => r.scheduled_at.slice(0, 10)))].sort();
/** `severity` ごとの件数(★片方だけ絞れている壊れ方の検出★) */
function bySeverity(items: { severity: string }[]): Record<string, number> {
const out: Record<string, number> = {};
for (const item of items) out[item.severity] = (out[item.severity] ?? 0) + 1;
return out;
}
describe("filterFixturePrechecks(指示書AC 段階4)", () => {
it("#1 範囲なし(全部 null)→ fixtures の全件", () => {
expect(filterFixturePrechecks(prechecks, ALL)).toHaveLength(prechecks.length);
});
it("★★#2 日付範囲 → `filterPrechecksByDate()` と完全一致★★", () => {
for (const day of days) {
const sql = filterFixturePrechecks(prechecks, { ...ALL, date_from: day, date_to: day });
const memory = filterPrechecksByDate(
prechecks,
memoryQuery({ dateFrom: day, dateTo: day })
);
expect(sql).toEqual(memory);
expect(sql.length).toBeGreaterThan(0);
}
});
it("#2 複数日の範囲でも一致する", () => {
const sql = filterFixturePrechecks(prechecks, {
...ALL,
date_from: days[0],
date_to: days[days.length - 1],
});
expect(sql).toEqual(
filterPrechecksByDate(
prechecks,
memoryQuery({ dateFrom: days[0], dateTo: days[days.length - 1] })
)
);
expect(sql).toHaveLength(prechecks.length);
});
it("★★#3 `severity` の内訳が全件版と一致する★★", () => {
for (const day of days) {
const sql = filterFixturePrechecks(prechecks, { ...ALL, date_from: day, date_to: day });
const memory = filterPrechecksByDate(
prechecks,
memoryQuery({ dateFrom: day, dateTo: day })
);
expect(bySeverity(sql)).toEqual(bySeverity(memory));
}
// ★全件は事前チェック異常だけ★(ユーザー判断 2026-10-29。旧: `launch_failed`(出発失敗)もそろっていた)
expect(Object.keys(bySeverity([...prechecks]))).toEqual(["precheck"]);
});
it("★★#4 絞り込みの基準は `scheduled_at`(=本清掃の予定時刻)★★。`detected_at` ではない", () => {
// ★`severity: "precheck"` の行は `detected_at` が予定時刻の60分前★ —
// `detected_at` で絞ると、日をまたぐ行(0時台の清掃の1時間前=前日)が消える。
const crossingMidnight = prechecks.filter(
(r) => r.detected_at.slice(0, 10) !== r.scheduled_at.slice(0, 10)
);
for (const row of crossingMidnight) {
const day = row.scheduled_at.slice(0, 10);
const rows = filterFixturePrechecks(prechecks, { ...ALL, date_from: day, date_to: day });
expect(rows.map((r) => r.id)).toContain(row.id);
const detectedDay = row.detected_at.slice(0, 10);
expect(
filterFixturePrechecks(prechecks, {
...ALL,
date_from: detectedDay,
date_to: detectedDay,
}).map((r) => r.id)
).not.toContain(row.id);
}
});
it("上限はその日を**含む**(★翌日 00:00 未満★)", () => {
const day = days[0];
const rows = filterFixturePrechecks(prechecks, { ...ALL, date_to: day });
expect(rows.some((r) => r.scheduled_at.slice(0, 10) === day)).toBe(true);
expect(rows.every((r) => r.scheduled_at.slice(0, 10) <= day)).toBe(true);
});
it("`date_from` がデータの最古日より前 → #1 と同じ件数", () => {
expect(filterFixturePrechecks(prechecks, { ...ALL, date_from: "2000-01-01" })).toHaveLength(
prechecks.length
);
});
it("`facility_ids` で絞れる(★`null` は「絞らない」★)", () => {
const id = prechecks[0].facility_id;
const rows = filterFixturePrechecks(prechecks, { ...ALL, facility_ids: [id] });
expect(rows).toEqual(prechecks.filter((r) => r.facility_id === id));
expect(rows.length).toBeGreaterThan(0);
expect(filterFixturePrechecks(prechecks, { ...ALL, facility_ids: null })).toHaveLength(
prechecks.length
);
});
it("★複数の施設ID(分類の解決結果)で絞れる★", () => {
const ids = [...new Set(prechecks.map((r) => r.facility_id))].slice(0, 2);
expect(ids.length).toBe(2);
const rows = filterFixturePrechecks(prechecks, { ...ALL, facility_ids: ids });
expect(rows).toEqual(prechecks.filter((r) => ids.includes(r.facility_id)));
expect(rows.length).toBeGreaterThan(0);
});
it("存在しない `facility_ids` → 0件(★条件が静かに消えていないことの検出★)", () => {
expect(filterFixturePrechecks(prechecks, { ...ALL, facility_ids: [999_999] })).toEqual(
[]
);
});
/**
* ★★`[]`(分類で絞って0件ヒット)は0件★★ — ★`null`(絞らない)に化けさせないこと★
* (★罠1 / 落とし穴1★。全件表示に化けても例外もテスト失敗も出ないので、ここで固定する)。
*/
it("★`facility_ids: []` → 0件(全件に化けないこと)★", () => {
expect(filterFixturePrechecks(prechecks, { ...ALL, facility_ids: [] })).toEqual([]);
});
it("日付と施設の AND が効く", () => {
const target = prechecks[0];
const day = target.scheduled_at.slice(0, 10);
const rows = filterFixturePrechecks(prechecks, {
date_from: day,
date_to: day,
facility_ids: [target.facility_id],
});
expect(
rows.every(
(r) => r.facility_id === target.facility_id && r.scheduled_at.slice(0, 10) === day
)
).toBe(true);
expect(rows.map((r) => r.id)).toContain(target.id);
});
it("★入力の配列を壊さない(純粋関数)★", () => {
const before = prechecks.length;
filterFixturePrechecks(prechecks, { ...ALL, date_from: days[0], date_to: days[0] });
expect(prechecks).toHaveLength(before);
});
});