Files
ttstd-infinite-canvas/web/test/director-diagnostics-recorder.test.ts
echoD886 ad0775605d fix(canvas): 3D 导演台 - 完成 Issue #305 P0 稳定性治理 (#324)
* fix(canvas): 3D 导演台 - 修复状态回弹与保存恢复生命周期

* style(canvas): 3D 导演台 - 对齐 CI 格式门禁
2026-08-27 10:32:45 +08:00

152 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, describe, expect, test } from "bun:test";
import { recordDirectorDiagnostic, resetDirectorDiagnosticDedupe } from "../src/lib/canvas/director/director-diagnostics-recorder";
import { getClientDiagnosticEvents } from "../src/services/diagnostics/client-diagnostics";
/** 只看导演台自己的稳定码事件,避免与其它测试写入的诊断流互相干扰。 */
function directorEvents() {
return getClientDiagnosticEvents().filter((event) => (event.code || "").startsWith("DIRECTOR_"));
}
function newestDirectorEvent() {
return directorEvents().at(-1);
}
const LEAK_PROBES = [
"https://cdn.example.com/actor.glb?token=abc123#frag",
"authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig",
"cookie=session=deadbeef; Path=/",
"api_key=sk-live-0000",
"Error: boom\n at DirectorModel (director-viewport.tsx:571:29)",
"演员 1 的镜头意图正文",
];
beforeEach(() => {
resetDirectorDiagnosticDedupe();
});
describe("记录进统一诊断缓冲区", () => {
test("写入的事件带稳定码、固定文案与 runtime 分类", () => {
const before = directorEvents().length;
expect(recordDirectorDiagnostic("DIRECTOR_VIEWPORT_RENDER_FAILED")).toBe(true);
expect(directorEvents().length).toBe(before + 1);
const event = newestDirectorEvent();
expect(event?.code).toBe("DIRECTOR_VIEWPORT_RENDER_FAILED");
expect(event?.level).toBe("error");
expect(event?.category).toBe("runtime");
expect(event?.message).toBe("导演台 3D 视口渲染失败");
// 绝不落 stack。
expect(event?.stack || "").toBe("");
});
test("安全字段以枚举后缀进入 code,便于检索", () => {
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-1", objectKind: "model", attempt: 2 })).toBe(true);
const event = newestDirectorEvent();
expect(event?.code).toBe("DIRECTOR_MODEL_LOAD_FAILED object=obj-1 kind=model attempt=2");
});
test("sceneId 只进 code 后缀,不冒充 canvasId", () => {
expect(recordDirectorDiagnostic("DIRECTOR_SAVE_FLUSH_FAILED", { sceneId: "repro-scene-1", revision: 3 })).toBe(true);
const event = newestDirectorEvent();
// DirectorScene.id 不是 canvas id,绝不写入既有后端契约字段。
expect(event?.canvasId).toBeUndefined();
expect(event?.code).toBe("DIRECTOR_SAVE_FLUSH_FAILED scene=repro-scene-1 revision=3");
});
test("三个保存相关码都能写入且级别正确", () => {
recordDirectorDiagnostic("DIRECTOR_SAVE_RETRY_RECOVERED", { revision: 1 });
expect(newestDirectorEvent()?.level).toBe("info");
recordDirectorDiagnostic("DIRECTOR_SAVE_DRAFT_UNAVAILABLE", { revision: 1 });
expect(newestDirectorEvent()?.level).toBe("error");
recordDirectorDiagnostic("DIRECTOR_CLOSE_BLOCKED", { saveOutcome: "stay" });
expect(newestDirectorEvent()?.level).toBe("warning");
});
});
describe("去重与噪声控制", () => {
test("同一 code+字段在窗口内只记一次", () => {
const before = directorEvents().length;
expect(recordDirectorDiagnostic("DIRECTOR_VIEWPORT_CONTEXT_LOST")).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_VIEWPORT_CONTEXT_LOST")).toBe(false);
expect(recordDirectorDiagnostic("DIRECTOR_VIEWPORT_CONTEXT_LOST")).toBe(false);
expect(directorEvents().length).toBe(before + 1);
});
test("字段不同视为不同事件,不会被误去重", () => {
const before = directorEvents().length;
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectKind: "model", attempt: 1 })).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectKind: "model", attempt: 2 })).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectKind: "actor", attempt: 1 })).toBe(true);
expect(directorEvents().length).toBe(before + 3);
});
test("不同 objectId 在窗口内不互相吞掉", () => {
const before = directorEvents().length;
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-1", objectKind: "model" })).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-2", objectKind: "model" })).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-3", objectKind: "model" })).toBe(true);
// 同一 objectId 重复仍要被去重。
expect(recordDirectorDiagnostic("DIRECTOR_MODEL_LOAD_FAILED", { objectId: "obj-1", objectKind: "model" })).toBe(false);
expect(directorEvents().length).toBe(before + 3);
});
test("不同 sceneId 在窗口内不互相吞掉", () => {
const before = directorEvents().length;
expect(recordDirectorDiagnostic("DIRECTOR_SAVE_FLUSH_FAILED", { sceneId: "scene-1", revision: 1 })).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_SAVE_FLUSH_FAILED", { sceneId: "scene-2", revision: 1 })).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_SAVE_FLUSH_FAILED", { sceneId: "scene-1", revision: 1 })).toBe(false);
expect(directorEvents().length).toBe(before + 2);
});
test("所有写入的 code 都以稳定 DIRECTOR_ 前缀开头", () => {
resetDirectorDiagnosticDedupe();
recordDirectorDiagnostic("DIRECTOR_CLOSE_BLOCKED", { sceneId: "scene-1", saveOutcome: "stay" });
for (const event of directorEvents()) {
expect((event.code || "").startsWith("DIRECTOR_")).toBe(true);
}
});
test("重置去重状态后可再次记录同一事件", () => {
expect(recordDirectorDiagnostic("DIRECTOR_VIEWPORT_CONTEXT_RESTORED")).toBe(true);
expect(recordDirectorDiagnostic("DIRECTOR_VIEWPORT_CONTEXT_RESTORED")).toBe(false);
resetDirectorDiagnosticDedupe();
expect(recordDirectorDiagnostic("DIRECTOR_VIEWPORT_CONTEXT_RESTORED")).toBe(true);
});
});
describe("不泄漏敏感内容", () => {
test("伪造的敏感字段不会进入缓冲区任何字段", () => {
for (const probe of LEAK_PROBES) {
resetDirectorDiagnosticDedupe();
recordDirectorDiagnostic("DIRECTOR_MODEL_ADOPT_FAILED", {
objectId: probe,
sceneId: probe,
// @ts-expect-error 故意传入非法枚举,验证运行时白名单而非仅靠类型。
objectKind: probe,
// @ts-expect-error 故意传入非法数值类型。
attempt: probe,
});
const event = newestDirectorEvent();
const serialized = JSON.stringify(event);
expect(serialized).not.toContain("cdn.example.com");
expect(serialized).not.toContain("token=abc123");
expect(serialized).not.toContain("sk-live-0000");
expect(serialized).not.toContain("deadbeef");
expect(serialized).not.toContain("Bearer");
expect(serialized).not.toContain("演员");
expect(serialized).not.toContain("director-viewport.tsx");
expect(event?.canvasId).toBeUndefined();
expect(event?.code).toBe("DIRECTOR_MODEL_ADOPT_FAILED");
}
});
test("未知 code 既不写入也不抛错", () => {
const before = directorEvents().length;
// @ts-expect-error 故意传入白名单外的 code。
expect(recordDirectorDiagnostic("DIRECTOR_NOT_A_REAL_CODE", { objectId: "obj-1" })).toBe(false);
expect(directorEvents().length).toBe(before);
});
});