/**
* `lib/db/error-list-source.ts` の期待値表(指示書BD §3-1・§3-2・§5-3。D-184)。
*
* 表 S(`isErrorListSelectable()`。★`ERROR_LIST_UNSELECTABLE_ERROR_TYPES` との完全一致★)
* | # | error_type | 期待 | 理由 |
* |----|-------------------------|-------|------|
* | S1 | `D-S_CONTROL_FAILURE` | false | 制御盤(ユーザー判断 2026-10-27) |
* | S2 | `SECURITY_ON` | false | セキュリティ |
* | S3 | `NOT_STARTED` | true | ★清掃開始失敗は選べる★ |
* | S4 | `EmergencyStop` | true | ロボットのエラー |
* | S5 | `security_on` | true | ★小文字は別の値(緩めない)★ |
* | S6 | `d-s_control_failure` | true | 同上 |
* | S7 | ` SECURITY_ON` | true | ★前後の空白も緩めない★ |
* | S8 | `SECURITY_ON_X` | true | ★前方一致にしない★ |
* | S9 | `""` | true | 空文字は選べない種類に入っていない |
*
* 表 A(`activeErrorListItems()`。選択肢)
* | 入力 id(並び) | inactive / deleted_at | 期待 |
* |-----------------------|----------------------------------------|------|
* | 131, 2, 10, 3, 4, 1 | 3 = inactive 1・4 = deleted_at あり・10 = inactive 2 | [1, 2, 131](★id の昇順。数値で並べる — 文字列順なら 10 / 131 が 2 より前★) |
*/
import { describe, expect, it } from "vitest";
import { ERROR_LIST_NOTE_REQUIRED_IDS, ERROR_LIST_UNSELECTABLE_ERROR_TYPES } from "@/lib/config";
import {
ACTIVE_ERROR_LIST_WHERE,
activeErrorListItems,
EMPTY_ERROR_LIST_INDEX,
ERROR_LIST_SELECT,
errorListIdsOf,
errorListIndex,
errorListItemFromRow,
isActiveErrorListRow,
isErrorListSelectable,
loadErrorListIndexByIds,
resolveErrorList,
type ErrorListRowSource,
type ErrorListSourcePrisma,
} from "@/lib/db/error-list-source";
const DELETED_AT = new Date("2026-10-01T10:00:00Z");
function listRow(id: number, overrides: Partial<ErrorListRowSource> = {}): ErrorListRowSource {
return {
id: BigInt(id),
error_category_main_en: `MAIN-EN-${id}`,
error_category_main_ja: `分類${id}`,
error_category_sub_en: `sub-en-${id}`,
error_category_sub_ja: `小分類${id}`,
error_detail_en: `detail-en-${id}`,
error_detail_ja: `詳細${id}`,
inactive: 0,
deleted_at: null,
...overrides,
};
}
describe("isErrorListSelectable(表 S)", () => {
it("設定の前提: 選べない種類は D-S_CONTROL_FAILURE と SECURITY_ON の2つ(NOT_STARTED を含まない)", () => {
expect([...ERROR_LIST_UNSELECTABLE_ERROR_TYPES]).toEqual(["D-S_CONTROL_FAILURE", "SECURITY_ON"]);
});
it.each([
["S1", "D-S_CONTROL_FAILURE", false],
["S2", "SECURITY_ON", false],
["S3", "NOT_STARTED", true],
["S4", "EmergencyStop", true],
["S5", "security_on", true],
["S6", "d-s_control_failure", true],
["S7", " SECURITY_ON", true],
["S8", "SECURITY_ON_X", true],
["S9", "", true],
] as const)("%s: %j → %s", (_no, errorType, expected) => {
expect(isErrorListSelectable(errorType)).toBe(expected);
});
});
describe("errorListItemFromRow", () => {
it("文字列は変換しない・id は number・inactive / deleted_at は出さない", () => {
expect(errorListItemFromRow(listRow(40, { inactive: 1, deleted_at: DELETED_AT }))).toEqual({
id: 40,
error_category_main_en: "MAIN-EN-40",
error_category_main_ja: "分類40",
error_category_sub_en: "sub-en-40",
error_category_sub_ja: "小分類40",
error_detail_en: "detail-en-40",
error_detail_ja: "詳細40",
note_required: false,
});
});
it("★note_required は ERROR_LIST_NOTE_REQUIRED_IDS(131)だけ true★", () => {
expect([...ERROR_LIST_NOTE_REQUIRED_IDS]).toEqual([131]);
expect(errorListItemFromRow(listRow(131)).note_required).toBe(true);
expect(errorListItemFromRow(listRow(130)).note_required).toBe(false);
expect(errorListItemFromRow(listRow(1)).note_required).toBe(false);
});
});
describe("activeErrorListItems(表 A)", () => {
const input = [
listRow(131),
listRow(2),
listRow(10, { inactive: 2 }),
listRow(3, { inactive: 1 }),
listRow(4, { deleted_at: DELETED_AT }),
listRow(1),
];
it("★有効な行だけ・id の昇順(数値順)★", () => {
expect(activeErrorListItems(input).map((i) => i.id)).toEqual([1, 2, 131]);
});
it("入力の配列を並べ替えない", () => {
const before = input.map((r) => r.id);
activeErrorListItems(input);
expect(input.map((r) => r.id)).toEqual(before);
});
it("isActiveErrorListRow: inactive 0 かつ deleted_at null だけ true", () => {
expect(isActiveErrorListRow({ inactive: 0, deleted_at: null })).toBe(true);
expect(isActiveErrorListRow({ inactive: 1, deleted_at: null })).toBe(false);
expect(isActiveErrorListRow({ inactive: 0, deleted_at: DELETED_AT })).toBe(false);
});
it("SQL 側の写し(ACTIVE_ERROR_LIST_WHERE)は inactive 0・deleted_at null", () => {
expect(ACTIVE_ERROR_LIST_WHERE).toEqual({ inactive: 0, deleted_at: null });
});
});
describe("名前解決(errorListIndex / resolveErrorList)", () => {
const index = errorListIndex([listRow(40), listRow(7, { inactive: 1 }), listRow(8, { deleted_at: DELETED_AT })]);
it("null → null(未選択)", () => {
expect(resolveErrorList(null, index)).toBeNull();
});
it("索引に無い id → null(不明)", () => {
expect(resolveErrorList(BigInt(9999), index)).toBeNull();
});
it("★無効化・削除済みの行も解決する(絞らない)★", () => {
expect(resolveErrorList(BigInt(7), index)?.id).toBe(7);
expect(resolveErrorList(BigInt(8), index)?.id).toBe(8);
expect(resolveErrorList(BigInt(40), index)?.error_detail_ja).toBe("詳細40");
});
it("空の索引は何も解決しない", () => {
expect(resolveErrorList(BigInt(40), EMPTY_ERROR_LIST_INDEX)).toBeNull();
});
});
describe("errorListIdsOf", () => {
it("非 null だけを重複なく集める(初出順)", () => {
const ids = errorListIdsOf([
{ error_list_id: BigInt(40) },
{ error_list_id: null },
{ error_list_id: BigInt(131) },
{ error_list_id: BigInt(40) },
]);
expect(ids).toEqual([BigInt(40), BigInt(131)]);
});
it("全部 null なら []", () => {
expect(errorListIdsOf([{ error_list_id: null }])).toEqual([]);
});
});
describe("loadErrorListIndexByIds(Prisma のスタブ)", () => {
function stub(rows: ErrorListRowSource[]) {
const calls: Parameters<ErrorListSourcePrisma["errorList"]["findMany"]>[0][] = [];
const prisma: ErrorListSourcePrisma = {
errorList: {
findMany: async (args) => {
calls.push(args);
const wanted = args.where.id.in.map((id) => id.toString());
return rows.filter((r) => wanted.includes(r.id.toString()));
},
},
};
return { prisma, calls };
}
it("★id が0件ならクエリを撃たない★", async () => {
const { prisma, calls } = stub([listRow(40)]);
const index = await loadErrorListIndexByIds(prisma, []);
expect(index.size).toBe(0);
expect(calls).toHaveLength(0);
});
it("★1回だけ撃つ・where は id IN だけ(inactive / deleted_at で絞らない)・select は ERROR_LIST_SELECT★", async () => {
const { prisma, calls } = stub([listRow(40), listRow(7, { inactive: 1 }), listRow(8, { deleted_at: DELETED_AT })]);
const index = await loadErrorListIndexByIds(prisma, [BigInt(40), BigInt(7), BigInt(8), BigInt(9999)]);
expect(calls).toHaveLength(1);
expect(calls[0]).toEqual({
where: { id: { in: [BigInt(40), BigInt(7), BigInt(8), BigInt(9999)] } },
select: ERROR_LIST_SELECT,
});
expect([...index.keys()].sort()).toEqual(["40", "7", "8"]);
});
it("ERROR_LIST_SELECT は API に要る列と inactive / deleted_at(created_at / updated_at は読まない)", () => {
expect(Object.keys(ERROR_LIST_SELECT).sort()).toEqual(
[
"deleted_at",
"error_category_main_en",
"error_category_main_ja",
"error_category_sub_en",
"error_category_sub_ja",
"error_detail_en",
"error_detail_ja",
"id",
"inactive",
].sort()
);
});
});