import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PATCH } from "@/app/api/prechecks/[id]/status/route";
import { GET } from "@/app/api/prechecks/route";
import { resetFixturePrecheckOverrides } from "@/lib/db/fixture-precheck-overrides";
import { prechecks } from "@/lib/fixtures/prechecks";
import type { PrecheckItem, SummaryListResponse } from "@/lib/types";
import type { PrecheckSummary } from "@/lib/prechecks/query";
/**
* `PATCH /prechecks/{id}/status`(`USE_FIXTURES=true`。指示書AT 段階4-3 / §5)。
*
* ★★なぜ別ファイルなのか★★ — `lib/db/index.ts` の `db` はモジュール読み込み時に束縛されるので、
* 同じファイルで true/false の両方は試せない(`app/api/ds-devices/[serial_number]/note/__tests__/` と同じ)。
*
* fixtures 版は `lib/fixtures/prechecks.ts` を書き換えず、モジュール内の上書き表を持つ。
* ★テストごとに `resetFixturePrecheckOverrides()` で空に戻す★。
*/
vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: true }));
vi.mock("@/lib/logger", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/logger")>();
return { ...actual, logEvent: vi.fn() };
});
const NOW_ISO = "2026-10-02T08:15:00+09:00";
beforeEach(() => {
resetFixturePrecheckOverrides();
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date(NOW_ISO));
});
afterEach(() => {
vi.useRealTimers();
resetFixturePrecheckOverrides();
});
async function patch(id: string, body: unknown): Promise<Response> {
return PATCH(
new Request(`http://localhost/api/prechecks/${encodeURIComponent(id)}/status`, {
method: "PATCH",
body: JSON.stringify(body),
}),
{ params: Promise.resolve({ id }) }
);
}
async function listAll(): Promise<SummaryListResponse<PrecheckItem, PrecheckSummary>> {
const res = await GET(new Request("http://localhost/api/prechecks?status=all&per_page=100"));
expect(res.status).toBe(200);
return (await res.json()) as SummaryListResponse<PrecheckItem, PrecheckSummary>;
}
describe("PATCH /prechecks/{id}/status(USE_FIXTURES=true)", () => {
it("★前提★: id \"1\" は未解消の事前チェック、\"4\"・\"8\" は出発失敗", () => {
const byId = new Map(prechecks.map((p) => [p.id, p]));
expect(byId.get("1")).toMatchObject({ severity: "precheck", resolution_status: "unresolved" });
expect(byId.get("4")?.severity).toBe("launch_failed");
expect(byId.get("8")?.severity).toBe("launch_failed");
});
it("PATCH → GET /prechecks に反映される(状態・日時・備考とサマリ)", async () => {
const before = await listAll();
const res = await patch("1", { status: "resolved", note: " 現場で解消 " });
expect(res.status).toBe(200);
const updated = (await res.json()) as PrecheckItem;
expect(updated).toMatchObject({
id: "1",
resolution_status: "resolved",
responded_at: NOW_ISO,
resolved_at: NOW_ISO,
note: "現場で解消",
});
const after = await listAll();
expect(after.items.find((p) => p.id === "1")).toEqual(updated);
expect(after.summary).toEqual({
unresolved: before.summary.unresolved - 1,
resolved: before.summary.resolved + 1,
total: before.summary.total,
});
});
it("戻すと日時を消し、備考を空にすると null になる(元の4項目に戻せる)", async () => {
const original = prechecks.find((p) => p.id === "9");
expect(original).toMatchObject({ resolution_status: "unresolved", note: null });
await patch("9", { status: "in_progress", note: "連絡済み" });
await patch("9", { status: "resolved" });
const res = await patch("9", { status: "unresolved", note: "" });
expect(res.status).toBe(200);
const back = (await res.json()) as PrecheckItem;
expect(back).toEqual(original);
});
it("★元の fixtures 配列は書き換えない★", async () => {
const snapshot = structuredClone(prechecks);
await patch("1", { status: "in_progress" });
expect(prechecks).toEqual(snapshot);
});
it.each(["4", "8"])("★`launch_failed` の id(%s)は 404★", async (id) => {
const res = await patch(id, { status: "resolved" });
expect(res.status).toBe(404);
});
it("存在しない id は 404", async () => {
const res = await patch("RER-NOT-EXIST", { status: "resolved" });
expect(res.status).toBe(404);
});
});