import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PATCH } from "@/app/api/errors/[error_id]/status/route";
import { GET } from "@/app/api/errors/route";
import { resetFixtureErrorReportOverrides } from "@/lib/db/fixture-error-report-overrides";
import type { ErrorReportItem, ListResponse } from "@/lib/types";
/**
* `GET /errors` → `PATCH /errors/{error_id}/status` → `GET /errors`(`USE_FIXTURES=true`。指示書AW 段階4-4-3)。
*
* ★★なぜ別ファイルなのか★★ — `lib/db/index.ts` の `db` はモジュール読み込み時に束縛されるので、
* 同じファイルで true/false の両方は試せない(`app/api/prechecks/[id]/status/__tests__/route.fixtures.test.ts` と同じ)。
*
* fixtures 版は `lib/fixtures/error-reports.ts` を書き換えず、モジュール内の上書き表を持つ。
* ★テストごとに `resetFixtureErrorReportOverrides()` で空に戻す★。
*/
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-06T08:15:00+09:00";
beforeEach(() => {
resetFixtureErrorReportOverrides();
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date(NOW_ISO));
});
afterEach(() => {
vi.useRealTimers();
resetFixtureErrorReportOverrides();
});
async function list(qs = ""): Promise<ListResponse<ErrorReportItem>> {
const res = await GET(new Request(`http://localhost/api/errors${qs ? `?${qs}` : ""}`));
expect(res.status).toBe(200);
return (await res.json()) as ListResponse<ErrorReportItem>;
}
async function patch(id: string, body: unknown): Promise<Response> {
return PATCH(
new Request(`http://localhost/api/errors/${encodeURIComponent(id)}/status`, {
method: "PATCH",
body: JSON.stringify(body),
}),
{ params: Promise.resolve({ error_id: id }) }
);
}
describe("GET /errors(USE_FIXTURES=true)", () => {
it("★既定で FX-ERR-03 が出ない(15行)・include_resolved=true で18行★", async () => {
const def = await list();
expect(def.meta.total).toBe(15);
expect(def.items.map((i) => i.id)).not.toContain("FX-ERR-03");
const all = await list("include_resolved=true");
expect(all.meta.total).toBe(18);
expect(all.items).toHaveLength(18);
});
it("★削除済みの施設(FX-ERR-18)・清掃エリア(FX-ERR-19)の行は既定でも include_resolved=true でも出ない★", async () => {
for (const qs of ["", "include_resolved=true"]) {
const ids = (await list(qs)).items.map((i) => i.id);
expect(ids).not.toContain("FX-ERR-18");
expect(ids).not.toContain("FX-ERR-19");
}
});
it("error_type=secur → FX-ERR-08 だけ", async () => {
expect((await list("error_type=secur")).items.map((i) => i.id)).toEqual(["FX-ERR-08"]);
});
it("★エラー内容の3項目(指示書BD §3-2・§5-2): 解決・不明・未選択・選べない行★", async () => {
const byId = new Map((await list("include_resolved=true")).items.map((i) => [i.id, i]));
expect(byId.get("FX-ERR-01")).toMatchObject({
error_list_id: 40,
error_list: { id: 40, error_detail_ja: "非常停止スイッチが作動しました", note_required: false },
error_list_selectable: true,
});
expect(byId.get("FX-ERR-05")).toMatchObject({ error_list_id: 9999, error_list: null, error_list_selectable: true });
expect(byId.get("FX-ERR-11")).toMatchObject({
error_type: "NOT_STARTED",
error_list_id: 131,
error_list: { id: 131, note_required: true },
error_list_selectable: true,
});
expect(byId.get("FX-ERR-04")).toMatchObject({ error_list_id: null, error_list: null, error_list_selectable: true });
expect(byId.get("FX-ERR-08")).toMatchObject({ error_type: "SECURITY_ON", error_list_selectable: false });
expect(byId.get("FX-ERR-10")).toMatchObject({ error_type: "D-S_CONTROL_FAILURE", error_list_selectable: false });
});
});
describe("PATCH → GET(USE_FIXTURES=true)", () => {
it("★事前チェック以外の行(FX-ERR-08: robot_id NULL)に書け、GET に反映される★", async () => {
const res = await patch("FX-ERR-08", { status: "in_progress", note: "警備会社に確認" });
expect(res.status).toBe(200);
expect((await res.json()) as ErrorReportItem).toMatchObject({
id: "FX-ERR-08",
resolution_status: "in_progress",
responded_at: NOW_ISO,
note: "警備会社に確認",
});
const item = (await list()).items.find((i) => i.id === "FX-ERR-08");
expect(item).toMatchObject({ resolution_status: "in_progress", responded_at: NOW_ISO, note: "警備会社に確認" });
});
it("解消にすると既定の一覧から外れ、「解消済みも表示」では出る", async () => {
expect((await patch("FX-ERR-04", { status: "resolved" })).status).toBe(200);
expect((await list()).items.map((i) => i.id)).not.toContain("FX-ERR-04");
const all = await list("include_resolved=true");
expect(all.items.find((i) => i.id === "FX-ERR-04")).toMatchObject({
resolution_status: "resolved",
responded_at: NOW_ISO,
resolved_at: NOW_ISO,
});
});
it("解消 → 未解消で日時を消す(FX-ERR-03)", async () => {
const res = await patch("FX-ERR-03", { status: "unresolved" });
expect((await res.json()) as ErrorReportItem).toMatchObject({
resolution_status: "unresolved",
responded_at: null,
resolved_at: null,
});
expect((await list()).meta.total).toBe(16);
});
it.each([
["存在しない id", "FX-ERR-99"],
["大小文字違い", "fx-err-01"],
["★削除済みの施設の行(表5 V2)★", "FX-ERR-18"],
["★削除済みの清掃エリアの行(表5 V6)★", "FX-ERR-19"],
])("%s は404", async (_label, id) => {
const res = await patch(id, { status: "resolved" });
expect(res.status).toBe(404);
expect(await res.json()).toEqual({
error: { code: "not_found", message: "エラーが見つかりません", details: null },
});
});
});