import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CLEAN_TIMETABLE_PRECHECK } from "@/lib/config";
import { db } from "@/lib/db";
import { unscheduledCleanLogWhere } from "@/lib/db/unscheduled-clean-log-source";
import { FACILITY_SECTION_VISIBLE_WHERE } from "@/lib/db/visibility";
import type { CleanLogDbQuery } from "@/lib/types";
/**
* Prisma 版 `cleanLogRunMeta()`(指示書AV 段階3-3)。
*
* 固定すること:
* - ★`where` が `cleanLogs()` の `where` と同じ★(4-8。`mainTimetableWhere()` の共有)
* - ★2表(`clean_timetables` / `clean_timetable_histories`)の両方に同じ引数が渡る★(`findTimetables()`)
* - ★`end_at` は壁時計値のまま ISO になる★(`2026-10-05 21:10:00` → `"2026-10-05T21:10:00+09:00"`。
* 9時間を足さない・引かない。`CLAUDE.md` §3 / 指示書AV 4-3)
*
* ★★DBには接続しない★★ — `USE_FIXTURES` を `false` に固定して `prismaSource` を走らせ、
* `getPrisma()` が最初に見るシングルトン `globalThis.__dwatchPrisma` にスタブを置く
* (`app/api/dashboard/__tests__/route.test.ts` と同じ作法。★`afterEach` で必ず片付ける★)。
*/
vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: false }));
type Args = Record<string, unknown>;
const calls: { cleanTimetable: Args[]; cleanTimetableHistory: Args[]; cleanTaskReport: Args[] } = {
cleanTimetable: [],
cleanTimetableHistory: [],
cleanTaskReport: [],
};
/** 本表が返す行(★`select` で射影して返す★) */
let mainRows: Record<string, unknown>[] = [];
/** ★予定外タスクの `clean_task_reports` の行★(指示書BG §2-2。`where` は当てない — 形は下のテストで見る) */
let reportRows: Record<string, unknown>[] = [];
function project(row: Record<string, unknown>, select: unknown): Record<string, unknown> {
if (!select) return row;
const out: Record<string, unknown> = {};
for (const key of Object.keys(select as Record<string, unknown>)) out[key] = row[key];
return out;
}
/**
* ★`cleanTimetable` / `cleanTimetableHistory` 以外のモデルは空を返す★
* (`cleanLogs()` は本清掃が0件なら関連テーブルを引かないが、念のため全モデルを空で受ける)。
*/
function prismaStub(): typeof globalThis.__dwatchPrisma {
const empty = { findMany: async () => [] };
const known: Record<string, unknown> = {
cleanTimetable: {
findMany: async (args?: Args) => {
calls.cleanTimetable.push(args ?? {});
return mainRows.map((row) => project(row, args?.select));
},
},
cleanTimetableHistory: {
findMany: async (args?: Args) => {
calls.cleanTimetableHistory.push(args ?? {});
return [];
},
},
cleanTaskReport: {
findMany: async (args?: Args) => {
calls.cleanTaskReport.push(args ?? {});
return reportRows.map((row) => project(row, args?.select));
},
},
};
return new Proxy(known, {
get: (target, prop: string) => target[prop] ?? empty,
}) as unknown as typeof globalThis.__dwatchPrisma;
}
/** DB に入っているのと同じ「壁時計値としての `Date`」(D-115。他のテストと同じヘルパー) */
function dbWallClock(jstIso: string): Date {
return new Date(jstIso.replace("+09:00", "Z"));
}
/** メタの呼び出し = `select` のキーがちょうど `id` / `section_id` / `end_at` の3つ */
function isMetaCall(args: Args): boolean {
const select = args.select as Record<string, unknown> | undefined;
if (!select) return false;
return Object.keys(select).sort().join(",") === "end_at,id,section_id";
}
beforeEach(() => {
calls.cleanTimetable = [];
calls.cleanTimetableHistory = [];
calls.cleanTaskReport = [];
mainRows = [];
reportRows = [];
globalThis.__dwatchPrisma = prismaStub();
});
afterEach(() => {
// ★★他のテストへ漏らさないこと★★(`getPrisma()` はここを最初に見る)
globalThis.__dwatchPrisma = undefined;
});
describe("Prisma 版 cleanLogRunMeta()(指示書AV 段階3-3)", () => {
it.each<[string, CleanLogDbQuery]>([
["日付の範囲+施設", { id: null, date_from: "2026-09-29", date_to: "2026-10-06", facility_ids: [1, 2] }],
["施設で絞らない(null)", { id: null, date_from: "2026-09-29", date_to: "2026-10-06", facility_ids: null }],
["0件ヒット([])", { id: null, date_from: "2026-09-29", date_to: "2026-10-06", facility_ids: [] }],
])("★`where` が `cleanLogs()` の `where` と同じ★(%s)", async (_label, query) => {
await db.cleanLogs(query);
await db.cleanLogRunMeta(query);
const logsCall = calls.cleanTimetable.find((c) => "include" in c);
const metaCall = calls.cleanTimetable.find(isMetaCall);
expect(logsCall).toBeDefined();
expect(metaCall).toBeDefined();
expect(metaCall?.where).toEqual(logsCall?.where);
// 本清掃だけ・期間が降りている(全件読みに戻っていない)
expect(metaCall?.where).toMatchObject({ precheck: CLEAN_TIMETABLE_PRECHECK.main });
expect(metaCall?.where).toHaveProperty("start_at");
// ★削除済みの施設・清掃エリアの条件も同じく入る(指示書BB U1・表1 V4)★
expect(metaCall?.where).toMatchObject(FACILITY_SECTION_VISIBLE_WHERE);
});
it("★2表の両方に同じ引数が渡る★(`findTimetables()`)", async () => {
await db.cleanLogRunMeta({ id: null, date_from: "2026-10-05", date_to: "2026-10-05", facility_ids: null });
const mainCall = calls.cleanTimetable.find(isMetaCall);
expect(mainCall).toBeDefined();
expect(calls.cleanTimetableHistory.find(isMetaCall)).toEqual(mainCall);
});
it("★`end_at` は壁時計値のまま ISO になる★(9時間を足さない・引かない)", async () => {
mainRows = [
{
id: BigInt(1001),
section_id: BigInt(77),
end_at: dbWallClock("2026-10-05T21:10:00+09:00"),
start_at: dbWallClock("2026-10-05T19:00:00+09:00"),
facility_id: BigInt(1),
},
];
const meta = await db.cleanLogRunMeta({
id: null,
date_from: "2026-10-05",
date_to: "2026-10-05",
facility_ids: null,
});
// ★鍵は `row_key`★(指示書BG §2-2。旧: `id: 1001`)
expect(meta).toEqual([
{ row_key: "tt-1001", section_key: "section:77", planned_end_at: "2026-10-05T21:10:00+09:00" },
]);
});
// ★★指示書BG §2-2 — 予定外タスクのメタ★★
it("★予定外タスクのメタも返す★(`where` は `cleanLogs()` の予定外の行と同じ `unscheduledCleanLogWhere()`)", async () => {
const query: CleanLogDbQuery = { id: null, date_from: "2026-10-28", date_to: "2026-10-28", facility_ids: [1] };
await db.cleanLogRunMeta(query);
expect(calls.cleanTaskReport).toHaveLength(1);
expect(calls.cleanTaskReport[0].where).toEqual(unscheduledCleanLogWhere(query));
expect(Object.keys(calls.cleanTaskReport[0]).sort()).toEqual(["select", "where"]);
});
it("★予定外の終了は `ended_at`、null なら `started_at`(壁時計値のまま)。予定のある行が紛れても混ぜない★", async () => {
reportRows = [
{
report_id: "9200000000000000001",
clean_timetable_id: null,
section_id: BigInt(77),
started_at: dbWallClock("2026-10-28T23:00:00+09:00"),
ended_at: dbWallClock("2026-10-29T00:30:00+09:00"),
},
{
report_id: "9200000000000000002",
clean_timetable_id: BigInt(0),
section_id: BigInt(78),
started_at: dbWallClock("2026-10-28T21:00:00+09:00"),
ended_at: null,
},
// ★SQL の写しが外れて予定のある行が返っても混ぜない(`isUnscheduledReport()`)★
{
report_id: "9200000000000000003",
clean_timetable_id: BigInt(5),
section_id: BigInt(79),
started_at: dbWallClock("2026-10-28T21:00:00+09:00"),
ended_at: null,
},
];
const meta = await db.cleanLogRunMeta({ id: null, date_from: "2026-10-28", date_to: "2026-10-28", facility_ids: null });
expect(meta).toEqual([
{ row_key: "rp-9200000000000000001", section_key: "section:77", planned_end_at: "2026-10-29T00:30:00+09:00" },
{ row_key: "rp-9200000000000000002", section_key: "section:78", planned_end_at: "2026-10-28T21:00:00+09:00" },
]);
});
});