@1agh/maude 0.28.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +4537 -0
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
- package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
- package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
- package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
- package/apps/studio/bin/read-annotations.mjs +573 -0
- package/apps/studio/bin/read-annotations.sh +11 -0
- package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
- package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
- package/apps/studio/bin/to-lottie-verify.html +68 -0
- package/apps/studio/bin/to-lottie.sh +128 -0
- package/{plugins/design/dev-server → apps/studio}/build.ts +6 -4
- package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
- package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
- package/apps/studio/canvas-create.ts +104 -0
- package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +48 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
- package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +360 -48
- package/apps/studio/client/app.jsx +6742 -0
- package/apps/studio/client/canvas-url.js +81 -0
- package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
- package/apps/studio/client/index.html +16 -0
- package/apps/studio/client/styles/1-tokens-maude.css +296 -0
- package/apps/studio/client/styles/3-shell-maude.css +1161 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
- package/apps/studio/client/styles/4-components-maude.css +74 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +396 -34
- package/apps/studio/client/styles/5-maude-overrides.css +137 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
- package/apps/studio/client/tour/overlay.jsx +279 -0
- package/apps/studio/client/tour/usage-tour.js +48 -0
- package/apps/studio/client/whats-new-seen.js +52 -0
- package/apps/studio/client/whats-new.jsx +250 -0
- package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +2 -2
- package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +2 -2
- package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
- package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
- package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
- package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
- package/apps/studio/dist/client.bundle.js +25545 -0
- package/apps/studio/dist/comment-mount.js +2046 -0
- package/apps/studio/dist/styles.css +8018 -0
- package/apps/studio/dom-selection.ts +342 -0
- package/apps/studio/draw/animate.ts +333 -0
- package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
- package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
- package/apps/studio/draw/morph.ts +204 -0
- package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
- package/apps/studio/draw/serialize-animate.ts +324 -0
- package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
- package/apps/studio/draw/test/animate.test.ts +172 -0
- package/apps/studio/draw/test/morph.test.ts +107 -0
- package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
- package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +16 -4
- package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
- package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
- package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
- package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
- package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
- package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
- package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
- package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
- package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
- package/apps/studio/sync/cold-start.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
- package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +321 -0
- package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
- package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
- package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/annotations-text-format.test.ts +396 -0
- package/apps/studio/test/asset-api.test.ts +149 -0
- package/apps/studio/test/canvas-create-api.test.ts +361 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
- package/apps/studio/test/canvas-edit.test.ts +319 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
- package/apps/studio/test/canvas-media-drop.test.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +25 -1
- package/apps/studio/test/canvas-url.test.ts +99 -0
- package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
- package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
- package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +52 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
- package/apps/studio/test/motion-sample.test.ts +35 -0
- package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
- package/apps/studio/test/read-annotations.test.ts +409 -0
- package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
- package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
- package/apps/studio/test/shared-doc-migrate.test.ts +369 -0
- package/apps/studio/test/sync-agent.test.ts +575 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-start.test.ts +244 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
- package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
- package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
- package/apps/studio/test/tour-overlay.test.tsx +56 -0
- package/apps/studio/test/use-annotation-resize.test.ts +402 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
- package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
- package/apps/studio/test/whats-new.test.ts +101 -0
- package/apps/studio/text-imports.d.ts +9 -0
- package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +15 -70
- package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
- package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
- package/apps/studio/use-annotation-resize.tsx +912 -0
- package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
- package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +3 -3
- package/apps/studio/use-canvas-media-drop.tsx +343 -0
- package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
- package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +18 -1
- package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +8 -2
- package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
- package/apps/studio/whats-new.json +264 -0
- package/apps/studio/whats-new.schema.json +86 -0
- package/apps/studio/whats-new.ts +87 -0
- package/cli/bin/maude.mjs +10 -0
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +16 -7
- package/cli/commands/design.test.mjs +40 -0
- package/cli/commands/help.mjs +8 -2
- package/cli/commands/hub.mjs +3 -3
- package/cli/commands/hub.test.mjs +1 -1
- package/cli/commands/scenario-report.mjs +2 -9
- package/cli/lib/design-link.mjs +51 -1
- package/cli/lib/flow-design-integration.test.mjs +1 -1
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
- package/cli/lib/preflight.mjs +1 -1
- package/package.json +15 -15
- package/plugins/design/templates/_shell.html +28 -4
- package/plugins/design/templates/brief-board.tsx.template +61 -0
- package/plugins/design/templates/canvas.tsx.template +1 -1
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/plugins/flow/.claude-plugin/config.schema.json +16 -0
- package/plugins/design/dev-server/annotations-layer.tsx +0 -3402
- package/plugins/design/dev-server/client/app.jsx +0 -2985
- package/plugins/design/dev-server/client/index.html +0 -15
- package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
- package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
- package/plugins/design/dev-server/dist/styles.css +0 -2710
- package/plugins/design/dev-server/dom-selection.ts +0 -156
- package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
- package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
- package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
- package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
- /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Sync journal unit tests — hub-sync cold-start safety (DDR-102).
|
|
2
|
+
//
|
|
3
|
+
// The journal is the divergence detector's memory: it records, per slug, the
|
|
4
|
+
// content hash this machine last reconciled disk↔doc. These tests cover the
|
|
5
|
+
// load/record/flush/invalidate API, the corrupt-file → absent posture, and
|
|
6
|
+
// the per-hub invalidation rule.
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
9
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { hashBytes } from '../sync/echo-guard.ts';
|
|
14
|
+
import { journalPath, loadJournal, type SyncJournal } from '../sync/journal.ts';
|
|
15
|
+
|
|
16
|
+
let dir: string;
|
|
17
|
+
let journal: SyncJournal | null = null;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
dir = mkdtempSync(join(tmpdir(), 'sync-journal-'));
|
|
21
|
+
journal = null;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
journal?.stop();
|
|
26
|
+
rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('loadJournal — basic API', () => {
|
|
30
|
+
test('get returns null for a never-recorded slug', () => {
|
|
31
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
32
|
+
expect(journal.get('ui-maskot')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('record + get round-trip with bodyHash and optional cssHash', () => {
|
|
36
|
+
journal = loadJournal(dir, { flushMs: 0, now: () => 42 });
|
|
37
|
+
const bodyHash = hashBytes('<div>v1</div>');
|
|
38
|
+
journal.record('ui-maskot', { bodyHash });
|
|
39
|
+
expect(journal.get('ui-maskot')).toEqual({ bodyHash, at: 42 });
|
|
40
|
+
|
|
41
|
+
const cssHash = hashBytes('.a{}');
|
|
42
|
+
journal.record('ui-maskot', { bodyHash, cssHash });
|
|
43
|
+
expect(journal.get('ui-maskot')).toEqual({ bodyHash, cssHash, at: 42 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('flush persists to <designRoot>/_state/sync-journal.json, creating _state/ on demand', () => {
|
|
47
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
48
|
+
expect(existsSync(join(dir, '_state'))).toBe(false);
|
|
49
|
+
journal.record('ui-a', { bodyHash: 'h1' });
|
|
50
|
+
|
|
51
|
+
const file = journalPath(dir);
|
|
52
|
+
expect(existsSync(file)).toBe(true);
|
|
53
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
54
|
+
expect(parsed.slugs['ui-a'].bodyHash).toBe('h1');
|
|
55
|
+
expect(typeof parsed.updatedAt).toBe('number');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('a fresh journal instance reads back what a previous one persisted', () => {
|
|
59
|
+
const first = loadJournal(dir, { flushMs: 0, now: () => 7 });
|
|
60
|
+
first.record('ui-a', { bodyHash: 'h1' });
|
|
61
|
+
first.stop();
|
|
62
|
+
|
|
63
|
+
journal = loadJournal(dir);
|
|
64
|
+
expect(journal.get('ui-a')).toEqual({ bodyHash: 'h1', at: 7 });
|
|
65
|
+
expect(journal.size()).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('debounces rapid records into one write', async () => {
|
|
69
|
+
let writes = 0;
|
|
70
|
+
journal = loadJournal(dir, {
|
|
71
|
+
flushMs: 20,
|
|
72
|
+
writer: () => {
|
|
73
|
+
writes++;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
for (let i = 0; i < 10; i++) journal.record('ui-a', { bodyHash: `h${i}` });
|
|
77
|
+
expect(writes).toBe(0); // still pending
|
|
78
|
+
await new Promise((res) => setTimeout(res, 60));
|
|
79
|
+
expect(writes).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('stop() flushes the pending debounce', () => {
|
|
83
|
+
let written: string | null = null;
|
|
84
|
+
journal = loadJournal(dir, {
|
|
85
|
+
flushMs: 10_000,
|
|
86
|
+
writer: (_p, bytes) => {
|
|
87
|
+
written = bytes;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
journal.record('ui-a', { bodyHash: 'h1' });
|
|
91
|
+
expect(written).toBeNull();
|
|
92
|
+
journal.stop();
|
|
93
|
+
expect(written).not.toBeNull();
|
|
94
|
+
expect(JSON.parse(written as unknown as string).slugs['ui-a'].bodyHash).toBe('h1');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('loadJournal — corrupt / hostile file posture', () => {
|
|
99
|
+
test('corrupt JSON → treated as absent, never throws', () => {
|
|
100
|
+
const file = journalPath(dir);
|
|
101
|
+
// Persist a valid journal first (creates _state/), then corrupt it.
|
|
102
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
103
|
+
journal.record('ui-a', { bodyHash: 'h1' });
|
|
104
|
+
journal.stop();
|
|
105
|
+
|
|
106
|
+
writeFileSync(file, '{not json');
|
|
107
|
+
journal = loadJournal(dir);
|
|
108
|
+
expect(journal.get('ui-a')).toBeNull();
|
|
109
|
+
expect(journal.size()).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('wrong-shape JSON (array / non-object slugs / bad entries) → absent entries', () => {
|
|
113
|
+
const file = journalPath(dir);
|
|
114
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
115
|
+
journal.record('seed', { bodyHash: 'x' }); // creates _state/
|
|
116
|
+
journal.stop();
|
|
117
|
+
|
|
118
|
+
writeFileSync(file, JSON.stringify([1, 2, 3]));
|
|
119
|
+
expect(loadJournal(dir).size()).toBe(0);
|
|
120
|
+
|
|
121
|
+
writeFileSync(file, JSON.stringify({ hubUrl: 1, slugs: { 'ui-a': { bodyHash: 99 } } }));
|
|
122
|
+
const j = loadJournal(dir);
|
|
123
|
+
expect(j.get('ui-a')).toBeNull();
|
|
124
|
+
expect(j.size()).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('writer failure is swallowed (never throws into the sync path)', () => {
|
|
128
|
+
journal = loadJournal(dir, {
|
|
129
|
+
flushMs: 0,
|
|
130
|
+
writer: () => {
|
|
131
|
+
throw new Error('disk full');
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
expect(() => journal?.record('ui-a', { bodyHash: 'h1' })).not.toThrow();
|
|
135
|
+
expect(journal.get('ui-a')?.bodyHash).toBe('h1'); // in-memory state intact
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('loadJournal — per-hub invalidation', () => {
|
|
140
|
+
test('first invalidateIfHubChanged adopts the URL without wiping', () => {
|
|
141
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
142
|
+
journal.record('ui-a', { bodyHash: 'h1' });
|
|
143
|
+
journal.invalidateIfHubChanged('https://hub.example.com');
|
|
144
|
+
expect(journal.get('ui-a')?.bodyHash).toBe('h1');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('same hub URL on a later boot keeps entries', () => {
|
|
148
|
+
const first = loadJournal(dir, { flushMs: 0 });
|
|
149
|
+
first.invalidateIfHubChanged('https://hub.example.com');
|
|
150
|
+
first.record('ui-a', { bodyHash: 'h1' });
|
|
151
|
+
first.stop();
|
|
152
|
+
|
|
153
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
154
|
+
journal.invalidateIfHubChanged('https://hub.example.com');
|
|
155
|
+
expect(journal.get('ui-a')?.bodyHash).toBe('h1');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('a DIFFERENT hub URL wipes every entry (hashes are per-hub)', () => {
|
|
159
|
+
const first = loadJournal(dir, { flushMs: 0 });
|
|
160
|
+
first.invalidateIfHubChanged('https://hub-one.example.com');
|
|
161
|
+
first.record('ui-a', { bodyHash: 'h1' });
|
|
162
|
+
first.record('ui-b', { bodyHash: 'h2' });
|
|
163
|
+
first.stop();
|
|
164
|
+
|
|
165
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
166
|
+
journal.invalidateIfHubChanged('https://hub-two.example.com');
|
|
167
|
+
expect(journal.get('ui-a')).toBeNull();
|
|
168
|
+
expect(journal.get('ui-b')).toBeNull();
|
|
169
|
+
expect(journal.size()).toBe(0);
|
|
170
|
+
|
|
171
|
+
// …and the wipe is persisted.
|
|
172
|
+
const parsed = JSON.parse(readFileSync(journalPath(dir), 'utf8'));
|
|
173
|
+
expect(parsed.hubUrl).toBe('https://hub-two.example.com');
|
|
174
|
+
expect(Object.keys(parsed.slugs)).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
existsSync,
|
|
12
12
|
mkdirSync,
|
|
13
13
|
mkdtempSync,
|
|
14
|
+
readdirSync,
|
|
14
15
|
readFileSync,
|
|
15
16
|
rmSync,
|
|
16
17
|
writeFileSync,
|
|
@@ -50,7 +51,6 @@ beforeEach(() => {
|
|
|
50
51
|
afterEach(() => {
|
|
51
52
|
// Node's process.env stringifies on assignment; assigning undefined yields
|
|
52
53
|
// the literal string "undefined". delete is the correct restoration.
|
|
53
|
-
// biome-ignore lint/performance/noDelete: process.env semantics.
|
|
54
54
|
if (cfgPathEnv === undefined) delete process.env.HUBS_CONFIG_PATH;
|
|
55
55
|
else process.env.HUBS_CONFIG_PATH = cfgPathEnv;
|
|
56
56
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -424,14 +424,15 @@ describe('createSyncRuntime', () => {
|
|
|
424
424
|
await runtime?.stop();
|
|
425
425
|
});
|
|
426
426
|
|
|
427
|
-
test('
|
|
427
|
+
test('DDR-102: reconcile over divergent local content records a diverged conflict + snapshots both sides', async () => {
|
|
428
428
|
const url = 'https://hub.example.com';
|
|
429
429
|
writeHubsConfig(url, 'mau_test');
|
|
430
430
|
const ctx = makeCtx({ url, linkedAt: 1 });
|
|
431
|
-
// Local disk has divergent, non-empty content.
|
|
431
|
+
// Local disk has divergent, non-empty content (and no journal — divergence).
|
|
432
432
|
writeFileSync(join(ctx.paths.designRoot, 'ui', 'screen.html'), '<button>LOCAL EDIT</button>');
|
|
433
433
|
|
|
434
|
-
// Provider whose doc already carries different hub state.
|
|
434
|
+
// Provider whose doc already carries different hub state. No syncMeta
|
|
435
|
+
// stamp (older peer) → newest-wins falls back to hub.
|
|
435
436
|
const factory = () => {
|
|
436
437
|
const document = new Y.Doc();
|
|
437
438
|
document.getText('html').insert(0, '<button>HUB STATE</button>');
|
|
@@ -451,7 +452,25 @@ describe('createSyncRuntime', () => {
|
|
|
451
452
|
|
|
452
453
|
const status = runtime?.status();
|
|
453
454
|
expect(status?.conflicts.length).toBeGreaterThanOrEqual(1);
|
|
454
|
-
|
|
455
|
+
const conflict = status?.conflicts[0];
|
|
456
|
+
expect(conflict?.kind).toBe('cold-start-diverged');
|
|
457
|
+
expect(conflict?.winner).toBe('hub');
|
|
458
|
+
// Both pre-resolution versions were snapshotted to _history/<slug>/.
|
|
459
|
+
expect(conflict?.snapshots?.local).toBeDefined();
|
|
460
|
+
expect(conflict?.snapshots?.hub).toBeDefined();
|
|
461
|
+
const historyDir = join(ctx.paths.historyDir, 'ui-screen');
|
|
462
|
+
const blobs = readdirSync(historyDir).filter((f) => f.endsWith('.html'));
|
|
463
|
+
expect(blobs.length).toBe(2);
|
|
464
|
+
// The loser (local) is recoverable byte-identical from its snapshot.
|
|
465
|
+
const metas = readdirSync(historyDir).filter((f) => f.endsWith('.json'));
|
|
466
|
+
const snapshotContents = blobs.map((f) => readFileSync(join(historyDir, f), 'utf8'));
|
|
467
|
+
expect(snapshotContents).toContain('<button>LOCAL EDIT</button>');
|
|
468
|
+
expect(snapshotContents).toContain('<button>HUB STATE</button>');
|
|
469
|
+
expect(metas.length).toBe(2);
|
|
470
|
+
// Hub won → disk now carries hub state.
|
|
471
|
+
expect(readFileSync(join(ctx.paths.designRoot, 'ui', 'screen.html'), 'utf8')).toBe(
|
|
472
|
+
'<button>HUB STATE</button>'
|
|
473
|
+
);
|
|
455
474
|
await runtime?.stop();
|
|
456
475
|
});
|
|
457
476
|
});
|
|
@@ -885,3 +904,339 @@ describe('toWsUrl', () => {
|
|
|
885
904
|
expect(toWsUrl('ws://localhost:1234')).toBe('ws://localhost:1234');
|
|
886
905
|
});
|
|
887
906
|
});
|
|
907
|
+
|
|
908
|
+
// DDR-102 — WS multiplexing: the default factory shares ONE
|
|
909
|
+
// HocuspocusProviderWebsocket per hub URL across all providers.
|
|
910
|
+
describe('createDefaultProviderFactory — shared socket multiplexing', () => {
|
|
911
|
+
class FakeEmitter {
|
|
912
|
+
handlers = new Map<string, Set<(arg?: unknown) => void>>();
|
|
913
|
+
on(evt: string, cb: (arg?: unknown) => void) {
|
|
914
|
+
if (!this.handlers.has(evt)) this.handlers.set(evt, new Set());
|
|
915
|
+
this.handlers.get(evt)?.add(cb);
|
|
916
|
+
}
|
|
917
|
+
off(evt: string, cb: (arg?: unknown) => void) {
|
|
918
|
+
this.handlers.get(evt)?.delete(cb);
|
|
919
|
+
}
|
|
920
|
+
emit(evt: string, arg?: unknown) {
|
|
921
|
+
for (const cb of this.handlers.get(evt) ?? []) cb(arg);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function makeFakeModule() {
|
|
926
|
+
const sockets: Array<{ url: string; destroyed: boolean }> = [];
|
|
927
|
+
const providers: Array<{
|
|
928
|
+
name: string;
|
|
929
|
+
attached: boolean;
|
|
930
|
+
destroyed: boolean;
|
|
931
|
+
socket: unknown;
|
|
932
|
+
}> = [];
|
|
933
|
+
|
|
934
|
+
class HocuspocusProviderWebsocket extends FakeEmitter {
|
|
935
|
+
url: string;
|
|
936
|
+
status = 'connecting';
|
|
937
|
+
destroyed = false;
|
|
938
|
+
constructor(cfg: { url: string }) {
|
|
939
|
+
super();
|
|
940
|
+
this.url = cfg.url;
|
|
941
|
+
sockets.push(this);
|
|
942
|
+
}
|
|
943
|
+
destroy() {
|
|
944
|
+
this.destroyed = true;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
class HocuspocusProvider extends FakeEmitter {
|
|
949
|
+
configuration: { name: string; websocketProvider: unknown; document: unknown };
|
|
950
|
+
awareness = undefined;
|
|
951
|
+
synced = false;
|
|
952
|
+
attached = false;
|
|
953
|
+
destroyed = false;
|
|
954
|
+
name: string;
|
|
955
|
+
socket: unknown;
|
|
956
|
+
constructor(cfg: { name: string; websocketProvider: unknown; document: unknown }) {
|
|
957
|
+
super();
|
|
958
|
+
this.configuration = cfg;
|
|
959
|
+
this.name = cfg.name;
|
|
960
|
+
this.socket = cfg.websocketProvider;
|
|
961
|
+
providers.push(this);
|
|
962
|
+
}
|
|
963
|
+
attach() {
|
|
964
|
+
this.attached = true;
|
|
965
|
+
}
|
|
966
|
+
destroy() {
|
|
967
|
+
this.destroyed = true;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return { mod: { HocuspocusProviderWebsocket, HocuspocusProvider }, sockets, providers };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
test('N providers for one hub URL share ONE socket, each explicitly attached', async () => {
|
|
975
|
+
const { mod, sockets, providers } = makeFakeModule();
|
|
976
|
+
const { createDefaultProviderFactory } = await import('../sync/index.ts');
|
|
977
|
+
const factory = createDefaultProviderFactory(async () => mod);
|
|
978
|
+
|
|
979
|
+
await factory({ url: 'https://hub.example.com', token: 't', documentName: 'ui-a' });
|
|
980
|
+
await factory({ url: 'https://hub.example.com', token: 't', documentName: 'ui-b' });
|
|
981
|
+
await factory({ url: 'https://hub.example.com', token: 't', documentName: 'ui-c' });
|
|
982
|
+
|
|
983
|
+
expect(sockets).toHaveLength(1);
|
|
984
|
+
expect(sockets[0].url).toBe('wss://hub.example.com');
|
|
985
|
+
expect(providers).toHaveLength(3);
|
|
986
|
+
for (const p of providers) {
|
|
987
|
+
expect(p.attached).toBe(true);
|
|
988
|
+
expect(p.socket).toBe(sockets[0]);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test('provider destroy() detaches only; dispose() destroys the shared socket', async () => {
|
|
993
|
+
const { mod, sockets, providers } = makeFakeModule();
|
|
994
|
+
const { createDefaultProviderFactory } = await import('../sync/index.ts');
|
|
995
|
+
const factory = createDefaultProviderFactory(async () => mod);
|
|
996
|
+
|
|
997
|
+
const p1 = await factory({ url: 'https://h.example.com', token: 't', documentName: 'ui-a' });
|
|
998
|
+
p1.destroy();
|
|
999
|
+
expect(providers[0].destroyed).toBe(true);
|
|
1000
|
+
expect(sockets[0].destroyed).toBe(false); // socket survives provider death
|
|
1001
|
+
|
|
1002
|
+
factory.dispose();
|
|
1003
|
+
expect(sockets[0].destroyed).toBe(true);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
test('onStatus seeds from the SOCKET status and forwards provider status events', async () => {
|
|
1007
|
+
const { mod, sockets, providers } = makeFakeModule();
|
|
1008
|
+
const { createDefaultProviderFactory } = await import('../sync/index.ts');
|
|
1009
|
+
const factory = createDefaultProviderFactory(async () => mod);
|
|
1010
|
+
|
|
1011
|
+
const wrapped = await factory({
|
|
1012
|
+
url: 'https://h.example.com',
|
|
1013
|
+
token: 't',
|
|
1014
|
+
documentName: 'ui-a',
|
|
1015
|
+
});
|
|
1016
|
+
const seen: string[] = [];
|
|
1017
|
+
const unsub = wrapped.onStatus?.((s) => seen.push(s));
|
|
1018
|
+
// Seeded from socket.status ('connecting') immediately.
|
|
1019
|
+
expect(seen).toEqual(['connecting']);
|
|
1020
|
+
// Provider-level forwarded events keep flowing.
|
|
1021
|
+
(providers[0] as unknown as FakeEmitter).emit('status', { status: 'connected' });
|
|
1022
|
+
expect(seen).toEqual(['connecting', 'connected']);
|
|
1023
|
+
unsub?.();
|
|
1024
|
+
(providers[0] as unknown as FakeEmitter).emit('status', { status: 'disconnected' });
|
|
1025
|
+
expect(seen).toEqual(['connecting', 'connected']); // unsubscribed
|
|
1026
|
+
expect(sockets).toHaveLength(1);
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// DDR-102 — auth-failure intelligence (classification, aggregation,
|
|
1031
|
+
// destroy-on-permanent + re-probe) and the honest settled boot summary.
|
|
1032
|
+
describe('DDR-102 — auth-failure intelligence + boot summary', () => {
|
|
1033
|
+
function fakeTimerQueue() {
|
|
1034
|
+
let nextId = 1;
|
|
1035
|
+
const timers = new Map<number, { cb: () => void; ms: number }>();
|
|
1036
|
+
return {
|
|
1037
|
+
setTimer: (cb: () => void, ms: number) => {
|
|
1038
|
+
const id = nextId++;
|
|
1039
|
+
timers.set(id, { cb, ms });
|
|
1040
|
+
return id as unknown as ReturnType<typeof setTimeout>;
|
|
1041
|
+
},
|
|
1042
|
+
clearTimer: (h: ReturnType<typeof setTimeout>) => {
|
|
1043
|
+
timers.delete(h as unknown as number);
|
|
1044
|
+
},
|
|
1045
|
+
/** Fire every pending timer with ms <= maxMs (one sweep). */
|
|
1046
|
+
fire(maxMs: number) {
|
|
1047
|
+
for (const [id, t] of [...timers.entries()]) {
|
|
1048
|
+
if (t.ms <= maxMs) {
|
|
1049
|
+
timers.delete(id);
|
|
1050
|
+
t.cb();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
pending: () => timers.size,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Stub factory whose providers expose onAuthFailed + track destroys. */
|
|
1059
|
+
function authStubFactory() {
|
|
1060
|
+
const made: Array<{
|
|
1061
|
+
documentName: string;
|
|
1062
|
+
destroyed: boolean;
|
|
1063
|
+
emitAuthFailure: (reason: string) => void;
|
|
1064
|
+
document: Y.Doc;
|
|
1065
|
+
}> = [];
|
|
1066
|
+
const factory = (args: { documentName: string; document?: Y.Doc }) => {
|
|
1067
|
+
const document = args.document ?? new Y.Doc();
|
|
1068
|
+
const cbs = new Set<(info: { reason: string }) => void>();
|
|
1069
|
+
const entry = {
|
|
1070
|
+
documentName: args.documentName,
|
|
1071
|
+
destroyed: false,
|
|
1072
|
+
emitAuthFailure: (reason: string) => {
|
|
1073
|
+
for (const cb of cbs) cb({ reason });
|
|
1074
|
+
},
|
|
1075
|
+
document,
|
|
1076
|
+
};
|
|
1077
|
+
made.push(entry);
|
|
1078
|
+
return {
|
|
1079
|
+
document,
|
|
1080
|
+
onAuthFailed(cb: (info: { reason: string }) => void) {
|
|
1081
|
+
cbs.add(cb);
|
|
1082
|
+
return () => cbs.delete(cb);
|
|
1083
|
+
},
|
|
1084
|
+
onceSynced() {
|
|
1085
|
+
return new Promise<void>(() => {}); // never settles (rejected docs)
|
|
1086
|
+
},
|
|
1087
|
+
destroy() {
|
|
1088
|
+
entry.destroyed = true;
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1091
|
+
};
|
|
1092
|
+
return { factory, made };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
test('classification + ONE aggregated warn + destroy-on-permanent + re-probe', async () => {
|
|
1096
|
+
const url = 'https://hub.example.com';
|
|
1097
|
+
writeHubsConfig(url, 'mau_test');
|
|
1098
|
+
const ctx = makeCtx({ url, linkedAt: 1 });
|
|
1099
|
+
writeFileSync(join(ctx.paths.designRoot, 'ui', 'a.html'), '<a/>');
|
|
1100
|
+
writeFileSync(join(ctx.paths.designRoot, 'ui', 'b.html'), '<b/>');
|
|
1101
|
+
writeFileSync(join(ctx.paths.designRoot, 'ui', 'c.html'), '<c/>');
|
|
1102
|
+
|
|
1103
|
+
const timers = fakeTimerQueue();
|
|
1104
|
+
const { factory, made } = authStubFactory();
|
|
1105
|
+
const warns: string[] = [];
|
|
1106
|
+
const origWarn = console.warn;
|
|
1107
|
+
console.warn = (...args: unknown[]) => {
|
|
1108
|
+
warns.push(args.map(String).join(' '));
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
try {
|
|
1112
|
+
const runtime = createSyncRuntime(ctx, {
|
|
1113
|
+
providerFactory: factory,
|
|
1114
|
+
auth: {
|
|
1115
|
+
warnDebounceMs: 2_000,
|
|
1116
|
+
reprobeMs: 300_000,
|
|
1117
|
+
settleTimeoutMs: 15_000,
|
|
1118
|
+
setTimer: timers.setTimer,
|
|
1119
|
+
clearTimer: timers.clearTimer,
|
|
1120
|
+
},
|
|
1121
|
+
});
|
|
1122
|
+
await runtime?.start();
|
|
1123
|
+
expect(made).toHaveLength(3);
|
|
1124
|
+
// readdir order is not deterministic — address providers by slug.
|
|
1125
|
+
const bySlug = (slug: string) => {
|
|
1126
|
+
const m = made.find((p) => p.documentName === slug);
|
|
1127
|
+
if (!m) throw new Error(`no provider for ${slug}`);
|
|
1128
|
+
return m;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// Hub rejects: a + b permanently (scope), c transiently (rate limit).
|
|
1132
|
+
bySlug('ui-a').emitAuthFailure('token not authorized for this documentName');
|
|
1133
|
+
bySlug('ui-b').emitAuthFailure('token not authorized for this documentName');
|
|
1134
|
+
bySlug('ui-c').emitAuthFailure('rate limit exceeded for this token');
|
|
1135
|
+
|
|
1136
|
+
// Per-slug statuses are honest immediately.
|
|
1137
|
+
const status = runtime?.status();
|
|
1138
|
+
expect(status?.docs?.rejected).toBe(3);
|
|
1139
|
+
expect(status?.rejectedSlugs?.sort()).toEqual(['ui-a', 'ui-b', 'ui-c']);
|
|
1140
|
+
|
|
1141
|
+
// Permanent classes destroyed (retry storm stopped); transient keeps its
|
|
1142
|
+
// provider (built-in backoff).
|
|
1143
|
+
expect(bySlug('ui-a').destroyed).toBe(true);
|
|
1144
|
+
expect(bySlug('ui-b').destroyed).toBe(true);
|
|
1145
|
+
expect(bySlug('ui-c').destroyed).toBe(false);
|
|
1146
|
+
|
|
1147
|
+
// No warn yet (debounced); ONE aggregated warn after the window.
|
|
1148
|
+
const authWarnsBefore = warns.filter((w) => w.includes('auth rejections')).length;
|
|
1149
|
+
expect(authWarnsBefore).toBe(0);
|
|
1150
|
+
timers.fire(2_000);
|
|
1151
|
+
const authWarns = warns.filter((w) => w.includes('auth rejections'));
|
|
1152
|
+
expect(authWarns).toHaveLength(1);
|
|
1153
|
+
// Reason-specific hints, both classes in the one warn.
|
|
1154
|
+
expect(authWarns[0]).toContain('not-authorized');
|
|
1155
|
+
expect(authWarns[0]).toContain('rate-limit');
|
|
1156
|
+
expect(authWarns[0]).toContain('mint a hub-wide token');
|
|
1157
|
+
expect(authWarns[0]).toContain('HUB_CONN_RATE_LIMIT');
|
|
1158
|
+
|
|
1159
|
+
// Re-probe (5 min): the two permanently-rejected docs reconnect with the
|
|
1160
|
+
// SAME doc instance (agent wiring survives the provider swap).
|
|
1161
|
+
const docA = bySlug('ui-a').document;
|
|
1162
|
+
timers.fire(300_000);
|
|
1163
|
+
await new Promise((res) => setTimeout(res, 10));
|
|
1164
|
+
const reprobed = made.slice(3);
|
|
1165
|
+
expect(reprobed.map((m) => m.documentName).sort()).toEqual(['ui-a', 'ui-b']);
|
|
1166
|
+
expect(reprobed.find((m) => m.documentName === 'ui-a')?.document).toBe(docA);
|
|
1167
|
+
|
|
1168
|
+
await runtime?.stop();
|
|
1169
|
+
} finally {
|
|
1170
|
+
console.warn = origWarn;
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test('boot summary prints AFTER settle with honest counts (not the premature N/N)', async () => {
|
|
1175
|
+
const url = 'https://hub.example.com';
|
|
1176
|
+
writeHubsConfig(url, 'mau_test');
|
|
1177
|
+
const ctx = makeCtx({ url, linkedAt: 1 });
|
|
1178
|
+
writeFileSync(join(ctx.paths.designRoot, 'ui', 'good.html'), '<g/>');
|
|
1179
|
+
writeFileSync(join(ctx.paths.designRoot, 'ui', 'bad.html'), '<b/>');
|
|
1180
|
+
|
|
1181
|
+
const timers = fakeTimerQueue();
|
|
1182
|
+
const logs: string[] = [];
|
|
1183
|
+
const origLog = console.log;
|
|
1184
|
+
const origWarn = console.warn;
|
|
1185
|
+
console.log = (...args: unknown[]) => {
|
|
1186
|
+
logs.push(args.map(String).join(' '));
|
|
1187
|
+
};
|
|
1188
|
+
console.warn = () => {};
|
|
1189
|
+
|
|
1190
|
+
// 'good' syncs immediately; 'bad' is auth-rejected and never settles.
|
|
1191
|
+
const { factory, made } = authStubFactory();
|
|
1192
|
+
const mixedFactory = (args: { documentName: string; document?: Y.Doc }) => {
|
|
1193
|
+
if (args.documentName === 'ui-good') {
|
|
1194
|
+
const document = args.document ?? new Y.Doc();
|
|
1195
|
+
return {
|
|
1196
|
+
document,
|
|
1197
|
+
async onceSynced() {},
|
|
1198
|
+
destroy() {},
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
return factory(args);
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const runtime = createSyncRuntime(ctx, {
|
|
1206
|
+
providerFactory: mixedFactory,
|
|
1207
|
+
auth: {
|
|
1208
|
+
settleTimeoutMs: 15_000,
|
|
1209
|
+
warnDebounceMs: 2_000,
|
|
1210
|
+
setTimer: timers.setTimer,
|
|
1211
|
+
clearTimer: timers.clearTimer,
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
await runtime?.start();
|
|
1215
|
+
made.find((m) => m.documentName === 'ui-bad')?.emitAuthFailure('invalid token');
|
|
1216
|
+
// Boot prints the linking line immediately, NOT a premature N/N.
|
|
1217
|
+
expect(logs.some((l) => l.includes('linking to'))).toBe(true);
|
|
1218
|
+
expect(logs.some((l) => l.includes('synced'))).toBe(false);
|
|
1219
|
+
|
|
1220
|
+
// Let the good handshake's microtask chain run, then hit the ceiling.
|
|
1221
|
+
await new Promise((res) => setTimeout(res, 10));
|
|
1222
|
+
timers.fire(15_000);
|
|
1223
|
+
await new Promise((res) => setTimeout(res, 10));
|
|
1224
|
+
|
|
1225
|
+
const summary = logs.find((l) => l.includes('synced'));
|
|
1226
|
+
expect(summary).toBeDefined();
|
|
1227
|
+
expect(summary).toContain('1/2 synced');
|
|
1228
|
+
expect(summary).toContain('1 auth-rejected');
|
|
1229
|
+
expect(summary).toContain('ui-bad');
|
|
1230
|
+
expect(summary).toContain('invalid-token');
|
|
1231
|
+
|
|
1232
|
+
// lastSyncAt reflects the real reconcile activity of the good canvas.
|
|
1233
|
+
expect(runtime?.status()?.lastSyncAt).not.toBeNull();
|
|
1234
|
+
expect(runtime?.status()?.docs?.synced).toBe(1);
|
|
1235
|
+
|
|
1236
|
+
await runtime?.stop();
|
|
1237
|
+
} finally {
|
|
1238
|
+
console.log = origLog;
|
|
1239
|
+
console.warn = origWarn;
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
@@ -78,3 +78,69 @@ describe('sync status store', () => {
|
|
|
78
78
|
expect(() => store.update(snap({ state: 'offline' }))).not.toThrow();
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
// DDR-102 — the payload carries the per-doc rollup + rich conflicts additively.
|
|
83
|
+
describe('sync status store — DDR-102 additive fields', () => {
|
|
84
|
+
test('docs + rejectedSlugs flow through update() into the payload', () => {
|
|
85
|
+
const writes: SyncStatusPayload[] = [];
|
|
86
|
+
const store = createSyncStatusStore({
|
|
87
|
+
url: 'https://h.example.com',
|
|
88
|
+
canvases: 3,
|
|
89
|
+
write: (p) => writes.push(p),
|
|
90
|
+
});
|
|
91
|
+
store.update({
|
|
92
|
+
state: 'online',
|
|
93
|
+
queuedOps: 0,
|
|
94
|
+
lastSyncAt: 123,
|
|
95
|
+
offlineSince: null,
|
|
96
|
+
flash: null,
|
|
97
|
+
updatedAt: 456,
|
|
98
|
+
docs: { synced: 1, pending: 0, rejected: 2 },
|
|
99
|
+
rejectedSlugs: ['ui-x', 'ui-y'],
|
|
100
|
+
});
|
|
101
|
+
const p = store.get();
|
|
102
|
+
expect(p.docs).toEqual({ synced: 1, pending: 0, rejected: 2 });
|
|
103
|
+
expect(p.rejectedSlugs).toEqual(['ui-x', 'ui-y']);
|
|
104
|
+
expect(p.lastSyncAt).toBe(123);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('addConflict carries winner + snapshots (cold-start-diverged)', () => {
|
|
108
|
+
const store = createSyncStatusStore({
|
|
109
|
+
url: 'https://h.example.com',
|
|
110
|
+
canvases: 1,
|
|
111
|
+
write: () => {},
|
|
112
|
+
now: () => 999,
|
|
113
|
+
});
|
|
114
|
+
store.addConflict({
|
|
115
|
+
slug: 'ui-maskot',
|
|
116
|
+
kind: 'cold-start-diverged',
|
|
117
|
+
winner: 'local',
|
|
118
|
+
snapshots: { local: '2026-06-11T10:00:00.000Z', hub: '2026-06-11T10:00:00.001Z' },
|
|
119
|
+
});
|
|
120
|
+
const c = store.get().conflicts[0];
|
|
121
|
+
expect(c.kind).toBe('cold-start-diverged');
|
|
122
|
+
expect(c.winner).toBe('local');
|
|
123
|
+
expect(c.snapshots?.local).toBe('2026-06-11T10:00:00.000Z');
|
|
124
|
+
expect(c.at).toBe(999);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('old-shape payload readers: a snapshot WITHOUT docs/rejectedSlugs still works', () => {
|
|
128
|
+
const store = createSyncStatusStore({
|
|
129
|
+
url: 'https://h.example.com',
|
|
130
|
+
canvases: 1,
|
|
131
|
+
write: () => {},
|
|
132
|
+
});
|
|
133
|
+
store.update({
|
|
134
|
+
state: 'online',
|
|
135
|
+
queuedOps: 0,
|
|
136
|
+
lastSyncAt: null,
|
|
137
|
+
offlineSince: null,
|
|
138
|
+
flash: null,
|
|
139
|
+
updatedAt: 1,
|
|
140
|
+
});
|
|
141
|
+
const p = store.get();
|
|
142
|
+
expect(p.docs).toBeUndefined();
|
|
143
|
+
expect(p.rejectedSlugs).toBeUndefined();
|
|
144
|
+
expect(p.state).toBe('online');
|
|
145
|
+
});
|
|
146
|
+
});
|