Newer
Older
client_dwatch / lib / error-reports / __tests__ / query.test.ts
/**
 * `lib/error-reports/query.ts` の期待値表(指示書AW §3 表1 の並び順・表2 / 段階1-2 / CLAUDE.md §9.4)
 *
 * ★表1 は監督確定(D-160 / 指示書AW §3)。変えないこと★ — ここでは並び順だけを見る
 *   (行の組み立ては `lib/db/__tests__/error-report-mapping.test.ts`)。
 * ★表2 は指示書AW で導いたもの(監督確認済み・正)★。ルート(SQL 側・ページ分け)の確認は段階4 のテスト。
 *
 * 入力は表1 の9行のキー(`facility_id` は E1〜E5 = 1、E6〜E9 = 2)。
 *   ★E7 は errored_at が NULL で、created_at(10/6 08:00)が9行の中で一番新しい★ → 先頭に来る。
 *   ★E4 = "E-10"・E5 = "E-9" は同じ検知時刻★ → 辞書順で "E-10" が先(数値順なら逆)。
 *
 * ★状態コードは `robotErrorStatusFromResolutionStatus()` から作る(0/1/2 を直書きしない。D2)★
 */
import { describe, expect, it } from "vitest";
import { PAGINATION } from "@/lib/config";
import {
  errorReportKeyFromRow,
  type ErrorReportKey,
  type ErrorReportKeySource,
} from "@/lib/db/error-report-mapping";
import { robotErrorStatusFromResolutionStatus } from "@/lib/db/precheck-mapping";
import {
  compareErrorReportKeys,
  ERROR_REPORT_SORT_KEYS,
  errorReportStatusCodes,
  filterSortErrorReportKeys,
  parseErrorReportQuery,
} from "@/lib/error-reports/query";
import { CLEAN_TIMETABLE_ID_UNSCHEDULED } from "@/lib/config";
import { toDbJst } from "@/lib/format/date";
import type { ErrorReportDbQuery, ErrorReportSortKey } from "@/lib/types";

const UNRESOLVED = robotErrorStatusFromResolutionStatus("unresolved");
const IN_PROGRESS = robotErrorStatusFromResolutionStatus("in_progress");
const RESOLVED = robotErrorStatusFromResolutionStatus("resolved");

function key(
  error_id: string,
  facility_id: number,
  detected_at: string,
  status: number,
  error_type = "EmergencyStop",
  visible = true
): ErrorReportKey {
  return {
    error_id,
    facility_id,
    serial_number: "8111R-0001",
    error_type,
    error_level: "ERROR",
    detected_at,
    status,
    note: null,
    scheduled: true,
    visible,
  };
}

// 表1 の9行(検知時刻は `errored_at ?? created_at` を `jstIsoFromDb()` に通した後の値)
const E1 = key("E-1", 1, "2026-10-05T19:05:00+09:00", UNRESOLVED);
const E2 = key("E-2", 1, "2026-10-05T19:30:00+09:00", IN_PROGRESS, "LowBattery");
const E3 = key("E-3", 1, "2026-10-05T20:00:00+09:00", RESOLVED);
const E4 = key("E-10", 1, "2026-10-05T21:00:00+09:00", UNRESOLVED);
const E5 = key("E-9", 1, "2026-10-05T21:00:00+09:00", UNRESOLVED);
const E6 = key("E-6", 2, "2026-10-05T20:30:00+09:00", UNRESOLVED);
const E7 = key("E-7", 2, "2026-10-06T08:00:00+09:00", UNRESOLVED); // created_at
const E8 = key("E-8", 2, "2026-10-05T21:30:00+09:00", UNRESOLVED, "SECURITY_ON");
const E9 = key("E-11", 2, "2026-10-05T22:00:00+09:00", UNRESOLVED, "HandleUp");

/** ★わざと並び順と違う順で渡す★ */
const KEYS: readonly ErrorReportKey[] = [E1, E2, E3, E4, E5, E6, E7, E8, E9];

const base: ErrorReportDbQuery = {
  include_resolved: false,
  facility_ids: null,
  date_from: null,
  date_to: null,
  error_type: "",
  sort: null,
  order: "asc",
  schedule: null,
  page: 1,
  per_page: PAGINATION.defaultPerPage,
};

const ids = (keys: readonly ErrorReportKey[]) => keys.map((k) => k.error_id);

/** 表1 の並び順(既定。E3 を除く) */
const DEFAULT_ORDER = ["E-7", "E-11", "E-8", "E-10", "E-9", "E-6", "E-2", "E-1"];

