/**
* 制御盤ログのイベント(指示書AY 段階10)に禁止キーが載らないことを確かめる。
* ★`file_name` / `file_ids` / `file_id` / `token` / `authorization` をキーにも値にも出さない★(D12)。
*/
import { describe, expect, test } from "vitest";
import type { LogEvent } from "@/lib/logger/events";
import { createLogger, emitTo } from "@/lib/logger";
import { runWithRequestId } from "@/lib/logger/context";
/** イベントのキー・入れ子のキーに出てはいけない名前(段階10) */
const FORBIDDEN_EVENT_KEYS = [
"file_name",
"file_ids",
"file_id",
"token",
"authorization",
] as const;
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
type EventKeys = KeysOfUnion<LogEvent>;
type ForbiddenHit = Extract<EventKeys, (typeof FORBIDDEN_EVENT_KEYS)[number]>;
/**
* LogEvent のキー集合に禁止キーが1つでも混ざると、この代入が型エラーになる。
*/
const _noForbiddenKeysOnLogEvent: ForbiddenHit extends never ? true : never = true;
void _noForbiddenKeysOnLogEvent;
/** オブジェクトのキーを再帰的に集める(配列の要素も辿る) */
function collectKeys(value: unknown, into: Set<string>): void {
if (value === null || typeof value !== "object") return;
if (Array.isArray(value)) {
for (const item of value) collectKeys(item, into);
return;
}
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
into.add(key);
collectKeys(child, into);
}
}
function emitJson(event: LogEvent, level: "info" | "warn" = "info"): Record<string, unknown> {
const lines: string[] = [];
const target = createLogger({
write(s: string) {
lines.push(s);
},
});
runWithRequestId("ds-agent-events-test", () => {
emitTo(target, level, event);
});
expect(lines).toHaveLength(1);
return JSON.parse(lines[0]) as Record<string, unknown>;
}
/** 段階10の表にあるイベントを1つずつ(フィールドは表どおり。禁止キーは付けない) */
const SAMPLE_EVENTS: Array<{ level: "info" | "warn"; event: LogEvent }> = [
{
level: "info",
event: {
event: "ds_agent.connected",
serial_number: "DS-002",
agent_version: "1.0.0",
},
},
{
level: "info",
event: {
event: "ds_agent.disconnected",
serial_number: "DS-002",
agent_version: null,
},
},
{
level: "warn",
event: {
event: "ds_agent.auth_failed",
route: "/api/ds-agent/commands",
method: "GET",
reason: "missing",
},
},
{
level: "info",
event: { event: "ds_agent.token_issued", serial_number: "DS-002" },
},
{
level: "info",
event: { event: "ds_agent.token_revoked", serial_number: "DS-002" },
},
{
level: "info",
event: {
event: "ds_logs.list",
serial_number: "DS-002",
item_count: 3,
truncated: false,
duration_ms: 12,
result: "ok",
},
},
{
level: "info",
event: {
event: "ds_logs.list",
serial_number: "DS-002",
item_count: 0,
truncated: false,
duration_ms: 5,
result: "agent_error",
agent_code: "io_error",
},
},
{
level: "info",
event: {
event: "ds_logs.export_prepared",
serial_number: "DS-002",
file_count: 2,
total_bytes: 1024,
duration_ms: 40,
},
},
{
level: "info",
event: {
event: "ds_logs.export_started",
serial_number: "DS-002",
file_count: 2,
},
},
{
level: "info",
event: {
event: "ds_logs.export_completed",
serial_number: "DS-002",
file_count: 2,
total_bytes: 1024,
duration_ms: 90,
},
},
{
level: "warn",
event: {
event: "ds_logs.export_failed",
serial_number: "DS-002",
stage: "prepare",
reason: "too_large",
bytes_sent: 0,
agent_code: "too_large",
},
},
];
describe("ds_agent / ds_logs イベント(指示書AY 段階10)", () => {
test.each(SAMPLE_EVENTS)(
"$event.event の出力に禁止キー(file_name / file_ids / file_id / token / authorization)が無い",
({ level, event }) => {
const out = emitJson(event, level);
const keys = new Set<string>();
collectKeys(out, keys);
for (const forbidden of FORBIDDEN_EVENT_KEYS) {
expect(keys.has(forbidden), `forbidden key present: ${forbidden}`).toBe(false);
}
// 値の文字列にもトークンっぽいキー名を埋め込まない(serial 等の通常値は除外)
const blob = JSON.stringify(out);
expect(blob).not.toMatch(/"file_name"/);
expect(blob).not.toMatch(/"file_ids"/);
expect(blob).not.toMatch(/"file_id"/);
expect(blob).not.toMatch(/"token"/);
expect(blob).not.toMatch(/"authorization"/);
}
);
test("token_issued / token_revoked は serial_number だけ(表3 KA8)", () => {
for (const name of ["ds_agent.token_issued", "ds_agent.token_revoked"] as const) {
const sample = SAMPLE_EVENTS.find((s) => s.event.event === name);
expect(sample).toBeDefined();
const keys = Object.keys(sample!.event).sort();
expect(keys).toEqual(["event", "serial_number"]);
}
});
});