/**
* 指示書AY 段階3-3 の V1〜V10・X1〜X6(クエリ・本体・パスの検証)と 段階7-2 の W1〜W11(制御盤の一覧の結果の検証)、
* あわせて prepared / complete / fail の本体の検証(§8・段階7-2 の末尾・表5 R43)。
* ブリーフ §5 と監督判断 第2ラウンドから本書で具体化。★P1・P2 の行は §11 Q14・Q15 の採用で確定★。
* ★V・W は 2026-11-05 に監督が改めた後の表が正(§11-4 M7・M8)★。
*
* ── 段階3-3(ブラウザ向けは 422 `validation_failed`)──
* | V1 | パラメータなし | { types: null, from: null, to: null, sort: "date_desc", page: 1, perPage: 200 } |
* | V2 | from=2026-10-19(日付だけ)/from=2026-10-19T11:00:00(オフセットなし)/from=abc | 422 |
* | V2b | from=2026-02-30T00:00:00+09:00/to=2026-02-29T00:00:00Z(平年)/from=2026-13-01T00:00:00+09:00 | 422 |
* | V2c | from=2026-10-19T02:30:00Z/from=2026-10-19T02:30:00+00:00 | 通る |
* | V3 | from > to | 422。from = to は通る(0件) |
* | V4 | types=・types=%20・from=・to=・sort=(空・空白だけ) | 省略と同じ(V1 と同じ値) |
* | V4b | types=app,,sys/types=app,(空でない並びの中の空の要素) | 422 |
* | V5 | sort=name | 422 |
* | V6 | per_page=30→30・500→200・0→1・abc→200・page=0→1・1.5→1・abc→1 | 422 にせず丸める |
* | V7 | POST の本体 { file_ids: [] } / 501件 / 重複あり / 要素が文字列でない / 8193文字 | 422 |
* | V8 | POST の本体が JSON だがオブジェクトでない(null・[]・1・"x") | 422「JSONの形式が不正です」 |
* | V9 | POST の本体が JSON でない({) | 422「JSONの形式が不正です」 |
* | V10 | wait=0・wait=26・wait=abc(制御盤向け) | 422(丸めない) |
*
* ── パスパラメータ(M8)──
* | X1 | export_id / command_id が randomUUID の形(36文字) | 通る |
* | X2 | 空文字・PATH_*_MAX_LENGTH + 1 文字 | 422(入力値を message / details に入れない) |
* | X3 | export_id=abc | 通る(文字種は絞らない) |
* | X4 | seq=0・1・10 | 通る |
* | X5 | seq=-1・01・1.0・abc・1e3・空 | 422 |
* | X6 | seq=1234567890(10桁) | 422(上限は9桁) |
*
* ── 段階7-2(制御盤の一覧の結果。不正なら制御盤には 400 `result_invalid`)──
* | W1 | 正しい(types 3・items 8・truncated: false) | 受理 |
* | W2 | types の key が重複 | 不正 |
* | W3 | items[].type が types に無い | 不正 |
* | W4 | modified_at にオフセットが無い | 不正 |
* | W4b | modified_at が存在しない日付(2026-02-30T00:00:00+09:00) | 不正 |
* | W5 | size_bytes が負・小数・文字列 | 不正 |
* | W6 | file_id が重複 | 不正 |
* | W7 | items が LIST_RESULT_MAX_ITEMS + 1 件 | 不正 |
* | W8 | 本体が LIST_RESULT_MAX_BYTES を超える | 不正(読んだ長さで判定) |
* | W9 | { error: { code, message } } | failCommand と同じ扱い |
* | W10 | 本体が JSON だがオブジェクトでない(null・[]・1・"x") | 不正 |
* | W11 | items[].name が api_client/api_client.log.2026-10-18 の形 | 受理(/ を理由に弾かない) |
*/
import { randomUUID } from "node:crypto";
import { describe, expect, it } from "vitest";
import {
DS_LOG_DEFAULT_PER_PAGE,
EXPORT_FILE_IDS_MAX_BYTES,
EXPORT_MAX_FILES,
LIST_RESULT_MAX_BYTES,
LIST_RESULT_MAX_ITEMS,
AGENT_POLL_WAIT_SEC,
PATH_COMMAND_ID_MAX_LENGTH,
PATH_EXPORT_ID_MAX_LENGTH,
} from "@/lib/config/ds-agent";
import { fileIdsOfJsonBytes } from "@/lib/ds-logs/__tests__/file-ids-of-bytes";
import {
EXPORT_FILE_IDS_TOO_LARGE_MESSAGE,
parseAgentComplete,
parseAgentFail,
parseAgentListResult,
parseAgentPollWait,
parseAgentPrepared,
parseDeviceLogQuery,
parseExportRequest,
parsePathCommandId,
parsePathExportId,
parsePathSeq,
} from "@/lib/ds-logs/validate";
import type { QueryParseResult } from "@/lib/api/query-result";
import type { DsAgentListResult } from "@/lib/types";
function expect422<T>(result: QueryParseResult<T>) {
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.status).toBe(422);
expect(result.error.error.code).toBe("validation_failed");
}
function expectResultInvalid<T>(result: QueryParseResult<T>, message?: string) {
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.status).toBe(400);
expect(result.error.error.code).toBe("result_invalid");
if (message !== undefined) expect(result.error.error.message).toBe(message);
}
function okQuery<T>(result: QueryParseResult<T>): T {
if (!result.ok) throw new Error(`期待は成功: ${JSON.stringify(result.error)}`);
return result.query;
}
const params = (qs: string) => new URLSearchParams(qs);
const V1_QUERY = { types: null, from: null, to: null, sort: "date_desc", page: 1, perPage: 200 };
describe("parseDeviceLogQuery() — 段階3-3 V1〜V6", () => {
it("V1 パラメータなし", () => {
expect(okQuery(parseDeviceLogQuery(params("")))).toEqual(V1_QUERY);
expect(DS_LOG_DEFAULT_PER_PAGE).toBe(200);
});
it("types はカンマ区切りの配列になる", () => {
expect(okQuery(parseDeviceLogQuery(params("types=app,sys"))).types).toEqual(["app", "sys"]);
});
it("from / to / sort を読む(from・to は瞬間の ms)", () => {
expect(
okQuery(
parseDeviceLogQuery(
params("from=2026-10-19T11:00:00%2B09:00&to=2026-10-19T12:00:00%2B09:00&sort=type_date_desc")
)
)
).toEqual({
...V1_QUERY,
from: Date.parse("2026-10-19T11:00:00+09:00"),
to: Date.parse("2026-10-19T12:00:00+09:00"),
sort: "type_date_desc",
});
});
it.each([
["日付だけ", "from=2026-10-19"],
["オフセットなし", "from=2026-10-19T11:00:00"],
["abc", "from=abc"],
])("V2 %s → 422", (_label, qs) => {
expect422(parseDeviceLogQuery(params(qs)));
});
it.each([
["2/30", "from=2026-02-30T00:00:00%2B09:00"],
["平年の 2/29(Z)", "to=2026-02-29T00:00:00Z"],
["13月", "from=2026-13-01T00:00:00%2B09:00"],
])("V2b 存在しない日付 %s → 422", (_label, qs) => {
expect422(parseDeviceLogQuery(params(qs)));
});
it.each([
["Z", "from=2026-10-19T02:30:00Z"],
["+00:00", "from=2026-10-19T02:30:00%2B00:00"],
])("V2c +09:00 以外のオフセット %s も通る", (_label, qs) => {
expect(okQuery(parseDeviceLogQuery(params(qs))).from).toBe(Date.parse("2026-10-19T02:30:00Z"));
});
it("V3 from > to → 422", () => {
expect422(
parseDeviceLogQuery(params("from=2026-10-19T12:00:00%2B09:00&to=2026-10-19T11:00:00%2B09:00"))
);
});
it("V3 from = to は通る", () => {
const q = okQuery(
parseDeviceLogQuery(params("from=2026-10-19T12:00:00%2B09:00&to=2026-10-19T03:00:00Z"))
);
expect(q.from).toBe(q.to);
});
it.each(["types=", "types=%20", "from=", "from=%20", "to=", "to=%20", "sort=", "sort=%20"])(
"V4 %s は省略と同じ",
(qs) => {
expect(okQuery(parseDeviceLogQuery(params(qs)))).toEqual(V1_QUERY);
}
);
it.each(["types=app,,sys", "types=app,"])("V4b %s → 422", (qs) => {
expect422(parseDeviceLogQuery(params(qs)));
});
it("V5 sort=name → 422", () => {
expect422(parseDeviceLogQuery(params("sort=name")));
});
it.each<[string, "page" | "perPage", number]>([
["per_page=30", "perPage", 30],
["per_page=500", "perPage", 200],
["per_page=0", "perPage", 1],
["per_page=abc", "perPage", 200],
["page=0", "page", 1],
["page=1.5", "page", 1],
["page=abc", "page", 1],
])("V6 %s は 422 にせず丸める", (qs, key, expected) => {
expect(okQuery(parseDeviceLogQuery(params(qs)))[key]).toBe(expected);
});
});
describe("parseExportRequest() — 段階3-3 V7〜V9", () => {
it("正しい本体(file_ids 1件・EXPORT_MAX_FILES 件・8192文字)は通る", () => {
expect(okQuery(parseExportRequest(JSON.stringify({ file_ids: ["a", "b"] })))).toEqual({
fileIds: ["a", "b"],
});
const max = Array.from({ length: EXPORT_MAX_FILES }, (_, i) => `id-${i}`);
expect(okQuery(parseExportRequest(JSON.stringify({ file_ids: max }))).fileIds).toHaveLength(
EXPORT_MAX_FILES
);
const long = "x".repeat(8192);
expect(okQuery(parseExportRequest(JSON.stringify({ file_ids: [long] }))).fileIds).toEqual([long]);
});
it.each<[string, unknown]>([
["空の配列", { file_ids: [] }],
["EXPORT_MAX_FILES + 1 件", { file_ids: Array.from({ length: EXPORT_MAX_FILES + 1 }, (_, i) => `id-${i}`) }],
["重複あり", { file_ids: ["a", "a"] }],
["要素が文字列でない", { file_ids: ["a", 1] }],
["8193文字", { file_ids: ["x".repeat(8193)] }],
["空文字の要素", { file_ids: [""] }],
["file_ids が無い", {}],
["file_ids が配列でない", { file_ids: "a" }],
])("V7 %s → 422", (_label, body) => {
expect422(parseExportRequest(JSON.stringify(body)));
});
it("K7-1 file_ids の JSON がちょうど EXPORT_FILE_IDS_MAX_BYTES → 通る", () => {
const fileIds = fileIdsOfJsonBytes(EXPORT_FILE_IDS_MAX_BYTES);
expect(new TextEncoder().encode(JSON.stringify(fileIds)).length).toBe(EXPORT_FILE_IDS_MAX_BYTES);
expect(okQuery(parseExportRequest(JSON.stringify({ file_ids: fileIds }))).fileIds).toEqual(fileIds);
});
it("K7-2 file_ids の JSON が EXPORT_FILE_IDS_MAX_BYTES + 1 → 422・多すぎます", () => {
const fileIds = fileIdsOfJsonBytes(EXPORT_FILE_IDS_MAX_BYTES + 1);
expect(new TextEncoder().encode(JSON.stringify(fileIds)).length).toBe(EXPORT_FILE_IDS_MAX_BYTES + 1);
const result = parseExportRequest(JSON.stringify({ file_ids: fileIds }));
expect422(result);
if (!result.ok) {
expect(result.error.error.message).toBe(EXPORT_FILE_IDS_TOO_LARGE_MESSAGE);
expect(result.error.error.details?.file_ids).toHaveLength(1);
}
});
it("K7-3 多バイト文字はバイトで数える(文字数では上限内でも 422)", () => {
const fileIds = ["あ".repeat(8192), "い".repeat(8192)];
expect(fileIds.join("").length).toBeLessThan(EXPORT_FILE_IDS_MAX_BYTES);
expect(new TextEncoder().encode(JSON.stringify(fileIds)).length).toBeGreaterThan(EXPORT_FILE_IDS_MAX_BYTES);
const result = parseExportRequest(JSON.stringify({ file_ids: fileIds }));
expect422(result);
if (!result.ok) expect(result.error.error.message).toBe(EXPORT_FILE_IDS_TOO_LARGE_MESSAGE);
});
it.each(["null", "[]", "1", '"x"'])("V8 オブジェクトでない JSON(%s)→ 422「JSONの形式が不正です」", (text) => {
const result = parseExportRequest(text);
expect422(result);
if (!result.ok) expect(result.error.error.message).toBe("JSONの形式が不正です");
});
it("V9 JSON でない({)→ 422「JSONの形式が不正です」", () => {
const result = parseExportRequest("{");
expect422(result);
if (!result.ok) expect(result.error.error.message).toBe("JSONの形式が不正です");
});
});
describe("parseAgentPollWait() — 段階3-3 V10(表5 R8)", () => {
it("省略は AGENT_POLL_WAIT_SEC(25)", () => {
expect(okQuery(parseAgentPollWait(null))).toBe(AGENT_POLL_WAIT_SEC);
expect(AGENT_POLL_WAIT_SEC).toBe(25);
});
it.each<[string, number]>([
["1", 1],
["25", 25],
])("wait=%s → %d", (value, expected) => {
expect(okQuery(parseAgentPollWait(value))).toBe(expected);
});
it.each(["0", "26", "abc"])("V10 wait=%s → 422(丸めない)", (value) => {
expect422(parseAgentPollWait(value));
});
});
describe("パスパラメータ — 段階3-3 X1〜X6(M8)", () => {
const idParsers = [
["export_id", parsePathExportId, PATH_EXPORT_ID_MAX_LENGTH],
["command_id", parsePathCommandId, PATH_COMMAND_ID_MAX_LENGTH],
] as const;
it.each(idParsers)("X1 %s が randomUUID の形(36文字)なら通る", (_name, parse) => {
const id = randomUUID();
expect(okQuery(parse(id))).toBe(id);
});
it.each(idParsers)("X1 %s が上限ちょうどなら通る", (_name, parse, max) => {
const id = "a".repeat(max);
expect(okQuery(parse(id))).toBe(id);
});
it.each(idParsers)("X2 %s が空文字・上限 + 1 文字なら 422(入力値をエコーしない)", (_name, parse, max) => {
expect422(parse(""));
const tooLong = "Q".repeat(max + 1);
const result = parse(tooLong);
expect422(result);
if (!result.ok) expect(JSON.stringify(result.error)).not.toContain(tooLong);
});
it.each(idParsers)("X3 %s=abc(形は違うが長さは範囲内)は通る", (_name, parse) => {
expect(okQuery(parse("abc"))).toBe("abc");
});
it.each<[string, number]>([
["0", 0],
["1", 1],
["10", 10],
["123456789", 123456789],
])("X4 seq=%s → %d", (value, expected) => {
expect(okQuery(parsePathSeq(value))).toBe(expected);
});
it.each(["-1", "01", "1.0", "abc", "1e3", "", " 1"])("X5 seq=%j → 422", (value) => {
expect422(parsePathSeq(value));
});
it("X6 seq=1234567890(10桁)→ 422", () => {
expect422(parsePathSeq("1234567890"));
});
});
// ── 段階7-2 W1〜W11 ──
const W_TYPES = [
{ key: "app", label: "アプリ" },
{ key: "comm", label: "通信" },
{ key: "sys", label: "システム" },
];
const W_ITEMS = [
{ 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" },
];
const W1_BODY = { types: W_TYPES, items: W_ITEMS, truncated: false };
function withItem(index: number, patch: Record<string, unknown>) {
return { ...W1_BODY, items: W_ITEMS.map((item, i) => (i === index ? { ...item, ...patch } : item)) };
}
/** 壊した key を types[0] と items の type:"app" の両方に使う(指示書BK K1) */
function withTypeKey(bad: string) {
return {
...W1_BODY,
types: [{ key: bad, label: "x" }, ...W_TYPES.slice(1)],
items: W_ITEMS.map((item) => (item.type === "app" ? { ...item, type: bad } : item)),
};
}
function manyItems(count: number) {
return {
types: [{ key: "app", label: "app" }],
items: Array.from({ length: count }, (_, i) => ({
file_id: `f${i}`,
name: `app/app.log.${i}`,
type: "app",
size_bytes: i,
modified_at: "2026-10-19T12:00:00+09:00",
})),
truncated: true,
};
}
const byteLength = (text: string) => new TextEncoder().encode(text).length;
/** 成功の形(`{ types, items, truncated }`)の items。失敗の報告(`{ error }`)なら落とす */
function listItems(result: QueryParseResult<DsAgentListResult>) {
const query = okQuery(result);
if ("error" in query) throw new Error("期待は一覧の結果");
return query.items;
}
describe("parseAgentListResult() — 段階7-2 W1〜W11", () => {
it("W1 正しい結果は受理(そのまま返す)", () => {
expect(okQuery(parseAgentListResult(JSON.stringify(W1_BODY)))).toEqual(W1_BODY);
});
it("truncated: true もそのまま返す(表5 R38・D18)", () => {
const body = { ...W1_BODY, truncated: true };
expect(okQuery(parseAgentListResult(JSON.stringify(body)))).toEqual(body);
});
it.each<[string, unknown]>([
["W2 types の key が重複", { ...W1_BODY, types: [...W_TYPES, { key: "app", label: "重複" }] }],
["W3 items[].type が types に無い", withItem(0, { type: "xyz" })],
["W4 modified_at にオフセットが無い", withItem(0, { modified_at: "2026-10-19T11:00:00" })],
["W4b modified_at が存在しない日付", withItem(0, { modified_at: "2026-02-30T00:00:00+09:00" })],
["W5 size_bytes が負", withItem(0, { size_bytes: -1 })],
["W5 size_bytes が小数", withItem(0, { size_bytes: 1.5 })],
["W5 size_bytes が文字列", withItem(0, { size_bytes: "1000" })],
["W6 file_id が重複", withItem(1, { file_id: "f1" })],
["truncated が無い(必須の項目が無い。R43)", { types: W_TYPES, items: W_ITEMS }],
["types が配列でない(型が違う。R43)", { ...W1_BODY, types: "app" }],
["items[].name が文字列でない(型が違う。R43)", withItem(0, { name: 1 })],
])("%s → 400 result_invalid", (_label, body) => {
expectResultInvalid(parseAgentListResult(JSON.stringify(body)));
});
it("W7 items が LIST_RESULT_MAX_ITEMS + 1 件 → 不正(ちょうどは受理)", () => {
expect(listItems(parseAgentListResult(JSON.stringify(manyItems(LIST_RESULT_MAX_ITEMS))))).toHaveLength(
LIST_RESULT_MAX_ITEMS
);
expectResultInvalid(parseAgentListResult(JSON.stringify(manyItems(LIST_RESULT_MAX_ITEMS + 1))));
});
it("W8 本体が LIST_RESULT_MAX_BYTES を超える → 不正(ちょうどは受理)", () => {
// ★ASCII だけの本体★(文字数とバイト数が一致するので、どちらで数えても境界が同じになる)
const body = JSON.stringify({ ...manyItems(3), truncated: false });
const exact = body + " ".repeat(LIST_RESULT_MAX_BYTES - byteLength(body));
expect(byteLength(exact)).toBe(LIST_RESULT_MAX_BYTES);
expect(listItems(parseAgentListResult(exact))).toHaveLength(3);
expectResultInvalid(parseAgentListResult(`${exact} `));
});
it("W9 { error: { code, message } } は失敗の報告として受理(failCommand と同じ扱い)", () => {
const body = { error: { code: "io_error", message: "読めませんでした" } };
expect(okQuery(parseAgentListResult(JSON.stringify(body)))).toEqual(body);
});
it.each<[string, unknown]>([
["code が空", { error: { code: "", message: "m" } }],
["code が65文字", { error: { code: "c".repeat(65), message: "m" } }],
["message が501文字", { error: { code: "io_error", message: "m".repeat(501) } }],
["message が無い", { error: { code: "io_error" } }],
])("W9 の error の形の誤り(%s)→ 不正", (_label, body) => {
expectResultInvalid(parseAgentListResult(JSON.stringify(body)));
});
it.each(["null", "[]", "1", '"x"'])("W10 オブジェクトでない JSON(%s)→ 400 result_invalid", (text) => {
expectResultInvalid(parseAgentListResult(text));
});
it("JSON でない本体(R43)→ 400 result_invalid", () => {
expectResultInvalid(parseAgentListResult("{"));
});
it("W11 name に / を含む({root の name}/{相対パス})は受理", () => {
const body = withItem(0, { name: "api_client/api_client.log.2026-10-18" });
expect(okQuery(parseAgentListResult(JSON.stringify(body)))).toEqual(body);
});
it.each<[string, unknown]>([
["R45 key が空", withTypeKey("")],
["R46 key にカンマ", withTypeKey("a,b")],
["R46b key が空白だけ", withTypeKey(" ")],
["R46b key の先頭が空白", withTypeKey(" app")],
])("%s → 400 result_invalid・typesの形式が不正です(指示書BK K1)", (_label, body) => {
expectResultInvalid(parseAgentListResult(JSON.stringify(body)), "typesの形式が不正です");
});
it.each<[string, unknown]>([
["R47 file_id が8193文字", withItem(0, { file_id: "x".repeat(8193) })],
])("%s → 400 result_invalid", (_label, body) => {
expectResultInvalid(parseAgentListResult(JSON.stringify(body)));
});
it("R46b key の中の空白(a b)はよい", () => {
const body = {
types: [{ key: "a b", label: "x" }],
items: [{ file_id: "f1", name: "n", type: "a b", size_bytes: 1, modified_at: "2026-10-19T11:00:00+09:00" }],
truncated: false,
};
expect(okQuery(parseAgentListResult(JSON.stringify(body)))).toEqual(body);
});
it("R47 file_id が8192文字はよい", () => {
const body = withItem(0, { file_id: "x".repeat(8192) });
expect(listItems(parseAgentListResult(JSON.stringify(body)))[0].file_id).toHaveLength(8192);
});
});
// ── prepared / complete / fail の本体(段階7-2 の末尾・表5 R43。形の誤りはすべて 400 result_invalid)──
const HEX64 = "a".repeat(64);
describe("parseAgentPrepared()", () => {
it.each<[string, unknown]>([
["total_bytes=1・file_count=1(下限。指示書BJ R48)", { total_bytes: 1, sha256: HEX64, file_count: 1 }],
["一般的な値", { total_bytes: 9437184, sha256: "0123456789abcdef".repeat(4), file_count: 2 }],
])("%s は通る", (_label, body) => {
expect(okQuery(parseAgentPrepared(JSON.stringify(body)))).toEqual(body);
});
it.each<[string, string]>([
["JSON でない", "{"],
["null", "null"],
["配列", "[]"],
["total_bytes が無い", JSON.stringify({ sha256: HEX64, file_count: 1 })],
["total_bytes が0(指示書BJ R48)", JSON.stringify({ total_bytes: 0, sha256: HEX64, file_count: 1 })],
["total_bytes が負", JSON.stringify({ total_bytes: -1, sha256: HEX64, file_count: 1 })],
["total_bytes が小数", JSON.stringify({ total_bytes: 1.5, sha256: HEX64, file_count: 1 })],
["total_bytes が文字列", JSON.stringify({ total_bytes: "1", sha256: HEX64, file_count: 1 })],
["sha256 が大文字", JSON.stringify({ total_bytes: 1, sha256: "A".repeat(64), file_count: 1 })],
["sha256 が63桁", JSON.stringify({ total_bytes: 1, sha256: "a".repeat(63), file_count: 1 })],
["file_count が0", JSON.stringify({ total_bytes: 1, sha256: HEX64, file_count: 0 })],
["file_count が無い", JSON.stringify({ total_bytes: 1, sha256: HEX64 })],
])("%s → 400 result_invalid", (_label, text) => {
expectResultInvalid(parseAgentPrepared(text));
});
});
describe("parseAgentComplete()", () => {
it("正しい本体は通る", () => {
const body = { total_bytes: 36, sha256: HEX64 };
expect(okQuery(parseAgentComplete(JSON.stringify(body)))).toEqual(body);
});
it.each<[string, string]>([
["JSON でない", "{"],
["数値", "0"],
["sha256 が無い", JSON.stringify({ total_bytes: 1 })],
["total_bytes が負", JSON.stringify({ total_bytes: -1, sha256: HEX64 })],
["total_bytes が0(指示書BK K3)", JSON.stringify({ total_bytes: 0, sha256: HEX64 })],
["sha256 が16進でない", JSON.stringify({ total_bytes: 1, sha256: "g".repeat(64) })],
])("%s → 400 result_invalid", (_label, text) => {
expectResultInvalid(parseAgentComplete(text));
});
});
describe("parseAgentFail()", () => {
it.each<[string, unknown]>([
["下限(1文字・1文字)", { code: "x", message: "m" }],
["上限(64文字・500文字)", { code: "c".repeat(64), message: "m".repeat(500) }],
["export_not_found(D25 ②)", { code: "export_not_found", message: "zip がありません" }],
])("%s は通る", (_label, body) => {
expect(okQuery(parseAgentFail(JSON.stringify(body)))).toEqual(body);
});
it.each<[string, string]>([
["JSON でない", "{"],
["文字列", '"x"'],
["code が空", JSON.stringify({ code: "", message: "m" })],
["code が65文字", JSON.stringify({ code: "c".repeat(65), message: "m" })],
["message が空", JSON.stringify({ code: "x", message: "" })],
["message が501文字", JSON.stringify({ code: "x", message: "m".repeat(501) })],
["code が文字列でない", JSON.stringify({ code: 1, message: "m" })],
])("%s → 400 result_invalid", (_label, text) => {
expectResultInvalid(parseAgentFail(text));
});
});