import { beforeEach, describe, expect, it, vi } from "vitest";
import { db } from "@/lib/db";
import { FACILITY_SECTION_VISIBLE_WHERE } from "@/lib/db/visibility";
import { logEvent } from "@/lib/logger";
/**
* `prismaSource.schedules()`(`GET /schedules`・ダッシュボードの予定件数)の期待値表(指示書BB)。
*
* | # | 入力 | 期待 |
* |---|---|---|
* | R1 | 清掃エリアに有効なロボット1台 | 今どおり(ログなし) |
* | R4 | ★0台★ | ★例外にならない★。`serial_number: null`。`data.anomaly`(`active_robots_per_section`・`actual: 0`)は★清掃エリアごとに1回★(`clean_schedule` の行ごとではない) |
* | R5 | 2台(id 10 と id 7) | `orderBy: { id: "asc" }` を SQL に頼む(並びはスタブが再現)→ id 7。ログ(`actual: 2`)1回 |
*
* ★DBには接続しません★ — `getPrisma()` をスタブに差し替え、`USE_FIXTURES` を `false` に固定して `prismaSource` を走らせる。
*/
interface StubRobot {
id: bigint;
serial_number: string;
}
interface StubSchedule {
id: bigint;
facility_id: bigint;
section_id: bigint;
weeks: string;
start_time: string;
end_time: string;
inactive: number;
deleted_at: Date | null;
facility: { facility_name: string };
section: { robots: StubRobot[] };
}
let schedules: StubSchedule[];
const calls: Record<string, unknown>[] = [];
vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: false }));
vi.mock("@/lib/logger", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/logger")>();
return { ...actual, logEvent: vi.fn() };
});
vi.mock("@/lib/db/prisma", () => ({
getPrisma: async () => ({
cleanSchedule: {
findMany: async (args: Record<string, unknown>) => {
calls.push(args);
// ★`where` は評価しない(条件の中身は下のテストで `args.where` を見る)★。
// ★`include.section.include.robots.orderBy` が `{ id: "asc" }` のときだけ並べ替える★(付け忘れたら R5 が赤)
const include = args.include as { section?: { include?: { robots?: { orderBy?: { id?: string } } } } };
const sortRobots = include.section?.include?.robots?.orderBy?.id === "asc";
return schedules.map((s) => ({
...s,
section: {
robots: sortRobots
? [...s.section.robots].sort((a, b) => Number(a.id) - Number(b.id))
: s.section.robots,
},
}));
},
},
}),
}));
/** ★実在の店名を使わない★(`CLAUDE.md` §7) */
function schedule(id: number, sectionId: number, robots: StubRobot[]): StubSchedule {
return {
id: BigInt(id),
facility_id: BigInt(1),
section_id: BigInt(sectionId),
weeks: "1111111",
start_time: "0200",
end_time: "0300",
inactive: 0,
deleted_at: null,
facility: { facility_name: "バリューマート北町店" },
section: { robots },
};
}
function robotAnomalies() {
return vi
.mocked(logEvent)
.mock.calls.filter(([, p]) => p.event === "data.anomaly" && p.metric === "active_robots_per_section")
.map(([, p]) => p);
}
beforeEach(() => {
vi.mocked(logEvent).mockClear();
calls.length = 0;
schedules = [schedule(21, 10, [{ id: BigInt(1), serial_number: "8111R5628060011" }])];
});
describe("prismaSource.schedules — 有効なロボットがいない清掃エリア(指示書BB U2・表2)", () => {
it("R1: 1台なら今どおり(ログを出さない)", async () => {
const rows = await db.schedules();
expect(rows.map((r) => [r.id, r.serial_number])).toEqual([[21, "8111R5628060011"]]);
expect(robotAnomalies()).toHaveLength(0);
});
it("★R4: 0台でも例外にならず `serial_number: null`。ログは清掃エリアごとに1回(行ごとではない)★", async () => {
schedules = [schedule(21, 10, []), schedule(22, 10, []), schedule(23, 20, [{ id: BigInt(2), serial_number: "S-2" }])];
const rows = await db.schedules();
expect(rows.map((r) => [r.id, r.serial_number])).toEqual([
[21, null],
[22, null],
[23, "S-2"],
]);
expect(robotAnomalies()).toEqual([
{ event: "data.anomaly", metric: "active_robots_per_section", expected: 1, actual: 0, section_id: "10" },
]);
});
it("★R5: 2台(id 10 と id 7)→ id 7 を採る。ログ(actual: 2)は1回★", async () => {
schedules = [
schedule(21, 10, [
{ id: BigInt(10), serial_number: "SERIAL-ID10" },
{ id: BigInt(7), serial_number: "SERIAL-ID7" },
]),
];
const rows = await db.schedules();
expect(rows[0].serial_number).toBe("SERIAL-ID7");
expect(robotAnomalies()).toEqual([
{ event: "data.anomaly", metric: "active_robots_per_section", expected: 1, actual: 2, section_id: "10" },
]);
});
});
describe("prismaSource.schedules — 削除済みの施設・清掃エリア(指示書BB U1・表1 V5)", () => {
it("★`where` は `clean_schedules.deleted_at` に加えて施設・清掃エリアの可視性★", async () => {
await db.schedules();
expect(calls[0].where).toEqual({ deleted_at: null, ...FACILITY_SECTION_VISIBLE_WHERE });
});
});