describe("filterSortErrorReportKeys(表1 の並び順・表2)", () => {
  it("F1: 既定 → E3 以外の8行・表1 の並び順(E7 が先頭・同時刻は \"E-10\" が \"E-9\" より先)", () => {
    expect(ids(filterSortErrorReportKeys(KEYS, base))).toEqual(DEFAULT_ORDER);
  });

  it("F2: include_resolved=true → 9行(E3 は検知時刻の位置に入る)", () => {
    expect(ids(filterSortErrorReportKeys(KEYS, { ...base, include_resolved: true }))).toEqual([
      "E-7",
      "E-11",
      "E-8",
      "E-10",
      "E-9",
      "E-6",
      "E-3",
      "E-2",
      "E-1",
    ]);
  });

  it("F4: error_type=secur(大小文字違い・部分一致)→ E8 だけ", () => {
    expect(ids(filterSortErrorReportKeys(KEYS, { ...base, error_type: "secur" }))).toEqual(["E-8"]);
  });

  it("F5: error_type が空 → F1 と同じ", () => {
    expect(ids(filterSortErrorReportKeys(KEYS, { ...base, error_type: "" }))).toEqual(DEFAULT_ORDER);
  });

  it("F6: date_from = date_to = E7 の created_at の日付 → E7 が入る(errored_at が NULL でも created_at の日付で当たる)", () => {
    expect(
      ids(filterSortErrorReportKeys(KEYS, { ...base, date_from: "2026-10-06", date_to: "2026-10-06" }))
    ).toEqual(["E-7"]);
  });

  it("期間 10/5 だけ → E7 が外れる(日付は detected_at の先頭10文字で比べる)", () => {
    expect(
      ids(filterSortErrorReportKeys(KEYS, { ...base, date_from: "2026-10-05", date_to: "2026-10-05" }))
    ).toEqual(DEFAULT_ORDER.filter((id) => id !== "E-7"));
  });

  it("下限だけ・上限だけでも絞れる", () => {
    expect(ids(filterSortErrorReportKeys(KEYS, { ...base, date_from: "2026-10-06" }))).toEqual(["E-7"]);
    expect(ids(filterSortErrorReportKeys(KEYS, { ...base, date_to: "2026-10-05" }))).toEqual(
      DEFAULT_ORDER.filter((id) => id !== "E-7")
    );
  });

  it("★F8: facility_ids = [] → 0件(全件にならない。罠1)★", () => {
    expect(filterSortErrorReportKeys(KEYS, { ...base, facility_ids: [] })).toEqual([]);
  });

  it("facility_ids = [2] → 施設2の行だけ(並び順は保つ)", () => {
    expect(ids(filterSortErrorReportKeys(KEYS, { ...base, facility_ids: [2] }))).toEqual([
      "E-7",
      "E-11",
      "E-8",
      "E-6",
    ]);
  });

  it("★F12: status = 3 の行が混ざっても、既定でも「解消済みも表示」でも出ない・例外にならない★", () => {
    const withOdd = [...KEYS, key("E-X", 1, "2026-10-06T09:00:00+09:00", 3)];
    expect(() => filterSortErrorReportKeys(withOdd, base)).not.toThrow();
    expect(ids(filterSortErrorReportKeys(withOdd, base))).not.toContain("E-X");
    expect(ids(filterSortErrorReportKeys(withOdd, { ...base, include_resolved: true }))).not.toContain("E-X");
  });

  /**
   * ★指示書AX 表5 V2・V3・V7(削除済みの施設・清掃エリアの行。U8)★ — キーの `visible` が false。
   *   ★状態は「既定で出るはず」の未解消・解消にしておき、外れるのが可視性のせいであることを見る★
   */
  it("★V2・V3・V7: visible=false のキーは既定でも「解消済みも表示」でも出ない・total に数えない★", () => {
    const hidden = [
      key("V-2", 1, "2026-10-06T09:00:00+09:00", UNRESOLVED, "EmergencyStop", false),
      key("V-3", 2, "2026-10-06T09:10:00+09:00", UNRESOLVED, "EmergencyStop", false),
      key("V-7", 2, "2026-10-06T09:20:00+09:00", RESOLVED, "D-S_CONTROL_FAILURE", false),
    ];
    const withHidden = [...KEYS, ...hidden];
    expect(ids(filterSortErrorReportKeys(withHidden, base))).toEqual(DEFAULT_ORDER);
    const all = filterSortErrorReportKeys(withHidden, { ...base, include_resolved: true });
    expect(all).toHaveLength(KEYS.length);
    for (const id of ["V-2", "V-3", "V-7"]) expect(ids(all)).not.toContain(id);
  });

  it("★V9: 同じ施設の生きている行は消えない(清掃エリア単位で決まる)★", () => {
    const withHidden = [...KEYS, key("V-3", 1, "2026-10-06T09:10:00+09:00", UNRESOLVED, "EmergencyStop", false)];
    expect(ids(filterSortErrorReportKeys(withHidden, { ...base, facility_ids: [1] }))).toEqual(
      DEFAULT_ORDER.filter((id) => ["E-10", "E-9", "E-2", "E-1"].includes(id))
    );
  });

  it("★入力の配列を書き換えない★", () => {
    const input = [...KEYS];
    const snapshot = [...input];
    filterSortErrorReportKeys(input, { ...base, include_resolved: true });
    expect(input).toEqual(snapshot);
  });
});

