diff --git a/.omo/qa/83-after.log b/.omo/qa/83-after.log new file mode 100644 index 000000000..f47b823b0 --- /dev/null +++ b/.omo/qa/83-after.log @@ -0,0 +1,61 @@ + +DevTools listening on ws://127.0.0.1:9283/devtools/browser/789f4b7f-38fb-4521-95f3-1625a2ac6ef7 +Trying to load the allocator multiple times. This is *not* supported. +[6183:13018501:0903/212435.477602:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212435.477818:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212435.478975:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212435.479122:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212435.498882:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212435.499720:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212435.499855:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212435.499945:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212435.500129:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212435.513357:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212435.532717:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212435.532787:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212436.012125:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212436.012191:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +PASS the studio renders a canvas +[6183:13018501:0903/212437.510606:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212437.513381:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212437.562015:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212437.562096:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +PASS the hierarchy offers an Add object control +PASS the catalogue lists primitives and set pieces +[6183:13018501:0903/212437.584771:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212437.584849:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212437.618903:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212437.618976:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212437.652031:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212437.652102:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212437.683798:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212437.683879:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018501:0903/212437.710559:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212437.710631:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6179:13018488:0903/212437.834576:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +PASS the new object opens in the inspector +PASS the new object appears in the hierarchy +PASS the new object exists in the 3D scene +PASS a new object is created unrotated +PASS the move gizmo exposes three axis arrows +PASS the X arrow is grabbable where it is drawn +FAIL dragging the X arrow slides the object along X — {"Position":[0.55,0,0.7],"Rotation":[0,0,0],"Scale":[1,1,1]} +PASS an X drag leaves the other axes alone +PASS E selects the rotate tool +[6183:13018501:0903/212441.190760:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[6183:13018570:0903/212441.190824:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +PASS rotate mode shows one ring per axis plus the screen ring +FAIL the Y ring is grabbable on screen — +file:///Users/yun/CozyClay-i83/test/verify-object-gizmo.mjs:233 +const turnRadius = Math.hypot(ringGrab.x - hubTurn.x, ringGrab.y - hubTurn.y); + ^ + +TypeError: Cannot read properties of null (reading 'x') + at file:///Users/yun/CozyClay-i83/test/verify-object-gizmo.mjs:233:40 + +Node.js v24.19.0 +[6179:13018488:0903/212441.776827:ERROR:google_apis/gcm/engine/registration_request.cc:275] Registration URL fetching failed. +[6179:13018488:0903/212441.776958:ERROR:google_apis/gcm/engine/connection_factory_impl.cc:484] ConnectionHandler failed with net error: -2 +[6179:13018488:0903/212441.777014:ERROR:google_apis/gcm/engine/registration_request.cc:275] Registration URL fetching failed. +[6179:13018462:0903/212441.781346:ERROR:content/browser/network_service_instance_impl.cc:650] Network service crashed or was terminated, restarting service. +[6179:13018462:0903/212441.800063:ERROR:content/browser/gpu/gpu_process_host.cc:1035] GPU process exited unexpectedly: exit_code=15 diff --git a/.omo/qa/83-before.log b/.omo/qa/83-before.log new file mode 100644 index 000000000..2d33bb3b0 --- /dev/null +++ b/.omo/qa/83-before.log @@ -0,0 +1,59 @@ + +DevTools listening on ws://127.0.0.1:9283/devtools/browser/e0b49148-1c30-46f9-86da-ecd57e9a5609 +Trying to load the allocator multiple times. This is *not* supported. +[992:12986669:0903/211805.422163:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211805.422353:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211805.422518:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211805.422549:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211805.422617:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211805.422644:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211805.422705:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211805.422726:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211805.422786:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211805.422809:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211805.457278:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211805.457321:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211806.113552:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211806.113609:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +PASS the studio renders a canvas +[992:12986669:0903/211807.489980:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211807.490163:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211807.546670:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211807.546745:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211807.569191:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211807.569242:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211807.601838:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211807.601924:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211807.623982:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211807.624050:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211807.659856:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211807.659913:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986669:0903/211807.683462:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +[992:12986737:0903/211807.683527:ERROR:ui/display/mac/cv_display_link_mac.mm:195] CVDisplayLinkCreateWithCGDisplay failed. CVReturn: -6670 +PASS the hierarchy offers an Add object control +PASS the catalogue lists primitives and set pieces +[988:12986655:0903/211807.859267:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +PASS the new object opens in the inspector +PASS the new object appears in the hierarchy +PASS the new object exists in the 3D scene +PASS a new object is created unrotated +PASS the move gizmo exposes three axis arrows +PASS the X arrow is grabbable where it is drawn +FAIL dragging the X arrow slides the object along X — {"Position":[0.55,0,0.7],"Rotation":[0,0,0],"Scale":[1,1,1]} +PASS an X drag leaves the other axes alone +PASS E selects the rotate tool +PASS rotate mode shows one ring per axis plus the screen ring +FAIL the Y ring is grabbable on screen — +file:///Users/yun/CozyClay-i83/test/verify-object-gizmo.mjs:233 +const turnRadius = Math.hypot(ringGrab.x - hubTurn.x, ringGrab.y - hubTurn.y); + ^ + +TypeError: Cannot read properties of null (reading 'x') + at file:///Users/yun/CozyClay-i83/test/verify-object-gizmo.mjs:233:40 + +Node.js v24.19.0 +[988:12986655:0903/211811.903003:ERROR:google_apis/gcm/engine/connection_factory_impl.cc:484] ConnectionHandler failed with net error: -2 +[988:12986655:0903/211811.903088:ERROR:google_apis/gcm/engine/registration_request.cc:275] Registration URL fetching failed. +[988:12986655:0903/211811.903123:ERROR:google_apis/gcm/engine/registration_request.cc:275] Registration URL fetching failed. +[988:12986628:0903/211811.906307:ERROR:content/browser/network_service_instance_impl.cc:650] Network service crashed or was terminated, restarting service. +[988:12986628:0903/211811.907559:ERROR:content/browser/gpu/gpu_process_host.cc:1035] GPU process exited unexpectedly: exit_code=15 diff --git a/.omo/qa/83-build.log b/.omo/qa/83-build.log new file mode 100644 index 000000000..60fabdaee --- /dev/null +++ b/.omo/qa/83-build.log @@ -0,0 +1,38 @@ + +> cozyclay@1.7.0 build +> vite build + +vite v8.2.0 building client environment for production... + transforming... +../fonts/inter-latin.woff2 referenced in ../fonts/inter-latin.woff2 didn't resolve at build time, it will remain unchanged to be resolved at runtime +✓ 1030 modules transformed. +rendering chunks... +computing gzip size... +dist/d/index.html 2.70 kB │ gzip: 1.03 kB +dist/app/index.html 3.33 kB │ gzip: 1.21 kB +dist/demo/index.html 5.01 kB │ gzip: 1.74 kB +dist/ai-camera-control/index.html 18.42 kB │ gzip: 5.46 kB +dist/index.html 22.43 kB │ gzip: 6.08 kB +dist/assets/basis_transcoder-o4Hde_L7.js 57.52 kB +dist/assets/draco_wasm_wrapper-fZCQGLGb.js 58.45 kB +dist/assets/draco_wasm_wrapper-DxJM36Ib.js 58.76 kB +dist/assets/draco_decoder-Z1_iN-Ht.wasm 192.42 kB │ gzip: 64.11 kB +dist/assets/draco_decoder-C32yEggz.wasm 285.74 kB │ gzip: 89.66 kB +dist/assets/basis_transcoder-VXdx5NbI.wasm 527.33 kB │ gzip: 248.94 kB +dist/assets/draco_decoder-fzg4nYZr.js 719.41 kB +dist/assets/config-DeoFTYzb.css 6.52 kB │ gzip: 2.02 kB +dist/assets/app-Q8cAV9Me.css 130.55 kB │ gzip: 25.64 kB +dist/assets/config-Bx7pYsD9.js 0.26 kB │ gzip: 0.23 kB +dist/assets/modulepreload-polyfill-P2Xu9kJm.js 0.67 kB │ gzip: 0.38 kB +dist/assets/demo-DEAy-8Eq.js 1.65 kB │ gzip: 1.00 kB +dist/assets/ticket-DbrIKBrJ.js 5.28 kB │ gzip: 2.21 kB +dist/assets/vision_bundle-jFkh-fIS.js 137.53 kB │ gzip: 40.87 kB +dist/assets/module-MzlRiBjI.js 250.50 kB │ gzip: 83.31 kB +dist/assets/app-BGTY6yIw.js 2,114.39 kB │ gzip: 632.78 kB + +✓ built in 794ms +[plugin builtin:vite-reporter] +(!) Some chunks are larger than 500 kB after minification. Consider: +- Using dynamic import() to code-split the application +- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting +- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit. diff --git a/.omo/qa/83-full.log b/.omo/qa/83-full.log new file mode 100644 index 000000000..edd348ae3 --- /dev/null +++ b/.omo/qa/83-full.log @@ -0,0 +1,3055 @@ +TEST MANIFEST scope=all runnable=118 total=130 +RUN node test/ardy/verify-base-free.mjs - runs directly under Node +RUN node test/ardy/verify-browser-motion.mjs - runs directly under Node +RUN node test/ardy/verify-collision-blockers.mjs - runs directly under Node +RUN node test/ardy/verify-compare-npz.mjs - runs directly under Node +RUN node test/ardy/verify-fill.mjs - runs directly under Node +RUN node test/ardy/verify-fk.mjs - runs directly under Node +RUN node test/ardy/verify-key-runs.mjs - runs directly under Node +RUN node test/ardy/verify-playback-skinning.mjs - runs directly under Node +RUN node test/ardy/verify-pose-export.mjs - runs directly under Node +RUN node test/ardy/verify-pose-pin.mjs - runs directly under Node +RUN node test/ardy/verify-pose-shape.mjs - runs directly under Node +RUN node test/ardy/verify-prompt-move.mjs - runs directly under Node +RUN node test/ardy/verify-rest.mjs - runs directly under Node +RUN node test/ardy/verify-root-drop.mjs - runs directly under Node +RUN node test/ardy/verify-secure-artifacts.mjs - runs directly under Node +RUN node test/ardy/verify-timeline-coordinates.mjs - runs directly under Node +RUN node test/ardy/verify-timeline-resize.mjs - runs directly under Node +RUN node test/demo/verify-demo-pages.mjs - runs directly under Node +RUN node test/demo/verify-demo-worker-loop.mjs - runs directly under Node +RUN node test/demo/verify-demo-worker-signing.mjs - runs directly under Node +RUN node test/demo/verify-motion-url-allowlist.mjs - runs directly under Node +RUN node test/demo/verify-ops.mjs - runs directly under Node +RUN node test/demo/verify-queue-concurrency.mjs - runs directly under Node +RUN node test/demo/verify-queue-policy.mjs - runs directly under Node +RUN node test/ik/verify-auto-physics.mjs - runs directly under Node +RUN node test/ik/verify-fix-collisions.mjs - runs directly under Node +RUN node test/ik/verify-ik.mjs - runs directly under Node +RUN node test/process/verify-bridge-launch.mjs - runs directly under Node +RUN node test/process/verify-lifecycle.mjs - runs directly under Node +EXCLUDE package-integration test/process/verify-mcp-package-isolation.mjs - installs the MCP runtime from the npm registry with an isolated cache +RUN node test/process/verify-package-telemetry.mjs - runs directly under Node +RUN node test/verify-analytics.mjs - runs directly under Node +RUN node test/verify-appearance.mjs - runs directly under Node +RUN node test/verify-asset-shelf.mjs - runs directly under Node +RUN node test/verify-auto-color.mjs - runs directly under Node +RUN node test/verify-blocking-depth.mjs - runs directly under Node +RUN node test/verify-burn-in.mjs - runs directly under Node +RUN node test/verify-bvh-cskel27.mjs - runs directly under Node +RUN node test/verify-camera-block.mjs - runs directly under Node +RUN node test/verify-camera-follow.mjs - runs directly under Node +RUN node test/verify-camera-move.mjs - runs directly under Node +EXCLUDE browser test/verify-camera-rail-browser.mjs - requires the QA browser wrapper and a running Vite app +RUN node test/verify-camera-rail-schedule.mjs - runs directly under Node +EXCLUDE browser test/verify-cutout-browser.mjs - requires the QA browser wrapper and a running Vite app +RUN node test/verify-cuts.mjs - runs directly under Node +RUN node test/verify-error-boundary.mjs - runs directly under Node +RUN node test/verify-footage-bridge.mjs - runs directly under Node +RUN node test/verify-g006-css.mjs - runs directly under Node +RUN node test/verify-gizmo-claim.mjs - runs directly under Node +RUN node test/verify-grid-view.mjs - runs directly under Node +RUN node test/verify-hierarchy.mjs - runs directly under Node +RUN node test/verify-history.mjs - runs directly under Node +EXCLUDE browser test/verify-ik-browser.mjs - requires the QA browser wrapper and a running Vite app +RUN node test/verify-image-pose.mjs - runs directly under Node +RUN node test/verify-kimodo-cskel27.mjs - runs directly under Node +RUN node test/verify-kimodo-edit.mjs - runs directly under Node +RUN node test/verify-kimodo-effector.mjs - runs directly under Node +RUN node test/verify-kimodo-pose.mjs - runs directly under Node +RUN node test/verify-kimodo-preserve.mjs - runs directly under Node +RUN node test/verify-kimodo-runner.mjs - runs directly under Node +RUN node test/verify-kimodo-setup.mjs - runs directly under Node +RUN node test/verify-kimodo-waypoints.mjs - runs directly under Node +RUN node test/verify-korean-ui.mjs - runs directly under Node +RUN node test/verify-layout.mjs - runs directly under Node +RUN node test/verify-line-edit-draw.mjs - runs directly under Node +RUN node test/verify-line-edit-pins.mjs - runs directly under Node +RUN node test/verify-live-control.mjs - runs directly under Node +RUN node test/verify-matte-editor.mjs - runs directly under Node +RUN node test/verify-matte.mjs - runs directly under Node +RUN node test/verify-mcp-invariants.mjs - runs directly under Node +EXCLUDE browser test/verify-motion-edit-browser.mjs - requires the QA browser wrapper and a running Vite app +RUN node test/verify-motion-edit.mjs - runs directly under Node +RUN node test/verify-motion-trail.mjs - runs directly under Node +RUN node test/verify-mp4-duration.mjs - runs directly under Node +RUN node test/verify-multimodel-ingest.mjs - runs directly under Node +EXCLUDE browser test/verify-object-gizmo.mjs - requires the QA browser wrapper and a running Vite app +RUN node test/verify-object-path.mjs - runs directly under Node +EXCLUDE browser test/verify-offscreen-export-browser.mjs - requires the QA browser wrapper and a running Vite app +RUN node test/verify-offscreen-export.mjs - runs directly under Node +RUN node test/verify-otio.mjs - runs directly under Node +RUN node test/verify-package-signature.mjs - runs directly under Node +RUN node test/verify-pose-extract.mjs - runs directly under Node +RUN node test/verify-pose-library.mjs - runs directly under Node +RUN node test/verify-pose-mirror.mjs - runs directly under Node +RUN node test/verify-pose-yaw.mjs - runs directly under Node +RUN node test/verify-preserve-bridge.mjs - runs directly under Node +EXCLUDE browser test/verify-project-menu-browser.mjs - requires the QA browser wrapper and a running Vite app +RUN node test/verify-project.mjs - runs directly under Node +RUN node test/verify-projflow-bridge.mjs - runs directly under Node +RUN node test/verify-projflow-cskel27.mjs - runs directly under Node +RUN node test/verify-projflow-replay.mjs - runs directly under Node +RUN node test/verify-projflow-runner.mjs - runs directly under Node +RUN node test/verify-projflow-service.mjs - runs directly under Node +RUN node test/verify-pwa.mjs - runs directly under Node +RUN node test/verify-record-mp4-source.mjs - runs directly under Node +RUN node test/verify-resilience.mjs - runs directly under Node +RUN node test/verify-retime.mjs - runs directly under Node +RUN node test/verify-sample-at.mjs - runs directly under Node +RUN node test/verify-scene-asset-cache.mjs - runs directly under Node +RUN node test/verify-scene-assets.mjs - runs directly under Node +RUN node test/verify-scene-objects.mjs - runs directly under Node +RUN node test/verify-scenes.mjs - runs directly under Node +RUN node test/verify-shot-authoring.mjs - runs directly under Node +RUN node test/verify-shot-guides.mjs - runs directly under Node +RUN node test/verify-speed-envelope.mjs - runs directly under Node +RUN node test/verify-stable-ids.mjs - runs directly under Node +RUN node test/verify-take-recipe.mjs - runs directly under Node +RUN node test/verify-telemetry-state.mjs - runs directly under Node +RUN node test/verify-theme.mjs - runs directly under Node +RUN node test/verify-timeline-camera.mjs - runs directly under Node +RUN node test/verify-timeline-extent.mjs - runs directly under Node +RUN node test/verify-timeline-shots.mjs - runs directly under Node +RUN node test/verify-trim.mjs - runs directly under Node +RUN node test/verify-update-check.mjs - runs directly under Node +RUN node test/verify-usd-camera.mjs - runs directly under Node +RUN node test/verify-video-frames.mjs - runs directly under Node +RUN node mcp/verify-http-origin.mjs - runs directly under Node +EXCLUDE browser mcp/verify-live-batch.mjs - drives a real Chrome editor over the live socket +EXCLUDE browser mcp/verify-live-capture.mjs - drives a real Chrome editor over the live socket +EXCLUDE browser mcp/verify-live-editor-model.mjs - drives a real Chrome editor over the live socket +RUN node mcp/verify-live-motion-job.mjs - runs directly under Node +RUN node mcp/verify-live-p0.mjs - runs directly under Node +RUN node mcp/verify-live-port.mjs - runs directly under Node +RUN node mcp/verify-live-routing.mjs - runs directly under Node +EXCLUDE browser mcp/verify-live-scene-parity.mjs - drives a real Chrome editor over the live socket +RUN node mcp/verify-live.mjs - runs directly under Node +RUN node mcp/verify-prompts.mjs - runs directly under Node +RUN node mcp/verify-protocol-version.mjs - runs directly under Node +RUN node mcp/verify-tool-annotations.mjs - runs directly under Node +RUN node mcp/verify.mjs - runs directly under Node + +RUNNING test/ardy/verify-base-free.mjs +PASS base-free root + stitch (reference leak 4.4e-16 m) + +RUNNING test/ardy/verify-browser-motion.mjs +PASS bind snapshot preserves the original local translation for IK FK reconstruction +PASS npz setup: local_rot_mats payload byteOffset is not a multiple of 4 -- absolute byteOffset=139 (3 mod 4) +PASS npz setup: direct Float32Array view at that offset throws (old failure) -- RangeError: start offset of Float32Array should be a multiple of 4 +PASS npz decode: unaligned archive decodes -- frames=2 fps=30 +PASS npz decode: exact member lengths -- rotMats=486 rootPos=6 posedJoints=162 +PASS npz decode: posed_joints exact selected values -- posedJoints[0]=-8 [17]=-8 +PASS npz decode: local_rot_mats exactly identity +PASS npz decode: root_positions exact selected values -- rootPos=[0,0,0,1,2,3] +PASS npz decode: shifted and original archives decode identically +PASS npz rejection: joint-count mutation rejected with exact message -- msg="local_rot_mats must have shape (F, 27, 3, 3), got (2, 26, 3, 3)" +PASS npz decode: an archive without person_scale reports the canonical 1 -- personScale=1 +PASS npz decode: a stored person_scale comes back on the motion -- personScale=0.8999999761581421 +PASS npz rejection: a zero person_scale is corrupt data, not a scale to clamp -- msg="motion person_scale 0 must be positive" +PASS npz round-trip: writer person_scale survives into the browser decoder -- personScale=0.9036999940872192 +PASS npz round-trip: a take written without a stature omits the member and reads back as 1 -- members=local_rot_mats,root_positions,posed_joints,fps personScale=1 +PASS npz writer: person_scale is appended last, so the four original members keep their order +PASS npz writer: a canonical 1 adds no member, so a decode/edit/rewrite round trip does not grow the archive +PASS npz writer: a non-positive stature is refused, never written +PASS character scale: a stored stature is used as-is inside the sane band +PASS character scale: a bad estimate is clamped, never a giant or a gnome +PASS character scale: no stature anywhere means canonical 1 +PASS character scale: the response fallback only applies when the take stores nothing +PASS playback setup: skinning map resolves the RT spine shift and the hips -- Spine->mixamorig:Spine Spine1->mixamorig:Spine1 +PASS playback: neutral frame reproduces the bind pose exactly (zero pop) -- max pos err 4.66e-6, max quat err 2.78e-17 +PASS playback: skinned world positions match s*posed + R@offset (all 8 bones) -- max pos err 4.53e-14 at mixamorig:Spine2 (scale 95.548) +PASS playback: skinned world quaternions match R @ bindQuat (all 8 bones) -- max quat err 2.22e-16 +PASS playback: restore reproduces pre-playback transforms exactly -- bones=8 bitwise +PASS waypoints: scene path is rebased to ARDY clip-local coordinates -- [{"frame":0,"x":0,"z":0,"heading":null},{"frame":40,"x":0.5,"z":-1.25,"heading":null},{"frame":79,"x":2,"z":-1,"heading":null}] +PASS waypoints: player rotation is removed before ARDY generation -- [{"frame":0,"x":0,"z":0,"heading":null},{"frame":40,"x":1.25,"z":0.49999999999999994,"heading":null},{"frame":79,"x":1.0000000000000002,"z":2,"heading":null}] +PASS waypoints: playback rotation restores the exact authored scene offset -- {"x":0.5,"z":-1.25} +PASS waypoints: local conversion does not mutate the authored scene path +PASS waypoints: densified path starts at frame 0, ascending, ≤32 points, keeps authored frames -- count=9 frames=[0,10,20,30,40,50,60,69,79] +PASS waypoints: densified headings are finite numbers in [-2π, 2π], never null -- first=2.859873230201436 last=1.331196515212981 +PASS waypoints: densify does not mutate the authored scene path +PASS waypoints: default density preserves the authored 0.4-second cadence at 24 fps -- frames=[0,10,20,30,40,50,60] +PASS waypoints: straight +Z path faces its travel direction (heading ≈ 0) -- count=7 headings=[0,0,0,0,0,0,0] +PASS waypoints: L-corner turns as one continuous monotonic arc -- corner=0.7853981633974483 headings=[-0.111,0.000,0.785,1.571,1.681] +PASS waypoints: actor rotation 90 is removed rigidly from coordinates and headings -- seg1=1.1903 seg2=-0.1651 count=9 +PASS waypoints: densified corner heading lies inside the turn -- corner=0.4497173581642502 seg1=1.1903 seg2=-0.1651 +PASS waypoints: stalled segment keeps the previous heading until motion resumes -- count=3 +PASS waypoints: fully stationary path has heading 0 everywhere -- count=2 +PASS waypoints: alignArdyPath starts with heading 0 and +Z progress -- rotation=163.858666032988 firstHeading=0 second=(0, 0.3620801061773453) +PASS waypoints: alignArdyPath rotation round-trips frame 40 to the scene offset -- rotation=163.858666032988 restored=(0.5, -1.25) +PASS waypoints: alignArdyPath with actor rotation 90 still starts at heading 0 and round-trips -- rotation=163.85866603298803 firstHeading=0 restored=(0.5, -1.25) +PASS limits: the frame-219 incident is rejected for crawl speed and window overrun -- ["leg 2 (frame 180->219): 0.28 m/s is below a sustainable walk (min 0.5) — pin frame ~188 instead, or click at least 1.0 m away","a root path generates the whole clip in one model call, and ARDY's trained window is 10 s — shorten the clip to 10 s, or split the prompt into blocks so the path rides a chained rollout"] +PASS limits: a chained rollout drops the whole-clip window error but keeps speed errors -- ["leg 2 (frame 180->219): 0.28 m/s is below a sustainable walk (min 0.5) — pin frame ~188 instead, or click at least 1.0 m away"] +PASS limits: a natural 16 s path is legal when the rollout is chained -- [] +PASS limits: a sub-gait pin is blocked and the fix names a workable frame -- 0.26 m/s is below a sustainable walk (min 0.5) — pin frame ~8 instead, or click at least 1.0 m away +PASS limits: a superhuman pin is blocked with the earliest legal frame -- 12.5 m/s is faster than anyone moves (max 3) — pin frame 34 or later, or click closer +PASS limits: pins closer than the rail gap are blocked -- pins need at least 8 frames between them (0.4 s) — scrub past frame 48 +PASS limits: a hold (same spot, any duration) is legal +PASS limits: a natural walking leg passes clean +PASS limits: a >2x pace change places with a gait-shift warning -- ["2.8x speed change from the previous leg — expect a visible gait shift"] +PASS limits: a sharp turn places with a turn-rate warning -- ["90° turn in 0.5 s is sharper than a walking arc — give it more frames or distance"] +PASS limits: a natural 10 s path passes with no errors -- [] +PASS waypoints: alignArdyPath keeps the actor rotation on stationary or short paths -- rot0=0 rot90=90 singleton=45 singletonPoints=0 +PASS waypoints: path generation aligns the full-body pose with frame-zero root +PASS waypoints: new numbered markers are immediately separated and draggable + +failures: 0 + +RUNNING test/ardy/verify-collision-blockers.mjs +PASS a plane (height 0) is not a blocker +PASS cube and cutout both become blockers +PASS cube blocker uses the obj: namespace +PASS cube blocker is a box +PASS cube centre sits half a height above its base at its floor position +PASS cube half extents are half the footprint and half the height +PASS cube yaw is rot in radians +PASS cutout becomes a thin card box (width = height × aspect, depth = thickness) +PASS cutout yaw follows its rot +PASS instance scale and raised base are respected +PASS a footprint-less record resolves from the library +PASS without a library a footprint-less record is skipped +PASS an attached prop is skipped +PASS skipIds drops a named object +PASS an empty scene yields no blockers +PASS a routed prop is sampled at the frame +PASS without a frame a routed prop stands at its authored position +PASS only the OTHER cast members become blockers +PASS a null rig is skipped +PASS every blocker uses the char:: namespace +PASS blockers are capsules with a, b and a radius +PASS the capsule table's own ids come through +PASS id round-trips to its character +PASS capsules are WORLD space — they carry the other rig's stage position +PASS rebuilt blockers follow the other rig's pose +PASS no active id means the whole cast blocks +PASS a Map of rigs reads the same as a plain object +PASS an empty cast yields no blockers +PASS an unsupported rig is skipped, the rest still block +PASS a rig WITH fingers still contributes only its 15 body capsules +PASS no finger capsule becomes a blocker +PASS the palm capsule itself is still a blocker +PASS a rig whose character has left the cast contributes nothing +PASS the surviving cast still blocks +PASS character RECORDS work as the cast list too +PASS no cast list means no opinion — every mounted rig blocks +PASS an empty cast list blocks nothing +PASS collisionBlockers passes the cast list through +PASS collisionBlockers merges cast and scene +PASS summary is JSON-safe +PASS summary keeps every id and kind +PASS summary flattens vectors to numbers +PASS box summary carries centre, half extents and yaw +PASS capsule summary carries both endpoints and the radius +all collision blocker checks PASS + +RUNNING test/ardy/verify-compare-npz.mjs +PASS v1 header: padded v1.0 npz parses and compares identical +PASS v1 header: shapes/dtypes parsed +PASS v1 header: values match the fixture +PASS v2.0 header: 12-byte prefix parsed +PASS dtype mismatch: -z) +PASS fov is radians +PASS non-finite quaternion throws loudly +PASS first depth-first match wins over the nested identity copy +PASS schema is cozyclay.pose.v1 +PASS root passes through +all pose-export math checks PASS + +RUNNING test/ardy/verify-pose-pin.mjs +PASS a pose start pins exactly one frame +PASS a pose pins at the frame it was placed on +PASS an out-of-range placement is clamped into the clip +PASS a plain prompt run stays unpinned +PASS a prompt schedule blocks the pose start by name +PASS an unrequested pin reports nothing-to-pin, not a conflict +PASS a root path pins only its first frame +PASS authored IK keys keep pinning themselves +PASS a placed pose joins authored IK keys +PASS IK keys outside the clip never reach the bridge +PASS block edits pin only their own frames +pose-pin: OK + +RUNNING test/ardy/verify-pose-shape.mjs +rest hierarchy vs bind Rb: max deviation 0.000040 deg (RightArm) + +alignment: rotation det=1.000000, fitted scale=0.010302 +ARDY cloud height=1.6597 RMS residual=0.07231 (4.36% of height) + +per-joint residuals after similarity alignment, worst first: + Spine1 0.14478 (8.72% h) + Spine2 0.14115 (8.50% h) + Spine 0.13834 (8.34% h) + Hips 0.08941 (5.39% h) + RightHand 0.08040 (4.84% h) + RightHandThumb1 0.07736 (4.66% h) + LeftHand 0.07685 (4.63% h) + LeftHandThumb1 0.07358 (4.43% h) + Head 0.07249 (4.37% h) + RightArm 0.06172 (3.72% h) + LeftArm 0.05737 (3.46% h) + LeftForeArm 0.05188 (3.13% h) + RightForeArm 0.05158 (3.11% h) + LeftToeBase 0.04869 (2.93% h) + RightToeBase 0.04770 (2.87% h) + RightUpLeg 0.04676 (2.82% h) + LeftUpLeg 0.04560 (2.75% h) + Neck 0.04320 (2.60% h) + LeftShoulder 0.03753 (2.26% h) + RightShoulder 0.03542 (2.13% h) + RightLeg 0.03026 (1.82% h) + LeftLeg 0.02879 (1.73% h) + RightFoot 0.02013 (1.21% h) + LeftFoot 0.01984 (1.20% h) + +VERDICT: PASS -- RMS residual 4.36% of skeleton height vs threshold 10.0% +the naive three.js Rb reconstructs the ARDY shape: no leg flip, no per-chain frame error. The residual is the +incongruence floor between the ARDY canonical skeleton and the x-bot rig (see the diagnosis below). + +diagnosis: rig rest bone direction (Rb_parent @ t_rest) vs ARDY offset, per joint +(the per-joint frame agreement the retarget contract depends on; K = minimal rotation rig->ARDY) + Spine 34.4 deg K axis=(-1.000, -0.000, -0.000) + LeftShoulder 29.6 deg K axis=(0.873, -0.284, 0.397) + RightShoulder 29.6 deg K axis=(0.873, 0.284, -0.397) + LeftUpLeg 24.6 deg K axis=(-0.100, -0.343, 0.934) + RightUpLeg 24.6 deg K axis=(-0.100, 0.343, -0.934) + LeftToeBase 19.2 deg K axis=(-1.000, 0.000, 0.000) + RightToeBase 19.2 deg K axis=(-1.000, 0.000, 0.000) + Neck 15.1 deg K axis=(1.000, -0.000, 0.000) + LeftArm 12.2 deg K axis=(-0.000, -0.973, 0.229) + RightArm 12.2 deg K axis=(-0.000, 0.973, -0.229) + LeftHandThumb1 11.7 deg K axis=(-0.506, -0.862, -0.041) + RightHandThumb1 11.7 deg K axis=(-0.505, 0.862, 0.042) + LeftFoot 3.9 deg K axis=(-1.000, -0.000, 0.000) + RightFoot 3.9 deg K axis=(-1.000, -0.000, 0.000) + Spine2 3.5 deg K axis=(1.000, 0.000, 0.000) + Spine1 1.5 deg K axis=(-1.000, -0.000, 0.000) + LeftLeg 0.4 deg K axis=(1.000, -0.000, 0.000) + RightLeg 0.4 deg K axis=(1.000, -0.000, 0.000) + Head 0.1 deg K axis=(1.000, -0.000, 0.000) + LeftForeArm 0.0 deg K axis=(0.000, 0.587, 0.810) + LeftHand 0.0 deg K axis=(-0.000, 0.472, 0.882) + RightHand 0.0 deg K axis=(0.000, -0.953, 0.302) + RightForeArm 0.0 deg K axis=(0.000, -0.492, 0.870) + +leg chain worst direction agreement: 24.6 deg (UpLeg is hip-joint placement, not a frame error; +shin/foot directions agree at 0.4-3.9 deg) +overall worst: Spine 34.4 deg +correction pattern: per-joint, no shared constant and no per-chain constant + +conclusion: the naive three.js Rb is NOT wrong for the leg chain (Blender's +bone.matrix_local equals it on all 8 leg joints, 0.0000 deg), and the old +verify-rest '170-180 deg legs' signal is the legs' rest orientation +(Rb_leg^T ~= rotZ(180)), not an Rb error. The residual above the +incongruence floor comes from per-joint frame differences of 0-35 deg +with no clean shared/per-chain K, plus proportion differences (ARDY +arms/neck 30-50% longer relative to the legs). No rest-correction.js is +justified; the tables above are the evidence. + +RUNNING test/ardy/verify-prompt-move.mjs +PASS 1x pixel delta maps to 40 frames +PASS 0.5x pixel delta maps to 40 frames +PASS 2x wide lane pixel delta maps to 40 frames +PASS 1000 repeated events never accumulate movement +PASS move snaps to the next 40-frame block +PASS move preserves duration +PASS left move clamps at frame zero +PASS move onto another block clamps to abut it +PASS move into a free adjacent slot succeeds +PASS off-grid drag clamps flush against the neighbor +PASS drag with no free room keeps the layout +all prompt move checks PASS + +RUNNING test/ardy/verify-rest.mjs +rest-direction agreement: rig (Rb_parent @ t_rest) vs ARDY offset, per resolved joint + Spine 34.4 deg + LeftShoulder 29.6 deg + RightShoulder 29.6 deg + LeftUpLeg 24.6 deg + RightUpLeg 24.6 deg + LeftToeBase 19.2 deg + RightToeBase 19.2 deg + Neck 15.1 deg + LeftArm 12.2 deg + RightArm 12.2 deg + LeftHandThumb1 11.7 deg + RightHandThumb1 11.7 deg + LeftFoot 3.9 deg + RightFoot 3.9 deg + Spine2 3.5 deg + Spine1 1.5 deg + LeftLeg 0.4 deg + RightLeg 0.4 deg + Head 0.1 deg + LeftForeArm 0.0 deg + LeftHand 0.0 deg + RightHand 0.0 deg + RightForeArm 0.0 deg + ok Spine: 34.4 deg <= 45 deg (documented incongruence, drift guard) + ok LeftShoulder: 29.6 deg <= 45 deg (documented incongruence, drift guard) + ok RightShoulder: 29.6 deg <= 45 deg (documented incongruence, drift guard) + ok LeftUpLeg: 24.6 deg <= 45 deg (documented incongruence, drift guard) + ok RightUpLeg: 24.6 deg <= 45 deg (documented incongruence, drift guard) + ok LeftToeBase: 19.2 deg <= 45 deg (documented incongruence, drift guard) + ok RightToeBase: 19.2 deg <= 45 deg (documented incongruence, drift guard) + ok Neck: 15.1 deg <= 45 deg (documented incongruence, drift guard) + ok LeftArm: 12.2 deg <= 45 deg (documented incongruence, drift guard) + ok RightArm: 12.2 deg <= 45 deg (documented incongruence, drift guard) + ok LeftHandThumb1: 11.7 deg <= 45 deg (documented incongruence, drift guard) + ok RightHandThumb1: 11.7 deg <= 45 deg (documented incongruence, drift guard) + ok LeftFoot: 3.9 deg <= 5 deg (agreement class) + ok RightFoot: 3.9 deg <= 5 deg (agreement class) + ok Spine2: 3.5 deg <= 5 deg (agreement class) + ok Spine1: 1.5 deg <= 5 deg (agreement class) + ok LeftLeg: 0.4 deg <= 5 deg (agreement class) + ok RightLeg: 0.4 deg <= 5 deg (agreement class) + ok Head: 0.1 deg <= 5 deg (agreement class) + ok LeftForeArm: 0.0 deg <= 5 deg (agreement class) + ok LeftHand: 0.0 deg <= 5 deg (agreement class) + ok RightHand: 0.0 deg <= 5 deg (agreement class) + ok RightForeArm: 0.0 deg <= 5 deg (agreement class) + +the naive three.js Rb ties the skeletons at the shins/feet/forearms/hands/head/ +mid-spine (agreement class, <= 5 deg). The remaining joints deviate per-joint +(11.7-34.4 deg, no shared or per-chain constant): the ARDY canonical skeleton is +not congruent with the x-bot rig there, which is skeleton incongruence, not an +Rb extraction error -- Blender's contract Rb (bone.matrix_local) equals the +three.js Rb on all 8 leg joints to 0.0000 deg, and verify-pose-shape.mjs +reconstructs the posed shape to 4.4% RMS of skeleton height. +The old test's 'legs 170-180 deg from the Hips frame' was Rb_leg^T ~= rotZ(180), +the legs' rest orientation (they point down), not an Rb error. + +failures: 0 + +RUNNING test/ardy/verify-root-drop.mjs +PASS a well-formed drop normalizes +PASS camelCase is accepted too +PASS malformed drops are refused +PASS the source clip is never mutated +PASS a new clip object is returned +PASS before the drop nothing moves +PASS the fall follows a gravity curve +PASS early fall is slower than late fall +PASS every joint drops by the same amount +PASS the root channel drops with the body +PASS x and z are untouched +PASS a landing past the clip end still falls +PASS no drop returns the clip untouched +PASS an invalid drop returns the clip untouched +PASS a clip without a clock is left alone +PASS walking off a roof stages a fall at the edge crossing +PASS the fall keeps a near-real gravity clock +PASS fallTimeScale 1 restores physical free-fall time +PASS the staged drop is applyRootDrop-compatible +PASS a grounded subject stages nothing +PASS a subject never on the support stages nothing +PASS a walk that stays on the roof stages nothing +PASS the walk is rotated into scene space before the edge test +PASS a lower support under the exit shortens the fall +PASS auto fall never mutates its source take +PASS airborne x follows launch velocity instead of the authored walk +PASS the body and root share the same ballistic translation +PASS landing reaches the requested floor height +PASS horizontal drift stops after impact +PASS the landing pose freezes during the impact hold +PASS the landing rotations freeze during the impact hold +PASS the take cuts shortly after impact +all root-drop checks PASS + +RUNNING test/ardy/verify-secure-artifacts.mjs +secure ARDY artifact creation preserves planted symlink targets and enforces private modes + +RUNNING test/ardy/verify-timeline-coordinates.mjs +PASS 0.25x click on tick 140 resolves frame 140 +PASS 0.5x click on tick 140 resolves frame 140 +PASS 1x click on tick 140 resolves frame 140 +PASS 2x scrolled click resolves frame 100 +PASS zoom-out virtual frames beyond clip clamp to final frame +PASS left of lane clamps to frame 0 +PASS TRIM_MIN_FRAMES is half a second at 24 fps +PASS end handle lands on the dragged frame +PASS end-only drag still reports start 0 (full-range restart) +PASS start handle lands on the dragged frame +PASS start-only drag leaves the take's last frame +PASS start cannot cross within TRIM_MIN_FRAMES of the end +PASS end cannot cross within TRIM_MIN_FRAMES of the start +PASS end clamps to the take's last frame +PASS zoom-out drag resolves the same take frame +PASS zoom-out drag keeps the full-range start +all timeline coordinate checks PASS + +RUNNING test/ardy/verify-timeline-resize.mjs +PASS 10 px does not resize a 2-second block +PASS 59 px stays below one-block threshold +PASS 60 px right adds one 2-second timeline block +PASS 60 px left removes one 2-second timeline block +PASS 120 px right adds one timeline block +PASS 360 px right adds three blocks, never an exponential value +PASS 1000 repeated pointermoves stay on the same frame +all prompt resize checks PASS + +RUNNING test/demo/verify-demo-pages.mjs +PASS build outputs present: dist/demo/index.html dist/demo/walk-then-stop.npz dist/d/index.html +PASS built demo bundles embed shared prompt cap 500 with no Infinity path +PASS service worker keeps /d/ and /demo/ network-only with cache v6 +PASS ticket bookmark notice, Google-only sign-in, robots rule, and sitemap visibility +PASS demo and ticket render user text without removed v1 surfaces +PASS Vite ignores divergent prompt-cap env and injects fixed shared value 500 +PASS executable submission error-code mapping +PASS session distinguishes unauthenticated responses from API failures +PASS executable queued/done ticket rendering and safe prompt text +PASS ticket polling request omits credentials +all CozyClay hosted demo page checks PASS + +RUNNING test/demo/verify-demo-worker-loop.mjs +demo worker loop checks PASS + +RUNNING test/demo/verify-demo-worker-signing.mjs +demo worker signing checks PASS + +RUNNING test/demo/verify-motion-url-allowlist.mjs +PASS verify-motion-url-allowlist + +RUNNING test/demo/verify-ops.mjs +PASS verify-ops (owner revoke, private R2, cron cleanup, removed-surface negatives) + +RUNNING test/demo/verify-queue-concurrency.mjs +PASS verify-queue-concurrency (FIFO claims, one active job, daily cap 2, admission boundary) + +RUNNING test/demo/verify-queue-policy.mjs +PASS verify-queue-policy (single FIFO queue, daily cap 2, shared prompt limits) + +RUNNING test/ik/verify-auto-physics.mjs +PASS Dempster segment fractions sum to 1 +PASS every segment names two Mixamo bones +PASS both sides are represented +PASS CoM resolves on a complete rig +PASS CoM sits inside the body's vertical extent +PASS CoM is centred left/right on a symmetric T-pose +PASS a rigid root lift moves the CoM by exactly the same amount +PASS a rig missing a segment bone reports no CoM +PASS a null rig reports no CoM +PASS marker heights cover both ankles and both toes +PASS a rig missing a toe marker reports no heights +PASS the planted floor is each marker's clip minimum +PASS a frame at the planted floor is not airborne +PASS a frame half a metre up is airborne +PASS one low toe keeps the frame grounded +PASS a missing planted floor is never airborne +PASS plantedFloor of nothing is null +PASS the lift threshold is a real clearance +PASS the standing rig floats above the old absolute thresholds +PASS a standing clip with floating soles has no airborne spans +PASS a standing clip with floating soles is never keyed +PASS a standing clip reports zero correction +PASS the planted floor was measured from the clip +PASS the QA standing frame cleared every OLD absolute threshold +PASS the measured standing frame is grounded under the clip-relative rule +PASS the measured flight frame is still airborne +PASS a marker exactly at its planted height is grounded +PASS contiguous spans are grouped with inclusive bounds +PASS a one-frame blip is rejected as noise +PASS spans report their length +PASS the minimum span length is at least 4 frames +PASS a span running to the last frame still closes +PASS a predicate + startFrame yields absolute frame numbers +PASS exact parabola recovers y0 +PASS exact parabola recovers v0 +PASS exact parabola has no residual +PASS fitted positions come back per frame +PASS gravity is fixed, not fitted +PASS the fit leaves x and z exactly as sampled +PASS a wandering lateral track is not straightened +PASS noisy parabola recovers y0 within a few mm +PASS noisy parabola recovers v0 within 0.05 m/s +PASS a hovering track leaves a large residual +PASS an empty sample list is harmless +PASS a single sample fits itself +PASS the airborne span is detected with the right bounds +PASS the floaty span was corrected +PASS every correction lands inside the airborne span +PASS the whole span is corrected +PASS no span was refused +PASS maxCorrection is positive and sane +PASS grounded frames outside the pinned boundary are never keyed +PASS the corrected CoM traces a true parabola across its core +PASS the corrected flight still rises and falls +PASS the mid-air hover is gone (velocity falls every frame) +PASS the arc's anchor frames are left exactly on the clip +PASS the tapered frames ease between the clip and the fitted arc +PASS no horizontal displacement is introduced +PASS the original clip really was non-ballistic +PASS the leaning lead-out does not disturb span detection +PASS boundary pins sit either side of the span +PASS the pins carry zero delta (the clip's own pose) +PASS grounded frames beyond the blend window do not move +PASS the lead-out really does rotate away from the pin (not vacuous) +PASS grounded frames inside the blend window keep the clip's rotation +PASS grounded neighbours drift by well under a millimetre +PASS the garbage stretch is still detected as one arc +PASS the garbage span is refused +PASS the refusal names a reason +PASS a refused span writes no keys at all +PASS a refused span reports no correction +PASS the low-landing span is corrected +PASS no corrected frame puts a foot below the floor clearance +PASS the guard overrode the fit where it had to +PASS the clamped correction stays under the limit +PASS hysteresis grows a span out to the exit threshold +PASS expansion cannot reach into the next span +PASS expansion stops at the range ends +PASS nothing to extend leaves spans as they were +PASS a single arc is not split +PASS a shallow ripple is not a split point +PASS a deep trough splits, and the trough ends the left arc +PASS splitting recurses into three arcs +PASS the split depth is respected +PASS the taper eases in and out symmetrically +PASS the taper never exceeds full strength +PASS a short arc still gets a ramp that fits inside it +PASS a disabled taper is uniform +PASS a fit range excludes the frames it is told to +PASS the trough is deep enough to split on +PASS without splitting, one merged fit is refused outright +PASS the merged span is split into two arcs +PASS neither arc is refused +PASS both arcs are corrected +PASS the first hop's core is a true parabola +PASS the first hop still rises and falls +PASS the second hop's core is a true parabola +PASS the second hop still rises and falls +PASS the entry lift alone truncates the real flight +PASS hysteresis recovers the take-off and landing frames +PASS the recovered frames really were below the entry lift +PASS the flight window's ballistic residual improves substantially +PASS the corrected flight recovers real gravity in its core +PASS the corrected core accelerates at exactly g +PASS the pins really do sit at zero correction +PASS the correction never accelerates harder than gravity +PASS an unanchored full-strength correction really would snap +PASS anchoring plus the taper removes most of that spike +PASS the first run corrected the clip +PASS a second run keys nothing new +PASS a second run reports zero correction +PASS the key set is identical after the second run +PASS the span survives a second detection pass +PASS the corrected arc is unchanged by the second run +PASS an already-ballistic span is still detected +PASS an already-ballistic clip gets zero keys +PASS an already-ballistic clip reports no correction +PASS an already-ballistic clip leaves the IK layer empty +PASS a grounded clip has no airborne spans +PASS a grounded clip gets no keys +PASS the collision fix keyed and tracked the arm +PASS AutoPhysics still corrects the span with an arm already tracked +PASS every frame AutoPhysics wrote carries the hips +PASS no chain the pass did not move is keyed by it +PASS the earlier arm key survives untouched +PASS a missing applyFrame is refused +PASS a missing hips joint is refused +PASS a missing driver is refused with its own reason +PASS a range shorter than the minimum span is refused +PASS a too-short range says so +PASS a rig whose CoM cannot be measured is refused, not called clean +PASS a clip that simply never leaves the ground is SUPPORTED +PASS an already-ballistic clip is supported too +PASS the legacy `margin` option still sets the lift threshold +PASS epsilon is 5 mm as documented + +all pass + +RUNNING test/ik/verify-fix-collisions.mjs +PASS builds all 15 body capsules +PASS capsule world positions respect rig scale +PASS T-pose reports no self-collision +PASS forearm-through-chest is detected +PASS detection names the arm and the torso +PASS fixer changed the pose +PASS no penetration remains after fixing +PASS touched chain is tracked for keying +PASS segment lengths survive the fix +PASS onlyChains blocks fixes on unlisted chains +PASS the penetrating pose is untouched when blocked +PASS only the penetrating frame is keyed +PASS the key landed in the IK layer +PASS clean frames stay keyless +PASS the range fix left the last frame clean +PASS a grazing pose is clean with no offset +PASS the same pose penetrates once a skin gap is demanded +PASS offset adds itself to the reported depth +PASS the default slack catches a shallow hand-in-thigh +PASS a looser slackFactor lets the same pose through +PASS fixCollisions forwards slackFactor to detection +PASS the default slackFactor fixes what 0.6 ignored +PASS a fingerless rig still builds every capsule +PASS the fingerless hand degrades to a wrist sphere +PASS the hand capsule reaches the middle finger when present +PASS both spellings of the rig are supported +PASS the wrist sphere misses a fingertip in the thigh +PASS the finger-extended hand catches it +PASS the fixable half still gets fixed +PASS a fruitless pass ends the loop instead of burning iterations +PASS only the unpushable penetration is left +PASS a rig missing a required bone is reported unsupported +PASS capsules cannot be built for it +PASS fixCollisions says supported:false, not a clean pose +PASS a supported rig reports supported:true +PASS the range walker bails on an unsupported rig +PASS the fixture really is a movable×movable pair +PASS the permitted side still gets fixed +PASS onlyChains leaves the unlisted MOVABLE limb exactly where it was +PASS the run reports only the chain it actually drove +PASS the permitted side owed the WHOLE separation (nothing left over) +PASS a pushed joint is never driven below the floor clearance +PASS a no-op fix hands back the clip's own bone translations +PASS the wobbled skeletons still get fixed +PASS no partial weight of the baked delta outruns the whole correction +PASS and a wobbled clip earns a correction of the same shape as a bind one +PASS the wobbly clip still keys only the frames that penetrate +PASS an unkeyed neighbour of a fix on a wobbly clip never moves further than the fix itself +PASS the fixture's rest pose DOES trip the uncalibrated rule +PASS the reported phantom depth is the one the default slack leaves standing +PASS the bind overlap is measured for exactly that pair +PASS the rest pose reads clean once the pair is calibrated against bind +PASS fixCollisions writes nothing at rest +PASS a genuine deep hit on a calibrated pair is still caught +PASS the calibration shifts the zero point, it does not disable the pair +PASS the tolerance is a few millimetres, not a licence +PASS a rig with no bind snapshot gets no calibration at all +PASS the source clip penetrates at exactly frames 2 and 18 +PASS the range pass keys only the frames the SOURCE clip needed +PASS a key names only the chains that frame actually drove +PASS a tracked-but-untouched limb is never re-keyed by the pass +PASS an unkeyed mid-gap frame reproduces the raw clip within 1 mm +PASS a frame inside the blend window still carries the correction +PASS a second pass over the fixed clip keys nothing new +PASS the fixture puts several fixes inside one blend window +PASS the range pass keys exactly the frames the raw clip needs +PASS and bakes the pose a raw-clip fix would have produced +PASS the fixture's bind is a T-pose while its rest hangs the arm down +PASS the standing rest pose DOES trip the uncalibrated rule at torso × forearm +PASS the phantom is the centimetre-scale one QA measured +PASS bind alone cannot see it — the arms are nowhere near the torso there +PASS the standing rest pose reads completely clean once calibrated +PASS the bind T-pose reads clean too +PASS Fix collisions on a fresh standing project writes nothing +PASS and it hands the pose back untouched +PASS the allowance came from the REST composition, not from bind +PASS a forearm genuinely driven into the torso is still detected +PASS the calibration moved the zero point, it did not disable the pair +PASS the bias constants are the documented ones +PASS the reach rig's rest pose holds the wrist out in FRONT of the chest +PASS the fixture buries the forearm in the chest from the front +PASS the chest fix leaves nothing penetrating +PASS the wrist keeps its OWN side of the sagittal plane +PASS and comes out IN FRONT of the chest, not sideways through it +PASS the chest fix preserves the arm's bone lengths +PASS a second run over the fixed pose changes nothing +PASS no deep chest hit walks the left hand over to the right side +PASS and those deep hits still come out clean +PASS the fixture sinks the hand deep into the head +PASS and the head's own escape normal points AWAY from the rest pose +PASS the head fix still separates completely +PASS the hand leaves the head TOWARD its rest position, not along the head normal +PASS the hand also stays on its own side coming out of the head +PASS the thigh fixture is a movable × static hit +PASS the thigh case still converges to zero residual +PASS in the same 4 passes the unbiased rule needed +PASS with the leg's bone lengths intact +PASS a full Mixamo hand adds one capsule per finger +PASS a finger capsule spans the base joint to the TIP joint +PASS fingers carry the fixed contact radius, not a measured one +PASS a finger with no tip bone falls back to the last knuckle +PASS a missing finger drops its capsule and nothing else +PASS a rig with no finger bones at all is still fully supported +PASS the wrist and the palm clear the thigh in that pose +PASS but the fingertips 2 cm inside it are caught +PASS every finger that reaches the thigh reports it, at the finger's own slack +PASS and the fix takes the fingers out of the thigh +PASS the finger fix is idempotent +PASS a closed hand reports no finger against its own hand, forearm or another finger +PASS the fist pose is a fold the fingers really do make +PASS the default slack is the documented 0.25 +PASS the fixture overlaps the forearm and the thigh by 17 mm +PASS the old 0.4 slack called that clean +PASS the 0.25 default catches it +PASS and the fixer clears it +PASS a shallow fix does not oscillate — the second run is a no-op +PASS the trim constants are the documented ones +PASS a knee driven into the belly is detected as thigh × torso +PASS and it is resolved by moving the LEG +PASS the belly case comes out clean +PASS with the leg's bone lengths intact +PASS a forearm folded flat onto its own upper arm is detected +PASS a normally bent elbow is not +PASS and neither is a deep, still-possible one +PASS the bind T-pose still reads clean with the hinged pairs on +PASS a T-posed rig with full hands reads clean too +PASS and a hinged pair is calibrated like any other +PASS a rig clear of everything is clean until the blocker arrives +PASS the blocker's id names the pair, the way fcDetect reads it out +PASS a blocker is never the movable side +PASS the arm is pushed out of the other character +PASS the arm keeps its bone lengths coming out +PASS a blocker fix is idempotent +PASS a cube where the arm swings is detected +PASS the fixture really does bury the hand inside the cube +PASS the cube fix leaves nothing penetrating +PASS and the limb ends OUTSIDE the cube, not just off its surface +PASS a plank parallel to the arm is clear of it +PASS the same plank yawed 45° into the arm is caught +PASS and the arm comes off the yawed plank completely +PASS no rest calibration can hide a blocker +PASS a malformed blocker is dropped, not thrown +PASS blockersAt is asked for every frame of the range, after that frame is posed +PASS only the frame the prop was in the way gets keyed +PASS a static blockers array applies to every frame of the range +PASS the fixture really is stuck without a yield +PASS with the FK joints in hand the torso yields +PASS and the residual drops because of it +PASS the yield is capped at 12° for the whole run +PASS the step cap is the documented one +PASS a yielded joint is named in touched, so the bake can key it +PASS a case the limb can solve never yields +PASS an unfixable residual does not spend the yield budget on nothing +PASS the fixture's clip pose and its bind pose straddle the crate's face +PASS the readout calls that pose clean +PASS and so does the fixer: no key, no touch, no motion +PASS a T-posed rig that reads clean is left bit-identical +PASS a rig with full hands that reads clean is left bit-identical +PASS a rig beside a yawed crate it clears that reads clean is left bit-identical +PASS a rig beside a capsule blocker it clears that reads clean is left bit-identical +PASS the yawed cube at the hand is fixed on the first press +PASS and the second press on that pose changes nothing at all +PASS and the third, and the fourth — no flip-flop +PASS a deep crate at yaw 0° never ends with more of the limb inside it +PASS a deep crate at yaw 45° never ends with more of the limb inside it +PASS a deep crate at yaw -60° never ends with more of the limb inside it +PASS the fixture is an ARM hit with no leg pair anywhere in it +PASS the arm hit is fixed +PASS and only the arm chain is named +PASS every leg bone is exactly where the clip left it +PASS with the clip's own translations, not bind's +PASS so no leg bone changes length +PASS and the feet keep the clearance they arrived with +PASS the chain that solved is at bind translations, as the solver requires +PASS the fixture is a head × upper-arm hit the limb cannot solve +PASS without a yield it stays exactly as deep as it was +PASS a caller that passes no FK joints still gets the yield +PASS the head leans off the arm and the residual drops with it +PASS more than one step of budget is spent when one cannot finish the job +PASS the pinched fixture presses the head from both sides at once +PASS a head pinched from both sides refuses to yield rather than rock +PASS the reporting floor is the documented 4 mm +PASS a walk-stride graze between the thighs is not reported +PASS a real thigh-through-thigh crossing still is, at full depth +PASS the sibling pair is judged by the ordinary rule, not a hinge allowance +PASS a millimetre of foot × foot in a stride is not worth a key +PASS the source clip penetrates at one frame only +PASS the correction's own blend ramp drags its neighbours into the torso +PASS one bounded round over the window catches them +PASS and the clip the viewer scrubs through is clean +PASS the round is ONE round: a second pass over the result keys nothing +PASS the keyed list is still an array the caller can count +PASS and it names the frames that still penetrate, with how deep +PASS each frame appears at most once, in order +PASS a clip with nothing left over reports an empty unresolved list +PASS the fixture buries the hand in the head on frames 4..9 +PASS and its own hand never moves more than a few centimetres a frame +PASS the pass cleans every frame of the run +PASS no frame steps further than 3× the clip's own worst step +PASS the frames of one buried run inherit one another's escape +PASS an arm-only run never moves a leg +PASS a second pass over the coherent result keys nothing +PASS a lone dirty frame is the only one keyed +PASS and it is solved exactly as a stand-alone fix solves it + +all pass + +RUNNING test/ik/verify-ik.mjs +PASS every IK control declares an occlusion allowance +PASS full-body controls include torso, head, neck, and both shoulders +PASS a control before the first blocker stays exposed +PASS a shallow under-skin control stays exposed +PASS a far-side control is hidden +PASS resolve finds all four chains +PASS resolve finds the FK swing joints +PASS seedTargets places the handle on the effector +PASS seeding changes no bone +PASS IK solve restores positional-playback chain translations to bind +PASS test target is reachable +PASS IK reaches a bent target within 2 cm +PASS IK preserves segment lengths +PASS straight chain falls back to pole (elbow backward) +PASS unreachable target stretches to full length, no NaN +PASS elbow stays on its own side through the sweep +PASS no elbow flip jump through the sweep +PASS key frames sorted +PASS key stores only the tracked chain +PASS key stores b0/b1/b2 local quats +PASS f20 bone rotation is the slerp midpoint (<0.5°) +PASS IK evaluation restores positional-playback chain translations to bind +PASS untracked chain is never written by evaluate +PASS moved character keeps the keyed local rotations +PASS restore returns the chain to the snapshot +PASS removeKeyframe drops the frame +PASS mid-joint drag moves the elbow on a STRAIGHT chain (> 5 cm) +PASS mid-joint drag lands the elbow on the clamped point +PASS clamped point sits on the root sphere (|p0→p1| = l0) +PASS mid-joint drag keeps the forearm direction +PASS mid-joint drag preserves segment lengths +PASS far mid-joint drag clamps, no NaN +PASS far mid-joint drag keeps lengths +PASS mesh contact radii are measured and clamped to the sane range +PASS missing contact bones fall back to a small measured default +PASS body contact lifts a hand below the floor to its marker radius +PASS body contact is a zero-cost no-op above the floor +PASS body contact leaves an above-floor hand untouched +PASS body contact leaves above-floor hips untouched +PASS hips drag clamps the pelvis contact height +PASS drag targets clamp at the measured hand floor height +PASS drag-time chain hold keeps a hand above its contact height +PASS FK swing applies the exact rotation from drag start +PASS FK swing is absolute, never compounds +PASS FK swing does not translate the joint +PASS FK swing visibly moves the head +PASS spine control changes its joint rotation +PASS chest control changes its joint rotation +PASS neck control changes its joint rotation +PASS head control changes its joint rotation +PASS leftShoulder control changes its joint rotation +PASS rightShoulder control changes its joint rotation +PASS FK key slerp midpoint (<0.5°) +PASS FK restore returns the joint to the snapshot +PASS hips joint carries its bind local position +PASS hips translate lowers the body 0.4 m in world +PASS hips translate converted through the cm scale (local Δ = 40 cm) +PASS hips translate moves the whole skeleton with it +PASS hips translate converts world delta through parent yaw +PASS hips key stores the local position +PASS hips position lerps between keys +PASS restore returns the hips bind position +PASS plantFeet captures both ankle positions +PASS without the planted solve the ankle follows the hips down +PASS planted solve returns the ankle to its plant +PASS the knee bent to keep the foot planted +PASS no plants → planted solve does nothing +PASS effector swing twists the hand by the drag angle +PASS effector swing leaves the forearm alone +PASS zero-angle swing is a no-op +PASS baked chain key stores three quats (b0, b1, b2) +PASS evaluate restores the authored effector rotation +PASS two-quat legacy keys leave the effector as-is +PASS a frame mid-gap between two key islands keeps the clip pose +PASS a frame one window past an island edge is fully back on the clip +PASS the ease into an island still ramps from its edge key +PASS authoring mode (blendFrames 0) still holds the whole keyed envelope +PASS keys within one blend window are one island (full-weight interpolation) +PASS a single key is an island of one, easing both ways +PASS the bake records the pose the key was made over +PASS the keyed frame reproduces the baked position exactly +PASS an in-window frame gets clip + eased delta +PASS an in-window frame is NOT lerped toward the key's absolute position +PASS a key with no base pose still uses the absolute blend +PASS the default bake still writes the whole tracked set +PASS onlyIds bakes exactly the named parts +PASS onlyIds touches its parts into the tracked set +PASS the bind measurement finds the mesh's actual radius +PASS a first measurement taken mid-clip reports the bind-pose radius +PASS the correction under test is the small one the criterion names (< 2 cm) +PASS the fixture reproduces the absolute blend's smear (the test is not vacuous) +PASS no unkeyed frame's foot moves more than 1 cm from the raw clip +PASS every bone's smear is bounded by that bone's own correction +PASS no unkeyed frame is dragged below its own clip foot height by 5 mm +PASS the keyed frames still reproduce the solved pose exactly +PASS legacy keys with no base rotation keep the old absolute blend +PASS a chain key stores the clip rotations it was solved from +PASS baseQ ∘ delta reconstructs the authored rotation +PASS a bake with no bases stores no baseQ +PASS a partial base array is refused rather than half-applied +PASS blendFrames 0 still applies the key absolutely +PASS the bind pose is a T-pose (forearm out to the side) +PASS the composed REST pose hangs the arm at the side +PASS the two poses are far apart — bind cannot stand in for rest +PASS restWorldPosition matches the pose applyPose really writes +PASS composing the rest pose never disturbed the live rig +PASS a rig with no bind snapshot composes nothing and reads current +all PASS + +RUNNING test/process/verify-bridge-launch.mjs +all bridge launch checks PASS + +RUNNING test/process/verify-lifecycle.mjs +PASS owned parent process starts +PASS owned grandchild process starts +PASS terminating owner removes parent +PASS terminating owner removes grandchild +PASS idle has no continuous render activity +PASS pointer drag activates continuous rendering +PASS camera key activates continuous rendering +PASS wheel pulse activates continuous rendering +PASS render activity keys cover WASD and crane controls +all lifecycle checks PASS + +RUNNING test/process/verify-package-telemetry.mjs +all packaged telemetry checks PASS + +RUNNING test/verify-analytics.mjs +all analytics checks PASS + +RUNNING test/verify-appearance.mjs +PASS legacy per-mesh outline modules are removed +PASS renderers contain no hull or geometry edge attachments +PASS one screen-space pass owns all shot linework +PASS line extraction uses a Sobel depth kernel +PASS line extraction uses a Sobel normal kernel +PASS depth silhouettes stay attached to the nearer surface +PASS distant outlines soften without disappearing or changing kernel width +PASS each pane renders into its own local edge target +PASS plan view remains free of shot post-processing +PASS IK controls emphasize the camera-nearest circles +PASS IK circles mirror their real clickability +PASS occluded IK controls remain visible and pickable +PASS IK occlusion caches sampled skin proxies instead of re-skinning every ray +PASS IK handles do not raycast on every camera pointer move +PASS replaced rigs cannot retain stale skin proxy clouds +PASS gizmo arrows have forgiving invisible hit volumes +PASS gizmo arrows and swing rings remain visible over the player body +PASS unchanged IK frames reuse the previous exposure pass +all screen-space NPR appearance checks PASS + +RUNNING test/verify-asset-shelf.mjs +PASS asset bytes use readable binary units +PASS asset bytes keep small values exact +PASS matte records expose their derivable kind +PASS ordinary records expose image kind +PASS a matted cutout's photograph is a source +PASS its rendered (cut) picture is derived and hidden +PASS its matte mask is derived and hidden +PASS an unmatted cutout's picture is a source +PASS a stored but unreferenced picture still shows +PASS stored order is kept +PASS an id that is anyone's source stays visible +PASS orphaned rendered and matte assets stay hidden +PASS scene lineage names both pipeline outputs for backfill +PASS an id that is also a source is never backfilled as derived +PASS hostile scenes yield an empty backfill set +PASS a cutout without lineage fields counts as unmatted +PASS nonsense scenes and ids are tolerated +PASS no stored ids means an empty shelf + +verify-asset-shelf: all green + +RUNNING test/verify-auto-color.mjs +PASS same id yields the same hex on repeated calls +PASS golden pin: cube +PASS golden pin: cube-2 +PASS an empty id still yields a valid color +PASS 40 sequential ids yield 40 distinct hexes +PASS every hex is lowercase #rrggbb +PASS the hash is 32-bit FNV-1a +PASS App maps a display-only autoColor marker, never the authored color +PASS cutouts are excluded from the mode +PASS the toggle persists under its own key +PASS the toggle reports its state +PASS the viewport renderers consume the marker with authored fallback +PASS auto-colored surfaces use the flat solid-view response, not the lit clay one +PASS the plan board agrees with the viewport +PASS the inspector shows the derived hex without swapping its data source +verify-auto-color: all checks passed + +RUNNING test/verify-blocking-depth.mjs +PASS the deck carries a grid texture +PASS the grid tiles in metres across the whole deck +PASS the grid wraps rather than stretching once +PASS the texture is released with the room +PASS no floating grid helper is layered over the deck +PASS the deck is lit rather than flat-filled +PASS the deck receives shadow +PASS the key light casts one shadow +PASS the shadow frustum covers the blocking area, not the whole 500 m deck +PASS the renderer actually has shadows enabled +PASS the subject casts its own shadow +PASS the capture fog stays inside the camera far plane +PASS the capture pushes the fog back and restores it +PASS the viewport fog is untouched +PASS the restore happens after the render, in a finally +PASS the prompt explains the grid encodes distance +PASS the prompt forbids drawing the grid +PASS the grid joins the do-not-reproduce list +PASS sheet mode still carries the grid instruction +PASS sheet mode drops the preset subject and environment text +blocking-frame depth checks PASS + +RUNNING test/verify-burn-in.mjs +verify-burn-in: all checks passed + +RUNNING test/verify-bvh-cskel27.mjs +PASS the real SAM-3D-Body BVH parses: 514 frames, 52 joints, 30 fps +PASS conversion fills the exact npz motion shape +PASS locals stay unit rotations through the clip +PASS the take moves CozyClay's canonical body, not SAM's per-clip bone lengths +PASS feet pin to the floor; drift is gone and only brief jump air survives +PASS the foot lock corrects slide, never throws the leg across the floor +PASS occlusion gaps are pinned; jumps and travelling steps never are +PASS orientation is upright throughout — no axis flip in the retarget +PASS the offline-pipeline take stays smooth (wrist accel < 3.5 cm) and grounded +PASS the arm One-Euro quiets the guard-band wrists without collapsing punch speed +PASS leg smoothing and the shaped floor guard cut foot tremble without killing footwork +PASS the shaped floor correction is never below the raw deficit — no sole can sink +PASS a 60 fps clip converts with the same jitter per second as the 30 fps one +PASS a truncated BVH fails closed by name +verify-bvh-cskel27: all checks passed + +RUNNING test/verify-camera-block.mjs +PASS legacy blocks carry no crane +PASS legacy start/end marks migrate to endpoint points +PASS point cranes normalize sorted with pinned endpoints +PASS malformed crane values normalize to null +PASS crane marks clamp to the 0.1 m floor +PASS out-of-range and duplicate ts are repaired +PASS point count caps at 8 +PASS a rail block is craned by default (flat at the follow height) +PASS updateCameraBlock round-trips a crane patch +PASS unrelated patches keep the crane +PASS an explicit null patch levels the crane +PASS deleting the rail deletes its crane +PASS blocks never share crane objects +all camera-block checks PASS + +RUNNING test/verify-camera-follow.mjs +PASS viewport framing measures planar distance +PASS viewport framing measures physical lens height +PASS viewport framing converts tilt to automatic-aim offset +PASS a high camera preserves its authored pitch through follow measurement and replay +PASS captured camera height has no six-metre ceiling +PASS viewport framing records a camera placed in front of the subject +PASS free follow emits one sample per subject frame +PASS free follow holds the requested distance exactly +PASS free follow keeps the requested height +PASS free follow opens already composed (no slide-in at frame 0) +PASS free follow trails behind the travel direction +PASS front-authored follow holds the requested distance +PASS front-authored follow stays in front of a +Z walk +PASS corner: damped aim never snaps (max yaw step under 3°/frame) +PASS corner: follow maintains exact planar distance +PASS accelerating follow maintains exact planar distance +PASS a stopped subject brings the camera to rest +PASS the track is deterministic (same input, same output) +PASS pre-move frames already face the coming walk (probe, not snap) +PASS RDP collapses pointer jitter on a straight stroke +PASS RDP keeps the corner point +PASS rail measures a plausible arc length +PASS railPoint clamps at both ends +PASS degenerate strokes yield no rail +PASS rail follow emits one sample per subject frame +PASS the camera never leaves the rail +PASS rail follow advances through the authored rail +PASS the dolly respects its speed cap +PASS response 0.1 pull-out reaches the authored far end at 4 m/s +PASS response 0.1 pull-out never travels backward at 4 m/s +PASS response 0.1 pull-out respects the 4 m/s cap +PASS response 0.1 pull-out reaches the authored far end at 1 m/s +PASS response 0.1 pull-out never travels backward at 1 m/s +PASS response 0.1 pull-out respects the 1 m/s cap +PASS head mode opens at exactly s=0 +PASS head mode frame 0 is exactly the rail head +PASS nearest mode preserves automatic opening placement +PASS dolly cap 0.5 m/s binds before every integration +PASS dolly cap 2 m/s binds before every integration +PASS dolly cap 6 m/s binds before every integration +PASS a slower dolly advances less over the same frames +PASS dolly speed does not change sample count +PASS capped rail tracks remain deterministic +PASS zero pitch offset preserves automatic pitch +PASS +10° pitch offset is exact +PASS -10° pitch offset is exact +PASS pitch offset leaves yaw, position and rail travel unchanged +PASS pitch safety clamp stays within ±85° +PASS pitch input covers the full possible authored offset range +PASS height changes rig position independently of pitch offset +PASS a too-short rail pins the dolly at its end +PASS panning from a pinned dolly still never snaps +PASS rail track is deterministic +PASS flat rail holds the follow height +PASS crane opens at its start height +PASS crane lands at its end height +PASS crane passes between its marks mid-move +PASS a descending crane never climbs back +PASS equal crane marks reproduce the flat rail +PASS craneHeightAt hits every mark exactly +PASS craneHeightAt clamps outside the arc +PASS the profile never overshoots its marks (monotone) +PASS legacy two-mark cranes evaluate as the straight lerp +PASS a point crane opens at its first mark +PASS a point crane dips toward its low mark +PASS a point crane never undershoots the low mark +PASS a point crane rises back toward its final mark +all camera-follow checks PASS + +RUNNING test/verify-camera-move.mjs +PASS fullFrame is the default sensor +PASS the four public sensor ids are stable +PASS 4:3 uses the full fullFrame height +PASS 16:9 crops fullFrame to 20.25mm high +PASS 2.39:1 crops fullFrame to width/aspect +PASS super16 1:1 focal/FOV round-trip +PASS super16 1.7777777777777777:1 focal/FOV round-trip +PASS super16 2.39:1 focal/FOV round-trip +PASS super35 1:1 focal/FOV round-trip +PASS super35 1.7777777777777777:1 focal/FOV round-trip +PASS super35 2.39:1 focal/FOV round-trip +PASS fullFrame 1:1 focal/FOV round-trip +PASS fullFrame 1.7777777777777777:1 focal/FOV round-trip +PASS fullFrame 2.39:1 focal/FOV round-trip +PASS 65mm 1:1 focal/FOV round-trip +PASS 65mm 1.7777777777777777:1 focal/FOV round-trip +PASS 65mm 2.39:1 focal/FOV round-trip +PASS t=0 reproduces framing A exactly +PASS t=1 reproduces framing B exactly +PASS orbit midpoint keeps the full 3m radius +PASS 90° same-radius move classifies as orbit +PASS orbit phrase reports the arc +PASS shortestArc wraps 170°->-170° to +20° +PASS interpolation crosses 180°, not the long way +PASS straight approach classifies as push-in +PASS push-in phrase names both shot sizes +PASS the reverse classifies as pull-out +PASS vertical rise classifies as crane up +PASS identical framings classify as static +PASS static phrase matches composePrompt's default +PASS rotation-only move classifies as a pan +PASS pitch-only move classifies as a tilt up +PASS distance x2 + focal x2 classifies as dolly-zoom +PASS dolly-zoom kept subject size within drift budget +PASS focal-only change classifies as zoom in +PASS lens interpolates in millimetres (20->80 mid is 50) +PASS lens interpolation keeps one cropped filmback (20->80 mid is 50) +PASS filmback does not rename the same FOV zoom +PASS a stored 2.39:1 shot keeps its authored vertical framing +PASS 2.39:1 reinterprets the lens against its cropped gate +PASS easeInOut pins both ends +PASS easeInOut midpoint is halfway +PASS easeInOut starts gently +PASS move slate reads A → B · MOVE +PASS no keys samples null +PASS before the first key holds its framing +PASS after the last key holds its framing +PASS landing on a key reproduces it exactly +PASS first segment midpoint keeps the 4m arc +PASS second segment midpoint halves the radius, holds the azimuth +PASS a single key holds everywhere +PASS one segment renders exactly like moveSlate +PASS sequence slate chains segments through the middle framing +PASS sequence phrase chains in time order +PASS one segment phrase is the segment phrase + +All camera-move checks passed + +RUNNING test/verify-camera-rail-schedule.mjs +PASS the minimum inclusive clip length is 10 frames +PASS default range is the whole timeline +PASS default range on a 10-frame timeline is exactly 0..9 +PASS no default range below 10 frames +PASS no default range for a non-finite length +PASS a redrawn rail keeps an authored range +PASS a redrawn rail drops stale OFF for the default range +PASS a first rail draw gets the default range +PASS two finite points make a rail usable +PASS null, empty and one-point rails are unusable +PASS garbage points make a rail unusable +PASS legacy-derived resolves to the whole timeline +PASS an authored range resolves as range +PASS explicit off resolves as off, never legacy +PASS no rail geometry resolves as none +PASS a timeline below 10 frames resolves as too-short even with an authored range +PASS a too-short timeline beats off +PASS the 10-inclusive-frame boundary resolves as a range +PASS a stored sub-10 range is extended to the minimum +PASS a range past the end is clamped into the timeline +PASS a corrupt inverted range folds to off +PASS activeAt is inclusive on both ends +PASS activeAt excludes the frames just outside +PASS activeAt covers every frame of a legacy clip +PASS activeAt is false for off/too-short/none schedules +PASS activeAt is false for a non-finite frame +PASS shrink clamp pulls the end in +PASS shrink clamp keeps the 10-frame minimum by pulling the start +PASS re-clamping after growth never resurrects the old range +PASS a 10-frame range is preserved, not grown +PASS a degenerate 1-frame range is extended to the minimum +PASS a 10-frame timeline clamps any range to the full timeline +PASS no clamp possible below 10 frames +PASS hostile ranges clamp to null +PASS move shifts the clip by delta +PASS move preserves length at the start edge +PASS move preserves length at the end edge +PASS a clip that cannot fit cannot move +PASS hostile ranges cannot move +PASS end resize clamps to the 10-frame minimum +PASS end resize clamps to the timeline end +PASS start resize clamps to the 10-frame minimum +PASS start resize clamps to the timeline start +PASS end resize cannot keep the minimum near the end +PASS start resize cannot keep the minimum near the start +PASS an unknown edge is rejected +PASS follow off: the rail never owns +PASS no usable geometry: the rail never owns +PASS off schedule: no rail owner +PASS too-short schedule: no rail owner +PASS none schedule: no rail owner +PASS inside the range the rail owns +PASS just outside the range the keys own +PASS legacy: the rail owns the whole timeline +all camera-rail-schedule checks PASS + +RUNNING test/verify-cuts.mjs +optional Shot overlay model verified + +RUNNING test/verify-error-boundary.mjs +PASS main mounts App inside the boundary +PASS boundary implements getDerivedStateFromError and componentDidCatch +PASS boundary offers a named reload recovery +PASS workspace layout write is try/catch guarded +all error-boundary checks PASS + +RUNNING test/verify-footage-bridge.mjs +PASS yt-dlp progress lines parse to a 0..1 ratio; other lines are null +PASS ffmpeg time= lines parse against the clip length, capped to 1 +PASS probe lines split duration, native rate and title; lives and garbage are null +PASS a rate over the ceiling comes down to the ceiling or below +PASS a source at or under the ceiling keeps its own rate — no upsampling +PASS an exact multiple halves rather than resample to a foreign rate +PASS the normalize pass carries the capped rate and nothing that alters duration +PASS ffprobe stream info parses to a real rate and length, or to null +PASS a probed platform rate is reported to the app already capped +verify-footage-bridge: all checks passed + +RUNNING test/verify-g006-css.mjs +PASS source offer hover uses the canonical foreground token +PASS hierarchy sidebar has no unresolved hierarchy height row +all G006 CSS checks PASS + +RUNNING test/verify-gizmo-claim.mjs +PASS GIZMO_LAYER literal matches dualview.jsx +PASS the cage line is actually under the sphere-bound ray (default 1 m Line slop) +PASS a press aimed at another object's body is NOT claimed by furniture +PASS a press on a marked handle proxy IS claimed +PASS the drawn arrow is hit but claims nothing +PASS the key-light sun claims through its group +PASS a marked GROUP claims for its child mesh +PASS pathAxis="y" claims its press +PASS pathIndex=0 claims its press +PASS craneAxis="x" claims its press +PASS craneIndex=0 claims its press +PASS the camera ghost never claims +PASS an unmarked mesh never claims +PASS every register* stamps the claim mark (axis, plane, corner proxies) +PASS the selection handler vetoes through claimsPress, not raw layer hits +OK verify-gizmo-claim + +RUNNING test/verify-grid-view.mjs +verify-grid-view: all checks passed + +RUNNING test/verify-hierarchy.mjs +PASS hierarchy IDs are unique +PASS Scene is the single hierarchy root +PASS Camera belongs directly to Scene +PASS Characters group owns Character 1 +PASS workflow nodes stay out of the scene tree +PASS Rig belongs to Character 1 +PASS Rig exposes five human-readable body groups +PASS torso, head, shoulders, elbows, hands, knees, and feet are directly selectable +PASS a rig node id parses into its row and bone token +PASS the rig ROOT parses with the bare rig token +PASS extra cast rows parse through their character id +PASS non-rig ids never parse +PASS rigSubtree namespaces every node under the row +PASS two rows' subtrees never collide +PASS Environment stays at the Scene level +PASS Props stay at the Scene level +PASS tree depth stays scannable +PASS attached object nests under its character row +PASS attached rows keep the object row id and kind +PASS attached object leaves the Props list +PASS bone attach carries the bone in the label +PASS root attach keeps the plain object name +PASS the second character carries its own attachments +PASS the second character's rig expands with its body groups +PASS extra cast rows carry namespaced rigs too +PASS attaching to Character 1 does not displace the rig subtree +PASS unknown characterId falls back to Props +PASS hidden characterId falls back to Props +PASS a null attach is an ordinary prop +PASS the fallback label carries no bone +PASS hidden characters still own no row +PASS grouped props still nest under their parent +PASS orphaned props still surface at the Props top level +PASS an attached object never nests under a prop +PASS children of an attached object surface in Props +PASS no nesting is built under an attached row +PASS bone keys read as English rig labels +PASS a missing bone yields no label +PASS row drags carry the private hierarchy MIME +PASS dragstart publishes the row id as a move +PASS only object rows are draggable, and never mid-rename +PASS the panel accepts a reparent prop +PASS canDrop gates both the highlight and the drop +PASS the drop calls back exactly once per drop +PASS row drops reuse the existing data-drop styling +PASS the dragged row id survives Chrome's blank dragover payload +PASS a Files drag keeps its original handlers +PASS row handlers never swallow a picture drop +PASS panel exposes onSceneSelect +PASS panel exposes onSceneCreate +PASS panel exposes onSceneDuplicate +PASS panel exposes onSceneRename +PASS panel exposes onSceneDelete +PASS scene selector is separate from entity tree +PASS scene rename supports double-click +PASS scene deletion requires a second deliberate click +PASS active scene clicks do not repeat selection callbacks +PASS last scene deletion is protected +PASS entity tree root follows the active scene name +PASS rig.head routes to its exact IK control +PASS rig.chest routes to its exact IK control +PASS rig.leftShoulder routes to its exact IK control +PASS rig.rightShoulder routes to its exact IK control +PASS every IK shutdown clears mode and stale control focus +PASS App wires scenes +PASS App wires activeSceneId +PASS App wires onSceneSelect +PASS App wires onSceneCreate +PASS App wires onSceneDuplicate +PASS App wires onSceneRename +PASS App wires onSceneDelete +PASS App seals shots inside the active Scene +PASS App persists the unified Scene document +PASS Scene switch snapshots outgoing work first +all hierarchy checks PASS + +RUNNING test/verify-history.mjs +PASS a push of the current present creates no entry +PASS undo on an empty history is null +PASS redo with nothing undone is null +PASS canUndo reads the past stack +PASS canRedo reads the future stack +PASS a push advances the present and keeps the past +PASS undo/redo round-trips by reference +PASS undo is null once the past is empty +PASS redo is null once the future is empty +PASS a push after an undo clears redo +PASS history is capped at HISTORY_LIMIT +PASS the cap keeps the most recent entries and drops the oldest +PASS end without begin is a no-op +PASS settle without begin is a no-op +PASS beginTransaction returns the issued token +PASS isCurrentTransaction recognises only the open token +PASS a nested begin settles the previous owner exactly once +PASS a stale token cannot close a newer transaction +PASS settleTransaction returns the retired token and owner +PASS a cancel that tries to close its own transaction is inert +PASS the token is retired before cancel runs +PASS an atomic no-op creates no entry +PASS an atomic edit pushes the post-change present +PASS the present never diverges from the store (atomic) +PASS a streamed interaction is exactly one entry +PASS the committed entry is the last applied value +PASS a cancelled interaction restores before and pushes nothing +PASS the present never diverges from the store (cancelled) +PASS end with a stale token returns false and changes nothing +PASS a stale apply after settle is dropped +PASS an interaction with no net change creates no entry +PASS a second begin settles the first drag into exactly one entry +PASS a stale end cannot close the newer transaction +PASS the newer transaction still lands after the stale end +PASS the newer transaction closes cleanly +PASS final depths and present are exact +PASS one undo restores B1 +PASS a second undo restores the original array by reference +PASS settle runs the owner's cancel exactly once +PASS a close attempted from inside cancel is inert +PASS undo restores the previous present +PASS canUndo and canRedo read the live stacks +PASS redo returns to it +PASS undo at the bottom is null +PASS redo at the top is null +PASS undo mid-interaction commits then undoes that interaction +PASS an atomic mutation mid-interaction settles first +PASS undo returns null with empty history and mutates nothing +PASS the present never diverges from the store (s1) +PASS the present never diverges from the store (s2) +PASS the present never diverges from the store (s3) +PASS the present never diverges from the store (s4) +PASS the present never diverges from the store (s5) +PASS the present never diverges from the store (s6) +PASS the present never diverges from the store (s7) +PASS the present never diverges from the store (s8) +PASS the present never diverges from the store (sN) +PASS the present never diverges from the store (sU) +PASS the present never diverges from the store (sM) +PASS the present never diverges from the store (sZ) +all history checks PASS + +RUNNING test/verify-image-pose.mjs +PASS imageFrames yields one frame at t=0 +PASS imageFrames names a decode failure +PASS createPoseDetector honours IMAGE mode +PASS createPoseDetector keeps the VIDEO contract +PASS collectLandmarkTrack accepts a still supply +PASS bakePoseFrame bakes a single measured frame +PASS bakePoseFrame names an empty still +image-pose: OK + +RUNNING test/verify-kimodo-cskel27.mjs +PASS output arrays have the cskel27 npz shapes and are finite +PASS mapping covers cskel27 and somaskel77 is 77 unique joints +PASS leg chain is mapped across the somaskel77 name shift +PASS FK over the emitted locals reproduces every source global (worst 2.40e-7) +PASS unmapped cskel27 joints emit identity locals +PASS emitted locals are proper rotations +PASS clip is floor-aligned on its lowest foot sample +PASS floor correction is one rigid shift, so vertical motion survives +PASS bad shapes and incomplete source skeletons are rejected +PASS a Kimodo npz round-trips through the reader with its shapes intact +PASS disk path and in-memory path agree exactly +PASS the converted take encodes and writes as a cclay motion npz +PASS a non-SOMA skeleton is rejected with an actionable message +PASS prompt joining matches kimodo_gen's period segmentation, or refuses +PASS continuity metric localises the worst seam +OK verify-kimodo-cskel27 + +RUNNING test/verify-kimodo-edit.mjs +PASS the edit manifest parses into a resolved plan +PASS context anchors are taken from the source on both sides of the range +PASS a context anchor carries the source motion's own pose at that frame +PASS a range touching frame 0 clamps its anchors instead of going negative +PASS malformed manifests are refused by name +PASS an edit keyframe outside the edited range is refused +OK verify-kimodo-edit + +RUNNING test/verify-kimodo-effector.mjs +PASS track -> effector map covers both hands, both feet and their mid-joints +PASS a single hand track emits exactly one left-hand entry +PASS an effector entry carries buildFullBodyConstraints' payload unchanged +PASS two effectors emit two entries over one shared payload +PASS elbow/knee edits are constrained through their chain's effector +PASS duplicate and overlapping tracks collapse to one entry per chain +PASS a track with no effector throws and is never widened to fullbody +PASS an edit with no tracks is refused rather than guessed at +PASS no poses produces no constraints, same as the fullbody builder +PASS malformed poses propagate buildFullBodyConstraints' own errors +PASS authored rotations and the grounded root survive into the effector entry +PASS emitted constraints match Kimodo's end-effector schema and round trip +OK verify-kimodo-effector + +RUNNING test/verify-kimodo-pose.mjs +PASS matrix -> axis-angle round trips, including 180 degrees and identity +PASS a pose emits one fullbody entry shaped [T,77,3] with [T,3] roots +PASS an authored rotation lands on the correct somaskel77 joint across the leg name shift +PASS unauthored and unmapped joints emit zero axis-angle +PASS a pose is grounded before it becomes a constraint, so float cannot compound +PASS multiple pinned poses share one fullbody entry, index-aligned +PASS no poses produces no constraints +PASS bad frames, wrong joint counts and non-rotations are refused by name +PASS emitted constraints match Kimodo's documented fullbody schema +OK verify-kimodo-pose + +RUNNING test/verify-kimodo-preserve.mjs +PASS an empty edit list preserves the whole take +PASS a single edit zeroes its span and tapers symmetrically on both sides +PASS 20 fps app ranges scale onto 30 fps generation frames +PASS overlapping edits combine by minimum weight +PASS an edit touching frame 0 has no leading shoulder and no out-of-range frames +PASS an edit touching the last frame frees it and never ramps past the clip +PASS a range that rounds onto a single generation frame is not dropped +PASS the boundary tapers: max per-frame delta stays well under a step +PASS influenceRadius 0 gives the documented hard edge (caller opt-out) +PASS the emitted mask matches the C1 v1 schema and round trips through JSON +PASS preserveMaskStats counts free, ramp and preserved frames +PASS every malformed edit list, clock and radius is refused by name +PASS TRACK_GROUPS matches the frozen track -> group map +PASS a tracked edit frees only the mapped group and leaves the top level preserved +PASS hips frees torso AND root; repeated tracks collapse by minimum +PASS a group array is the union of its own ranges and every whole-body range +PASS a plain untracked call still emits the exact v1 shape +PASS wideWeights is the same Gaussian at a wider radius and is never narrower +PASS an empty edit list emits the v1 all-ones mask with no width schedule +PASS rootFreeMask hands the whole clip's root to the drawn path +PASS preserveMaskStats reports per-group free/ramp/preserved counts +PASS unknown track ids, malformed tracks lists and bad wide scales are refused by name +PASS the emitted mask matches the C1 v2 schema and round trips through JSON +OK verify-kimodo-preserve + +RUNNING test/verify-kimodo-runner.mjs +PASS Kimodo is the default backend when a Kimodo host is configured +PASS CCLAY_MOTION_BACKEND=kimodo selects the Kimodo runner +PASS the removed ARDY backend name is refused +PASS an unknown backend name is refused +PASS the Kimodo runner exposes every method bridge.mjs calls +PASS sequenceCommand builds a spawnable command whose done line the bridge can parse +PASS root waypoints are forwarded as --root-2d instead of refused +PASS base clips still refuse by name rather than generating a wrong take +PASS editCommand builds a spawnable command whose done line the bridge can parse +PASS pinned poses are forwarded as --pose instead of refused +PASS singleCommand degenerates to a one-segment sequence +PASS the Kimodo backend refuses to start without a configured box +OK verify-kimodo-runner + +RUNNING test/verify-kimodo-setup.mjs +OK verify-kimodo-setup + +RUNNING test/verify-kimodo-waypoints.mjs +PASS three waypoints map to one root2d entry in canonical space +PASS an all-null heading path omits global_root_heading +PASS an empty or missing path produces no constraints +PASS a single waypoint only constrains when it carries a heading +PASS a frame beyond the clip clamps to the last frame instead of vanishing +PASS frames colliding after rescale are deduped and stay index-aligned +PASS a non-ascending path is refused by name +PASS NaN positions, negative frames and non-finite headings are refused +PASS emitted constraints match Kimodo's documented root2d schema +PASS segmentBoundaries (unused by default) re-expresses constraints per segment +PASS a boundary between waypoints samples the path for its segment origin +PASS a single-segment take keeps whole-clip anchoring +PASS a constraint on a segment boundary is nudged clear of the transition window +PASS a constraint outside the transition window keeps its authored frame +PASS a single-segment take is unaffected by the transition-window rule +PASS densifyStride lays interpolated constraints so segment 1 sees the path +PASS densification stops at the last authored waypoint +PASS densifyStride defaults off and 0 is a no-op +PASS densified frames borrow the next authored heading and stay aligned +PASS a negative or fractional densifyStride is refused by name +OK verify-kimodo-waypoints + +RUNNING test/verify-korean-ui.mjs +all Korean option locale checks PASS + +RUNNING test/verify-layout.mjs +PASS workspace layout persists across reloads +PASS sidebar width has a pointer resize path +PASS frame monitor height has a pointer resize path +PASS inset view has a diagonal resize path +PASS inset view collapses to its tag pill +PASS collapse toggle lives inside the tag strip +PASS the tag drags the inset in both states +PASS inset collapse state persists with the workspace layout +PASS collapsed inset skips its render pass +PASS resize handle hides while collapsed +PASS resize grip is visible on the clay viewport +PASS Top-View plan zooms with the wheel over the inset +PASS plan zoom persists with the workspace layout +PASS zoom shrinks the ortho extent, not the pane +PASS demand loop wakes on mount and model commit +PASS double-click no longer swaps Scene and Top-View +PASS Scene toolbar exposes shot preset, aspect, FOV, recenter, and Top-View controls +PASS Scene and PlayView tools share one horizontal title bar +PASS PlayView toolbar exposes framing readouts, playback, and recording +PASS letterbox bars are painted in the editor's tone, not the sky +PASS the scene draw is scissored to the image so the bars survive it +PASS selected aspect reaches the shot renderer +PASS video and still capture use the selected output dimensions +PASS double-clicking the inset body folds it +PASS the tag strip works like a foldout header +PASS one fold per gesture, even on a double-click +PASS body double-click skips tag-started gestures +PASS workspace has a dedicated left hierarchy window +PASS inspector is always visible beside the scene +PASS legacy hierarchy/inspector splitter is removed +PASS bottom window exposes only Animation and Assets tabs +PASS ARDY status stays inline without a console history surface +PASS the sidebar has no mode tabs left +PASS the legacy ARDY motion inspector card is removed +PASS the camera owns the lens controls +PASS the legacy image/video generation prompt inspector is removed +PASS the prompt does not leak onto selections that do not own it +PASS selection routing is derived once, not repeated per foldout +PASS an unowned selection explains itself instead of showing a blank column +PASS the heavy motion pipeline starts collapsed +PASS Prompt Block panel exposes one batch generation action +PASS new sessions start without prompt blocks +PASS new sessions start with an empty motion prompt +PASS recording captures the clean render without a frame stamp +PASS recording uses current motion content instead of a stale timeline tail +PASS recording uses WebCodecs with explicit timestamps and frame count +PASS recording no longer uses MediaRecorder or a wall-clock capture loop +PASS pre-motion timeline initializes to 15 seconds +PASS motion preview stays at native 1x speed +PASS timeline cadence and readout expose native preview speed +PASS legacy greeting demo migration is removed +PASS batch generation spans through the final block frame +PASS batch generation forwards all prompt clips +PASS normal motion generation excludes the prompt block schedule +PASS unedited batch blocks use one unpinned autoregressive schedule +PASS the pose pin is an explicit, off-by-default choice +PASS the pinned pose can be placed anywhere in the clip +PASS the placement names the exact frame it will pin +PASS a collapsed panel can be revealed from outside the Inspector +PASS selecting or adding a prompt block reveals the panel that edits it +PASS a schedule conflict is reported instead of silently dropping the pose +PASS IK-edited blocks use the motion edit session +PASS IK regeneration inherits loaded clip duration +PASS motion edits send only tracked pending joints +PASS successful motion edits commit and clear pending IK +PASS pending IK clears only after exact commit verification +PASS failed key verification leaves pending IK intact +PASS inactive live motion and its prompt clips commit in one character update +PASS a deleted inactive motion target cannot clear the active editing motion +PASS a B completion after selection changes retains B ownership through decode and rig waits +PASS an active B receives its own completion after an A to B selection interleaving +PASS individual block generation action is removed +PASS Prompt Block edits stay synced with ARDY input +PASS desktop stage fills the remaining viewport +PASS sidebar width is bounded +PASS timeline height is bounded +PASS timeline IK text controls size to their labels +PASS the active character exclusively owns the frame zero root start +PASS root guidance sends the aligned, densified path to ARDY +PASS a root path and a prompt schedule are sent together, judged per block +PASS ARDY bridge health recovers after the sidecar starts late +PASS disabled Prompt Block generation explains the exact missing prerequisite +PASS generated motion anchors frame zero at the active character +PASS returned playback has no CozyClay root coordinate warp +PASS Top-View root path draws from Subject 1 without a duplicate marker +PASS Top-View characters use their real meshes without covering hex pucks +PASS Top-View uses saturated role colors and readable path widths +PASS Top-View shows ARDY player endpoints and direction while composing a rail +PASS only generated ARDY motion drives the player guide +PASS Subject position uses the same X Y Z numeric row as scene objects +PASS the character gizmo no longer caps lift at 4 m +PASS the head scrub is floored at min with no upper clamp +PASS a soft-max readout shows the true stored value past the track +PASS resize handles opt out on compact layouts +PASS a character owns a Video capture foldout without a legacy ARDY card +PASS ingest and extraction reach the ported core modules +PASS extraction routes to the GPU box when the bridge is up and the browser otherwise +PASS every named ingest failure is a message in both locales +PASS a character's stature comes from its own take and is applied on the world transform +PASS each extra take carries its own stature, never the active character's +PASS the response person scale is only a fallback for an npz that stores none +PASS stature survives the save: only the session clip is stripped from the stage +PASS extra takes land on their OWN layer, not through the editing buffer +PASS trim composes from the per-character full-take map +PASS a cut take drops its source url and clears the IK keys authored on the old frames +PASS a browser-baked take is trimmable too +PASS the timeline receives the active take and both trim handlers +PASS a deleted character takes its full take with it +PASS the last inspector section may grow but never shrink below its content +PASS the last inspector section does not clip what it should hand upward +PASS the inspector scroll container still owns the scrolling +all resizable workspace checks PASS + +RUNNING test/verify-line-edit-draw.mjs +strokeToCurve — arc-length parameterization + ok a drawn stroke replaces the interior with points sampled along its LENGTH + ok drawing speed is irrelevant: slow-then-fast lands the same curve as uniform + ok a two-sample stroke is a straight line, evenly spaced in space + ok a stroke drawn backwards is not the same curve as one drawn forwards +strokeToCurve — the pinned ends + ok the first and last PINNED_CURVE_ENDS points come back BY REFERENCE + ok the pinned ends stay on the ORIGINAL trail even when the stroke is far away + ok pinnedEnds is honoured when overridden, and can never eat the whole curve +strokeToCurve — the no-op click + ok fewer than 2 samples is a click, not a stroke + ok a stroke of zero length is a click + ok minLength refuses a stroke below the caller's threshold + ok a no-op refusal is null, never a mutated or emptied base curve + ok a garbage stroke is refused rather than producing NaN points + ok a base curve that is not a curve is refused +strokeToCurve — null gaps + ok frames with no image stay null, exactly as dragCurve leaves them + ok a null gap does not shift the arc-length mapping of the points around it + ok pinned ends that are themselves null stay null +strokeToCurve — the drawn curve is an ordinary curve + ok drawing twice: the second stroke wins + ok a drag after a draw refines the drawn curve like any other + ok a drawn curve goes on the wire like a dragged one + ok a stroke drawn off the image is refused by the wire gate, not silently clamped +matchStrokeWindow — the stroke picks its own frames + ok a stroke drawn over a stretch of the trail matches THAT stretch + ok a stroke drawn end-to-start matches the SAME window, and reports reversed + ok a bunched trail falls back to a window of about a second, centred on the match + ok the separation test is in PIXELS: the same trail on a huge pane IS matchable + ok a bunched match at the very start of the clip slides in rather than going negative + ok a clip shorter than the bunched window is clipped, not overflowed + ok a trail with nothing on screen matches nothing — the caller keeps its range + ok matching is in PIXELS, so a 16:9 pane does not bias the match vertically +curveWindow — slicing the clip curve by frames + ok the window is sliced by FRAME NUMBER, not by array index + ok a window the curve does not fully cover is refused, never silently shortened +seamEaseWeight / seamEaseFrames — the ease ramp + ok the ramp is 0 at both edges, 1 in the interior, and monotone in between + ok the ramp leaves the seam along the ORIGINAL slope — smoothstep, not linear + ok easeFrames is 15% of the window, floored at 2 and capped at 12 + ok an eased stroke glides out of the trail instead of teleporting to it +baseArcFractions — the take's own velocity profile + ok a fast-then-slow base maps the stroke unevenly, matching its arc-length profile + ok a stationary joint has no profile to preserve and falls back to uniform +strokeToCurve — reversed strokes + ok a reversed stroke lands the same curve as the forward one drawn over the same path +drawStrokeEdit — the whole gesture + ok drawing over a stretch of path returns that window, its base and an eased curve + ok the same stroke drawn backwards produces the same window and a forward-running curve + ok the timing comes from the BASE window, not from the stroke + ok no trail to match against falls back to the panel's range and still draws + ok a click through drawStrokeEdit is still a no-op, and refuses without a base + ok a drawn edit goes on the wire exactly like a dragged one + ok a fresh pull's committed range is the frames the falloff touched, not the clip + ok a tiny flick still asks for a workable window, clamped inside the clip + ok the wire slice pairs the touched window's own frames with the range + +46 checks passed + +RUNNING test/verify-line-edit-pins.mjs +pinsFrameRange — the pins choose the frames + ok a single pin gets a window of about 1.5 s centred on it + ok two pins span themselves plus half a second of context each side + ok a pin near the clip's edge slides its window in rather than off + ok a clip shorter than the window is clipped, and every pin still fits + ok no pins, or no clip, is no range +upsertPin — placing, replacing, capping + ok pinning the same frame twice REPLACES rather than stacking + ok pins come back sorted by frame however they were placed + ok over the cap the OLDEST pin is dropped, never the new one + ok garbage is refused by identity, so the caller's list survives +validation — the app gate and the wire gate agree + ok a well-formed pins payload passes both gates + ok pins and points2d together are refused — one edit is one gesture + ok a camera beside pins is refused rather than ignored + ok frames must be strictly ascending + ok a pin outside its own frameRange is refused + ok the count is bounded at both ends + ok malformed pin entries are named individually + ok a DRAWN payload still validates exactly as before — no message drifted + ok a pinned edit survives into a take's recipe and validates as a C10 replay entry +the clock — pins scale like the range that holds them + ok a pin claims the generation frames its app frame is REBUILT from + ok positions ride through the clock change untouched + ok brackets that overlap collapse to one row per frame, last pin wins + ok scalePins refuses rubbish rather than shipping it to the box +unprojectDeltaC6 — the screen drag becomes a 3D target + ok dragging by du/dv lands the point exactly under the pointer + ok the drag keeps the point's own DEPTH — it slides, it does not dolly + ok a farther point moves FARTHER in world for the same screen drag + ok a point at or behind the lens has no image plane to slide in +reprojectCurveWorld — the drifted ghost is world-anchored, not stale uv + ok an UNEDITED curve reprojects onto the trajectory under the new lens + ok an EDITED point shows its world displacement through the new lens + ok reprojection refuses rubbish and passes nulls through by slot +the job's pin -> row arithmetic, with the box stubbed out + ok runLineEditJob sends pins (not points2d, not a camera) on the generation clock + +30 checks passed + +RUNNING test/verify-live-control.mjs +all live control checks PASS + +RUNNING test/verify-matte-editor.mjs +PASS the editor paints something the size of the picture +PASS nothing is marked until someone asks — the photograph is the default +PASS with nothing painted there is nothing to apply +PASS the brush marks where it touched while the pointer is down +PASS letting go grows the cut out from the seeds — one dab takes the whole wall +PASS the growth stops at the subject +PASS what is marked is counted +PASS erasing gives the whole cut region back, not a disc of it +PASS erasing where nothing is selected changes nothing +PASS clear puts the photograph back +PASS auto-detect marks the background and leaves the subject +PASS a stroke on the subject cuts the subject out too +PASS running auto-detect again keeps what is already marked +PASS the mask is the picture's own size, and is exactly what is purple +PASS no tolerance travels with it — the growth already happened on screen +PASS the ground is painted around the picture, not around the canvas +PASS a click lands on the picture where the cursor is, not where the canvas starts +PASS a click on the ground beside the picture is ignored +PASS a scaled-up display still maps to the right pixel +PASS a click on a retina panel lands where the cursor is, not at twice the offset +PASS and the picture still fills the stage, rather than a quarter of it +PASS a freshly loaded picture has nothing to undo +PASS a cut is one undoable decision +PASS undo takes the whole cut back, not one dab of it +PASS redo puts it back +PASS redo runs out at the present +PASS editing after an undo drops the redo +PASS the buffer keeps 7 steps and no more +PASS the oldest state left is still a real one +PASS a picture opens at fit +PASS zooming reports itself +PASS a click at 2x lands on the pixel it is over, not on the one it would be at fit +PASS space-drag pans instead of painting +PASS the view actually moved +PASS fit puts the whole picture back +PASS zoom stops at the far end +PASS and never goes below the whole picture +PASS clear puts the photograph back +PASS a saved selection is restored onto the picture +PASS a selection from a different picture is refused +PASS disposing lets go of the pointer +all matte editor checks PASS + +RUNNING test/verify-matte.mjs +PASS the wall is gone +PASS the subject is untouched +PASS every subject pixel survives and every wall pixel does not +PASS the removal is reported as a count +PASS the source picture is not mutated +PASS a wall that shades top to bottom still goes in one piece +PASS the subject survives the gradient +PASS a subject standing on the frame edge is not eaten from its own feet +PASS the wall around it still goes +PASS clicking one background keeps the other side +PASS seeds come from the click, not the frame +PASS shrink eats the fringe of the kept region +PASS feather softens the boundary without hollowing the subject +PASS the trim box is exactly the subject +PASS cropping keeps the picture, not the margin +PASS a picture with nothing left has no box +PASS the cut comes back as its own asset +PASS the cut asset is trimmed to the subject +PASS the name follows the picture +PASS the height scale keeps the subject the size it was +PASS how much went is reported +PASS trimming can be declined +PASS a tolerance that eats everything fails with a readable reason +PASS a missing asset is refused +PASS the mask marks the wall and nothing else +PASS a stroke is a disc, and reports what it touched +PASS painting the same pixels again touches nothing +PASS a stroke at the edge is clipped, not wrapped +PASS a remove stroke cuts into the subject +PASS a restore stroke puts the wall back +PASS no paint layer leaves the mask alone +PASS a stroke painted on a half-size preview lands in the right place +PASS applying a mask writes the alpha and counts what went +PASS a hand-edited mask can be applied directly, strokes and all +PASS the glue takes a paint layer with it +PASS a mask made by hand is applied as-is, not re-grown +PASS a mask from another picture is refused +PASS a mask encodes to storable bytes +PASS a stored mask comes back exactly as it went in +PASS a fenced growth stays inside the fence +PASS a seed outside the fence grows nothing +PASS a one, two or three pixel structure survives the trim whole +PASS a mass still gets its rim taken, one pixel from each side +PASS without the trim the blended rim survives as a ring of half-wall +PASS with it the rim goes and the subject stays +all matte checks PASS + +RUNNING test/verify-mcp-invariants.mjs +SELF-TEST G009 detector rejected fixture: G009 tool run accepts raw execution argument code at mcp/server.mjs:1 +SELF-TEST G010 detector rejected fixture: G010 CRDT/OT import yjs at mcp/server.mjs:1 +SELF-TEST G012 detector rejected fixture: G012 unsupported protocol construct mcp/server.mjs:1 "tasks/get" +SELF-TEST G013 detector rejected fixture: G013 mcp runtime dependencies drifted from baseline: {"@modelcontextprotocol/sdk":"^1.30.0","ws":"^8.19.0","zod":"^3.25.0","drift":"1.0.0"} +SELF-TEST G014 detector rejected fixture: G014 non-loopback bind mcp/server.mjs:1 0.0.0.0 +SELF-TEST executable entrypoints detector rejected fixture: published entrypoint is not executable: mcp/server.mjs mode=644 +G009 MCP tools scanned=25: describe_scene, live_status, describe_shot, capture_frame, set_camera, frame_shot, add_character, place_character, remove_character, focus_character, place_object, group_objects, set_prompt_blocks, load_motion, generate_motion, update_object, remove_object, apply_batch, render_prompt, mark_camera_move, describe_camera_move, add_scene, switch_scene, open_project, save_project +G009 live handlers scanned=16: ping, describe, set_camera, add_character, update_character, remove_character, place_object, update_object, remove_object, load_scenes, group_objects, ungroup_objects, apply_batch, set_prompt_blocks, capture_frame, load_motion +G010 agent mutation path: MCP tool -> appliedLiveMutation/liveHub.command -> WebSocket cmd frame -> dispatchLiveFrame -> App liveHandlersRef React-state handlers +G010 source files scanned=175; CRDT/OT imports=0 +G012 MCP source files scanned=18; unsupported protocol constructs=0 +G013 root runtime dependencies=0; MCP baseline={"@modelcontextprotocol/sdk":"^1.30.0","ws":"^8.19.0","zod":"^3.25.0"} +G014 listen sites checked=18: tools/ardy/bridge.mjs:1699, tools/dev-full.mjs:38, bin/cozyclay.mjs:470, mcp/live-hub.mjs:316, mcp/server.mjs:2106, mcp/verify-http-origin.mjs:21, mcp/verify-live-batch.mjs:20, mcp/verify-live-capture.mjs:25, mcp/verify-live-editor-model.mjs:19, mcp/verify-live-motion-job.mjs:23, mcp/verify-live-motion-job.mjs:66, mcp/verify-live-p0.mjs:21, mcp/verify-live-p0.mjs:59, mcp/verify-live-port.mjs:15, mcp/verify-live-routing.mjs:18, mcp/verify-live-scene-parity.mjs:20, mcp/verify-live-scene-parity.mjs:225, mcp/verify-live.mjs:13 +G014 loopback client URL sites checked=37: src/ardy/motion-url.js:47 http://127.0.0.1:5180, src/live-control.js:6 ws://127.0.0.1, tools/ardy/run-local.mjs:27 http://127.0.0.1:9550, tools/ardy/runners/local.mjs:62 http://127.0.0.1:9550, tools/ardy/runners/remote.mjs:51 http://127.0.0.1:9550, tools/ardy/visual-qa.mjs:13 http://127.0.0.1:5180, tools/ardy/visual-qa.mjs:20 http://127.0.0.1:9222, tools/ardy/vq-car.mjs:5 http://127.0.0.1:9222, tools/ardy/vq-car.mjs:40 http://127.0.0.1:5180, tools/qa-browser.mjs:32 http://127.0.0.1:5180, tools/qa-browser.mjs:33 http://127.0.0.1, bin/cozyclay.mjs:366 http://127.0.0.1, bin/cozyclay.mjs:474 http://127.0.0.1, mcp/server.mjs:1311 http://127.0.0.1:5181, mcp/server.mjs:2022 http://127.0.0.1, mcp/server.mjs:2076 http://127.0.0.1, mcp/server.mjs:2082 http://127.0.0.1, mcp/server.mjs:2108 http://127.0.0.1, mcp/verify-http-origin.mjs:87 http://127.0.0.1, mcp/verify-http-origin.mjs:93 http://127.0.0.1, mcp/verify-live-batch.mjs:86 http://127.0.0.1, mcp/verify-live-batch.mjs:135 http://127.0.0.1, mcp/verify-live-capture.mjs:69 ws://127.0.0.1, mcp/verify-live-capture.mjs:132 http://127.0.0.1, mcp/verify-live-capture.mjs:158 http://127.0.0.1, mcp/verify-live-editor-model.mjs:87 http://127.0.0.1, mcp/verify-live-editor-model.mjs:136 http://127.0.0.1, mcp/verify-live-motion-job.mjs:73 http://127.0.0.1, mcp/verify-live-motion-job.mjs:77 ws://127.0.0.1, mcp/verify-live-p0.mjs:113 http://127.0.0.1, mcp/verify-live-p0.mjs:118 ws://127.0.0.1, mcp/verify-live-port.mjs:91 http://127.0.0.1, mcp/verify-live-port.mjs:93 ws://127.0.0.1, mcp/verify-live-port.mjs:99 ws://127.0.0.1:5184, mcp/verify-live-routing.mjs:71 ws://127.0.0.1, mcp/verify-live-scene-parity.mjs:52 ws://127.0.0.1, mcp/verify-live.mjs:18 ws://127.0.0.1 +Executable entrypoints checked=2: bin/cozyclay.mjs=755, mcp/server.mjs=755 +PASS MCP invariants G009, G010, G012, G013, G014 and executable entrypoints + +RUNNING test/verify-motion-edit.mjs +PASS an untouched take is one 1x segment with identity sampling +PASS cutting at the playhead creates a non-destructive source boundary once +PASS a 0.5x segment doubles only its own timeline duration +PASS fast and slow segments join without skipping their seam poses +PASS outer trim handles preserve cuts and speed inside the kept range +PASS speed accepts continuous 0.1x steps and rejects off-grid values +PASS rendering a slow segment creates a normal 24 fps clip for every consumer +PASS removing a segment deletes its span and keeps the source restorable +PASS stretch widths map onto the clamped 0.1x speed grid +PASS a slowdown moves pinned frames proportionally +PASS trailing pins shift by the segment's length delta +PASS a pure cut migrates nothing +PASS segment deletion drops orphaned pins and slides the rest +PASS remapFrameKeyMap migrates values and resolves collisions first-wins +PASS a retime and its reset round-trip every key home +PASS motionFrameToTimelineFrame inverts the sampling map +PASS App routes every segment-timing commit through migrateTimelinePins +verify-motion-edit: all checks passed + +RUNNING test/verify-motion-trail.mjs +all motion trail checks passed + +RUNNING test/verify-mp4-duration.mjs +MP4 media duration verified + +RUNNING test/verify-multimodel-ingest.mjs +PASS an absolute https address normalises +PASS a root-relative path is kept verbatim +PASS surrounding whitespace is trimmed, not rejected +PASS an empty address is refused by name +PASS a protocol-relative address is refused by name +PASS a javascript: address is refused by name +PASS a data: address is refused by name +PASS a non-string address is refused by name +PASS a YouTube watch page is refused as a page, not as a network fault +PASS a youtu.be short link is refused the same way +PASS a Vimeo page is refused the same way +PASS a direct file on an ordinary host is still accepted +PASS a host merely containing youtube in a label is not caught +PASS the label is the file, not the query +PASS progress is a ratio while the total is known +PASS progress never exceeds 1 when a server undercounts +PASS progress is null without a content length +PASS progress is null when the total is not a number +PASS frame count includes the frame at t=0 +PASS a 17.1333 s clip at 30 fps exposes 514 frames +PASS a zero-length clip still has one frame +PASS an unknown rate cannot fabricate frames +PASS a real 30 fps cadence measures 30 +PASS a real 24 fps cadence measures 24 +PASS no samples means the rate is reported as unmeasured +PASS stalled identical timestamps do not become an infinite rate +PASS the summary reports what was read, and flags an unmeasured rate +PASS the whole body is streamed, not sampled +PASS progress moves monotonically to 1 +PASS a length-less response reports null progress, never a guess +PASS a 404 is refused by status, not treated as empty footage +PASS an HTML fallback page is named as a wrong target, not a codec fault +PASS a JSON body is refused before it reaches the decoder +PASS an octet-stream video is still accepted +PASS a blocked cross-origin fetch is named, not swallowed +PASS an abort propagates as an abort, not as a failure +PASS a probe reports the decoded duration and size +PASS a probe measures the real rate and derives the frame count +PASS an undecodable container fails closed by name +PASS an unreadable duration never becomes a timeline length +PASS unreadable dimensions are refused by name +PASS a probe with no rVFC still reports an unmeasured rate +PASS an encoder-declared rate outranks measurement and is never a guess +PASS a source that never yields metadata times out by name +PASS a bridge download surfaces staged progress and the local address +PASS a bridge error event fails closed by its own name +PASS a stream that ends without footage never reads as success +PASS an HTTP refusal carries the bridge's named reason +PASS an unreachable bridge is named, not a network stack trace +PASS a 60 fps download comes back declared at the capped rate +PASS the timeline counts frames at the bridge's capped rate, not the source's + +failures: 0 + +RUNNING test/verify-object-path.mjs +PASS a path needs two points +PASS junk is not a path +PASS a stroke that never moves is not a path +PASS repeated drag samples collapse +PASS points are clamped into the room and above the floor +PASS point count is capped +PASS faceTravel defaults on, loop and extend default off +PASS a negative or absurd speed falls back to fill-the-timeline +PASS cumulative length walks the stroke +PASS frame 0 sits at the stroke's start +PASS the middle frame is halfway along +PASS the last frame lands on the end +PASS an explicit speed travels metres per second +PASS travel stops at the last point by default +PASS extend keeps travelling past the end +PASS loop wraps back onto the stroke +PASS faceTravel yaws toward the direction of travel +PASS faceTravel off leaves rotation to the author +PASS a lifted path carries the object's height +PASS an object without a path samples to nothing +PASS a frame beyond the take clamps +PASS a straight drag is two points +PASS a dog-leg keeps its corner +PASS no stroke exceeds the ceiling +PASS the stroke's ends survive simplification +PASS a stroke that is not a stroke yields nothing +PASS stroke points come in floor form, height authored later +PASS the strip has a travel track for a selected prop +PASS selecting a prop swaps the performer's lanes instead of joining them +PASS the prop's track carries the route controls +PASS the prop's track names its subject +PASS the prop's track folds duration into the graph header +PASS prop motion did not become a separate bottom tab +PASS the inspector still does not host the path controls +PASS the route takes a mid-path point on double-click +PASS an inserted point lands on the segment, so adding one never moves the route +PASS a point cannot be dropped on top of its neighbour +PASS the route refuses to grow past the point ceiling +PASS deleting the last removable point clears the route instead of leaving a stub +PASS a selected point owns Delete, so the prop survives the press +PASS the Top-View takes a mid-path point on double-click +PASS the board draws every route point, not just the ends +PASS route points outrank pucks when picking on the board +PASS dragging a board point is one undo entry +PASS the board edits the floor route and leaves height to the scene +PASS the strip teaches both gestures instead of leaving them to be found +PASS the speed slider paints a track and a thumb +PASS the slider has a real width +PASS speed reads as one unit rather than drifting apart +PASS a long prop name is clipped, not spilled +PASS the hint shares the controls row instead of owning an empty one +PASS the strip keeps the default height — no growth hack for the graph +PASS the speed editor is a real instrument: header, graph body, axes +PASS cuts are visible affordances, not hidden gestures +PASS the travel bar row is gone, folded into the graph +PASS the speed graph never display-clamps: the axis follows the data +PASS the axis never shrinks under the pointer mid-drag +PASS the dolly graph lives inside its shot card, not a floating row +PASS dragging a routed prop carries its route with it +PASS a lifted prop lifts its route by the same metres +PASS the route keeps its shape, so the prop still travels the same distance +PASS an authored route edit wins over the drag it arrives with +PASS a routed child is carried by its parent, route and all +PASS a standing prop is untouched by the carry +all object-path checks PASS + +RUNNING test/verify-offscreen-export.mjs +PASS 6-second range addresses and encodes exactly 144 frames +PASS two exports have identical per-frame SHA-256 pixel hashes +PASS WebCodecs chunks are muxed into an MP4 container +PASS browsers without an MP4-capable encoder fail by name +PASS H.264 export fails closed without AVC decoder metadata + +RUNNING test/verify-otio.mjs +OTIO cut list verified: 2 clips, 72 shot frames, 24 timeline fps + +RUNNING test/verify-package-signature.mjs +all official package signature checks PASS + +RUNNING test/verify-pose-extract.mjs +PASS coordinates are hip-centred, torso-scaled and Y-up +PASS pose-change peak selection emits ordinary frame/pose keys on the 20 fps grid +PASS all fitted cskel27 matrices are right-handed and orthonormal +PASS fitting preserves cskel27 limb lengths and follows the raised-arm direction +PASS low-visibility landmarks release their affected limb targets +PASS browser detector boundary shares one async frame path for upload and webcam +PASS baking fills the npz motion shape densely from sparse fitted samples +PASS holes hold the previous fitted frame, never a T-pose +PASS a leading detection hole backfills from the first fitted frame +PASS rootPos is the baked Hips trajectory +PASS a track with no fittable sample is refused by name +all video-to-pose-key checks PASS + +RUNNING test/verify-pose-library.mjs +pose library checks PASS (user-owned library, no shipped presets) + +RUNNING test/verify-pose-mirror.mjs +PASS the still path can name the heavy model without touching a URL +PASS the left/right pair table matches the MediaPipe Pose topology +PASS mirrorLandmarks is its own inverse +PASS a left-raised arm mirrors to a right-raised arm with x negated +PASS mirrorLandmarks names a wrong-shaped input +PASS averageLandmarkSets weights position by visibility and means the confidence +PASS a zero-visibility pair averages without dividing by zero +PASS averageLandmarkSets fills per-landmark holes from the other set +PASS averageLandmarkSets handles a missing set on either side +PASS a symmetric detection averages to itself +PASS two disagreeing detections average to their midpoint +PASS a one-sided detection falls back, and no detection at all returns null +PASS detectMirrorAveraged names its own failures +PASS a mirrored-then-unmirrored still fits to the original pose +PASS averaging two agreeing views leaves the fitted pose where it was +pose-mirror: OK + +RUNNING test/verify-pose-yaw.mjs +PASS a yawed subject fits to the same pose as a head-on subject +PASS Hips no longer bakes in the camera-relative facing +PASS a degenerate facing skips normalization without throwing +pose-yaw: OK + +RUNNING test/verify-preserve-bridge.mjs +PASS the mask is sized with the CLI's truncation, not with rounding +PASS a preserve request reaches the box through CCLAY_KIMODO_PRESERVE +PASS preserve + waypoints compose: path on argv, rootFree in the preserve JSON +PASS a whole-clip run keeps Kimodo's own npz beside its take +PASS an edit run preserves but never keeps a base of its own +PASS a plain generation clears both inherited preserve variables +PASS preserve is supported while base clips still refuse by name +PASS baseMotionFor finds the native npz beside a take, or reports none +PASS generate.mjs refuses an unshippable preserve plan before touching the box +PASS the strength dial scales weights, wideWeights and every group array +PASS editRanges[].tracks reach the mask builder and free only their groups +PASS preserve + waypoints frees the root group for the whole clip +PASS editRanges + waypoints + preserve compose: the edit's mask with the root freed +PASS the strength dial scales the composed mask without re-preserving the root +PASS strength maps to sigma_s = max(50, round(1000*(1-s))) with sigma_e capped at 50 +PASS rootFree is set by the presence of waypoints, and only there +PASS a hand edit pins the hand chain and leaves the anchors fullbody +PASS two edited limbs emit one entry per chain over the same author frames +PASS a non-limb, mixed or untracked edit keeps round 1's fullbody pin +PASS with no context frames an effector edit ships only its own chain +PASS preserve must be an object +PASS preserve must not be a string +PASS preserve must not be null +PASS sourceMotion is required +PASS sourceMotion must be a motion URL +PASS sourceMotion must carry a well-formed run id +PASS strength is required +PASS strength 0 means omit the field +PASS strength above 1 +PASS strength must be a number +PASS strength must be finite +PASS editRanges is required +PASS editRanges must be an array +PASS a range must be an object +PASS range frames must be integers +PASS a range must be non-empty +PASS a range must not be inverted +PASS a range must not start before the clip +PASS a range must not run past the clip +PASS the offending range is named by index +PASS tracks must be an array +PASS an empty tracks list is refused rather than guessed +PASS an unknown track id is named, with the valid ones +PASS a non-string track id is refused +PASS a prototype property is not a track id +PASS preserve cannot ride with a prompt schedule +PASS preserve cannot ride with regenerateSegments +PASS a well-formed preserve passes validation and is resolved against the motion allowlist +PASS an empty edit list is valid: preserve the whole take +PASS an edit range scoped to known IK tracks is accepted +PASS preserve rides with waypoints (the drawn path owns the root) +PASS editRanges + waypoints + preserve are accepted together +PASS overlapping edit ranges are accepted +PASS preserve rides with motionEdit +PASS requests without preserve validate exactly as before +OK verify-preserve-bridge + +RUNNING test/verify-project.mjs +embedded asset round-trip and compatibility checks PASS +all project file checks PASS + +RUNNING test/verify-projflow-bridge.mjs +PASS the 24 -> 20 scaler is round(frames * 20/24), one rule everywhere +PASS frameRange scales with the same rule and is clamped into the driver's inclusive index space +PASS the 24 -> 20 source resample lerps between frames +PASS writeNpyFloat32 round-trips through this module's own reader + seam deltas (0.5 m step spliced into a 0.05 m/frame walk): start 0.5025 m, end 0.5025 m, median 0.0500 m/frame, ratio 10.05x +PASS a hard splice keeps the take outside the range and measures both seams +PASS seam measurement reports 1x on an unchanged splice and skips absent seams + composition: 100 gen frames, seams 0.2465 / 0.2089 m vs median 0.0500 m/frame +PASS runLineEditJob composes take -> hml22 -> box -> cskel27 -> retime -> splice -> npz +PASS the job refuses a range the take does not contain and a result of the wrong length +PASS health advertises capabilities.lineEdit when the projflow probe passes +PASS health reports capabilities.lineEdit:false when the projflow probe fails +PASS a reachable box with no ACMDM checkpoint does not advertise line editing +PASS lineEdit must be an object +PASS lineEdit must not be a string +PASS lineEdit + preserve is refused by name +PASS lineEdit + waypoints is refused by name +PASS lineEdit + segments is refused by name +PASS lineEdit + regenerateSegments is refused by name +PASS lineEdit + motionEdit is refused by name +PASS lineEdit + poses is refused by name +PASS lineEdit + pose is refused by name +PASS a line edit must say posePin:false +PASS sourceMotion must be a motion URL +PASS sourceMotion must carry a well-formed run id +PASS sourceMotion is required +PASS an unknown track is named, with the valid ones +PASS a non-string track is refused +PASS a prototype property is not a track id +PASS chest is refused by name, with the reason and an alternative +PASS frameRange must be an object +PASS frameRange must be integers +PASS frameRange must not be inverted +PASS frameRange must not start before the clip +PASS frameRange must not run past the clip +PASS a one-frame range is a pin, not a line +PASS points2d must be an array +PASS one point is not a line +PASS the 64-point cap is the box's solver budget +PASS a point must be a pair +PASS a point must be finite +PASS pixels are not normalised coordinates +PASS camera is required +PASS camera must not be an array +PASS camera.fx must be finite +PASS camera.fy must be finite +PASS camera.cx must be finite +PASS camera.cy must be finite +PASS a negative focal length means the uv flip was applied twice +PASS pixel focal lengths beside normalised points +PASS R must be 9 numbers, not a 3x3 +PASS R must be full +PASS t must be a 3-vector +PASS a non-finite extrinsic is refused +PASS prompt must be a string when present +PASS a well-formed line edit is resolved against the motion allowlist +PASS every mappable track, the whole clip and a full 64-point stroke are accepted +PASS requests without lineEdit validate exactly as before +PASS line editing is engine-per-task, capability-gated, and registers its take +OK verify-projflow-bridge + +RUNNING test/verify-projflow-cskel27.mjs +PASS mapping is a 22-driven / 5-filled bijection over cskel27 +PASS SMPL naming traps are mapped across, not by matching name text +PASS filled joints resolve to their nearest mapped ancestor +PASS cskel27 is parent-before-child, so one ascending pass lifts the whole body +PASS output is the five soma77 fields, same names and same types +PASS fps is carried through at 20 and no retiming happens here +PASS a rest-pose clip lifts to identity locals on all 27 joints +PASS the 15 line-edit tracks resolve to cskel27 joints via ik.js and COZYCLAY_TO_CSKEL27 + + walk-then-stop (360f, 13.8 ms) — line-edit track round trip, cm + leftHand -> LeftHand world mean 0.079 max 0.100; pose-only max 0.0400 + rightHand -> RightHand world mean 0.079 max 0.100; pose-only max 0.0400 + leftFoot -> LeftFoot world mean 0.087 max 0.087; pose-only max 0.0001 + rightFoot -> RightFoot world mean 0.087 max 0.087; pose-only max 0.0001 + leftElbow -> LeftForeArm world mean 0.079 max 0.100; pose-only max 0.0400 + rightElbow -> RightForeArm world mean 0.079 max 0.100; pose-only max 0.0400 + leftKnee -> LeftLeg world mean 0.087 max 0.087; pose-only max 0.0001 + rightKnee -> RightLeg world mean 0.087 max 0.087; pose-only max 0.0001 + hips -> Hips world mean 0.087 max 0.087; pose-only max 0.0000 + spine -> Spine1 world mean 0.087 max 0.087; pose-only max 0.0000 + chest -> Spine2 world mean 0.216 max 0.604; pose-only max 0.5947 (target is a FILLED joint) + neck -> Neck world mean 0.079 max 0.100; pose-only max 0.0400 + head -> Head world mean 0.079 max 0.100; pose-only max 0.0400 + leftShoulder -> LeftShoulder world mean 0.079 max 0.100; pose-only max 0.0400 + rightShoulder -> RightShoulder world mean 0.079 max 0.100; pose-only max 0.0400 + -> worst track: chest 0.604 cm (gate 5 cm) + -> worst of the 22 DRIVEN joints: Head 0.100 cm +PASS walk-then-stop: all 15 line-edit tracks and all 22 driven joints round-trip under 5 cm + filled (accepted loss, GP4): Spine2 0.60, RightHandEnd 1.64, RightHandThumb1 4.68, LeftHandEnd 1.58, LeftHandThumb1 4.24 cm max +PASS walk-then-stop: the 5 filled joints are finite everywhere and emit identity locals + 9720 matrices: worst |det-1| 8.65e-8, worst |MᵀM-I| 8.04e-8 +PASS walk-then-stop: all 9720 emitted locals are proper rotations + continuity: worst frame-to-frame local delta 22.740° (RightLeg, frame 145); source clip's own worst 22.742° +PASS walk-then-stop: no twist flips — worst local delta 22.74° +PASS walk-then-stop: FK over the emitted locals reproduces every lifted global +PASS walk-then-stop: floor-aligned by one rigid shift, matching soma77ToCskel27Motion + + qa-lying (80f, 2.1 ms) — line-edit track round trip, cm + leftHand -> LeftHand world mean 0.192 max 0.210; pose-only max 0.0704 + rightHand -> RightHand world mean 0.192 max 0.210; pose-only max 0.0704 + leftFoot -> LeftFoot world mean 0.190 max 0.190; pose-only max 0.0000 + rightFoot -> RightFoot world mean 0.190 max 0.190; pose-only max 0.0000 + leftElbow -> LeftForeArm world mean 0.192 max 0.210; pose-only max 0.0704 + rightElbow -> RightForeArm world mean 0.192 max 0.210; pose-only max 0.0704 + leftKnee -> LeftLeg world mean 0.190 max 0.190; pose-only max 0.0000 + rightKnee -> RightLeg world mean 0.190 max 0.190; pose-only max 0.0000 + hips -> Hips world mean 0.190 max 0.190; pose-only max 0.0000 + spine -> Spine1 world mean 0.190 max 0.190; pose-only max 0.0000 + chest -> Spine2 world mean 0.283 max 0.704; pose-only max 0.5137 (target is a FILLED joint) + neck -> Neck world mean 0.192 max 0.210; pose-only max 0.0704 + head -> Head world mean 0.192 max 0.210; pose-only max 0.0704 + leftShoulder -> LeftShoulder world mean 0.192 max 0.210; pose-only max 0.0704 + rightShoulder -> RightShoulder world mean 0.192 max 0.210; pose-only max 0.0704 + -> worst track: chest 0.704 cm (gate 5 cm) + -> worst of the 22 DRIVEN joints: LeftHand 0.210 cm +PASS qa-lying: all 15 line-edit tracks and all 22 driven joints round-trip under 5 cm + filled (accepted loss, GP4): Spine2 0.70, RightHandEnd 2.75, RightHandThumb1 6.07, LeftHandEnd 4.14, LeftHandThumb1 5.13 cm max +PASS qa-lying: the 5 filled joints are finite everywhere and emit identity locals + 2160 matrices: worst |det-1| 8.26e-8, worst |MᵀM-I| 8.37e-8 +PASS qa-lying: all 2160 emitted locals are proper rotations + continuity: worst frame-to-frame local delta 19.732° (LeftForeArm, frame 30); source clip's own worst 17.163° +PASS qa-lying: no twist flips — worst local delta 19.73° +PASS qa-lying: FK over the emitted locals reproduces every lifted global +PASS qa-lying: floor-aligned by one rigid shift, matching soma77ToCskel27Motion + + antiparallel sweep: continued branch max step 5.000° (input 5°/frame); cold branch is 180.0° away +PASS the antiparallel branch is resolved by continuing the previous frame, not by a seed +PASS the exported pose-level convert reproduces the motion loop frame for frame +PASS bad shapes, NaNs, collapsed skeletons and bad fps are rejected by name + + 360-frame convert, 5 runs (ms): 13.4, 10.1, 8.5, 8.8, 8.1 +PASS 360 frames convert in 8.1 ms +OK verify-projflow-cskel27 + +RUNNING test/verify-projflow-replay.mjs +PASS blockBoundaries returns the internal block seams and nothing else +PASS boundaryWarningFor intersects the half-open range with [boundary-5, boundary+5] +PASS validateReplay pins the array, the 16-entry cap, exclusivity and every per-entry field +PASS the per-field rules are shared between C6 and C10 and differ only in the label +PASS runReplay chains take -> replay-0 -> replay-1 -> replay-2 and reports every entry +PASS a failed entry is reported and skipped; the chain continues from the last good take +PASS replay rides on plain prompt generation +PASS replay rides on a pose-pinned request +PASS replay rides on a prompt schedule (chained blocks) +PASS replay rides on a preserved regeneration +PASS a full 16-entry recipe is accepted +PASS replay + lineEdit is refused +PASS replay + motionEdit is refused +PASS replay + regenerateSegments is refused +PASS replay must be an array +PASS the 16-entry cap is enforced +PASS an unknown track names the entry it came from +PASS a frameRange past the clip is refused +PASS pixels are not normalised coordinates here either +PASS a replay entry may not name a source take +PASS a replay entry may not ask for a preview +PASS a per-entry seed must be an integer +PASS lineEdit.preview:true is accepted +PASS lineEdit.preview must be a boolean +PASS requests without replay or preview validate exactly as before +PASS the bridge replays after the take is verified, before it is registered +OK verify-projflow-replay + +RUNNING test/verify-projflow-runner.mjs +PASS every one of the 15 pose-studio tracks is either mapped to an hml22 joint or refused by name +PASS the map contains no track the pose studio cannot produce +PASS every mapped joint is a valid hml22 index and the map is injective +PASS S2's naming traps resolve to the joints the mapping table documents +PASS unmappable and unknown tracks are refused with a named reason +PASS a valid request resolves its joint and serialises deterministically +PASS frameRange must be two ascending non-negative integers +PASS points2d must be >= 2 finite, viewport-normalised points +PASS the camera must carry finite normalised intrinsics, a 3x3 R and a 3-vector t +PASS a declared jointId must agree with the track, and a bad envelope is refused +PASS the runner refuses to exist without a host +PASS CCLAY_PROJFLOW_HOST falls back to CCLAY_KIMODO_HOST and wins when both are set +PASS the repo default matches S1's clone path and is overridable +PASS the ProjFlow runner exposes the surface the bridge calls +PASS non-line-edit run modes refuse by name instead of half-running +PASS lineEditCommand builds a spawnable command naming the raw hml22 output +PASS the done line matches doneRe and the status line does not +PASS lineEditCommand's environment carries the box, the venv and the HOME override +PASS --preview replaces --steps rather than competing with it +PASS lineEditCommand names the input it is missing +PASS a stale CCLAY_PROJFLOW_NATIVE_OUT cannot leak into the next run +PASS readNpyFloat32 reads a C-order float32 .npy and its shape +PASS readNpyFloat32 refuses Fortran order, foreign dtypes, truncation and non-npy files +PASS the generation constants match S1's measurements and contract C7 +PASS driver.py compiles under the local python3 +projflow runner checks complete + +RUNNING test/verify-projflow-service.mjs +PASS a float32 array survives the base64 round trip bit for bit +PASS a full 196-frame hml22 motion round-trips through the base64 blob +PASS a payload whose bytes disagree with its own shape is refused, not reshaped +PASS CCLAY_PROJFLOW_RESIDENT=0 disables the resident and anything else leaves it on +PASS the restart backoff is 1 s, 5 s, 25 s and then capped +PASS a warm line edit carries the whole one-shot request and returns positions + meta +PASS concurrent callers are queued onto the single child with unique ids +PASS a mismatched response id is a transport failure that kills and restarts the child +PASS an unsolicited response line is refused instead of being cached for the next request +PASS a non-JSON line on stdout is a protocol failure, not status output +PASS a driver error envelope falls back cold WITHOUT killing the warm child +PASS a result without the driver's meta is refused +PASS a resident speaking another protocol version is refused and replaced +PASS a request that outlives its timeout kills the child instead of waiting for a stale answer +PASS a resident that never loads its model is given up on and the edit goes cold +PASS a child that dies mid-request fails that edit, restarts in the background and serves the next one +PASS an unreachable box stops the retry loop instead of spawning ssh forever +PASS a stopped resident stays stopped and kills its child +PASS the same protocol works over real pipes: two edits on one child, stderr as status, envelope errors survivable +PASS an idle resident lets its owner exit and is killed when the owner does +SKIP driver.py --serve wiring (no local python3 with numpy — the box venv is the only one that matters) +projflow resident service checks complete + +RUNNING test/verify-pwa.mjs +PASS manifest launches the Cozy Clay studio at /app/ +PASS manifest exposes installable 192px, 512px, and maskable PNG icons +PASS document metadata advertises the manifest, theme, and Apple icon +PASS service worker registers install, activation, offline fetch, range requests, and update handling +PASS service worker installation fails fast when a required offline asset is missing +PASS root Vite base keeps studio assets reachable from /app/ +all Cozy Clay PWA checks PASS + +RUNNING test/verify-record-mp4-source.mjs +PASS Record downloads a .mp4 file + +RUNNING test/verify-resilience.mjs +PASS failure containment is wired: boundary, GL context guard, guarded layout write + +RUNNING test/verify-retime.mjs +PASS an 80-frame 20 fps take becomes a 96-frame 24 fps take of the same length +PASS frame 0 and every exact-ratio frame are copies, not re-interpolations +PASS rotations slerp and the root lerps onto the exact source time +PASS retimed posedJoints are the FK of the retimed rotations, so every bone keeps its length +PASS a 30 fps take retimes onto a rigid 24 fps skeleton too +PASS output frames that land on a source frame still hold that frame's pose +PASS a take whose positions do not follow its rotations is not rewritten by FK +PASS every retimed matrix stays a unit rotation +PASS a take's person scale survives the retime onto the timeline clock +PASS same-rate input passes through and anchor frames rescale +verify-retime: all checks passed + +RUNNING test/verify-sample-at.mjs +PASS same frame returns frame-accurate state +PASS samples own their nested values +PASS sampling does not mutate scene or shot +PASS camera uses the complete shared shape +PASS sensorId is available before the filmback producer lands +PASS follow camera derives focalMm on the selected sensor +PASS 24fps and 60fps render pacing produce frame-accurate states +PASS key and follow cameras share one shape +PASS key camera keeps its authored FOV +PASS partial rail range uses keyed cameras outside its authored range +PASS partial rail range uses rail track samples inside its authored range +PASS motion root is sampled as scene state +PASS follow spring uses one fixed authored-frame timestep +all sample-at checks PASS + +RUNNING test/verify-scene-asset-cache.mjs +scene asset cache generation race checks PASS + +RUNNING test/verify-scene-assets.mjs +PASS the same bytes always get the same id +PASS different bytes get a different id +PASS an id is prefixed and hex +PASS an ArrayBuffer and its view agree +PASS a digest becomes an id, case and whitespace tolerated +PASS a short or non-hex digest is not an id +PASS only a well-formed id passes isAssetId +PASS a missing SubtleCrypto is a clear failure +PASS the image types an import accepts +PASS svg and non-images are refused +PASS a picture inside the cap is stored untouched +PASS the longest edge is capped and the aspect kept +PASS a portrait picture caps on its height +PASS a degenerate edge still survives as one pixel +PASS a zero or nonsense size has no target +PASS the cap never enlarges +PASS a well-formed record survives repair +PASS a derived record preserves its storage role +PASS the aspect comes from the stored size +PASS a record with no drawable bytes is dropped +PASS a record with an unusable size is dropped +PASS a record with a foreign id or type is dropped +PASS a non-record is dropped, not fatal +PASS usage counts include every unique asset reference across all scene objects +PASS usage counts ignore invalid ids and hostile scene shapes +PASS asset graph signature changes when lineage changes at the same usage count +PASS asset graph signature is deterministic +PASS a graph change during delete restores the record +PASS a matted cutout keeps its rendered picture, source and matte reachable +PASS a source-only cutout has no phantom references +PASS duplicated matted cutouts count their shared id trio once +PASS an asset in a second scene is reachable +PASS a malformed document is empty, not fatal +PASS only truly unreferenced ids are swept +PASS a picture shared by two scenes is never swept +PASS a junk stored key is not mistaken for an asset +PASS a picture inside the cap is stored as it arrived +PASS an unscaled import is identified by its own bytes +PASS nothing is re-encoded when nothing is resized +PASS the imported aspect is the card's aspect +PASS an oversized picture is capped before it is stored +PASS a resized import is identified by its stored bytes, not its source +PASS the decoded bitmap is released +PASS a resized picture keeps a format that can carry alpha +PASS a non-image is refused with a readable reason +PASS an svg is refused with the rest +PASS a file past the source ceiling is refused before it is decoded +PASS a file-shaped nothing is refused +PASS an undecodable picture is refused +PASS the images in a drop come back in order +PASS a file with no MIME falls back to its extension +PASS documents, folders and text are left where they are +PASS a drop with nothing in it is not fatal +PASS a pasted picture is imported +PASS the generic clipboard name is replaced +PASS the pasted bytes survive +PASS a real filename is kept +PASS pasted text is ignored +PASS an unsupported image type is refused +PASS an empty clipboard yields nothing +PASS a clipboard exposing only files still works +all scene asset checks PASS + +RUNNING test/verify-scene-objects.mjs +PASS default objects have unique IDs +PASS default registry excludes the car +PASS default registry excludes the chair +PASS only the chair renderer applies the 90 percent scale +PASS default player scene excludes the small plane +PASS default player scene contains no props +PASS Props children come from live scene objects +PASS arbitrary object name appears in hierarchy +PASS arbitrary object ID is namespaced +PASS hierarchy ID round-trips to object ID +PASS non-object hierarchy ID is rejected +PASS transform update returns a new collection +PASS transform update preserves untouched object identity +PASS transform update changes only requested object +PASS transform update preserves renderer metadata +PASS invalid numeric update is ignored +PASS unknown object update is ignored +PASS object removal returns a new collection +PASS object removal removes only the requested object +PASS unknown object removal is ignored +PASS catalogue offers the Unity primitive set +PASS catalogue keeps the hand-built set pieces +PASS every catalogue entry carries a footprint and a height +PASS created object starts neutral on the floor +PASS primitives are grey-box grey, set pieces keep their clay +PASS created object carries its own footprint copy +PASS unknown kinds create nothing +PASS repeat creation gets a unique id +PASS repeat creation gets a numbered name +PASS creation clamps placement onto the stage +PASS an explicit placement angle is still wrapped +PASS new objects land down the lens, not on the lens +PASS placement never rotates the new object +PASS a turned camera still creates an unrotated object +PASS an object created from any angle starts at zero rotation +PASS axis drag is absolute from the drag start +PASS axis drag snaps to the 5 cm grid +PASS height drags write the Y channel +PASS a non-axis drag writes nothing +PASS the Y ring drives the yaw the plan board reads +PASS the X ring drives tilt and the Z ring roll +PASS ring rotation wraps instead of running away +PASS angles fold into [-180, 180) +PASS an axis scale knob scales only its own axis +PASS the centre knob scales all three axes +PASS scale snaps to 5 percent steps +PASS scale never collapses to nothing +PASS a degenerate scale drag writes nothing +PASS world size folds scale into the footprint +PASS a view-axis spin about world Y lands on the Y channel +PASS a view-axis spin about world Z lands on the Z channel +PASS the screen ring snaps to the 5 degree grid +PASS an unsnapped screen spin keeps the exact angle +PASS a degenerate screen spin writes nothing +PASS transforms stay on the stage and above the floor +PASS scale stays within a usable range +PASS name and colour are editable +PASS empty names are rejected +PASS a serialized scene round-trips +PASS an absent payload is tagged absent +PASS malformed JSON is tagged corrupt +PASS a non-integer or non-positive version is corrupt, not future +PASS only an integer newer than SCENE_VERSION is future +PASS the supported version range is exactly 1..SCENE_VERSION +PASS an unknown renderer is dropped, not fatal +PASS footprint and height are rebuilt from the library +PASS a single scale field fans out to three axes +PASS out-of-room values are clamped on load +PASS duplicate ids are dropped and counted +PASS the storage keys are namespaced, versioned and distinct +PASS an object over empty floor drops to zero +PASS a drop never mutates its inputs +PASS an object over a box rests on its top +PASS scaleY raises the support surface +PASS a non-overlapping neighbour is not a support +PASS an edge-touching neighbour is not a support +PASS a support above the object is ignored +PASS an object already inside a box falls through it +PASS a drop preserves snapped X and Z byte-for-byte +PASS the applied drop lands exactly on the off-grid top, not on the 5 cm grid +PASS a drop is idempotent — the second drop changes nothing +PASS a redundant drop keeps the SAME array reference, so no history entry is possible +PASS the drop patch itself is not clamped +PASS a composed drop still hits the ceiling clamp +PASS the yawed AABB widens the plank beyond its unrotated footprint +PASS a 45-degree long support widens the overlap window +PASS a 45-degree support does not create phantom overlap beyond its AABB +PASS a cutout is sized by its measured height and its picture's aspect +PASS a cutout keeps the asset id its picture lives under +PASS a cutout without a picture is refused +PASS cutouts are not creatable from the catalogue +PASS the catalogue menu does not offer cutouts +PASS cutout names and ids stay unique against the scene +PASS editing a cutout's height rederives its footprint +PASS a cutout's footprint cannot be patched directly +PASS a cutout height has no upper limit and stays above zero +PASS a nonsense height or aspect leaves the card alone +PASS a library object ignores the cutout channels +PASS a fresh cutout is its own original, with no selection on it +PASS a cut card keeps the picture it renders, the photograph it came from and the selection +PASS a stored cut card round-trips with all three +PASS a record from before the cut editor still loads, as its own original +PASS a nonsense trim factor is clamped, not trusted +PASS a card can be pointed at a new picture, resizing with it +PASS an empty or unchanged asset id writes nothing +PASS a cutout resolves its edge with coverage, not with a hard threshold +PASS and still writes depth — no transparent blending on a card +PASS a cutout round-trips through storage +PASS a stored cutout footprint is rebuilt from height and aspect, never trusted +PASS a cutout record with no asset id is dropped like an unknown renderer +PASS a cutout stored at an extreme height keeps that height +PASS a card stands on a box like any other object +PASS a new card wears the picture's proportions +PASS a width in metres is accepted as given +PASS widening records the factor +PASS widening leaves the height alone +PASS widening does not touch scaleX +PASS the picture's own aspect is preserved +PASS height and width stay independent +PASS resetting returns to the picture's proportions +PASS an absurd width is clamped +PASS a negative width is clamped +PASS a stretch survives a storage round trip +PASS a record written before stretching reads as unstretched +PASS a corner drag grows both dimensions +PASS a corner drag keeps the picture's shape +PASS a proportional resize leaves the stretch alone +PASS a corner drag shrinks too +PASS shrinking keeps the shape +PASS a stretched card keeps its stretch through a corner drag +PASS and both of its dimensions still scale together +PASS a matted cutout duplicate keeps the picture, the original and the mask +PASS a matted cutout duplicate keeps the trim factor +PASS a matted cutout duplicate keeps the stretch and its derived footprint +PASS a matted duplicate is a NEW record (fresh id and offset name), not the original +PASS a never-matted duplicate keeps empty mask and self-as-original +PASS the object delete-undo offer expires on its own +PASS the expiry is a timer effect keyed to the pending deletion +PASS the offer is withdrawn the moment a newer edit invalidates it +PASS the image delete-undo offer expires the same way +all scene object checks PASS +PASS a new object starts unparented +PASS parenting records the parent +PASS moving a parent carries its child +PASS the carry reaches grandchildren +PASS a child keeps its own rotation and scale +PASS moving a child leaves the parent alone +PASS rotation and scale stay on the parent alone +PASS an object cannot parent itself +PASS a cycle is refused +PASS an unknown parent is refused +PASS detaching clears the parent +PASS deleting a parent promotes its children instead of orphaning them +PASS parent survives a serialize/load round trip +PASS the attach bone list is exactly the app's IK track ids +PASS the bone list is frozen, so no caller can edit the vocabulary +PASS a new object — primitive or cutout — starts world-anchored +PASS attaching returns a NEW collection and leaves the others identical +PASS an omitted bone means the character's animated ROOT frame +PASS an explicit null bone is the same root frame +PASS a bone attach records the track id, not a three.js bone name +PASS extra keys are stripped, never stored +PASS an unknown character id is NOT the store's business to refuse +PASS an unknown object id is refused +PASS a malformed attach is refused, leaving the SAME array +PASS re-attaching to the very same frame keeps the SAME array, so no history entry is possible +PASS detaching an already-detached object keeps the SAME array +PASS attaching drops the object out of its group +PASS taking an object parent cancels the attachment +PASS ungrouping is not detaching — a null parent leaves the attachment alone +PASS detaching clears only the attachment +PASS a world-anchored scene round-trips with attach:null intact +PASS a bone attachment survives a serialize/load round trip byte-for-byte +PASS a malformed stored attach decodes to detached +PASS a stored bone is validated against the track list and stripped to two keys +PASS a record written before attachment existed reads as world-anchored + +RUNNING test/verify-scenes.mjs +all scene document checks PASS + +RUNNING test/verify-shot-authoring.mjs +all shot-authoring checks PASS + +RUNNING test/verify-shot-guides.mjs +verify-shot-guides: all checks passed + +RUNNING test/verify-speed-envelope.mjs +PASS nothing is not a timing +PASS a flat timing is one segment of ones +PASS createTiming repairs and renormalizes +PASS cuts must strictly increase in both t and d +PASS negative speeds are repaired to zero +PASS a drag conserves the integral +PASS the raised stretch rises and the rest sinks +PASS speed zero is legal: the object may stand still +PASS an extreme pull floors the rest at zero and still conserves +PASS a pull past the whole budget shrinks itself to fit +PASS no timing is the identity +PASS progress is pinned at both ends +PASS a front-loaded envelope runs ahead of the clock +PASS progress is monotonic even under a wild envelope +PASS a cut pins (t, d) exactly +PASS editing one segment never moves another +PASS inserting a cut changes nothing about the motion +PASS a cut lands at the motion's own position +PASS a cut too near an existing one is refused +PASS removing a cut merges without moving the motion much +PASS no timing: constant speed, midpoint at mid-take +PASS a front-loaded timing runs ahead, then lands exactly at the end +PASS the cut frame is pinned regardless of edits elsewhere +PASS timing rides inside a speed window +PASS createObjectPath carries timing and survives a round-trip +PASS garbage timing is dropped, not crashed on +PASS without timing the dolly walks the whole rail +PASS a zero first half holds the dolly, then it still arrives +PASS the dolly's cut frame is pinned like the prop's +PASS the camera block carries dollyTiming and heals garbage +all speed-envelope checks PASS + +RUNNING test/verify-stable-ids.mjs +stable ID checks PASS + +RUNNING test/verify-take-recipe.mjs +PASS the seed rule: empty is rolled, typed is kept, both are always concrete +PASS blocks derive from single-prompt and segments bodies on the right clock +PASS line edits push immutably, sourceMotion-free, in application order +PASS replay is a sourceMotion-free prefix capped at 16 +PASS the version strip holds 20 checkpoints, each with its own recipe +all take-recipe checks PASS + +RUNNING test/verify-telemetry-state.mjs +all telemetry state checks PASS + +RUNNING test/verify-theme.mjs +PASS header brand is Cozy Clay +PASS browser title names the studio +PASS Inter is the only bundled active UI family +PASS display and UI roles both use Inter +PASS numeric editing data has a monospace role +PASS wordmark uses a modern heavy display treatment +PASS chrome backdrop token matches Unity dark +PASS chrome foreground token is light on dark +PASS chrome panel token matches Unity dark chrome +PASS chrome accent token matches Unity selection blue family +PASS timeline lanes sit on a dark surface +PASS IK uses pencil red +PASS current frame uses lightbox amber +PASS Canvas uses a bright neutral toon background +PASS Character uses bright ivory clay +PASS Room uses a high-key floor +PASS the stage has no walls +PASS the deck is large enough to read as open +PASS Room has no ceiling plane +PASS Studio uses directional high-key toon lighting +all Cozy Clay theme checks PASS + +RUNNING test/verify-timeline-camera.mjs +PASS timeline has one combined Shot/Camera track +PASS ruler labels step in 10-frame units +PASS lane gridlines ride one 10-frame cadence +PASS gridlines render from the ruler's framePct +PASS chips and markers share one frame scale +PASS camera keys render as dots, not a chip +PASS block key strip keys framing while a dedicated crane graph authors Rail points +PASS key strip is a crosshair affordance +PASS dot click jumps the playhead and selects the camera +PASS dot right-click removes the key +PASS dot drag re-times the key +PASS keys stay frame-unique on re-time +PASS re-keying a frame overwrites its framing +PASS the move model is per-shot N keys, not A/B +PASS interpolation samples keys segment by segment +PASS MoveRig plays and follows keys through the pure frame sampler +PASS sequence slate and phrase derive per segment +PASS generation exports first/last key conditioning frames +PASS the duration slider is gone — dots own timing +PASS PlayView restarts the piece from frame 0 +PASS PlayView always rides the camera move +PASS Scene tab keeps the authoring gates on Follow mode +PASS Draw Rail row owns the distance Follow On Off toggle +PASS surface lays out four tracks, and the Shot row is never the one that falls off +PASS lane gridlines are frame-based, not width-based +PASS camera dots have a distinct violet identity +PASS the Rail Follow ribbon is gone; the crane height editor owns the card interaction +PASS no ribbon editing gestures survive in the timeline +PASS the rail schedule model still resolves per shot in the app +PASS Rail range playback is resolved per shot +PASS the crane graph itself selects, adds, and vertically drags height points +PASS authoring a crane point shows it: the box switches to the curve it just edited +PASS the crane line is sampled from the model that flies the camera +PASS curves drag at a fixed rate, not at the surface's height +PASS unified lane renders exactly one block from each shot geometry +PASS unified blocks preserve the violet camera identity +PASS camera blocks summarize authored state +PASS camera block selection also selects its owning shot +PASS one add button creates a separate shared shot-camera card +PASS one edge set owns shot and camera boundaries +PASS selected camera mini editor sits above the lane body +PASS mini editor exposes preview, rail drawing and director controls +PASS height and pitch are adjacent in the camera bar +PASS distance, height and pitch are measured from viewport manipulation instead of typed +PASS manual viewport framing stays put until preview or playback +PASS the rail crane is always on, with a per-point height input +PASS crane points are added on the Shot key strip only, removed explicitly +PASS rail crane points are authored and selected directly in the crane graph +PASS waypoint mode replaces camera controls with one clear message +PASS damping and look-ahead stay behind advanced disclosure +PASS timeline editor is the single follow-camera settings surface +PASS Draw Rail exposes an explicit delete action +PASS preview starts at the selected shot and stops at its end +PASS rail ribbon cannot cover the shot frame range +PASS the bottom window never grows for camera, rail, or path content +PASS camera editor emits shot-camera patches without owning global state +PASS key dots remain overlaid above unified blocks and draggable +all timeline camera checks PASS + +RUNNING test/verify-timeline-extent.mjs +verify-timeline-extent: all checks passed + +RUNNING test/verify-timeline-shots.mjs +timeline shots lane verified + +RUNNING test/verify-trim.mjs +PASS a mid-clip cut keeps exactly its frames and re-anchors at frame 0 +PASS the take's person scale survives repeated cuts and is never invented +PASS a full-range cut is still an independent copy +PASS out-of-range and non-integer cuts are refused by name +verify-trim: all checks passed + +RUNNING test/verify-update-check.mjs +PASS compareVersions orders older, newer, and equal releases +PASS compareVersions ranks prereleases below their release and against each other +PASS compareVersions survives malformed input without throwing or returning NaN +PASS checkForUpdate reports a newer published version and caches it +PASS checkForUpdate stays quiet when the registry is level with or behind us +PASS checkForUpdate serves a cache hit inside the 24h TTL without refetching +PASS checkForUpdate refetches once the cached entry is older than the TTL +PASS checkForUpdate refetches through a corrupt or timestamp-less cache file +PASS checkForUpdate returns null on an unreachable registry instead of throwing +PASS checkForUpdate returns null on a registry error response and caches nothing +PASS checkForUpdate ignores junk, versionless, and non-string registry payloads +PASS checkForUpdate returns null when the registry URL itself is unusable +all Cozy Clay update-check checks PASS + +RUNNING test/verify-usd-camera.mjs +frame 12 USD forward = (-0.4698463475, 0.3420201451, -0.8137976715) +frame 13 USD forward = (-0.6830127118, 0.2588190508, -0.6830127027) +frame 14 USD forward = (-0.9396925755, 0.3420201337, -0.0000000459) +USD camera verified: 3 quaternion-resolved frames, 36x20.25mm cropped gate, 24 fps + +RUNNING test/verify-video-frames.mjs +PASS sampleTimes counts like frameCountFor and backs the tail off the end +PASS sampleTimes refuses unusable inputs with an empty list +PASS a sub-step clip still yields its first frame +PASS frames step the real element through ascending sample times +PASS an undecodable clip fails closed by name +PASS a decode error mid-clip fails closed by name +PASS a stalled seek is named, never an endless hang +PASS a missing JS runtime is named +PASS an unreachable model download is named +PASS an unreachable wasm engine is named +PASS engine init tries GPU then CPU before failing by name +PASS a working detector rounds timestamps and closes cleanly +verify-video-frames: all checks passed + +RUNNING mcp/verify-http-origin.mjs +HTTP origin guard passed: forgedOrigin=403, forgedHost=403, loopback=200 + +RUNNING mcp/verify-live-motion-job.mjs +motion job confirmed installation, rejection, cancellation, reconnect recovery, uncertain delivery, expiry, and no polling tools passed + +RUNNING mcp/verify-live-p0.mjs +{"livePort":60659,"modelRoundTrip":{"isError":false,"described":true},"rejected":{"isError":true},"uncertainAfterFailedVerification":{"isError":true,"doNotRetry":true},"uncertainAfterDisconnect":{"isError":true,"doNotRetry":true},"loadMotion":{"isError":false,"timeoutMs":30000},"defaultCommandTimeoutMs":5000,"seededDescribe":{"content":[{"type":"text","text":"Project: Untitled\nScene: MCP P0 LIVE (1 scene in project)\n\nCAMERA\n position x 0 y 1.6 z 4.5\n lens 35mm (nearest prime 35mm)\n filmback super35 · 1.78:1\n framing CLOSE-UP · FRONT ¾ R · EYE LEVEL · 35MM\n distance 2.71m to the subject's centre of mass\n\nCAST (total: 1, returned: 1, truncated: false, revision: 5592224e13ff)\n A char-a \"a prior performer\" at x 1, z 2, facing 30deg [x-bot-tpose] posed <- framed\n model: x-bot-tpose pose: {\"label\":\"Walking\",\"bones\":{\"hips\":[1,2,3]}} tint: #123456 scale: 1.4\n motionRef: {\"url\":\"/ardy/motions/123456-abcdef\",\"prompt\":\"Walk.\",\"rotationDeg\":30,\"anchorX\":1,\"anchorZ\":2}\n layer: {\"waypoints\":[{\"frame\":12,\"x\":1,\"z\":2,\"id\":\"waypoint-1lt75v8\"}],\"promptClips\":[{\"startFrame\":0,\"endFrame\":24,\"text\":\"Walk.\",\"id\":\"prompt-clip-p7284a\"}]}\n\nSET (total: 1, returned: 1, truncated: false, revision: 5592224e13ff)\n cube-1 Cube at x 1, y 0, z -2 yaw 15deg size 1x1x1m\n rotX: 12 rotZ: -8 color: #d9b18c parent: null\n\nSTAGE\n shotAspect: 2.39:1 sensorId: super35 hasCharSheet: true\n keyLight: x 6 y 9 z 4 intensity 1.12\n\nTIMELINE\n currentFrame: 17 frameCount: 240 fps: 24"}]},"emptyDescribe":{"content":[{"type":"text","text":"Project: Untitled\nScene: MCP P0 LIVE (1 scene in project)\n\nCAMERA\n position x 0 y 1.6 z 4.5\n lens 35mm (nearest prime 35mm)\n filmback super35 · 1.78:1\n framing MEDIUM SHOT · FRONT · EYE LEVEL · 35MM\n distance 4.51m to the subject's centre of mass\n\nCAST (total: 0, returned: 0, truncated: false, revision: 81ff0dca94f2)\n\nSET (total: 0, returned: 0, truncated: false, revision: 81ff0dca94f2)\n empty — add with place_object\n\nSTAGE\n shotAspect: 2.39:1 sensorId: super35 hasCharSheet: true\n keyLight: x 6 y 9 z 4 intensity 1.12\n\nTIMELINE\n currentFrame: 17 frameCount: 240 fps: 24"}]},"descriptionMerge":{"before":{"model":"x-bot-tpose","pose":"Walking","tint":"#123456","scale":1.4,"layer":true,"motionRef":"/ardy/motions/123456-abcdef"},"describe":{"subject":"the described performer","x":7},"after":{"model":"x-bot-tpose","pose":"Walking","tint":"#123456","scale":1.4,"layer":1,"motionRef":"/ardy/motions/123456-abcdef","subject":"the described performer","x":7}}} + +RUNNING mcp/verify-live-port.mjs +configured live port contract passed + +RUNNING mcp/verify-live-routing.mjs +{"livePort":60692,"handles":{"first":"10322be1-f8a2-4e6b-961c-1a662cd43159","second":"3329c5ef-48e8-47fa-b4e5-0927e8456fd2","reconnected":"905317df-0080-4ca8-9e00-3b68a5233fb9"},"isolation":{"first":{"document":{"version":4,"activeSceneId":"scene-mtli03ou-2","scenes":[{"id":"first-scene","name":"FIRST","objects":[],"shotDocument":null,"stage":{"characters":[{"id":"char-a","model":"y-bot-tpose","x":0,"y":0,"z":0,"rot":0,"hidden":false,"tint":null,"pose":null,"scale":1,"subject":"FIRST","layer":{"waypoints":[],"promptClips":[]},"motionRef":null}],"hasCharSheet":false,"shotAspect":"16:9","sensorId":"super35","keyLight":{"x":6,"y":9,"z":4,"intensity":1.12,"warmth":0.5}}},{"id":"scene-mtli03ou-2","name":"A2","objects":[],"shotDocument":null,"stage":{"characters":[{"id":"char-a","model":"y-bot-tpose","x":0,"y":0,"z":0,"rot":0,"hidden":false,"tint":null,"pose":null,"scale":1,"subject":"a young woman in a tan coat","layer":{"waypoints":[],"promptClips":[]},"motionRef":null}],"hasCharSheet":false,"shotAspect":"16:9","sensorId":"fullFrame","keyLight":{"x":6,"y":9,"z":4,"intensity":1.12,"warmth":0.5}}}]},"sceneName":"A2","camera":{"x":11,"y":1.6,"z":4.5,"focalMm":35,"sensorId":"super35","aspectRatio":1.78},"stage":{"shotAspect":"16:9","sensorId":"super35","hasCharSheet":false},"timeline":{"currentFrame":0,"frameCount":240,"fps":24},"characters":[{"id":"char-a","model":"y-bot-tpose","subject":"FIRST","x":0,"y":0,"z":0,"rot":0,"hidden":false}],"objects":[]},"second":{"document":{"version":3,"activeSceneId":"second-scene","scenes":[{"id":"second-scene","name":"SECOND","objects":[],"shotDocument":null,"stage":{"characters":[],"hasCharSheet":false,"shotAspect":"16:9","sensorId":"super35"}}]},"sceneName":"SECOND","camera":{"x":0,"y":1.6,"z":4.5,"focalMm":35,"sensorId":"super35","aspectRatio":1.78},"stage":{"shotAspect":"16:9","sensorId":"super35","hasCharSheet":false},"timeline":{"currentFrame":0,"frameCount":240,"fps":24},"characters":[{"id":"char-a","model":"y-bot-tpose","subject":"SECOND","x":0,"y":0,"z":0,"rot":0,"hidden":false}],"objects":[]}},"ambiguity":"Live command set_camera requires workspace_handle; connected workspaces: 10322be1-f8a2-4e6b-961c-1a662cd43159, 3329c5ef-48e8-47fa-b4e5-0927e8456fd2.","stale":"Unknown or stale live workspace handle \"3329c5ef-48e8-47fa-b4e5-0927e8456fd2\".","guards":{"untrustedOrigin":{"code":1008,"reason":"Live editor origin must be loopback"},"duplicateWorkspace":{"code":1008,"reason":"Workspace id is already connected"}},"single":{"isError":false,"state":{"document":{"version":3,"activeSceneId":"reconnected-scene","scenes":[{"id":"reconnected-scene","name":"RECONNECTED","objects":[],"shotDocument":null,"stage":{"characters":[],"hasCharSheet":false,"shotAspect":"16:9","sensorId":"super35"}}]},"sceneName":"RECONNECTED","camera":{"x":55,"y":1.6,"z":4.5,"focalMm":35,"sensorId":"super35","aspectRatio":1.78},"stage":{"shotAspect":"16:9","sensorId":"super35","hasCharSheet":false},"timeline":{"currentFrame":0,"frameCount":240,"fps":24},"characters":[{"id":"char-a","model":"y-bot-tpose","subject":"RECONNECTED","x":0,"y":0,"z":0,"rot":0,"hidden":false}],"objects":[]}}} + +RUNNING mcp/verify-live.mjs +live status, forwarding, live describe, and disconnect fallback passed + +RUNNING mcp/verify-prompts.mjs +all ARDY prompt checks passed + +RUNNING mcp/verify-protocol-version.mjs +MCP protocol 2025-11-25 only PASS + +RUNNING mcp/verify-tool-annotations.mjs +tools/list response: +name | readOnlyHint | destructiveHint | idempotentHint | openWorldHint | opening sentence +describe_scene | true | false | true | false | Read the authoring state: camera, lens, current framing, cast positions and set objects. +live_status | true | false | true | false | Report connected CozyClay editor workspaces and their handles. +describe_shot | true | false | true | false | Turn the current camera geometry into film vocabulary — shot size, angle on the subject, camera level, and the nearest real prime lens. +capture_frame | false | false | false | true | Unlike render_prompt, capture_frame reads the connected editor's rendered 640x360 preview and computed spatial assertions without changing authored scene, playback or camera state. +set_camera | false | true | true | false | Unlike frame_shot, set_camera applies explicit camera coordinates or focal length rather than deriving a shot. +frame_shot | false | true | true | false | Unlike set_camera, frame_shot derives camera position and lens from shot intent rather than applying explicit coordinates. +add_character | false | false | false | false | Unlike place_character, add_character adds a new cast member instead of changing an existing one. +place_character | false | true | true | false | Unlike add_character, place_character changes an existing cast member instead of adding one. +remove_character | false | true | false | false | Take a character out of the cast. +focus_character | false | true | true | false | Pick which character the shot is measured against. +place_object | false | false | false | false | Unlike update_object, place_object adds a new prop instead of changing an existing one. +group_objects | false | true | true | false | Attach objects to a parent object. +set_prompt_blocks | false | true | true | false | Write Prompt Blocks onto the timeline WITHOUT generating — the beats and their frame ranges, so a schedule can be read and revised before any GPU time is spent. +load_motion | false | true | true | false | Unlike generate_motion, load_motion installs a previously generated or assembled motion take WITHOUT regenerating — re-using a completed /ardy/motions/ take or an /ardy/assembled/*.npz tile (the long-take recipe). +generate_motion | false | true | false | true | Start a multi-phase Kimodo motion job for the connected live editor and return immediately with its task-shaped identity. +update_object | false | true | true | false | Unlike place_object, update_object changes an existing prop instead of adding one. +remove_object | false | true | false | false | Take a prop out of the set. +apply_batch | false | true | false | false | Apply up to 100 object mutations in the connected CozyClay editor as one user-visible undo entry. +render_prompt | true | false | true | false | Turn the current camera, cast and set into a prompt for an AI image or video model. +mark_camera_move | false | true | true | false | Snapshot the current framing as the A position of a camera move. +describe_camera_move | true | false | true | false | Compare the marked A position against the camera's current B position and name the move the way a crew would — dolly in, crane down, arc left, push, and so on. +add_scene | false | false | false | false | Add another scene to the project and make it active. +switch_scene | false | true | true | false | Make a different scene active. +open_project | false | true | true | true | Load a project authored in the CozyClay studio (or saved here). +save_project | false | true | true | true | Write the current state as a .cclayproject file. +PASS place_character versus add_character: "Unlike add_character, place_character changes an existing cast member instead of adding one." / "Unlike place_character, add_character adds a new cast member instead of changing an existing one." +PASS place_object versus update_object: "Unlike update_object, place_object adds a new prop instead of changing an existing one." / "Unlike place_object, update_object changes an existing prop instead of adding one." +PASS frame_shot versus set_camera: "Unlike set_camera, frame_shot derives camera position and lens from shot intent rather than applying explicit coordinates." / "Unlike frame_shot, set_camera applies explicit camera coordinates or focal length rather than deriving a shot." +PASS G005 tools/list safety contract (25 tools; deterministic order) + +RUNNING mcp/verify.mjs +25 tools registered +420 framing combinations checked + +all checks passed + +PASS 118 Node verification files diff --git a/.omo/qa/83-vite.log b/.omo/qa/83-vite.log new file mode 100644 index 000000000..68c65c973 --- /dev/null +++ b/.omo/qa/83-vite.log @@ -0,0 +1,16 @@ + +> cozyclay@1.7.0 dev:ui +> vite --host 127.0.0.1 --port 5283 --strictPort + + + VITE v8.2.0 ready in 551 ms + + ➜ Local: http://127.0.0.1:5283/ +오후 9:18:06 [vite] (client) [console.warn] THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. +오후 9:20:12 [vite] (client) [console.warn] THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. +오후 9:20:20 [vite] (client) [console.warn] THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. +오후 9:20:28 [vite] (client) [console.warn] THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. +오후 9:22:34 [vite] (client) [console.warn] THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. +오후 9:22:52 [vite] (client) [console.warn] THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. +오후 9:24:09 [vite] (client) hmr update /src/App.jsx +오후 9:24:36 [vite] (client) [console.warn] THREE.Clock: This module has been deprecated. Please use THREE.Timer instead. diff --git a/.omo/report-83.md b/.omo/report-83.md new file mode 100644 index 000000000..ae7647aeb --- /dev/null +++ b/.omo/report-83.md @@ -0,0 +1,122 @@ +# Issue #83 implementation report + +## Outcome + +Implemented the scoped selection-settle fix and promoted the browser assertion to require both `past + 1` and `future === 0`. The change is committed on `fix/issue-83` as described below. No push or PR was performed. + +## Root cause + +The hierarchy selection handler used the render-captured `store` binding: + +```js +store.settle(); +``` + +Scene changes replace `storeRef.current` with a new scene-history coordinator in `openScene()`. A hierarchy click can be handled by a callback from a render that predates that replacement, so the handler can settle the coordinator for the scene that was left rather than the live coordinator owning the active producer. The active gizmo drag then remains applied in the live store without its transaction being committed, which explains the observed persistence with unchanged history depths. + +The fix resolves the coordinator at event time: + +```js +storeRef.current.settle(); +``` + +This preserves the coordinator contract: the active transaction is retired, the producer teardown runs, the post-travel array is pushed once, and `pushHistory` clears the redo branch. A zero-travel settle still coalesces to no history entry. + +## Files changed + +- `src/App.jsx`: hierarchy selection settle call now uses the live store ref. +- `test/verify-object-gizmo.mjs`: the selection-change regression assertion is gating within the suite and checks `past === pastBeforeSwitch + 1 && future === 0`. + +No `src/history.js` coordinator change was needed; its existing unit-tested settle implementation already commits post-travel state and truncates redo. + +## Verification + +### Required browser baseline (RED) + +Command: + +```text +QA_URL=http://127.0.0.1:5283/app/ CDP_PORT=9283 node tools/qa-browser.mjs -- node test/verify-object-gizmo.mjs +``` + +Artifact: `.omo/qa/83-before.log` + +This workstation's headless Chrome could not reach the issue-83 lifecycle section. The first pre-existing gizmo movement check was already red and the suite then crashed before the selection-change case: + +```text +PASS the X arrow is grabbable where it is drawn +FAIL dragging the X arrow slides the object along X — {"Position":[0.55,0,0.7],"Rotation":[0,0,0],"Scale":[1,1,1]} +PASS an X drag leaves the other axes alone +PASS E selects the rotate tool +PASS rotate mode shows one ring per axis plus the screen ring +FAIL the Y ring is grabbable on screen — +TypeError: Cannot read properties of null (reading 'x') + at .../test/verify-object-gizmo.mjs:233:40 +``` + +The log also records repeated macOS headless-render errors (`CVDisplayLinkCreateWithCGDisplay failed`, followed by GPU process exit). Therefore there is no evidence from this environment that the target selection assertion ran in the baseline; the issue's reported RED observation remains the motivating reproduction and must be rerun on the real-GPU browser QA surface. + +### Required browser after run + +Command: + +```text +QA_URL=http://127.0.0.1:5283/app/ CDP_PORT=9283 node tools/qa-browser.mjs -- node test/verify-object-gizmo.mjs +``` + +Artifact: `.omo/qa/83-after.log` + +The same pre-existing headless-render limitation stopped this run before the lifecycle section, with the same first movement failure and ring-probe crash. There is consequently no browser GREEN excerpt to claim from this workstation. The test assertion itself is strengthened and will fail if the redo branch is not truncated when the target case runs. + +### GREEN results available in this environment + +History unit suite: + +```text +npm run test:history +... +PASS a streamed interaction is exactly one entry +PASS an interaction with no net change creates no entry +PASS undo mid-interaction commits then undoes that interaction +PASS an atomic mutation mid-interaction settles first +... +all history checks PASS +``` + +Build: + +```text +npm run build +... +✓ 1030 modules transformed. +✓ built in 794ms +BUILD_STATUS=0 +``` + +The build emitted only existing Vite warnings for the unresolved font reference and large chunks; it reported no source diagnostics or build errors. + +Full runner: + +```text +node tools/run-tests.mjs +... +PASS 118 Node verification files +FULL_STATUS=0 +``` + +LSP diagnostics returned `No diagnostics found` for both changed files, and `git diff --check` passed. + +## QA artifacts + +- `.omo/qa/83-before.log` - required browser baseline. +- `.omo/qa/83-after.log` - required browser after run. +- `.omo/qa/83-vite.log` - Vite server output. +- `.omo/qa/83-build.log` - build output. +- `.omo/qa/83-full.log` - full Node runner output. + +## Merge notes + +- Commit: `fix(gizmo): settle hierarchy selection against the live scene coordinator (#83)` (created after verification). +- The browser matrix tier was not changed. +- Browser QA must be rerun on the lead's real-GPU surface to obtain the required target-case RED/GREEN excerpts; this macOS headless session cannot execute past the pre-existing ring-probe failure. +- Only `src/App.jsx` and `test/verify-object-gizmo.mjs` are intended to be part of the commit. diff --git a/src/App.jsx b/src/App.jsx index 44ddbb2ca..6c04957a4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1174,7 +1174,10 @@ globalThis.playMode = centerTab === "play"; // This MUST stay inside the handler, never in the render body: a settle // during render runs the producer's cancel teardown, and since the first // applied tick re-renders, every drag would die after exactly one tick. - store.settle(); + // Resolve the live store at event time: opening a scene replaces the + // coordinator in storeRef before the next hierarchy click, while a + // render-captured store can still settle the scene that was left. + storeRef.current.settle(); setSelectedHierarchyId(id); // Moving the focus anywhere but the camera releases the crane dot too: // a press on the floor or the sky must not leave a mark selected. diff --git a/test/verify-object-gizmo.mjs b/test/verify-object-gizmo.mjs index 2c89b841b..c735d0799 100644 --- a/test/verify-object-gizmo.mjs +++ b/test/verify-object-gizmo.mjs @@ -786,8 +786,11 @@ expect( // Isolation: only now reset the store for anything that follows. The app // itself persists the whole stage under cozyclay.scenes.v4 — clearing just // the legacy pair leaves every object the cases above created to reload -// into the drop-to-surface section. Sweep all cozyclay keys but the locale. -await evaluate("Object.keys(localStorage).filter(k => k.startsWith('cozyclay.') && k !== 'cozyclay.locale').forEach(k => localStorage.removeItem(k))"); +// into the drop-to-surface section. Sweep all cozyclay keys but the editor +// preferences: the locale, and the project session — without it the studio +// reopens on the first-run project chooser, whose dialog sits over the +// viewport and takes every canvas press the lifecycle section dispatches. +await evaluate("Object.keys(localStorage).filter(k => k.startsWith('cozyclay.') && k !== 'cozyclay.locale' && k !== 'cozyclay.project-session.v1').forEach(k => localStorage.removeItem(k))"); await reloadPage(); /* -------------------------------------------------- drop-to-surface ---- */ @@ -1066,7 +1069,7 @@ await click("[...document.querySelectorAll('.hierarchy-row')].find(b => b.textCo await waitFor(`window.__sceneHistory().past === ${pastBeforeSwitch + 1}`); expect( "a selection change mid-drag commits the travel as exactly one entry", - await evaluate("window.__sceneHistory().past") === pastBeforeSwitch + 1, + await evaluate(`(() => { const history = window.__sceneHistory(); return history.past === ${pastBeforeSwitch + 1} && history.future === 0; })()`), JSON.stringify(await evaluate("window.__sceneHistory()")), ); // keep moving with the button still down: the producer was torn down, so diff --git a/tools/qa-browser.mjs b/tools/qa-browser.mjs index b2b23825d..1d6a599fa 100644 --- a/tools/qa-browser.mjs +++ b/tools/qa-browser.mjs @@ -31,6 +31,65 @@ const port = Number(process.env.CDP_PORT || 9222); // pointed there fails every selector before a single assertion is useful. const pageUrl = process.env.QA_URL || "http://127.0.0.1:5180/app/"; const cdpUrl = `http://127.0.0.1:${port}/json/version`; + +/** Give the QA profile a project session before the studio opens, so QA drives + * the studio a returning author sees rather than the first-run chooser. */ +async function seedProjectSession(cdpPort, url) { + const targets = await (await fetch(`http://127.0.0.1:${cdpPort}/json`)).json(); + const page = targets.find((target) => target.type === "page" && target.webSocketDebuggerUrl); + if (!page) throw new Error("QA browser exposed no page target to seed"); + const ws = new WebSocket(page.webSocketDebuggerUrl); + await new Promise((resolve, reject) => { + ws.onopen = resolve; + ws.onerror = reject; + }); + let nextId = 1; + const pending = new Map(); + ws.onmessage = (event) => { + const message = JSON.parse(event.data); + if (!message.id || !pending.has(message.id)) return; + const { resolve, reject } = pending.get(message.id); + pending.delete(message.id); + if (message.error) reject(new Error(JSON.stringify(message.error))); + else resolve(message.result); + }; + const send = (method, params = {}) => + new Promise((resolve, reject) => { + const id = nextId++; + pending.set(id, { resolve, reject }); + ws.send(JSON.stringify({ id, method, params })); + }); + const evaluate = (expression) => send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }); + try { + // Chrome's first document for a headless launch URL can still deny + // storage access (about:blank origin) while the studio is loading, so + // the seed goes through a plain same-origin document instead, and the + // studio is opened afterwards with the session already in place. The + // value is written once, into storage: nothing re-runs on later + // navigations, so a suite that sweeps storage to test a first-run path + // still gets one. + const origin = new URL(url).origin; + await send("Page.enable"); + const loadedOnce = () => new Promise((resolve) => { + const onMessage = (event) => { + if (JSON.parse(event.data).method !== "Page.loadEventFired") return; + ws.removeEventListener("message", onMessage); + resolve(); + }; + ws.addEventListener("message", onMessage); + }); + let loaded = loadedOnce(); + await send("Page.navigate", { url: `${origin}/favicon.ico` }); + await loaded; + await evaluate("(() => { try { const key = 'cozyclay.project-session.v1'; if (!localStorage.getItem(key)) localStorage.setItem(key, JSON.stringify({ name: 'QA', updatedAt: Date.now() })); } catch {} })()"); + loaded = loadedOnce(); + await send("Page.navigate", { url }); + await loaded; + } finally { + ws.close(); + } +} + try { await fetch(cdpUrl); throw new Error(`CDP port ${port} is already in use; stop the existing QA browser first`); @@ -54,7 +113,12 @@ try { children.push(chrome); removeSignalCleanup = installSignalCleanup(() => children, cleanupProfile); - const deadline = Date.now() + 10000; + // Chrome's first launch on a cold CI runner (SwiftShader, no GPU process + // cache) can take well over ten seconds to bring the DevTools port up, and + // a timeout here reads as a suite failure with zero checks run. Thirty + // seconds covers what the gating jobs have been seen to need; QA_CDP_TIMEOUT_MS + // overrides it. + const deadline = Date.now() + Number(process.env.QA_CDP_TIMEOUT_MS || 30000); while (true) { if (chrome.exitCode !== null || chrome.signalCode !== null) { throw new Error("QA browser exited before CDP became ready"); @@ -69,6 +133,15 @@ try { await new Promise((resolve) => setTimeout(resolve, 100)); } + // The studio opens a modal project chooser on a first run (no project + // session in storage). Its dialog sits over the viewport and takes the + // pointer, so every canvas press a suite dispatches lands on the dialog + // instead of the gizmo — the suite then reads "nothing moved" as a + // regression (#83). Seed a session before navigation so QA drives the + // studio a returning author sees. QA_STARTUP_CHOOSER=1 keeps the chooser + // for suites that exercise it. + if (process.env.QA_STARTUP_CHOOSER !== "1") await seedProjectSession(port, pageUrl); + const runner = spawnOwned(command, commandArgs, { env: { ...process.env, CDP_PORT: String(port) }, });