ai-chatbot/tests/routes/model3d-style-pro-regressions.test.ts

150 lines
5.7 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { modelSupportsReferenceImageInput } from "@/lib/ai/reference-image-capability";
function readSource(path: string) {
return readFileSync(resolve(process.cwd(), path), "utf8");
}
test.describe("3D workspace and Style Pro regressions", () => {
test("opens World splats in first-person mode without the debug HUD", () => {
const page = readSource("app/canvas/[id]/model3d/page.tsx");
const viewer = readSource(
"components/canvas/world3d/World3DViewer.tsx"
);
expect(page).toContain(
'workspaceMode === "world" && worldViewportMode === "splat"'
);
expect(page).toContain("showInfoOverlay={false}");
expect(viewer).toContain("showInfoOverlay = !minimalFrame");
expect(viewer).toContain("showInfoOverlay={showInfoOverlay}");
});
test("keeps each GLB viewer material instance isolated and texture-aware", () => {
const viewer = readSource(
"components/canvas/model3d/Model3DViewer.tsx"
);
expect(viewer).toContain("cloneSceneWithIndependentMaterials");
expect(viewer).toContain("const clonedMaterial = material.clone()");
expect(viewer).toContain("typedSourceTexture.clone()");
expect(viewer).toContain("clonedMaterialRecord[key] = clonedTexture");
expect(viewer).toContain("disposedTextures.add(texture)");
expect(viewer).toContain("texture.dispose()");
expect(viewer).toContain("<primitive dispose={null}");
expect(viewer).toContain(
"configureMaterialTextures(clonedMaterial, node.geometry)"
);
});
test("loads Qiniu-hosted GLB assets through the authenticated same-origin proxy", () => {
const assetUrl = readSource("lib/canvas/three-asset-url.ts");
const proxy = readSource("app/api/canvas/3d/model-proxy/route.ts");
for (const host of [
"yybb.top",
"qiniucs.com",
"qiniudn.com",
"clouddn.com",
"qbox.me",
]) {
expect(assetUrl).toContain(`\"${host}\"`);
expect(proxy).toContain(`\"${host}\"`);
}
});
test("does not block a completed Tripo task while permanent assets are archived", () => {
const route = readSource("app/api/canvas/3d/tasks/[taskId]/route.ts");
const page = readSource("app/canvas/[id]/model3d/page.tsx");
const imageNode = readSource("components/canvas/nodes/ImageNode.tsx");
expect(route).toContain("return { url: sourceUrl, pending: true }");
expect(route).toContain('String(taskStatus).trim().toLowerCase() === "success" && assetsPending');
expect(route).toContain('? "running"');
expect(route).toContain("assetsPending,");
expect(page).toContain("pollFailureStreakRef.current < 5");
expect(page).toContain("cancelled || pollInFlight");
expect(page).toContain("restoreRecoverableTaskForPolling");
expect(imageNode).toContain("CANVAS_3D_MAX_CONSECUTIVE_QUERY_FAILURES");
});
test("uses the same hidden-model access rule in Style Pro as canvas", () => {
const route = readSource("app/api/style-pro/model-options/route.ts");
const canvasModelsRoute = readSource("app/(chat)/api/models/route.ts");
expect(route).toContain("getUserById");
expect(route).toContain("canAccessHiddenAiModels");
expect(route).toContain("databaseUserRole: currentUser?.role");
expect(route).toContain("includeHidden: allowHiddenModels");
expect(route).toContain('"Cache-Control": "private, no-store, max-age=0"');
expect(canvasModelsRoute).toContain("canAccessHiddenAiModels");
expect(canvasModelsRoute).toContain("databaseUserRole: currentUser?.role");
});
test("refreshes Style Pro models after an administrator changes the catalog", () => {
const workspace = readSource(
"components/style-pro/StyleProWorkspace.tsx"
);
expect(workspace).toContain("refreshInterval: 30_000");
expect(workspace).toContain("revalidateOnFocus: true");
expect(workspace).toContain("revalidateOnReconnect: true");
});
test("enforces Style Pro model availability on both display and execution", () => {
const options = readSource("lib/style-pro/model-options.ts");
const availability = readSource(
"lib/style-pro/model-availability.ts"
);
const referenceCapability = readSource(
"lib/ai/reference-image-capability.ts"
);
expect(options).toContain(".filter(isStyleProModelRunnable)");
expect(options).toContain("getStyleProModelAvailability(entry)");
expect(availability).toContain("modelSupportsReferenceImageInput");
expect(availability).toContain('availabilityLabel: "仅文生图"');
expect(availability).toContain('availabilityLabel: "暂不可用"');
expect(referenceCapability).toContain("GEMINI_TEXT_ONLY_IMAGE_MODEL_REGEX");
const task = readSource("lib/style-pro/task.ts");
expect(task).toContain("const runnableModels = await getStyleProImageModelEntries");
expect(task).toContain("当前不支持参考图,请重新选择");
});
test("keeps known text-only Gemini image aliases out of reference-image tools", () => {
expect(
modelSupportsReferenceImageInput(
{
modelId: "nano-banana-pro",
displayName: "BanA Pro",
supportsVision: true,
},
{ type: "gemini" }
)
).toBe(false);
expect(
modelSupportsReferenceImageInput(
{
modelId: "gemini-3-pro-image-preview",
displayName: "BanA Pro V",
supportsVision: true,
},
{ type: "bananaimage" }
)
).toBe(true);
});
test("opens the canvas conversation panel by default", () => {
const page = readSource(
"app/canvas/[id]/CanvasEditorLegacyPageClient.tsx"
);
expect(page).toContain(
"const [isPanelOpen, setIsPanelOpen] = useState(true)"
);
});
});