describe("compareErrorReportKeys(D7)", () => {
  it("検知時刻の降順", () => {
    expect(compareErrorReportKeys(E7, E1)).toBeLessThan(0);
    expect(compareErrorReportKeys(E1, E7)).toBeGreaterThan(0);
  });

  it("★同時刻は error_id の昇順(文字列の辞書順。\"E-10\" が \"E-9\" より先)★", () => {
    expect(compareErrorReportKeys(E4, E5)).toBeLessThan(0);
    expect(compareErrorReportKeys(E5, E4)).toBeGreaterThan(0);
  });

  it("同じキーは 0", () => {
    expect(compareErrorReportKeys(E1, { ...E1 })).toBe(0);
  });

  it("★大文字は小文字より前(コードポイント順。localeCompare ではない)★", () => {
    const upper = key("B", 1, "2026-10-05T19:00:00+09:00", UNRESOLVED);
    const lower = key("a", 1, "2026-10-05T19:00:00+09:00", UNRESOLVED);
    expect(compareErrorReportKeys(upper, lower)).toBeLessThan(0);
  });
});

describe("errorReportStatusCodes(D2)", () => {
  it("false → 未解消・対応中のコード", () => {
    expect(errorReportStatusCodes(false)).toEqual(
      (["unresolved", "in_progress"] as const).map(robotErrorStatusFromResolutionStatus)
    );
  });

  it("true → それに解消を足したもの", () => {
    expect(errorReportStatusCodes(true)).toEqual(
      (["unresolved", "in_progress", "resolved"] as const).map(robotErrorStatusFromResolutionStatus)
    );
  });
});

