@1agh/maude 1.0.9 → 1.2.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/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/probe.ts +268 -16
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/api.ts +139 -26
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/client/app.jsx +257 -7
- package/apps/studio/client/export-center.jsx +15 -7
- package/apps/studio/client/panels/ReadinessList.jsx +7 -1
- package/apps/studio/client/panels/SyncPanel.jsx +125 -4
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/styles/3-shell-maude.css +34 -0
- package/apps/studio/client/styles/4-components.css +26 -0
- package/apps/studio/collab/persistence.ts +132 -16
- package/apps/studio/collab/registry.ts +10 -18
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1128 -1128
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +7 -1
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/readiness.ts +42 -7
- package/apps/studio/server.ts +11 -4
- package/apps/studio/sync/agent.ts +136 -29
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/codec.ts +122 -31
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-plane.ts +623 -27
- package/apps/studio/sync/index.ts +711 -20
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +40 -3
- package/apps/studio/sync/presentation.ts +26 -3
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/status.ts +122 -5
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/whats-new.json +91 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +889 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +32 -9
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -301
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -13
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -226
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- package/cli/lib/workspace-plan.test.mjs +0 -699
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
// Cloud Phase 25 C2 — the local half of the read-only gate.
|
|
2
|
-
//
|
|
3
|
-
// When the linked hub's credential carries the `viewer` role (vouched at
|
|
4
|
-
// workspace sign-in, stored in hubs.json), the LOCAL dev-server refuses
|
|
5
|
-
// project-mutating writes with a legible 403 — so a viewer's clone can never
|
|
6
|
-
// silently diverge from the cell (which refuses the same writes at sync,
|
|
7
|
-
// Phase 25 C1). Writes are default-denied with a short allowlist
|
|
8
|
-
// (READ_ONLY_ALLOWED_WRITES in http.ts): per-user runtime state (camera,
|
|
9
|
-
// ui-prefs) plus the two role-granted verbs the cell itself allows (export,
|
|
10
|
-
// session management).
|
|
11
|
-
|
|
12
|
-
import { afterAll, describe, expect, test } from 'bun:test';
|
|
13
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import type { Subprocess } from 'bun';
|
|
16
|
-
|
|
17
|
-
import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
|
|
18
|
-
|
|
19
|
-
const HUB_URL = 'http://hub.invalid:9';
|
|
20
|
-
|
|
21
|
-
/** Sandbox linked to a hub whose credential vouches `role`. */
|
|
22
|
-
function makeLinkedSandbox(role?: string) {
|
|
23
|
-
const { root, designRoot } = makeSandbox();
|
|
24
|
-
const cfgPath = join(designRoot, 'config.json');
|
|
25
|
-
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
26
|
-
cfg.linkedHub = { url: HUB_URL, linkedAt: Date.now() };
|
|
27
|
-
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
28
|
-
// The hubs.json credential store the role is read from (isHubReadOnly).
|
|
29
|
-
const hubsPath = join(root, 'hubs.json');
|
|
30
|
-
writeFileSync(
|
|
31
|
-
hubsPath,
|
|
32
|
-
JSON.stringify({
|
|
33
|
-
hubs: { [HUB_URL]: { token: 't', linkedAt: Date.now(), ...(role ? { role } : {}) } },
|
|
34
|
-
})
|
|
35
|
-
);
|
|
36
|
-
return { root, designRoot, hubsPath };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const procs: Subprocess[] = [];
|
|
40
|
-
afterAll(async () => {
|
|
41
|
-
for (const p of procs) await killProc(p);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
async function boot(role?: string) {
|
|
45
|
-
const { root, hubsPath } = makeLinkedSandbox(role);
|
|
46
|
-
const port = nextPort();
|
|
47
|
-
const proc = await bootServer(root, port, { HUBS_CONFIG_PATH: hubsPath });
|
|
48
|
-
procs.push(proc);
|
|
49
|
-
return { base: `http://localhost:${port}` };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe('read-only gate — viewer role refuses project writes locally', () => {
|
|
53
|
-
test('viewer: writes 403 read-only, per-user + role-granted writes pass, /_config says so', async () => {
|
|
54
|
-
const { base } = await boot('viewer');
|
|
55
|
-
|
|
56
|
-
// /_config tells the client at boot.
|
|
57
|
-
const cfg = (await (await fetch(`${base}/_config`)).json()) as { readOnly?: boolean };
|
|
58
|
-
expect(cfg.readOnly).toBe(true);
|
|
59
|
-
|
|
60
|
-
// Project-mutating writes: refused with the legible read-only shape.
|
|
61
|
-
const editText = await fetch(`${base}/_api/edit-text`, {
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers: { 'content-type': 'application/json' },
|
|
64
|
-
body: JSON.stringify({ file: 'ui/fixture.html', text: 'x' }),
|
|
65
|
-
});
|
|
66
|
-
expect(editText.status).toBe(403);
|
|
67
|
-
const body = (await editText.json()) as { error?: string; message?: string };
|
|
68
|
-
expect(body.error).toBe('read-only');
|
|
69
|
-
expect(body.message).toContain('viewer');
|
|
70
|
-
|
|
71
|
-
// Canvas create + delete-element + git commit: same refusal.
|
|
72
|
-
for (const [path, method] of [
|
|
73
|
-
['/_api/canvas', 'POST'],
|
|
74
|
-
['/_api/delete-element', 'POST'],
|
|
75
|
-
['/_api/git/commit', 'POST'],
|
|
76
|
-
['/_api/fs-mkdir', 'POST'],
|
|
77
|
-
['/_api/annotations', 'PUT'],
|
|
78
|
-
['/_api/acp/chat', 'POST'],
|
|
79
|
-
// Cloud Phase 27 D2 — both USED to be on READ_ONLY_ALLOWED_WRITES, on the
|
|
80
|
-
// reasoning that "viewing another branch is not changing one". Both
|
|
81
|
-
// rewrite the working TREE, and the tree is shared: a viewer switching
|
|
82
|
-
// branches replaces the files under whoever is mid-edit. The cell's proxy
|
|
83
|
-
// refuses them too, but this is the gate the hub-linked desktop and the
|
|
84
|
-
// self-host path consult, where no manifest runs at all.
|
|
85
|
-
['/_api/git/checkout', 'POST'],
|
|
86
|
-
['/_api/git/pull', 'POST'],
|
|
87
|
-
] as const) {
|
|
88
|
-
const r = await fetch(base + path, { method, body: '{}' });
|
|
89
|
-
expect(`${path} ${r.status}`).toBe(`${path} 403`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// canvas-meta: the LAYOUT lane is refused…
|
|
93
|
-
const layout = await fetch(`${base}/_api/canvas-meta`, {
|
|
94
|
-
method: 'PATCH',
|
|
95
|
-
headers: { 'content-type': 'application/json' },
|
|
96
|
-
body: JSON.stringify({
|
|
97
|
-
file: '.design/ui/fixture.html',
|
|
98
|
-
patch: { layout: { artboards: [] } },
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
expect(layout.status).toBe(403);
|
|
102
|
-
// …the VIEWPORT lane (per-user camera) is not.
|
|
103
|
-
const viewport = await fetch(`${base}/_api/canvas-meta`, {
|
|
104
|
-
method: 'PATCH',
|
|
105
|
-
headers: { 'content-type': 'application/json' },
|
|
106
|
-
body: JSON.stringify({
|
|
107
|
-
file: '.design/ui/fixture.html',
|
|
108
|
-
patch: { viewport: { x: 0, y: 0, zoom: 1 } },
|
|
109
|
-
}),
|
|
110
|
-
});
|
|
111
|
-
expect(viewport.status).not.toBe(403);
|
|
112
|
-
|
|
113
|
-
// Per-user runtime state stays writable (DDR-115 taxonomy).
|
|
114
|
-
const camera = await fetch(`${base}/_canvas-state`, {
|
|
115
|
-
method: 'POST',
|
|
116
|
-
headers: { 'content-type': 'application/json' },
|
|
117
|
-
body: JSON.stringify({ file: '.design/ui/fixture.html', viewport: { x: 1, y: 2, zoom: 1 } }),
|
|
118
|
-
});
|
|
119
|
-
expect(camera.status).toBe(204);
|
|
120
|
-
|
|
121
|
-
// Role-granted verb: export reaches its handler (may 400 on shape, never 403).
|
|
122
|
-
const exp = await fetch(`${base}/_api/export`, { method: 'POST', body: '{}' });
|
|
123
|
-
expect(exp.status).not.toBe(403);
|
|
124
|
-
|
|
125
|
-
// Reads are untouched.
|
|
126
|
-
expect((await fetch(`${base}/_index-data`)).status).toBe(200);
|
|
127
|
-
expect((await fetch(`${base}/_comments-all`)).status).toBe(200);
|
|
128
|
-
}, 30000);
|
|
129
|
-
|
|
130
|
-
test('member (no role on the credential): writes are NOT gated', async () => {
|
|
131
|
-
const { base } = await boot(undefined);
|
|
132
|
-
const cfg = (await (await fetch(`${base}/_config`)).json()) as { readOnly?: boolean };
|
|
133
|
-
expect(cfg.readOnly).toBe(false);
|
|
134
|
-
// A write reaches its handler (400 on bad shape — not the 403 refusal).
|
|
135
|
-
const r = await fetch(`${base}/_api/edit-text`, { method: 'POST', body: '{}' });
|
|
136
|
-
expect(r.status).not.toBe(403);
|
|
137
|
-
}, 30000);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
// Cloud Phase 27 — the one write a viewer holds.
|
|
142
|
-
//
|
|
143
|
-
// The role matrix has said `viewer.comment === true` since Phase 25 C4 and the
|
|
144
|
-
// People page promises it in those words. This list did not have it, so a
|
|
145
|
-
// viewer's comment was allowed by the authority (the cell's proxy) and then
|
|
146
|
-
// refused by the defence-in-depth layer behind it. Found by commenting as a
|
|
147
|
-
// viewer against a real cell, not by reading either file.
|
|
148
|
-
|
|
149
|
-
describe('a viewer may comment — the matrix says so, and so must this gate', () => {
|
|
150
|
-
test('comment + reply pass; annotate still does not', async () => {
|
|
151
|
-
const { base } = await boot('viewer');
|
|
152
|
-
|
|
153
|
-
const comment = await fetch(`${base}/_comments`, {
|
|
154
|
-
method: 'POST',
|
|
155
|
-
headers: { 'content-type': 'application/json' },
|
|
156
|
-
body: JSON.stringify({ file: 'ui/Sample.tsx', text: 'looks off to me' }),
|
|
157
|
-
});
|
|
158
|
-
expect(comment.status).not.toBe(403);
|
|
159
|
-
|
|
160
|
-
const reply = await fetch(`${base}/_api/comments/abc123/reply`, {
|
|
161
|
-
method: 'POST',
|
|
162
|
-
headers: { 'content-type': 'application/json' },
|
|
163
|
-
body: JSON.stringify({ text: 'agreed' }),
|
|
164
|
-
});
|
|
165
|
-
expect(reply.status).not.toBe(403);
|
|
166
|
-
|
|
167
|
-
// An annotation is drawn ON the design and versioned with it — the matrix
|
|
168
|
-
// files it with `edit`, and that distinction has to survive this change.
|
|
169
|
-
const annotate = await fetch(`${base}/_api/annotations`, {
|
|
170
|
-
method: 'PUT',
|
|
171
|
-
headers: { 'content-type': 'application/json' },
|
|
172
|
-
body: JSON.stringify({ file: 'ui/Sample.tsx', svg: '<svg/>' }),
|
|
173
|
-
});
|
|
174
|
-
expect(annotate.status).toBe(403);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('the reply pattern does not open the whole comment namespace', async () => {
|
|
178
|
-
const { base } = await boot('viewer');
|
|
179
|
-
for (const path of [
|
|
180
|
-
'/_api/comments/abc/delete',
|
|
181
|
-
'/_api/comments/abc/reply/../../edit-text',
|
|
182
|
-
'/_api/comments//reply',
|
|
183
|
-
]) {
|
|
184
|
-
const res = await fetch(`${base}${path}`, { method: 'POST' });
|
|
185
|
-
expect(res.status).not.toBe(200);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// The client learns it is a viewer at BOOT — Cloud Phase 25 C2.
|
|
2
|
-
//
|
|
3
|
-
// The alternative is learning it from the first refused write, which means
|
|
4
|
-
// drawing somebody an editor and then taking it away. So the role the
|
|
5
|
-
// workspace vouched for at sign-in is persisted with the credential and
|
|
6
|
-
// resolved before the UI paints.
|
|
7
|
-
//
|
|
8
|
-
// This flag decides what the UI OFFERS. It is never what stops a write: the
|
|
9
|
-
// cell enforces that (Phase 25 C1 — a read-only peer token, Hocuspocus
|
|
10
|
-
// dropping SyncStep2/Update, one HTTP gate), and it holds whatever a patched
|
|
11
|
-
// client believes about itself.
|
|
12
|
-
|
|
13
|
-
import { describe, expect, test } from 'bun:test';
|
|
14
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
15
|
-
import { tmpdir } from 'node:os';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
|
|
18
|
-
import { saveHubCredential } from '../sync/hub-link.ts';
|
|
19
|
-
import { signInToWorkspace } from '../sync/workspace-signin.ts';
|
|
20
|
-
|
|
21
|
-
function withHome<T>(fn: (home: string) => T): T {
|
|
22
|
-
const home = mkdtempSync(join(tmpdir(), 'maude-ro-'));
|
|
23
|
-
const prevXdg = process.env.XDG_CONFIG_HOME;
|
|
24
|
-
process.env.XDG_CONFIG_HOME = home;
|
|
25
|
-
try {
|
|
26
|
-
return fn(home);
|
|
27
|
-
} finally {
|
|
28
|
-
if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
29
|
-
else process.env.XDG_CONFIG_HOME = prevXdg;
|
|
30
|
-
rmSync(home, { recursive: true, force: true });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('the vouched role is persisted with the credential', () => {
|
|
35
|
-
test('a viewer is recorded as one', () => {
|
|
36
|
-
withHome((home) => {
|
|
37
|
-
saveHubCredential('https://p.cloud.maude.sh', 'mau_tok', 'viewer');
|
|
38
|
-
const cfg = JSON.parse(readFileSync(join(home, 'maude', 'hubs.json'), 'utf8'));
|
|
39
|
-
expect(cfg.hubs['https://p.cloud.maude.sh'].role).toBe('viewer');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('a credential written without a role stays write-capable', () => {
|
|
44
|
-
// The upgrade promise: nobody is demoted by shipping this.
|
|
45
|
-
withHome((home) => {
|
|
46
|
-
saveHubCredential('https://p.cloud.maude.sh', 'mau_tok');
|
|
47
|
-
const cfg = JSON.parse(readFileSync(join(home, 'maude', 'hubs.json'), 'utf8'));
|
|
48
|
-
expect(cfg.hubs['https://p.cloud.maude.sh'].role).toBeUndefined();
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('sign-in carries the role through to the credential', () => {
|
|
54
|
-
const okHub = (role: string) =>
|
|
55
|
-
(async (url: string) => {
|
|
56
|
-
const u = String(url);
|
|
57
|
-
if (u.endsWith('/health')) return Response.json({ ok: true, version: '1.0.0' });
|
|
58
|
-
return Response.json({ token: 'mau_minted', user: { email: 'v@example.com', role } });
|
|
59
|
-
}) as unknown as typeof fetch;
|
|
60
|
-
|
|
61
|
-
test('a viewer sign-in saves the role, and reports it back', async () => {
|
|
62
|
-
const saved: Array<[string, string, string | undefined]> = [];
|
|
63
|
-
const res = await signInToWorkspace(
|
|
64
|
-
{ url: 'https://p.cloud.maude.sh', email: 'v@example.com', password: 'x'.repeat(12) },
|
|
65
|
-
{
|
|
66
|
-
save: (u, t, r) => saved.push([u, t, r]),
|
|
67
|
-
fetchImpl: okHub('viewer'),
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
expect(res.status).toBe(200);
|
|
71
|
-
expect(saved[0][2]).toBe('viewer');
|
|
72
|
-
expect((res.json as { user: { role: string } }).user.role).toBe('viewer');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('a hub that names no role is treated as member, never as viewer', async () => {
|
|
76
|
-
// Failing OPEN on the UI flag is correct: the cell is what refuses a
|
|
77
|
-
// write, so a wrong guess here costs a redundant affordance, never
|
|
78
|
-
// access. Guessing "viewer" instead would lock out every self-hoster
|
|
79
|
-
// whose hub predates this field.
|
|
80
|
-
const saved: Array<[string, string, string | undefined]> = [];
|
|
81
|
-
const res = await signInToWorkspace(
|
|
82
|
-
{ url: 'https://hub.example.com', email: 'm@example.com', password: 'x'.repeat(12) },
|
|
83
|
-
{
|
|
84
|
-
save: (u, t, r) => saved.push([u, t, r]),
|
|
85
|
-
fetchImpl: (async (url: string) => {
|
|
86
|
-
const u = String(url);
|
|
87
|
-
if (u.endsWith('/health')) return Response.json({ ok: true, version: '1.0.0' });
|
|
88
|
-
return Response.json({ token: 'mau_minted', user: { email: 'm@example.com' } });
|
|
89
|
-
}) as unknown as typeof fetch,
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
expect(res.status).toBe(200);
|
|
93
|
-
expect(saved[0][2]).toBe('member');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
// Readiness probe (DDR-128) — plugin-registry scan + PATH resolution. The
|
|
2
|
-
// login-shell fallback's full correctness is proven on the packaged `.app`
|
|
3
|
-
// (native-verification ceiling); here we cover the registry-scan branches, the
|
|
4
|
-
// no-throw contracts, and the report shape. The probe is async (the fallback shells
|
|
5
|
-
// out off the event loop — DDR-128 hardening), so everything awaits.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, test } from 'bun:test';
|
|
8
|
-
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
|
|
12
|
-
import { probeReadiness, resolveOnPath } from '../readiness.ts';
|
|
13
|
-
|
|
14
|
-
const MARKET_OK = {
|
|
15
|
-
maude: { source: { source: 'github', repo: '1aGh/maude' }, installLocation: '/x' },
|
|
16
|
-
};
|
|
17
|
-
const INSTALLED_BOTH = {
|
|
18
|
-
version: 2,
|
|
19
|
-
plugins: {
|
|
20
|
-
'design@maude': [{ scope: 'user', version: '0.29.0' }],
|
|
21
|
-
'flow@maude': [{ scope: 'user', version: '0.29.0' }],
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
const INSTALLED_DESIGN_ONLY = { version: 2, plugins: { 'design@maude': [{ scope: 'user' }] } };
|
|
25
|
-
// `/flow` is no longer part of the chat (2026-07-03) — the gate tracks `design`
|
|
26
|
-
// alone, so a registry with flow-but-not-design is the "design missing" case.
|
|
27
|
-
const INSTALLED_FLOW_ONLY = { version: 2, plugins: { 'flow@maude': [{ scope: 'user' }] } };
|
|
28
|
-
|
|
29
|
-
function fixtureClaudeDir(opts: { markets?: unknown; installed?: unknown }): string {
|
|
30
|
-
const dir = mkdtempSync(join(tmpdir(), 'maude-readiness-'));
|
|
31
|
-
const pluginsDir = join(dir, 'plugins');
|
|
32
|
-
mkdirSync(pluginsDir, { recursive: true });
|
|
33
|
-
if (opts.markets !== undefined)
|
|
34
|
-
writeFileSync(join(pluginsDir, 'known_marketplaces.json'), JSON.stringify(opts.markets));
|
|
35
|
-
if (opts.installed !== undefined)
|
|
36
|
-
writeFileSync(join(pluginsDir, 'installed_plugins.json'), JSON.stringify(opts.installed));
|
|
37
|
-
return dir;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function withClaudeDir<T>(dir: string, fn: () => Promise<T> | T): Promise<T> {
|
|
41
|
-
const saved = process.env.CLAUDE_CONFIG_DIR;
|
|
42
|
-
process.env.CLAUDE_CONFIG_DIR = dir;
|
|
43
|
-
try {
|
|
44
|
-
return await fn();
|
|
45
|
-
} finally {
|
|
46
|
-
if (saved === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
47
|
-
else process.env.CLAUDE_CONFIG_DIR = saved;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Plugins row under the given ~/.claude fixture. `nativeBootstrap:false` sets
|
|
52
|
-
* MAUDE_NO_PLUGIN_BOOTSTRAP to force the web / manual (DDR-128) path — needed to
|
|
53
|
-
* isolate the registry scan, since this suite runs in the dev tree where the
|
|
54
|
-
* bundled plugin dirs resolve and DDR-143 auto-load would otherwise mask it. */
|
|
55
|
-
const pluginsItem = async (dir: string, opts: { nativeBootstrap?: boolean } = {}) => {
|
|
56
|
-
const savedOptOut = process.env.MAUDE_NO_PLUGIN_BOOTSTRAP;
|
|
57
|
-
if (opts.nativeBootstrap === false) process.env.MAUDE_NO_PLUGIN_BOOTSTRAP = '1';
|
|
58
|
-
try {
|
|
59
|
-
return (await withClaudeDir(dir, () => probeReadiness())).items.find(
|
|
60
|
-
(i) => i.id === 'plugins'
|
|
61
|
-
)!;
|
|
62
|
-
} finally {
|
|
63
|
-
if (savedOptOut === undefined) delete process.env.MAUDE_NO_PLUGIN_BOOTSTRAP;
|
|
64
|
-
else process.env.MAUDE_NO_PLUGIN_BOOTSTRAP = savedOptOut;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Registry-scan branches under the WEB / manual path (auto-bootstrap opted out) —
|
|
69
|
-
// the pure DDR-128 detect-and-guide behavior, still what a `maude design serve`
|
|
70
|
-
// user sees.
|
|
71
|
-
describe('readiness — plugin registry scan (web / manual path)', () => {
|
|
72
|
-
test('design@maude + marketplace present → plugins item is present, no remediation', async () => {
|
|
73
|
-
const item = await pluginsItem(
|
|
74
|
-
fixtureClaudeDir({ markets: MARKET_OK, installed: INSTALLED_DESIGN_ONLY }),
|
|
75
|
-
{ nativeBootstrap: false }
|
|
76
|
-
);
|
|
77
|
-
expect(item.status).toBe('present');
|
|
78
|
-
expect(item.remediation).toBeUndefined();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('design@maude NOT installed → missing, names the absent design@maude + offers the fix', async () => {
|
|
82
|
-
// flow-only registry: `/flow` is irrelevant to the chat now, so this reads as
|
|
83
|
-
// "design missing" — the gate tracks design alone.
|
|
84
|
-
const item = await pluginsItem(
|
|
85
|
-
fixtureClaudeDir({ markets: MARKET_OK, installed: INSTALLED_FLOW_ONLY }),
|
|
86
|
-
{ nativeBootstrap: false }
|
|
87
|
-
);
|
|
88
|
-
expect(item.status).toBe('missing');
|
|
89
|
-
expect(item.detail).toContain('design@maude');
|
|
90
|
-
expect(item.remediation).toContain('/plugin install');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('registry absent → unknown (Claude Code internal contract; never throws)', async () => {
|
|
94
|
-
const item = await pluginsItem(fixtureClaudeDir({}), { nativeBootstrap: false });
|
|
95
|
-
expect(item.status).toBe('unknown');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('plugin presence is the gate — a foreign-repo marketplace does not change a design-installed verdict', async () => {
|
|
99
|
-
const item = await pluginsItem(
|
|
100
|
-
fixtureClaudeDir({
|
|
101
|
-
markets: { other: { source: { repo: 'someone/else' } } },
|
|
102
|
-
installed: INSTALLED_BOTH,
|
|
103
|
-
}),
|
|
104
|
-
{ nativeBootstrap: false }
|
|
105
|
-
);
|
|
106
|
-
expect(item.status).toBe('present');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// DDR-143 / DDR-168 — on the native/desktop path the ACP session ALWAYS loads
|
|
111
|
-
// the bundled plugins, so the row is satisfied even against a pristine
|
|
112
|
-
// registry, AND even when a marketplace copy is also installed at the user
|
|
113
|
-
// level (DDR-168 removed the old scan-gated no-op — the bundled copy wins
|
|
114
|
-
// regardless of disk state). This suite runs in the dev tree, so the bundled
|
|
115
|
-
// plugin dirs resolve → native context is on by default (no opt-out).
|
|
116
|
-
describe('readiness — plugin auto-bootstrap (native, DDR-143/DDR-168)', () => {
|
|
117
|
-
test('pristine registry → present + "Bundled with this app" detail, no manual remediation', async () => {
|
|
118
|
-
const item = await pluginsItem(fixtureClaudeDir({}));
|
|
119
|
-
expect(item.status).toBe('present');
|
|
120
|
-
expect(item.detail).toContain('Bundled with this app');
|
|
121
|
-
expect(item.remediation).toBeUndefined();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('design NOT installed (flow-only registry) → still present (design is bundled, no red wall)', async () => {
|
|
125
|
-
const item = await pluginsItem(
|
|
126
|
-
fixtureClaudeDir({ markets: MARKET_OK, installed: INSTALLED_FLOW_ONLY })
|
|
127
|
-
);
|
|
128
|
-
expect(item.status).toBe('present');
|
|
129
|
-
expect(item.detail).toContain('Bundled with this app');
|
|
130
|
-
expect(item.remediation).toBeUndefined();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('design ALSO installed on disk → present, STILL reads as bundled (DDR-168 — no more "is installed" framing)', async () => {
|
|
134
|
-
const item = await pluginsItem(
|
|
135
|
-
fixtureClaudeDir({ markets: MARKET_OK, installed: INSTALLED_DESIGN_ONLY })
|
|
136
|
-
);
|
|
137
|
-
expect(item.status).toBe('present');
|
|
138
|
-
expect(item.detail).toContain('Bundled with this app');
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('readiness — resolveOnPath', () => {
|
|
143
|
-
test('finds a binary that is on PATH', async () => {
|
|
144
|
-
const hit = await resolveOnPath('sh');
|
|
145
|
-
expect(hit).not.toBeNull();
|
|
146
|
-
expect(existsSync(hit!)).toBe(true);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('returns null for a bogus binary without throwing', async () => {
|
|
150
|
-
expect(await resolveOnPath('maude-definitely-not-real-xyz')).toBeNull();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test('login-shell fallback recovers a binary hidden from the app PATH', async () => {
|
|
154
|
-
const saved = process.env.PATH;
|
|
155
|
-
try {
|
|
156
|
-
// Simulate the truncated/empty GUI env so Bun.which misses and the fallback fires.
|
|
157
|
-
process.env.PATH = '';
|
|
158
|
-
const hit = await resolveOnPath('sh');
|
|
159
|
-
// When a login shell is usable it must recover an absolute, real path. In a
|
|
160
|
-
// sandbox with no usable login shell the fallback returns null — we don't fail
|
|
161
|
-
// on that, but a non-null result must be a real absolute path.
|
|
162
|
-
if (hit !== null) {
|
|
163
|
-
expect(hit.startsWith('/')).toBe(true);
|
|
164
|
-
expect(existsSync(hit)).toBe(true);
|
|
165
|
-
}
|
|
166
|
-
} finally {
|
|
167
|
-
if (saved === undefined) delete process.env.PATH;
|
|
168
|
-
else process.env.PATH = saved;
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('readiness — report shape', () => {
|
|
174
|
-
test('always returns the five items with stable ids and a boolean ready', async () => {
|
|
175
|
-
const report = await probeReadiness();
|
|
176
|
-
expect(report.items.map((i) => i.id)).toEqual([
|
|
177
|
-
'claude',
|
|
178
|
-
'maude',
|
|
179
|
-
'plugins',
|
|
180
|
-
'agent-browser',
|
|
181
|
-
'adapter',
|
|
182
|
-
]);
|
|
183
|
-
expect(typeof report.ready).toBe('boolean');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('screenshot engine: required + present (bundled) on native → never blocks ready', async () => {
|
|
187
|
-
// Native (this dev tree): agent-browser is bundled, so the row is first-class
|
|
188
|
-
// (required) but PRESENT — it can't turn `ready` false.
|
|
189
|
-
const ab = (await probeReadiness()).items.find((i) => i.id === 'agent-browser')!;
|
|
190
|
-
expect(ab.required).toBe(true);
|
|
191
|
-
expect(ab.status).toBe('present');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('screenshot engine: optional on the web/opt-out path', async () => {
|
|
195
|
-
const saved = process.env.MAUDE_NO_PLUGIN_BOOTSTRAP;
|
|
196
|
-
process.env.MAUDE_NO_PLUGIN_BOOTSTRAP = '1'; // force native=false
|
|
197
|
-
try {
|
|
198
|
-
const ab = (await probeReadiness()).items.find((i) => i.id === 'agent-browser')!;
|
|
199
|
-
expect(ab.required).toBe(false);
|
|
200
|
-
} finally {
|
|
201
|
-
if (saved === undefined) delete process.env.MAUDE_NO_PLUGIN_BOOTSTRAP;
|
|
202
|
-
else process.env.MAUDE_NO_PLUGIN_BOOTSTRAP = saved;
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test('adapter row tracks the real chat gate (resolveAdapterEntry) and is required', async () => {
|
|
207
|
-
const saved = process.env.MAUDE_ACP_ADAPTER_ENTRY;
|
|
208
|
-
try {
|
|
209
|
-
// Point the override at a real file → the bridge is "bundled".
|
|
210
|
-
process.env.MAUDE_ACP_ADAPTER_ENTRY = import.meta.path;
|
|
211
|
-
const present = (await probeReadiness()).items.find((i) => i.id === 'adapter')!;
|
|
212
|
-
expect(present.required).toBe(true);
|
|
213
|
-
expect(present.status).toBe('present');
|
|
214
|
-
|
|
215
|
-
// Point it at a missing path → the row goes missing with remediation, the
|
|
216
|
-
// honest signal the v0.31–0.32 builds lacked (all other rows green, chat dead).
|
|
217
|
-
process.env.MAUDE_ACP_ADAPTER_ENTRY = join(tmpdir(), 'no-such-acp-adapter-xyz');
|
|
218
|
-
const missing = (await probeReadiness()).items.find((i) => i.id === 'adapter')!;
|
|
219
|
-
expect(missing.status).toBe('missing');
|
|
220
|
-
expect(missing.remediation).toBeDefined();
|
|
221
|
-
} finally {
|
|
222
|
-
if (saved === undefined) delete process.env.MAUDE_ACP_ADAPTER_ENTRY;
|
|
223
|
-
else process.env.MAUDE_ACP_ADAPTER_ENTRY = saved;
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// DDR-132 — remote ahead/behind probe: in-memory TTL cache + in-flight dedupe.
|
|
2
|
-
//
|
|
3
|
-
// Verifies the cache WRAPPER (TTL-serve / invalidate / in-flight coalesce) fully
|
|
4
|
-
// offline, with no network and no git spawn. Trick: the probe's DDR-131 transport
|
|
5
|
-
// gate classifies a bare local-path remote as `unsafe`, so `remoteAheadBehind`
|
|
6
|
-
// returns a FRESH `{ahead:0,behind:0}` object on every *uncached* evaluation
|
|
7
|
-
// without touching the network. Object IDENTITY (`Object.is`) therefore tells a
|
|
8
|
-
// cache-serve (same reference) apart from a re-run (new reference) — a stronger
|
|
9
|
-
// signal than value equality, and one the security hardening can't invalidate.
|
|
10
|
-
//
|
|
11
|
-
// (The real fetch/count path needs a reachable github.com remote and is covered
|
|
12
|
-
// by the manual native dogfood — same offline ceiling DDR-131 records for
|
|
13
|
-
// `gitFetchRemote`. This file owns the wrapper semantics only.)
|
|
14
|
-
//
|
|
15
|
-
// This file deliberately does NOT set MAUDE_USE_SYSTEM_GIT (it would leak into the
|
|
16
|
-
// shared bun-test process and flip other files' iso-engine expectations).
|
|
17
|
-
|
|
18
|
-
import { afterEach, beforeEach, expect, test } from 'bun:test';
|
|
19
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
-
import { tmpdir } from 'node:os';
|
|
21
|
-
import { join } from 'node:path';
|
|
22
|
-
import { invalidateRemoteProbe, remoteAheadBehind } from '../git/service.ts';
|
|
23
|
-
|
|
24
|
-
function git(cwd: string, args: string[]): void {
|
|
25
|
-
const p = Bun.spawnSync(['git', ...args], {
|
|
26
|
-
cwd,
|
|
27
|
-
env: { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' },
|
|
28
|
-
});
|
|
29
|
-
if (p.exitCode !== 0) throw new Error(`git ${args.join(' ')}: ${p.stderr.toString()}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let work: string;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
work = mkdtempSync(join(tmpdir(), 'maude-ab-work-'));
|
|
36
|
-
git(work, ['init', '-q', '-b', 'main']);
|
|
37
|
-
git(work, ['config', 'user.email', 't@t.dev']);
|
|
38
|
-
git(work, ['config', 'user.name', 'Tester']);
|
|
39
|
-
writeFileSync(join(work, 'a.txt'), 'hello\n');
|
|
40
|
-
git(work, ['add', '.']);
|
|
41
|
-
git(work, ['commit', '-q', '-m', 'init']);
|
|
42
|
-
// A bare local path → DDR-131 transport gate classifies it `unsafe` → the probe
|
|
43
|
-
// returns {0,0} with no spawn. (Any value is fine; we assert on identity.)
|
|
44
|
-
git(work, ['remote', 'add', 'origin', '/var/empty/maude-test-remote']);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
invalidateRemoteProbe(work);
|
|
49
|
-
rmSync(work, { recursive: true, force: true });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('a second call within the TTL serves the cached object (no re-run)', async () => {
|
|
53
|
-
const a = await remoteAheadBehind(work, undefined);
|
|
54
|
-
expect(a).toEqual({ ahead: 0, behind: 0 });
|
|
55
|
-
const b = await remoteAheadBehind(work, undefined);
|
|
56
|
-
expect(Object.is(a, b)).toBe(true); // same reference ⇒ served from cache
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('invalidateRemoteProbe forces a fresh evaluation (new object)', async () => {
|
|
60
|
-
const a = await remoteAheadBehind(work, undefined);
|
|
61
|
-
invalidateRemoteProbe(work);
|
|
62
|
-
const c = await remoteAheadBehind(work, undefined);
|
|
63
|
-
expect(c).toEqual({ ahead: 0, behind: 0 });
|
|
64
|
-
expect(Object.is(a, c)).toBe(false); // new reference ⇒ re-ran after invalidation
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('SECURITY (DDR-133 F1): the unattended probe never fetches a non-github HTTP remote', async () => {
|
|
68
|
-
// Repoint origin to a non-github HTTPS host (a poisoned `.git/config` riding a clone).
|
|
69
|
-
// With system git auto-preferred (DDR-133), the host-allowlist must gate this probe
|
|
70
|
-
// BEFORE the engine branch — otherwise an http transport reached `git fetch` against
|
|
71
|
-
// ANY host on the unattended 45s poll (SSRF / presence beacon). The guard returns
|
|
72
|
-
// {0,0} with NO spawn; a regression would attempt the fetch and reject/hang here.
|
|
73
|
-
git(work, ['remote', 'set-url', 'origin', 'https://evil.example.invalid/repo.git']);
|
|
74
|
-
invalidateRemoteProbe(work);
|
|
75
|
-
const r = await remoteAheadBehind(work, 'tok_must_never_be_sent');
|
|
76
|
-
expect(r).toEqual({ ahead: 0, behind: 0 });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('concurrent callers coalesce onto one shared in-flight result', async () => {
|
|
80
|
-
const [a, b, c] = await Promise.all([
|
|
81
|
-
remoteAheadBehind(work, undefined),
|
|
82
|
-
remoteAheadBehind(work, undefined),
|
|
83
|
-
remoteAheadBehind(work, undefined),
|
|
84
|
-
]);
|
|
85
|
-
// One shared in-flight promise (or the just-written cache) ⇒ identical reference,
|
|
86
|
-
// never three independent evaluations.
|
|
87
|
-
expect(Object.is(a, b)).toBe(true);
|
|
88
|
-
expect(Object.is(b, c)).toBe(true);
|
|
89
|
-
});
|