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"); }); });