describe("parseErrorReportQuery", () => {
  const parse = (qs: string) => parseErrorReportQuery(new URLSearchParams(qs));

  it("パラメータなし → 既定値", () => {
    expect(parse("")).toEqual({
      ok: true,
      query: {
        dateFrom: null,
        dateTo: null,
        includeResolved: false,
        facilityName: "",
        errorType: "",
        groupId: null,
        customerId: null,
        sort: null,
        order: "asc",
        schedule: null,
        page: 1,
        perPage: PAGINATION.defaultPerPage,
      },
    });
  });

  it("include_resolved: true → true / false・空 → false", () => {
    const pick = (qs: string) => {
      const r = parse(qs);
      return r.ok ? r.query.includeResolved : "fail";
    };
    expect(pick("include_resolved=true")).toBe(true);
    expect(pick("include_resolved=false")).toBe(false);
    expect(pick("include_resolved=")).toBe(false);
  });

  it("★F3: include_resolved=yes(true / false 以外)→ 422(黙って false に倒さない)★", () => {
    const r = parse("include_resolved=yes");
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(422);
      expect(r.error.error.code).toBe("validation_failed");
    }
    // 大文字も true / false 以外として扱う
    expect(parse("include_resolved=TRUE").ok).toBe(false);
  });

  it("★エラーに入力値をエコーしない★", () => {
    const r = parse("include_resolved=evil-input-value");
    expect(r.ok).toBe(false);
    if (!r.ok) expect(JSON.stringify(r.error)).not.toContain("evil-input-value");
  });

  it("★F7: date_from > date_to → 422(parseCleanLogQuery() と同じ文言)★", () => {
    const r = parse("date_from=2026-10-06&date_to=2026-10-05");
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(422);
      expect(r.error.error.message).toBe("date_fromはdate_to以前の日付を指定してください");
    }
  });

  it("date_from = date_to は通る", () => {
    const r = parse("date_from=2026-10-06&date_to=2026-10-06");
    expect(r.ok && r.query.dateFrom).toBe("2026-10-06");
    expect(r.ok && r.query.dateTo).toBe("2026-10-06");
  });

  it("日付の形式が不正 → 422", () => {
    expect(parse("date_from=2026/10/05").ok).toBe(false);
    expect(parse("date_to=20261005").ok).toBe(false);
    const r = parse("date_from=abc");
    if (!r.ok) expect(r.status).toBe(422);
  });

  it("★空の日付は null に正規化する★", () => {
    const r = parse("date_from=&date_to=");
    expect(r.ok && r.query.dateFrom).toBeNull();
    expect(r.ok && r.query.dateTo).toBeNull();
  });

  it("★期間の日数の上限は設けない(/prechecks と同じ。§11 Q13)★", () => {
    expect(parse("date_from=2020-01-01&date_to=2026-12-31").ok).toBe(true);
  });

  it("group_id=0 → 422(parseMasterIdFilters() に任せる)", () => {
    const r = parse("group_id=0");
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(422);
  });

  it("group_id / customer_id を読む", () => {
    const r = parse("group_id=3&customer_id=7");
    expect(r.ok && r.query.groupId).toBe(3);
    expect(r.ok && r.query.customerId).toBe(7);
  });

  it("page / per_page は parsePrecheckQuery() と同じ丸め", () => {
    const pick = (qs: string) => {
      const r = parse(qs);
      return r.ok ? [r.query.page, r.query.perPage] : "fail";
    };
    expect(pick("page=0")).toEqual([1, PAGINATION.defaultPerPage]);
    expect(pick("page=abc")).toEqual([1, PAGINATION.defaultPerPage]);
    expect(pick("page=3&per_page=10")).toEqual([3, 10]);
    expect(pick("per_page=0")).toEqual([1, 1]);
    expect(pick(`per_page=${PAGINATION.maxPerPage + 1}`)).toEqual([1, PAGINATION.maxPerPage]);
  });

  it("error_type / facility_name は前後の空白を除く(F5: 空白だけ → \"\")", () => {
    const r = parse("error_type=%20%20secur%20&facility_name=%20%E6%96%BD%E8%A8%AD%20");
    expect(r.ok && r.query.errorType).toBe("secur");
    expect(r.ok && r.query.facilityName).toBe("施設");
    const blank = parse("error_type=%20%20%20");
    expect(blank.ok && blank.query.errorType).toBe("");
  });

  it("E2-1: sort・order・schedule 省略 → sort null・order asc・schedule null", () => {
    const r = parse("");
    expect(r.ok && r.query.sort).toBeNull();
    expect(r.ok && r.query.order).toBe("asc");
    expect(r.ok && r.query.schedule).toBeNull();
  });

  it("E2-2: sort/order/schedule が空文字・空白だけ → 省略と同じ", () => {
    const r = parse("sort=&order=%20&schedule=%20");
    expect(r.ok && r.query.sort).toBeNull();
    expect(r.ok && r.query.order).toBe("asc");
    expect(r.ok && r.query.schedule).toBeNull();
  });

  it.each([
    "facility_name",
    "schedule_link",
    "error_list_id",
    "foo",
  ] as const)("E2-3: sort=%s → 422", (sort) => {
    const r = parse(`sort=${sort}`);
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(422);
  });

  it("E2-4: order=up → 422", () => {
    const r = parse("order=up");
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(422);
  });

  it.each(["all", "main", "Scheduled"] as const)("E2-5: schedule=%s → 422", (schedule) => {
    const r = parse(`schedule=${schedule}`);
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(422);
  });

  it("E2-6: 422 の本文に入力値をエコーしない・details の形", () => {
    const badSort = parse("sort=evil-sort-value");
    expect(badSort.ok).toBe(false);
    if (!badSort.ok) {
      expect(badSort.status).toBe(422);
      expect(JSON.stringify(badSort.error)).not.toContain("evil-sort-value");
      expect(badSort.error.error.details).toEqual({
        sort: [`${ERROR_REPORT_SORT_KEYS.join(" / ")} のいずれかを指定してください`],
      });
    }
    const badOrder = parse("order=evil-order");
    expect(badOrder.ok).toBe(false);
    if (!badOrder.ok) {
      expect(badOrder.error.error.details).toEqual({
        order: ["asc / desc のいずれかを指定してください"],
      });
    }
    const badSchedule = parse("schedule=evil-schedule");
    expect(badSchedule.ok).toBe(false);
    if (!badSchedule.ok) {
      expect(badSchedule.error.error.details).toEqual({
        schedule: ["scheduled / unscheduled のいずれかを指定してください"],
      });
    }
  });

  it("order= / order=%20 / sort=%20 → 省略と同じ", () => {
    for (const qs of ["order=", "order=%20", "sort=%20"]) {
      const r = parse(qs);
      expect(r.ok && r.query.sort).toBeNull();
      expect(r.ok && r.query.order).toBe("asc");
      expect(r.ok && r.query.schedule).toBeNull();
    }
  });

  it("E2-7: sort=detected_at&order=desc → sort null に畳む", () => {
    const r = parse("sort=detected_at&order=desc");
    expect(r.ok && r.query.sort).toBeNull();
    expect(r.ok && r.query.order).toBe("desc");
  });

  it("E2-8: sort=error_level(order 省略)→ order asc", () => {
    const r = parse("sort=error_level");
    expect(r.ok && r.query.sort).toBe("error_level");
    expect(r.ok && r.query.order).toBe("asc");
  });
});

