/**
* 指示書AY §3 表1(`lib/ds-logs/query.ts` の `queryDeviceLogs()`)。
* ブリーフ §5 と監督判断 第2ラウンドから本書で具体化。★P1・P2 の行は §11 Q14・Q15 の採用で確定★。
*
* ★表1 は純粋関数の入力なので架空の名前のまま★(§3 冒頭・§11-4 M6。実機の種類・name の形は fixtures でだけ使う)。
*
* types(制御盤から来た順)= app「アプリ」/ comm「通信」/ sys「システム」。now = 2026-10-19T12:00:00+09:00
*
* | file_id | name | type | size | modified_at | 備考 |
* | f1 | app_20261019_1100.log | app | 1000 | 2026-10-19T11:00:00+09:00 | f3 と同時刻 |
* | f2 | app_20261019_0900.log | app | 2000 | 2026-10-19T09:00:00+09:00 | |
* | f3 | comm_20261019_1100.log | comm | 3000 | 2026-10-19T11:00:00+09:00 | f1 と同時刻 |
* | f4 | comm_20261018_1200.log | comm | 4000 | 2026-10-18T12:00:00+09:00 | ちょうど24時間前 |
* | f5 | sys_20261019_1130.log | sys | 5000 | 2026-10-19T11:30:00+09:00 | |
* | f6 | sys_20261018_1159.log | sys | 6000 | 2026-10-18T11:59:59+09:00 | 24時間前の1秒前 |
* | f7 | app_20261019_0200.log | app | 7000 | 2026-10-18T17:00:00Z | = 10/19 02:00 JST |
* | f8 | app_20261019_1200.log | app | 8000 | 2026-10-19T12:00:00+09:00 | ちょうど now |
*
* | # | 入力 | 期待(file_id の並び) | total |
* | Q1 | 指定なし(sort=date_desc) | f8, f5, f1, f3, f2, f7, f4, f6 | 8 |
* | Q2 | types=app | f8, f1, f2, f7 | 4 |
* | Q3 | types=app,sys | f8, f5, f1, f2, f7, f6 | 6 |
* | Q4 | types=xyz | (空) | 0 |
* | Q5 | types=app,xyz | f8, f1, f2, f7 | 4 |
* | Q6 | from=2026-10-19T11:00:00+09:00 | f8, f5, f1, f3 | 4 |(ちょうど from は入る)
* | Q7 | to=2026-10-19T11:00:00+09:00 | f2, f7, f4, f6 | 4 |(ちょうど to は入らない)
* | Q8 | from=10-18T12:00+09:00 & to=10-19T12:00+09:00 | f5, f1, f3, f2, f7, f4 | 6 |
* | Q9 | from=10-19T02:00:00+09:00 & to=10-19T02:00:01+09:00 | f7 | 1 |(比較は瞬間)
* | Q10 | from=2026-10-19T02:30:00Z | f8, f5 | 2 |
* | Q11 | sort=type_date_desc | f8, f1, f2, f7, f3, f4, f5, f6 | 8 |
* | Q12 | sort=type_date_desc & types=comm,app | f8, f1, f2, f7, f3, f4 | 6 |(制御盤の types の順)
* | Q13 | from=2026-10-18T12:00:00+09:00(直近24時間) | f8, f5, f1, f3, f2, f7, f4 | 7 |
* | Q14 | Q1 を perPage=3, page=1 | f8, f5, f1 | 8 |
* | Q15 | Q1 を perPage=3, page=3 | f4, f6 | 8 |
* | Q16 | Q1 を perPage=3, page=4 | (空) | 8 |
* | Q17 | 出力の modified_at | f7 は 2026-10-19T02:00:00+09:00、他は入力どおり | — |
*/
import { describe, expect, it } from "vitest";
import { deviceLogTypesParam, queryDeviceLogs, type DeviceLogQuery } from "@/lib/ds-logs/query";
import type { DsLogFile, DsLogType } from "@/lib/types";
const TYPES: readonly DsLogType[] = [
{ key: "app", label: "アプリ" },
{ key: "comm", label: "通信" },
{ key: "sys", label: "システム" },
];
const ITEMS: readonly DsLogFile[] = [
{ file_id: "f1", name: "app_20261019_1100.log", type: "app", size_bytes: 1000, modified_at: "2026-10-19T11:00:00+09:00" },
{ file_id: "f2", name: "app_20261019_0900.log", type: "app", size_bytes: 2000, modified_at: "2026-10-19T09:00:00+09:00" },
{ file_id: "f3", name: "comm_20261019_1100.log", type: "comm", size_bytes: 3000, modified_at: "2026-10-19T11:00:00+09:00" },
{ file_id: "f4", name: "comm_20261018_1200.log", type: "comm", size_bytes: 4000, modified_at: "2026-10-18T12:00:00+09:00" },
{ file_id: "f5", name: "sys_20261019_1130.log", type: "sys", size_bytes: 5000, modified_at: "2026-10-19T11:30:00+09:00" },
{ file_id: "f6", name: "sys_20261018_1159.log", type: "sys", size_bytes: 6000, modified_at: "2026-10-18T11:59:59+09:00" },
{ file_id: "f7", name: "app_20261019_0200.log", type: "app", size_bytes: 7000, modified_at: "2026-10-18T17:00:00Z" },
{ file_id: "f8", name: "app_20261019_1200.log", type: "app", size_bytes: 8000, modified_at: "2026-10-19T12:00:00+09:00" },
];
/** 全件が1ページに収まる大きさ(表1 の Q1〜Q13 はページ分けを見ない) */
const ALL = 1000;
function q(overrides: Partial<DeviceLogQuery> = {}): DeviceLogQuery {
return {
types: null,
from: null,
to: null,
sort: "date_desc",
page: 1,
perPage: ALL,
...overrides,
};
}
const at = (iso: string): number => Date.parse(iso);
describe("queryDeviceLogs() — 表1", () => {
it.each<[string, DeviceLogQuery, string[], number]>([
["Q1 指定なし(新しい順・同時刻は name 昇順)", q(), ["f8", "f5", "f1", "f3", "f2", "f7", "f4", "f6"], 8],
["Q2 types=app", q({ types: ["app"] }), ["f8", "f1", "f2", "f7"], 4],
["Q3 types=app,sys", q({ types: ["app", "sys"] }), ["f8", "f5", "f1", "f2", "f7", "f6"], 6],
["Q4 types=xyz(未知のキーは0件。D23)", q({ types: ["xyz"] }), [], 0],
["Q5 types=app,xyz(D23)", q({ types: ["app", "xyz"] }), ["f8", "f1", "f2", "f7"], 4],
["Q6 ちょうど from は入る", q({ from: at("2026-10-19T11:00:00+09:00") }), ["f8", "f5", "f1", "f3"], 4],
["Q7 ちょうど to は入らない", q({ to: at("2026-10-19T11:00:00+09:00") }), ["f2", "f7", "f4", "f6"], 4],
[
"Q8 f4 は入る・f8 は入らない・f6 は前",
q({ from: at("2026-10-18T12:00:00+09:00"), to: at("2026-10-19T12:00:00+09:00") }),
["f5", "f1", "f3", "f2", "f7", "f4"],
6,
],
[
"Q9 比較は瞬間(Date.parse)。文字列で比べない",
q({ from: at("2026-10-19T02:00:00+09:00"), to: at("2026-10-19T02:00:01+09:00") }),
["f7"],
1,
],
["Q10 from 側の Z も瞬間で", q({ from: at("2026-10-19T02:30:00Z") }), ["f8", "f5"], 2],
[
"Q11 type_date_desc(types 配列の順 → 新しい順 → name 昇順)",
q({ sort: "type_date_desc" }),
["f8", "f1", "f2", "f7", "f3", "f4", "f5", "f6"],
8,
],
[
"Q12 クエリの順ではなく制御盤の types の順",
q({ sort: "type_date_desc", types: ["comm", "app"] }),
["f8", "f1", "f2", "f7", "f3", "f4"],
6,
],
[
"Q13 直近24時間(f6 は入らない)",
q({ from: at("2026-10-18T12:00:00+09:00") }),
["f8", "f5", "f1", "f3", "f2", "f7", "f4"],
7,
],
["Q14 perPage=3, page=1", q({ perPage: 3, page: 1 }), ["f8", "f5", "f1"], 8],
["Q15 perPage=3, page=3(最終ページの端数)", q({ perPage: 3, page: 3 }), ["f4", "f6"], 8],
["Q16 perPage=3, page=4(範囲外は空配列・total は正しい)", q({ perPage: 3, page: 4 }), [], 8],
])("%s", (_label, query, expectedIds, expectedTotal) => {
const result = queryDeviceLogs(TYPES, ITEMS, query);
expect(result.items.map((i) => i.file_id)).toEqual(expectedIds);
expect(result.total).toBe(expectedTotal);
});
it("Q17 出力の modified_at は toJstIso で JST に揃える(f7 だけ変わる)", () => {
const result = queryDeviceLogs(TYPES, ITEMS, q());
const byId = new Map(result.items.map((i) => [i.file_id, i.modified_at]));
expect(byId.get("f7")).toBe("2026-10-19T02:00:00+09:00");
for (const item of ITEMS) {
if (item.file_id === "f7") continue;
expect(byId.get(item.file_id)).toBe(item.modified_at);
}
});
it("入力の配列を書き換えない", () => {
const items = ITEMS.map((i) => ({ ...i }));
const snapshot = JSON.stringify(items);
queryDeviceLogs(TYPES, items, q({ sort: "type_date_desc" }));
expect(JSON.stringify(items)).toBe(snapshot);
});
});
describe("deviceLogTypesParam() — 指示書BJ 表Q", () => {
it.each<[string, readonly string[], ReadonlySet<string> | null, string | null]>([
["Q1 selected null", ["a", "b", "c"], null, null],
["Q2 全種類", ["a", "b", "c"], new Set(["a", "b", "c"]), null],
["Q3 一部(known の順)", ["a", "b", "c"], new Set(["a", "b"]), "a,b"],
["Q4 飛び(known の順)", ["a", "b", "c"], new Set(["c", "a"]), "a,c"],
["Q5 件数同じだが known を含まない", ["a", "b"], new Set(["a", "x"]), "a,x"],
["Q6 種類が無い", [], new Set(), null],
])("%s", (_label, known, selected, expected) => {
expect(deviceLogTypesParam(known, selected)).toBe(expected);
});
});