187 lines
9.4 KiB
TypeScript
187 lines
9.4 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
|
||
import { DIRECTOR_DIAGNOSTIC_CODES, directorDiagnosticObjectKind, formatDirectorDiagnosticCode, isDirectorDiagnosticCode, projectDirectorDiagnostic } from "../src/lib/canvas/director/director-diagnostics";
|
||
|
||
/** 任何一个都不允许出现在投影输出里。 */
|
||
const LEAK_PROBES = [
|
||
"https://cdn.example.com/actor.glb?token=abc123",
|
||
"http://localhost:3000/api/tasks?authorization=Bearer%20xyz",
|
||
"Bearer eyJhbGciOiJIUzI1NiJ9.payload.signature",
|
||
"authorization: Bearer secret-token",
|
||
"cookie=session=deadbeef; Path=/",
|
||
"api_key=sk-live-0000",
|
||
"Error: adopt failed\n at DirectorModel (director-viewport.tsx:571:29)",
|
||
"演员 1 的镜头意图正文",
|
||
"/Users/someone/secret/path/scene.glb",
|
||
];
|
||
|
||
function serialize(value: unknown) {
|
||
return JSON.stringify(value ?? {});
|
||
}
|
||
|
||
describe("code 白名单", () => {
|
||
test("白名单内的 code 全部被接受且有稳定文案与级别", () => {
|
||
for (const code of DIRECTOR_DIAGNOSTIC_CODES) {
|
||
expect(isDirectorDiagnosticCode(code)).toBe(true);
|
||
const event = projectDirectorDiagnostic(code, {});
|
||
expect(event).not.toBeNull();
|
||
expect(event?.code).toBe(code);
|
||
expect(event?.message.length).toBeGreaterThan(0);
|
||
expect(["info", "warning", "error"]).toContain(event?.level ?? "");
|
||
}
|
||
});
|
||
|
||
test("未知 code 一律拒绝,不透传", () => {
|
||
const rejected = ["", "director_viewport_render_failed", "DIRECTOR_UNKNOWN", "SOME_OTHER_CODE", "DIRECTOR_VIEWPORT_RENDER_FAILED ", null, undefined, 42, {}, []];
|
||
for (const code of rejected) {
|
||
expect(isDirectorDiagnosticCode(code)).toBe(false);
|
||
expect(projectDirectorDiagnostic(code, {})).toBeNull();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("字段白名单", () => {
|
||
test("未知字段不会出现在输出中", () => {
|
||
const event = projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", {
|
||
objectId: "obj-1",
|
||
sceneName: "演员 1 场景",
|
||
url: "https://cdn.example.com/a.glb",
|
||
stack: "Error: boom\n at x",
|
||
prompt: "镜头意图正文",
|
||
storageKey: "assets/secret.glb",
|
||
token: "sk-live-0000",
|
||
});
|
||
|
||
expect(event?.fields).toEqual({ objectId: "obj-1" });
|
||
expect(serialize(event)).not.toContain("cdn.example.com");
|
||
expect(serialize(event)).not.toContain("镜头意图正文");
|
||
expect(serialize(event)).not.toContain("sk-live-0000");
|
||
});
|
||
|
||
test("伪造的敏感输入无法通过任一字段进入输出", () => {
|
||
for (const probe of LEAK_PROBES) {
|
||
const event = projectDirectorDiagnostic("DIRECTOR_SAVE_FLUSH_FAILED", {
|
||
objectId: probe,
|
||
sceneId: probe,
|
||
objectKind: probe,
|
||
saveOutcome: probe,
|
||
attempt: probe,
|
||
revision: probe,
|
||
draftStored: probe,
|
||
userInitiated: probe,
|
||
});
|
||
|
||
expect(event).not.toBeNull();
|
||
// 所有字段都应被拒绝:probe 既不是 safe-id,也不在枚举内,也不是数值/布尔。
|
||
expect(event?.fields).toEqual({});
|
||
const text = serialize(event);
|
||
expect(text).not.toContain("http");
|
||
expect(text).not.toContain("token");
|
||
expect(text).not.toContain("cookie");
|
||
expect(text).not.toContain("Bearer");
|
||
expect(text).not.toContain("演员");
|
||
}
|
||
});
|
||
|
||
test("safe-id 只接受受限字符集", () => {
|
||
expect(projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-1_a.b:c" })?.fields.objectId).toBe("obj-1_a.b:c");
|
||
for (const bad of ["obj 1", "obj/1", "obj?1", "obj#1", "a".repeat(97), "", " ", "obj\n1", "obj%201"]) {
|
||
expect(projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: bad })?.fields.objectId).toBeUndefined();
|
||
}
|
||
});
|
||
|
||
test("枚举字段只接受白名单值", () => {
|
||
expect(projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectKind: "actor" })?.fields.objectKind).toBe("actor");
|
||
expect(projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectKind: "spaceship" })?.fields.objectKind).toBeUndefined();
|
||
expect(projectDirectorDiagnostic("DIRECTOR_CLOSE_BLOCKED", { saveOutcome: "stay" })?.fields.saveOutcome).toBe("stay");
|
||
expect(projectDirectorDiagnostic("DIRECTOR_CLOSE_BLOCKED", { saveOutcome: "explode" })?.fields.saveOutcome).toBeUndefined();
|
||
});
|
||
|
||
test("数值字段有界且拒绝非有限值", () => {
|
||
expect(projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_RETRY", { attempt: 3 })?.fields.attempt).toBe(3);
|
||
expect(projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_RETRY", { attempt: 2.6 })?.fields.attempt).toBe(3);
|
||
for (const bad of [-1, 1000, Number.NaN, Number.POSITIVE_INFINITY, "3", null]) {
|
||
expect(projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_RETRY", { attempt: bad })?.fields.attempt).toBeUndefined();
|
||
}
|
||
expect(projectDirectorDiagnostic("DIRECTOR_SAVE_FLUSH_FAILED", { revision: 0 })?.fields.revision).toBe(0);
|
||
expect(projectDirectorDiagnostic("DIRECTOR_SAVE_FLUSH_FAILED", { revision: -1 })?.fields.revision).toBeUndefined();
|
||
});
|
||
|
||
test("布尔字段只接受真布尔", () => {
|
||
expect(projectDirectorDiagnostic("DIRECTOR_SAVE_RETRY_FAILED", { draftStored: false })?.fields.draftStored).toBe(false);
|
||
expect(projectDirectorDiagnostic("DIRECTOR_SAVE_RETRY_FAILED", { draftStored: true })?.fields.draftStored).toBe(true);
|
||
for (const bad of ["true", 1, 0, null, {}]) {
|
||
expect(projectDirectorDiagnostic("DIRECTOR_SAVE_RETRY_FAILED", { draftStored: bad })?.fields.draftStored).toBeUndefined();
|
||
}
|
||
});
|
||
|
||
test("非对象 fields 不抛错且产出空字段", () => {
|
||
for (const fields of [null, undefined, 42, "objectId=1", [1, 2, 3]]) {
|
||
const event = projectDirectorDiagnostic("DIRECTOR_VIEWPORT_CONTEXT_LOST", fields);
|
||
expect(event?.fields).toEqual({});
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("稳定码格式化", () => {
|
||
test("只拼接安全枚举与数值", () => {
|
||
const event = projectDirectorDiagnostic("DIRECTOR_SAVE_RETRY_FAILED", { sceneId: "scene-1", revision: 4, draftStored: true, userInitiated: true });
|
||
expect(event).not.toBeNull();
|
||
if (!event) return;
|
||
const formatted = formatDirectorDiagnosticCode(event);
|
||
expect(formatted).toBe("DIRECTOR_SAVE_RETRY_FAILED scene=scene-1 revision=4 draft=1 user=1");
|
||
// scene/object 后缀只可能是 safe-id 投影后的值。
|
||
expect(formatted.startsWith("DIRECTOR_")).toBe(true);
|
||
});
|
||
|
||
test("scene 与 object 后缀都进入 code,顺序确定", () => {
|
||
const event = projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { sceneId: "scene-1", objectId: "obj-9", objectKind: "model", attempt: 2 });
|
||
expect(event).not.toBeNull();
|
||
if (!event) return;
|
||
expect(formatDirectorDiagnosticCode(event)).toBe("DIRECTOR_MODEL_LOAD_FAILED scene=scene-1 object=obj-9 kind=model attempt=2");
|
||
});
|
||
|
||
test("不同 objectId / sceneId 产出不同签名", () => {
|
||
const a = projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-1" });
|
||
const b = projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-2" });
|
||
const c = projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { sceneId: "scene-1" });
|
||
const d = projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { sceneId: "scene-2" });
|
||
expect(a && b && c && d).toBeTruthy();
|
||
if (!a || !b || !c || !d) return;
|
||
const signatures = [a, b, c, d].map(formatDirectorDiagnosticCode);
|
||
expect(new Set(signatures).size).toBe(4);
|
||
});
|
||
|
||
test("非法 id 不进入 code 后缀", () => {
|
||
for (const bad of ["https://cdn.example.com/a.glb?token=abc", "obj 1", "obj/1", "a".repeat(97)]) {
|
||
const event = projectDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: bad, sceneId: bad });
|
||
expect(event).not.toBeNull();
|
||
if (!event) continue;
|
||
const formatted = formatDirectorDiagnosticCode(event);
|
||
expect(formatted).toBe("DIRECTOR_MODEL_LOAD_FAILED");
|
||
expect(formatted).not.toContain("scene=");
|
||
expect(formatted).not.toContain("object=");
|
||
expect(formatted).not.toContain("cdn.example.com");
|
||
}
|
||
});
|
||
|
||
test("无字段时只有 code 本身", () => {
|
||
const event = projectDirectorDiagnostic("DIRECTOR_VIEWPORT_RENDER_FAILED", {});
|
||
expect(event).not.toBeNull();
|
||
if (!event) return;
|
||
expect(formatDirectorDiagnosticCode(event)).toBe("DIRECTOR_VIEWPORT_RENDER_FAILED");
|
||
});
|
||
});
|
||
|
||
describe("对象种类推导", () => {
|
||
test("按形态映射,不读取名称或地址", () => {
|
||
expect(directorDiagnosticObjectKind({ kind: "actor" })).toBe("actor");
|
||
expect(directorDiagnosticObjectKind({ kind: "model" })).toBe("model");
|
||
expect(directorDiagnosticObjectKind({ kind: "billboard" })).toBe("billboard");
|
||
expect(directorDiagnosticObjectKind({ primitive: "box" })).toBe("primitive");
|
||
expect(directorDiagnosticObjectKind({})).toBe("unknown");
|
||
expect(directorDiagnosticObjectKind(null)).toBe("unknown");
|
||
expect(directorDiagnosticObjectKind(undefined)).toBe("unknown");
|
||
});
|
||
});
|