/** 指示書BH 表E1 — `errorReportKeyFromRow()` 経由でキーを作る */
describe("filterSortErrorReportKeys(指示書BH 表E1)", () => {
  const db = (iso: string): Date => toDbJst(new Date(iso));

  function bhSource(
    overrides: Partial<ErrorReportKeySource> & Pick<ErrorReportKeySource, "error_id">
  ): ErrorReportKeySource {
    return {
      clean_timetable_id: BigInt(501),
      facility_id: BigInt(1),
      serial_number: "8111R-0002",
      error_type: "Offline",
      error_level: "ERROR",
      status: 0,
      comment: null,
      errored_at: db("2026-11-03T02:00:00+09:00"),
      created_at: db("2026-11-03T02:01:00+09:00"),
      facility: { deleted_at: null },
      section: { deleted_at: null },
      ...overrides,
    };
  }

  const BH_E1 = errorReportKeyFromRow(bhSource({ error_id: "E-1" }));
  const BH_E2 = errorReportKeyFromRow(
    bhSource({
      error_id: "E-2",
      clean_timetable_id: BigInt(CLEAN_TIMETABLE_ID_UNSCHEDULED),
      serial_number: "---",
      error_type: "SECURITY_ON",
      error_level: "CRITICAL",
      status: robotErrorStatusFromResolutionStatus("in_progress"),
      comment: "回線確認中",
    })
  );
  const BH_E3 = errorReportKeyFromRow(
    bhSource({
      error_id: "E-3",
      clean_timetable_id: null,
      serial_number: "8111R-0001",
      error_type: "BrushError",
      error_level: "WARNING",
      comment: "",
      errored_at: db("2026-11-02T23:00:00+09:00"),
      created_at: db("2026-11-02T23:01:00+09:00"),
    })
  );
  const BH_E4 = errorReportKeyFromRow(
    bhSource({
      error_id: "E-4",
      clean_timetable_id: BigInt(502),
      serial_number: "DS-005",
      error_type: "D-S_CONTROL_FAILURE",
      error_level: "CRITICAL",
      status: robotErrorStatusFromResolutionStatus("resolved"),
      comment: "復帰を確認",
      errored_at: db("2026-11-02T22:00:00+09:00"),
      created_at: db("2026-11-02T22:01:00+09:00"),
    })
  );
  const BH_E5 = errorReportKeyFromRow(
    bhSource({
      error_id: "E-5",
      clean_timetable_id: BigInt(503),
      serial_number: "8111R-0001",
      status: robotErrorStatusFromResolutionStatus("in_progress"),
      errored_at: db("2026-11-01T10:00:00+09:00"),
      created_at: db("2026-11-01T10:01:00+09:00"),
    })
  );
  const BH_E10 = errorReportKeyFromRow(
    bhSource({
      error_id: "E-10",
      clean_timetable_id: BigInt(504),
      serial_number: "8111R-0003",
      error_type: "LowBattery",
      error_level: "WARNING",
      comment: "現場に連絡済み",
      errored_at: db("2026-11-02T22:00:00+09:00"),
      created_at: db("2026-11-02T22:02:00+09:00"),
    })
  );

  const BH_KEYS: readonly ErrorReportKey[] = [BH_E5, BH_E4, BH_E10, BH_E3, BH_E2, BH_E1];

  const bhBase: ErrorReportDbQuery = {
    include_resolved: true,
    facility_ids: null,
    date_from: null,
    date_to: null,
    error_type: "",
    sort: null,
    order: "asc",
    schedule: null,
    page: 1,
    per_page: PAGINATION.defaultPerPage,
  };

  const bhIds = (keys: readonly ErrorReportKey[]) => keys.map((k) => k.error_id);

  function bhQuery(
    overrides: Partial<ErrorReportDbQuery> & {
      sort?: ErrorReportSortKey | null;
      order?: "asc" | "desc";
    }
  ): ErrorReportDbQuery {
    return { ...bhBase, ...overrides };
  }

  it("E1-1: sort null → 既定順", () => {
    expect(bhIds(filterSortErrorReportKeys(BH_KEYS, bhBase))).toEqual([
      "E-1",
      "E-2",
      "E-3",
      "E-10",
      "E-4",
      "E-5",
    ]);
  });

  it("E1-2: detected_at asc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "detected_at", order: "asc" })))
    ).toEqual(["E-5", "E-10", "E-4", "E-3", "E-1", "E-2"]);
  });

  it("E1-3: serial_number asc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "serial_number", order: "asc" })))
    ).toEqual(["E-2", "E-3", "E-5", "E-1", "E-10", "E-4"]);
  });

  it("E1-4: serial_number desc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "serial_number", order: "desc" })))
    ).toEqual(["E-4", "E-10", "E-1", "E-3", "E-5", "E-2"]);
  });

  it("E1-5: error_type asc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "error_type", order: "asc" })))
    ).toEqual(["E-3", "E-4", "E-10", "E-1", "E-5", "E-2"]);
  });

  it("E1-6: error_type desc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "error_type", order: "desc" })))
    ).toEqual(["E-2", "E-1", "E-5", "E-10", "E-4", "E-3"]);
  });

  it("E1-7: error_level asc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "error_level", order: "asc" })))
    ).toEqual(["E-2", "E-4", "E-1", "E-5", "E-3", "E-10"]);
  });

  it("E1-8: error_level desc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "error_level", order: "desc" })))
    ).toEqual(["E-3", "E-10", "E-1", "E-5", "E-2", "E-4"]);
  });

  it("E1-9: resolution_status asc", () => {
    expect(
      bhIds(
        filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "resolution_status", order: "asc" }))
      )
    ).toEqual(["E-1", "E-3", "E-10", "E-2", "E-5", "E-4"]);
  });

  it("E1-10: resolution_status desc", () => {
    expect(
      bhIds(
        filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "resolution_status", order: "desc" }))
      )
    ).toEqual(["E-4", "E-2", "E-5", "E-1", "E-3", "E-10"]);
  });

  it("E1-11: note asc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "note", order: "asc" })))
    ).toEqual(["E-2", "E-4", "E-10", "E-1", "E-3", "E-5"]);
  });

  it("E1-12: note desc", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ sort: "note", order: "desc" })))
    ).toEqual(["E-10", "E-4", "E-2", "E-1", "E-3", "E-5"]);
  });

  it("E1-13: schedule scheduled", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ schedule: "scheduled" })))
    ).toEqual(["E-1", "E-10", "E-4", "E-5"]);
  });

  it("E1-14: schedule unscheduled", () => {
    expect(
      bhIds(filterSortErrorReportKeys(BH_KEYS, bhQuery({ schedule: "unscheduled" })))
    ).toEqual(["E-2", "E-3"]);
  });

  it("E1-15: schedule unscheduled + serial_number desc", () => {
    expect(
      bhIds(
        filterSortErrorReportKeys(
          BH_KEYS,
          bhQuery({ schedule: "unscheduled", sort: "serial_number", order: "desc" })
        )
      )
    ).toEqual(["E-3", "E-2"]);
  });

  it("E1-16: include_resolved false + resolution_status asc", () => {
    expect(
      bhIds(
        filterSortErrorReportKeys(BH_KEYS, {
          ...bhBase,
          include_resolved: false,
          sort: "resolution_status",
          order: "asc",
        })
      )
    ).toEqual(["E-1", "E-3", "E-10", "E-2", "E-5"]);
  });

  it("E1-17: include_resolved false + schedule scheduled", () => {
    expect(
      bhIds(
        filterSortErrorReportKeys(BH_KEYS, {
          ...bhBase,
          include_resolved: false,
          schedule: "scheduled",
        })
      )
    ).toEqual(["E-1", "E-10", "E-5"]);
  });
});