ai-chatbot/tests/routes/openclaw-3d-artifacts.test.ts

272 lines
8.1 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
buildOpenClawArtifactProxyUrl,
extractOpenClawArtifacts,
getOpenClaw3DArtifactBundle,
isOpenClawInteractive3DArtifact,
normalizeOpenClawArtifacts,
} from "@/lib/openclaw-chat/artifacts";
import {
getOpenClawExplodedOffsetMm,
mapOpenClawCadOffsetToGlb,
normalizeOpenClawModelInteractionManifest,
} from "@/lib/openclaw-chat/model-interaction";
import { normalizeClawdTaskMetadata } from "@/lib/openclaw-chat/task-state";
const modelFile = {
type: "file",
filename: "wood-demo-interactive.glb",
display_name: "可旋转3D拆件模型",
artifact_role: "interactive_3d",
display_mode: "interactive",
mime_type: "model/gltf-binary",
download_url: "/files/wood-demo-interactive.glb",
viewer: {
type: "model_3d",
format: "glb",
camera_controls: true,
auto_rotate: false,
background: "#F7F9FC",
fallback_artifact_role: "cad_preview",
},
};
const previewFile = {
type: "image",
filename: "wood-demo-preview.png",
artifact_role: "cad_preview",
display_mode: "inline",
mime_type: "image/png",
download_url: "https://files.example.com/wood-demo-preview.png",
};
const assembledModelFile = {
...modelFile,
filename: "wood-demo-assembled.glb",
display_name: "可旋转3D整装成品模型",
artifact_role: "assembled_3d",
viewer: {
...modelFile.viewer,
variant: "assembled",
default: true,
fallback_artifact_role: "assembled_preview",
alternate_artifact_role: "interactive_3d",
interaction_manifest_artifact_role: "model_interaction_manifest",
part_selection: true,
part_dragging: true,
reset_parts: true,
one_click_explode: true,
one_click_assemble: true,
explode_action_id: "explode_all",
assemble_action_id: "assemble_all",
transition_duration_ms: 850,
explode_slider: true,
},
};
const manifestFile = {
type: "file",
filename: "wood-demo-model-interaction.json",
artifact_role: "model_interaction_manifest",
display_mode: "inline_only",
mime_type: "application/json",
download_url: "https://files.example.com/wood-demo-model-interaction.json",
};
test.describe("OpenClaw 3D artifacts", () => {
test("normalizes the documented GLB viewer contract", () => {
const artifacts = normalizeOpenClawArtifacts([modelFile, previewFile], {
baseUrl: "https://claw.example.com",
});
expect(artifacts).toHaveLength(2);
expect(artifacts[0]).toMatchObject({
artifactRole: "interactive_3d",
displayMode: "interactive",
displayName: "可旋转3D拆件模型",
downloadUrl: "https://claw.example.com/files/wood-demo-interactive.glb",
mimeType: "model/gltf-binary",
viewer: {
type: "model_3d",
format: "glb",
cameraControls: true,
autoRotate: false,
background: "#F7F9FC",
fallbackArtifactRole: "cad_preview",
},
});
expect(isOpenClawInteractive3DArtifact(artifacts[0])).toBe(true);
expect(isOpenClawInteractive3DArtifact(artifacts[1])).toBe(false);
});
test("keeps the new viewer controls and selects the default assembled bundle", () => {
const artifacts = normalizeOpenClawArtifacts(
[
modelFile,
manifestFile,
assembledModelFile,
{ ...previewFile, artifact_role: "assembled_preview" },
],
{ baseUrl: "https://claw.example.com" }
);
const bundle = getOpenClaw3DArtifactBundle(artifacts);
expect(bundle?.model.artifactRole).toBe("assembled_3d");
expect(bundle?.model.viewer).toMatchObject({
variant: "assembled",
isDefault: true,
alternateArtifactRole: "interactive_3d",
interactionManifestArtifactRole: "model_interaction_manifest",
partSelection: true,
partDragging: true,
resetParts: true,
oneClickExplode: true,
oneClickAssemble: true,
explodeActionId: "explode_all",
assembleActionId: "assemble_all",
transitionDurationMs: 850,
explodeSlider: true,
});
expect(bundle?.manifestIndex).toBe(1);
expect(bundle?.alternateModelIndex).toBe(0);
expect(bundle?.previewIndex).toBe(3);
});
test("normalizes v2 interaction transforms and CAD-to-GLB coordinates", () => {
const manifest = normalizeOpenClawModelInteractionManifest({
schema: "dalongxia.furniture-model-interaction.v2",
coordinates: { scale_mm_to_glb: 0.001 },
capabilities: {
one_click_explode: true,
one_click_assemble: true,
animated_transition: true,
explode_slider: true,
},
actions: [
{
id: "explode_all",
label: "一键炸开",
to_state: "exploded",
offset_field: "exploded_offset_mm",
duration_ms: 700,
easing: "easeInOutCubic",
interruptible: true,
},
],
drag: {
axes: ["x", "y", "z"],
snap_distance_mm: 25,
max_distance_mm: 1800,
},
parts: [
{
node_name: "DOOR-001:1",
part_id: "DOOR-001",
part_name: "门板",
dimensions_mm: { x: 578, y: 20, z: 720 },
exploded_offset_mm: { x: 2726, y: 120, z: -40 },
draggable: true,
},
],
});
expect(manifest).not.toBeNull();
expect(manifest?.parts[0]).toMatchObject({
nodeName: "DOOR-001:1",
partId: "DOOR-001",
partName: "门板",
draggable: true,
});
const offset = getOpenClawExplodedOffsetMm(manifest!.parts[0]);
expect(offset).toEqual({ x: 2726, y: 120, z: -40 });
expect(mapOpenClawCadOffsetToGlb(offset!, manifest!.scaleMmToGlb)).toEqual({
x: 2.726,
y: -0.04,
z: -0.12,
});
});
test("derives exploded offsets and default actions for historical v1 manifests", () => {
const manifest = normalizeOpenClawModelInteractionManifest({
schema: "dalongxia.furniture-model-interaction.v1",
parts: [
{
node_name: "TOP-001:1",
part_id: "TOP-001",
assembled_position_mm: { x: 20, y: 10, z: 40 },
exploded_position_mm: { x: 220, y: -40, z: 140 },
},
],
});
expect(manifest?.actions.map((action) => action.id)).toEqual([
"explode_all",
"assemble_all",
]);
expect(getOpenClawExplodedOffsetMm(manifest!.parts[0])).toEqual({
x: 200,
y: -50,
z: 100,
});
expect(manifest?.capabilities.explodeSlider).toBe(true);
});
test("extracts files from durable task results without duplicating them", () => {
const artifacts = extractOpenClawArtifacts(
{
output: { files: [modelFile, previewFile] },
task: { result: { files: [modelFile] } },
},
{ baseUrl: "https://claw.example.com" }
);
expect(artifacts).toHaveLength(2);
expect(artifacts.map((artifact) => artifact.artifactRole)).toEqual([
"interactive_3d",
"cad_preview",
]);
});
test("rejects private schemes and keeps the original URL out of proxy links", () => {
expect(
normalizeOpenClawArtifacts([
{ ...modelFile, download_url: "file:///tmp/private.glb" },
{ ...modelFile, download_url: "javascript:alert(1)" },
])
).toEqual([]);
const proxyUrl = buildOpenClawArtifactProxyUrl({
chatId: "11111111-1111-4111-8111-111111111111",
messageId: "22222222-2222-4222-8222-222222222222",
artifactIndex: 0,
download: true,
});
expect(proxyUrl).toContain("download=1");
expect(proxyUrl).not.toContain("files.example.com");
});
test("restores persisted artifacts with Claw task metadata", () => {
const task = normalizeClawdTaskMetadata({
composerMode: "clawd",
source: "openclaw",
taskKind: "clawd_chat",
status: "completed",
userMessageId: "user-message-id",
assistantMessageId: "assistant-message-id",
processingHint: "",
progressEvents: [],
artifacts: [
{
...modelFile,
download_url: "https://files.example.com/model.glb",
},
],
createdAt: 100,
updatedAt: 200,
});
expect(task?.artifacts).toHaveLength(1);
expect(task?.artifacts?.[0].viewer?.format).toBe("glb");
});
});