@1agh/maude 1.0.11 → 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/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/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/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 +72 -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 -545
- 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/backgrounding-claude-auth.sh +0 -12
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
- 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/noisy-claude-auth.mjs +0 -27
- 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/fixtures/wedged-claude-auth.sh +0 -11
- 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 -337
- 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,931 +0,0 @@
|
|
|
1
|
-
// DDR-217 + the 2026-08-11 addendum — the desktop→cell asset push, both
|
|
2
|
-
// classes. Properties under test: the sweep finds top-level content-addressed
|
|
3
|
-
// assets AND nested DS/brand assets under system/*/assets/; each routes to the
|
|
4
|
-
// right hub endpoint (bucket `/assets/` vs checkout `/_asset-file/`); non-asset
|
|
5
|
-
// files (.photo.json) and junk are filtered; the sweep is HEAD-first; the
|
|
6
|
-
// credential is read at call time; a failure is reported, never thrown.
|
|
7
|
-
|
|
8
|
-
import { describe, expect, test } from 'bun:test';
|
|
9
|
-
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
-
import { tmpdir } from 'node:os';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
isPushableAssetRel,
|
|
15
|
-
listPushableAssets,
|
|
16
|
-
pushAssets,
|
|
17
|
-
putTimeoutMs,
|
|
18
|
-
} from '../sync/asset-push.ts';
|
|
19
|
-
|
|
20
|
-
function scratchDesignRoot(): string {
|
|
21
|
-
return mkdtempSync(join(tmpdir(), 'asset-push-'));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('listPushableAssets — classifier membership over the whole design root', () => {
|
|
25
|
-
test('finds top-level content-addressed AND nested DS/brand assets', () => {
|
|
26
|
-
const designRoot = scratchDesignRoot();
|
|
27
|
-
// Class 1 — top-level content-addressed uploads (+ a .photo.json sidecar).
|
|
28
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
29
|
-
writeFileSync(join(designRoot, 'assets/a1b2c3d4.png'), 'x');
|
|
30
|
-
writeFileSync(join(designRoot, 'assets/a1b2c3d4.photo.json'), '{}'); // metadata, not an asset
|
|
31
|
-
// Class 2 — DS/brand assets under system/<ds>/assets/ (the ones that stayed grey).
|
|
32
|
-
mkdirSync(join(designRoot, 'system/alligators/assets/logos'), { recursive: true });
|
|
33
|
-
mkdirSync(join(designRoot, 'system/alligators/assets/fonts'), { recursive: true });
|
|
34
|
-
mkdirSync(join(designRoot, 'system/alligators/assets/photos'), { recursive: true });
|
|
35
|
-
writeFileSync(join(designRoot, 'system/alligators/assets/logos/horizontal-green.svg'), 'x');
|
|
36
|
-
writeFileSync(join(designRoot, 'system/alligators/assets/fonts/Gators-Bold.woff2'), 'x');
|
|
37
|
-
writeFileSync(join(designRoot, 'system/alligators/assets/photos/P1020428.JPG'), 'x'); // uppercase ext
|
|
38
|
-
// Not under any assets/ dir → never swept.
|
|
39
|
-
mkdirSync(join(designRoot, 'system/alligators/preview'), { recursive: true });
|
|
40
|
-
writeFileSync(join(designRoot, 'system/alligators/preview/logo.tsx'), 'x');
|
|
41
|
-
// Runtime state is skipped.
|
|
42
|
-
mkdirSync(join(designRoot, '_history/assets'), { recursive: true });
|
|
43
|
-
writeFileSync(join(designRoot, '_history/assets/old.png'), 'x');
|
|
44
|
-
|
|
45
|
-
// `syncFiles: false` is now stated rather than assumed: the flag defaults
|
|
46
|
-
// ON from Increment 4, so a test that meant "the narrow media sweep" has
|
|
47
|
-
// to say so. What it pins is unchanged.
|
|
48
|
-
expect(listPushableAssets(designRoot, { syncFiles: false })).toEqual([
|
|
49
|
-
'assets/a1b2c3d4.png',
|
|
50
|
-
'system/alligators/assets/fonts/Gators-Bold.woff2',
|
|
51
|
-
'system/alligators/assets/logos/horizontal-green.svg',
|
|
52
|
-
'system/alligators/assets/photos/P1020428.JPG',
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
|
-
// With the plane on, the photo sidecar travels too — it is the
|
|
56
|
-
// non-destructive edit state for that image, and an image whose edits stay
|
|
57
|
-
// behind arrives looking wrong rather than arriving late.
|
|
58
|
-
expect(listPushableAssets(designRoot, { syncFiles: true })).toContain(
|
|
59
|
-
'assets/a1b2c3d4.photo.json'
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('non-asset extensions and dotfiles are filtered; missing root is []', () => {
|
|
64
|
-
const designRoot = scratchDesignRoot();
|
|
65
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
66
|
-
writeFileSync(join(designRoot, 'assets/notes.txt'), 'x');
|
|
67
|
-
writeFileSync(join(designRoot, 'assets/data.json'), 'x');
|
|
68
|
-
writeFileSync(join(designRoot, 'assets/.hidden'), 'x');
|
|
69
|
-
expect(listPushableAssets(designRoot)).toEqual([]);
|
|
70
|
-
expect(listPushableAssets(join(designRoot, 'nope'))).toEqual([]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('depth beyond the hub 8-segment cap is skipped', () => {
|
|
74
|
-
const designRoot = scratchDesignRoot();
|
|
75
|
-
// assets/a/b/c/d/e/f/g/x.png = 9 segments > 8 → skipped.
|
|
76
|
-
mkdirSync(join(designRoot, 'assets/a/b/c/d/e/f/g'), { recursive: true });
|
|
77
|
-
writeFileSync(join(designRoot, 'assets/a/b/c/d/e/f/g/x.png'), 'x');
|
|
78
|
-
expect(listPushableAssets(designRoot)).toEqual([]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('the file-plane widening: the RCA miss-list goes up, canvas lanes stay home', () => {
|
|
82
|
-
// feature-sync-file-plane — the sweep now carries what the fresh-link RCA
|
|
83
|
-
// named laneless: token stylesheets, docs, shared modules, underscore
|
|
84
|
-
// FILES. A canvas body and its named sidecars stay the CRDT lanes'.
|
|
85
|
-
const designRoot = scratchDesignRoot();
|
|
86
|
-
mkdirSync(join(designRoot, 'system/ds/preview'), { recursive: true });
|
|
87
|
-
writeFileSync(join(designRoot, 'config.json'), '{}');
|
|
88
|
-
writeFileSync(join(designRoot, 'system/ds/brand.css'), 'x');
|
|
89
|
-
writeFileSync(join(designRoot, 'system/ds/README.md'), 'x');
|
|
90
|
-
writeFileSync(join(designRoot, 'system/ds/preview/_brand-css.ts'), 'x');
|
|
91
|
-
writeFileSync(join(designRoot, 'system/ds/preview/_layout.css'), 'x');
|
|
92
|
-
writeFileSync(join(designRoot, 'system/ds/preview/specimen.tsx'), 'x');
|
|
93
|
-
writeFileSync(join(designRoot, 'system/ds/preview/specimen.css'), 'x'); // sibling css lane
|
|
94
|
-
writeFileSync(join(designRoot, 'system/ds/preview/specimen.meta.json'), '{}');
|
|
95
|
-
expect(listPushableAssets(designRoot, { syncFiles: true })).toEqual([
|
|
96
|
-
'system/ds/README.md',
|
|
97
|
-
'system/ds/brand.css',
|
|
98
|
-
'system/ds/preview/_brand-css.ts',
|
|
99
|
-
'system/ds/preview/_layout.css',
|
|
100
|
-
]);
|
|
101
|
-
// Explicitly OFF ⇒ the pre-Sync-v2 reach, byte-for-byte: none of these are
|
|
102
|
-
// binary media under an assets/ dir, so none of them move. This is the
|
|
103
|
-
// documented per-project rollback, so it keeps its own test.
|
|
104
|
-
expect(listPushableAssets(designRoot, { syncFiles: false })).toEqual([]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('declared canvasGroups decide what a canvas body IS', () => {
|
|
108
|
-
const designRoot = scratchDesignRoot();
|
|
109
|
-
mkdirSync(join(designRoot, 'mocks'), { recursive: true });
|
|
110
|
-
mkdirSync(join(designRoot, 'lib'), { recursive: true });
|
|
111
|
-
writeFileSync(join(designRoot, 'mocks/screen.tsx'), 'x');
|
|
112
|
-
writeFileSync(join(designRoot, 'lib/helpers.tsx'), 'x');
|
|
113
|
-
const groups = [{ path: 'mocks' }];
|
|
114
|
-
// `mocks/screen.tsx` is a canvas here; `lib/helpers.tsx` a shared module.
|
|
115
|
-
expect(listPushableAssets(designRoot, { canvasGroups: groups, syncFiles: true })).toEqual([
|
|
116
|
-
'lib/helpers.tsx',
|
|
117
|
-
]);
|
|
118
|
-
// …and both the groups and the flag reach the walk through config.json —
|
|
119
|
-
// the out-of-process worker's path.
|
|
120
|
-
writeFileSync(
|
|
121
|
-
join(designRoot, 'config.json'),
|
|
122
|
-
JSON.stringify({ canvasGroups: groups, linkedHub: { url: 'http://h', syncFiles: true } })
|
|
123
|
-
);
|
|
124
|
-
expect(listPushableAssets(designRoot)).toEqual(['lib/helpers.tsx']);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('pushAssets — routes each class to the right hub endpoint', () => {
|
|
129
|
-
test('top-level → /assets/, DS → /_asset-file/, HEAD-first, live token', async () => {
|
|
130
|
-
const designRoot = scratchDesignRoot();
|
|
131
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
132
|
-
writeFileSync(join(designRoot, 'assets/aaaaaaaa.png'), 'top-level');
|
|
133
|
-
mkdirSync(join(designRoot, 'system/ds/assets/logos'), { recursive: true });
|
|
134
|
-
writeFileSync(join(designRoot, 'system/ds/assets/logos/mark.svg'), 'brand-bytes');
|
|
135
|
-
|
|
136
|
-
const calls: Array<{ method: string; url: string; auth: string }> = [];
|
|
137
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
138
|
-
const url = String(input);
|
|
139
|
-
const method = init?.method ?? 'GET';
|
|
140
|
-
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
141
|
-
calls.push({ method, url, auth: String(headers.authorization) });
|
|
142
|
-
if (method === 'HEAD') return new Response(null, { status: 404 }); // nothing there yet
|
|
143
|
-
return new Response('{"ok":true}', { status: 200 });
|
|
144
|
-
}) as typeof fetch;
|
|
145
|
-
|
|
146
|
-
const r = await pushAssets({
|
|
147
|
-
designRoot,
|
|
148
|
-
hubUrl: 'https://alligators.cloud.maude.sh/',
|
|
149
|
-
token: () => 'mau_tok',
|
|
150
|
-
fetchImpl,
|
|
151
|
-
log: { log: () => {}, warn: () => {} },
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
expect(r.failed).toEqual([]);
|
|
155
|
-
expect(r.pushed.sort()).toEqual(['assets/aaaaaaaa.png', 'system/ds/assets/logos/mark.svg']);
|
|
156
|
-
// Top-level → bucket route; DS → checkout route.
|
|
157
|
-
const puts = calls.filter((c) => c.method === 'PUT').map((c) => c.url);
|
|
158
|
-
expect(puts).toContain('https://alligators.cloud.maude.sh/assets/aaaaaaaa.png');
|
|
159
|
-
expect(puts).toContain(
|
|
160
|
-
'https://alligators.cloud.maude.sh/_asset-file/system/ds/assets/logos/mark.svg'
|
|
161
|
-
);
|
|
162
|
-
expect(calls.every((c) => c.auth === 'Bearer mau_tok')).toBe(true);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('skips what the cloud already holds (HEAD 200)', async () => {
|
|
166
|
-
const designRoot = scratchDesignRoot();
|
|
167
|
-
mkdirSync(join(designRoot, 'system/ds/assets/logos'), { recursive: true });
|
|
168
|
-
writeFileSync(join(designRoot, 'system/ds/assets/logos/there.svg'), 'x');
|
|
169
|
-
const fetchImpl = (async (_i: RequestInfo | URL, init?: RequestInit) =>
|
|
170
|
-
new Response(null, { status: init?.method === 'HEAD' ? 200 : 500 })) as typeof fetch;
|
|
171
|
-
const r = await pushAssets({
|
|
172
|
-
designRoot,
|
|
173
|
-
hubUrl: 'https://x.example',
|
|
174
|
-
token: () => 't',
|
|
175
|
-
fetchImpl,
|
|
176
|
-
log: { log: () => {}, warn: () => {} },
|
|
177
|
-
});
|
|
178
|
-
expect(r.skipped).toBe(1);
|
|
179
|
-
expect(r.pushed).toEqual([]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('a refused upload is reported, not thrown', async () => {
|
|
183
|
-
const designRoot = scratchDesignRoot();
|
|
184
|
-
mkdirSync(join(designRoot, 'system/ds/assets/logos'), { recursive: true });
|
|
185
|
-
writeFileSync(join(designRoot, 'system/ds/assets/logos/x.svg'), 'x');
|
|
186
|
-
const fetchImpl = (async (_i: RequestInfo | URL, init?: RequestInit) =>
|
|
187
|
-
new Response(null, { status: init?.method === 'HEAD' ? 404 : 413 })) as typeof fetch;
|
|
188
|
-
const r = await pushAssets({
|
|
189
|
-
designRoot,
|
|
190
|
-
hubUrl: 'https://x.example',
|
|
191
|
-
token: () => 't',
|
|
192
|
-
fetchImpl,
|
|
193
|
-
log: { log: () => {}, warn: () => {} },
|
|
194
|
-
});
|
|
195
|
-
expect(r.pushed).toEqual([]);
|
|
196
|
-
expect(r.failed).toEqual([{ key: 'system/ds/assets/logos/x.svg', reason: 'HTTP 413' }]);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('no assets dir → zero network calls', async () => {
|
|
200
|
-
const designRoot = scratchDesignRoot();
|
|
201
|
-
let calls = 0;
|
|
202
|
-
const fetchImpl = (async () => {
|
|
203
|
-
calls += 1;
|
|
204
|
-
return new Response(null, { status: 200 });
|
|
205
|
-
}) as typeof fetch;
|
|
206
|
-
const r = await pushAssets({
|
|
207
|
-
designRoot,
|
|
208
|
-
hubUrl: 'https://x.example',
|
|
209
|
-
token: () => 't',
|
|
210
|
-
fetchImpl,
|
|
211
|
-
log: { log: () => {}, warn: () => {} },
|
|
212
|
-
});
|
|
213
|
-
expect(calls).toBe(0);
|
|
214
|
-
expect(r).toEqual({ pushed: [], skipped: 0, failed: [] });
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('pushAssets — 429 pacing, 5xx retry, honest failure reasons (RCA 2026-08-11)', () => {
|
|
219
|
-
/** One asset, and a fetch that answers HEAD 404 (nothing on the cloud yet). */
|
|
220
|
-
function oneAsset(): string {
|
|
221
|
-
const designRoot = scratchDesignRoot();
|
|
222
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
223
|
-
writeFileSync(join(designRoot, 'assets/one.png'), 'bytes');
|
|
224
|
-
return designRoot;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
test('a 429 with Retry-After is waited out once, then the asset lands', async () => {
|
|
228
|
-
const designRoot = oneAsset();
|
|
229
|
-
const slept: number[] = [];
|
|
230
|
-
let puts = 0;
|
|
231
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
232
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
233
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
234
|
-
puts += 1;
|
|
235
|
-
return puts === 1
|
|
236
|
-
? new Response('{"error":"rate limit exceeded"}', {
|
|
237
|
-
status: 429,
|
|
238
|
-
headers: { 'retry-after': '30' },
|
|
239
|
-
})
|
|
240
|
-
: new Response('{"ok":true}', { status: 200 });
|
|
241
|
-
}) as typeof fetch;
|
|
242
|
-
|
|
243
|
-
const r = await pushAssets({
|
|
244
|
-
designRoot,
|
|
245
|
-
hubUrl: 'https://x.example',
|
|
246
|
-
token: () => 't',
|
|
247
|
-
fetchImpl,
|
|
248
|
-
log: { log: () => {}, warn: () => {} },
|
|
249
|
-
sleep: async (ms) => {
|
|
250
|
-
slept.push(ms);
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
expect(r.pushed).toEqual(['assets/one.png']);
|
|
254
|
-
expect(r.failed).toEqual([]);
|
|
255
|
-
expect(slept).toEqual([30_000]); // the hub's own number, not a guess
|
|
256
|
-
expect(puts).toBe(2);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test('an absent Retry-After falls back to the 60 s window; a huge one is clamped', async () => {
|
|
260
|
-
for (const [header, expected] of [
|
|
261
|
-
[null, 60_000],
|
|
262
|
-
['9999', 60_000],
|
|
263
|
-
['bogus', 60_000],
|
|
264
|
-
] as const) {
|
|
265
|
-
const designRoot = oneAsset();
|
|
266
|
-
const slept: number[] = [];
|
|
267
|
-
let puts = 0;
|
|
268
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
269
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
270
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
271
|
-
puts += 1;
|
|
272
|
-
return puts === 1
|
|
273
|
-
? new Response(null, { status: 429, headers: header ? { 'retry-after': header } : {} })
|
|
274
|
-
: new Response(null, { status: 200 });
|
|
275
|
-
}) as typeof fetch;
|
|
276
|
-
const r = await pushAssets({
|
|
277
|
-
designRoot,
|
|
278
|
-
hubUrl: 'https://x.example',
|
|
279
|
-
token: () => 't',
|
|
280
|
-
fetchImpl,
|
|
281
|
-
log: { log: () => {}, warn: () => {} },
|
|
282
|
-
sleep: async (ms) => {
|
|
283
|
-
slept.push(ms);
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
expect(slept).toEqual([expected]);
|
|
287
|
-
expect(r.pushed).toEqual(['assets/one.png']);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test('a second 429 fails the asset and the sweep keeps going', async () => {
|
|
292
|
-
const designRoot = scratchDesignRoot();
|
|
293
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
294
|
-
writeFileSync(join(designRoot, 'assets/a-limited.png'), 'x');
|
|
295
|
-
writeFileSync(join(designRoot, 'assets/b-fine.png'), 'x');
|
|
296
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
297
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
298
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
299
|
-
return String(input).includes('a-limited')
|
|
300
|
-
? new Response('{"error":"rate limit exceeded"}', {
|
|
301
|
-
status: 429,
|
|
302
|
-
headers: { 'retry-after': '1' },
|
|
303
|
-
})
|
|
304
|
-
: new Response(null, { status: 200 });
|
|
305
|
-
}) as typeof fetch;
|
|
306
|
-
const r = await pushAssets({
|
|
307
|
-
designRoot,
|
|
308
|
-
hubUrl: 'https://x.example',
|
|
309
|
-
token: () => 't',
|
|
310
|
-
fetchImpl,
|
|
311
|
-
log: { log: () => {}, warn: () => {} },
|
|
312
|
-
sleep: async () => {},
|
|
313
|
-
});
|
|
314
|
-
expect(r.pushed).toEqual(['assets/b-fine.png']);
|
|
315
|
-
expect(r.failed).toEqual([
|
|
316
|
-
{ key: 'assets/a-limited.png', reason: 'HTTP 429 — {"error":"rate limit exceeded"}' },
|
|
317
|
-
]);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test('a 5xx gets one immediate retry — no wait, no whole new boot', async () => {
|
|
321
|
-
const designRoot = oneAsset();
|
|
322
|
-
const slept: number[] = [];
|
|
323
|
-
let puts = 0;
|
|
324
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
325
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
326
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
327
|
-
puts += 1;
|
|
328
|
-
return new Response(null, { status: puts === 1 ? 503 : 200 });
|
|
329
|
-
}) as typeof fetch;
|
|
330
|
-
const r = await pushAssets({
|
|
331
|
-
designRoot,
|
|
332
|
-
hubUrl: 'https://x.example',
|
|
333
|
-
token: () => 't',
|
|
334
|
-
fetchImpl,
|
|
335
|
-
log: { log: () => {}, warn: () => {} },
|
|
336
|
-
sleep: async (ms) => {
|
|
337
|
-
slept.push(ms);
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
expect(r.pushed).toEqual(['assets/one.png']);
|
|
341
|
-
expect(puts).toBe(2);
|
|
342
|
-
expect(slept).toEqual([]);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test('a 4xx that is not 429 is final — one attempt, reported as-is', async () => {
|
|
346
|
-
const designRoot = oneAsset();
|
|
347
|
-
let puts = 0;
|
|
348
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
349
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
350
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
351
|
-
puts += 1;
|
|
352
|
-
return new Response(null, { status: 413 });
|
|
353
|
-
}) as typeof fetch;
|
|
354
|
-
const r = await pushAssets({
|
|
355
|
-
designRoot,
|
|
356
|
-
hubUrl: 'https://x.example',
|
|
357
|
-
token: () => 't',
|
|
358
|
-
fetchImpl,
|
|
359
|
-
log: { log: () => {}, warn: () => {} },
|
|
360
|
-
sleep: async () => {},
|
|
361
|
-
});
|
|
362
|
-
expect(puts).toBe(1);
|
|
363
|
-
expect(r.failed).toEqual([{ key: 'assets/one.png', reason: 'HTTP 413' }]);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
test('the failure reason carries the body — a Cloudflare page is not our 429', async () => {
|
|
367
|
-
const designRoot = oneAsset();
|
|
368
|
-
const page = `<html>\n <head><title>503 Service Unavailable</title></head>\n <body>error 1016 — origin DNS error, and a great deal more text than we keep</body>\n</html>`;
|
|
369
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
370
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
371
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
372
|
-
return new Response(page, { status: 503 });
|
|
373
|
-
}) as typeof fetch;
|
|
374
|
-
const r = await pushAssets({
|
|
375
|
-
designRoot,
|
|
376
|
-
hubUrl: 'https://x.example',
|
|
377
|
-
token: () => 't',
|
|
378
|
-
fetchImpl,
|
|
379
|
-
log: { log: () => {}, warn: () => {} },
|
|
380
|
-
sleep: async () => {},
|
|
381
|
-
});
|
|
382
|
-
const reason = r.failed[0].reason;
|
|
383
|
-
expect(reason.startsWith('HTTP 503 — <html> <head><title>503 Service Unavailable')).toBe(true);
|
|
384
|
-
expect(reason.includes('\n')).toBe(false); // collapsed, never multi-line in the panel
|
|
385
|
-
expect(reason.length).toBeLessThanOrEqual('HTTP 503 — '.length + 80);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test('every PUT carries an explicit Content-Length (the .mov chunked-503 probe)', async () => {
|
|
389
|
-
const designRoot = scratchDesignRoot();
|
|
390
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
391
|
-
writeFileSync(join(designRoot, 'assets/sized.png'), 'twelve bytes');
|
|
392
|
-
const lengths: string[] = [];
|
|
393
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
394
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
395
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
396
|
-
const sent = (init?.headers ?? {}) as Record<string, string>;
|
|
397
|
-
lengths.push(String(sent['content-length']));
|
|
398
|
-
return new Response(null, { status: 200 });
|
|
399
|
-
}) as typeof fetch;
|
|
400
|
-
await pushAssets({
|
|
401
|
-
designRoot,
|
|
402
|
-
hubUrl: 'https://x.example',
|
|
403
|
-
token: () => 't',
|
|
404
|
-
fetchImpl,
|
|
405
|
-
log: { log: () => {}, warn: () => {} },
|
|
406
|
-
});
|
|
407
|
-
expect(lengths).toEqual([String('twelve bytes'.length)]);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test('the per-sweep backoff budget is finite — a 429 wall stops costing time', async () => {
|
|
411
|
-
// 10 assets, every one 429 with a 60 s Retry-After: the 5-minute sweep
|
|
412
|
-
// budget buys 5 paced retries, the rest fail fast for the next boot.
|
|
413
|
-
const designRoot = scratchDesignRoot();
|
|
414
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
415
|
-
for (let i = 0; i < 10; i++) writeFileSync(join(designRoot, `assets/a${i}.png`), 'x');
|
|
416
|
-
const slept: number[] = [];
|
|
417
|
-
const fetchImpl = (async (_i: RequestInfo | URL, init?: RequestInit) =>
|
|
418
|
-
init?.method === 'HEAD'
|
|
419
|
-
? new Response(null, { status: 404 })
|
|
420
|
-
: new Response(null, { status: 429, headers: { 'retry-after': '60' } })) as typeof fetch;
|
|
421
|
-
const r = await pushAssets({
|
|
422
|
-
designRoot,
|
|
423
|
-
hubUrl: 'https://x.example',
|
|
424
|
-
token: () => 't',
|
|
425
|
-
fetchImpl,
|
|
426
|
-
log: { log: () => {}, warn: () => {} },
|
|
427
|
-
sleep: async (ms) => {
|
|
428
|
-
slept.push(ms);
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
expect(r.failed).toHaveLength(10);
|
|
432
|
-
expect(slept).toEqual([60_000, 60_000, 60_000, 60_000, 60_000]);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
describe('pushAssets — one batch probe replaces N per-file probes (RCA 2026-08-11 part 2)', () => {
|
|
437
|
-
function twoAssets(): string {
|
|
438
|
-
const designRoot = scratchDesignRoot();
|
|
439
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
440
|
-
writeFileSync(join(designRoot, 'assets/there.png'), 'x');
|
|
441
|
-
mkdirSync(join(designRoot, 'system/ds/assets/logos'), { recursive: true });
|
|
442
|
-
writeFileSync(join(designRoot, 'system/ds/assets/logos/missing.svg'), '<svg/>');
|
|
443
|
-
return designRoot;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
test('the probe answers once; present files are skipped without any HEAD', async () => {
|
|
447
|
-
const designRoot = twoAssets();
|
|
448
|
-
const calls: string[] = [];
|
|
449
|
-
let probedPaths: string[] = [];
|
|
450
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
451
|
-
const method = init?.method ?? 'GET';
|
|
452
|
-
calls.push(`${method} ${new URL(String(input)).pathname}`);
|
|
453
|
-
if (String(input).endsWith('/_asset-probe')) {
|
|
454
|
-
probedPaths = JSON.parse(String(init?.body)).paths;
|
|
455
|
-
return new Response(JSON.stringify({ present: ['assets/there.png'] }), { status: 200 });
|
|
456
|
-
}
|
|
457
|
-
return new Response(null, { status: 200 });
|
|
458
|
-
}) as typeof fetch;
|
|
459
|
-
|
|
460
|
-
const r = await pushAssets({
|
|
461
|
-
designRoot,
|
|
462
|
-
hubUrl: 'https://x.example',
|
|
463
|
-
token: () => 't',
|
|
464
|
-
fetchImpl,
|
|
465
|
-
log: { log: () => {}, warn: () => {} },
|
|
466
|
-
sleep: async () => {},
|
|
467
|
-
});
|
|
468
|
-
expect(r.skipped).toBe(1);
|
|
469
|
-
expect(r.pushed).toEqual(['system/ds/assets/logos/missing.svg']);
|
|
470
|
-
expect(probedPaths.sort()).toEqual(['assets/there.png', 'system/ds/assets/logos/missing.svg']);
|
|
471
|
-
// One probe, one upload — and NOT a single HEAD.
|
|
472
|
-
expect(calls).toEqual([
|
|
473
|
-
'POST /_asset-probe',
|
|
474
|
-
'PUT /_asset-file/system/ds/assets/logos/missing.svg',
|
|
475
|
-
]);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test('a hub that does not know the probe falls back to per-file HEAD', async () => {
|
|
479
|
-
// Old-hub interop: 404 means "ask the old way", never "it holds nothing" —
|
|
480
|
-
// reading it as an empty set would re-upload everything against every hub
|
|
481
|
-
// older than this change.
|
|
482
|
-
const designRoot = twoAssets();
|
|
483
|
-
const methods: string[] = [];
|
|
484
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
485
|
-
const method = init?.method ?? 'GET';
|
|
486
|
-
methods.push(method);
|
|
487
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
488
|
-
if (method === 'HEAD') {
|
|
489
|
-
return new Response(null, { status: String(input).includes('there.png') ? 200 : 404 });
|
|
490
|
-
}
|
|
491
|
-
return new Response(null, { status: 200 });
|
|
492
|
-
}) as typeof fetch;
|
|
493
|
-
const r = await pushAssets({
|
|
494
|
-
designRoot,
|
|
495
|
-
hubUrl: 'https://x.example',
|
|
496
|
-
token: () => 't',
|
|
497
|
-
fetchImpl,
|
|
498
|
-
log: { log: () => {}, warn: () => {} },
|
|
499
|
-
sleep: async () => {},
|
|
500
|
-
});
|
|
501
|
-
expect(methods.filter((m) => m === 'HEAD')).toHaveLength(2);
|
|
502
|
-
expect(r.skipped).toBe(1);
|
|
503
|
-
expect(r.pushed).toEqual(['system/ds/assets/logos/missing.svg']);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
test('a 405 on the per-file probe means "cannot answer", never "absent"', async () => {
|
|
507
|
-
// The bug in one line: a cell turns our HEAD into a GET and answers 405.
|
|
508
|
-
// Treating that as a refusal would strand the file; treating it as "absent"
|
|
509
|
-
// is right — upload it — but it must not be reported as a failure.
|
|
510
|
-
const designRoot = twoAssets();
|
|
511
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
512
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
513
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 405 });
|
|
514
|
-
return new Response(null, { status: 200 });
|
|
515
|
-
}) as typeof fetch;
|
|
516
|
-
const r = await pushAssets({
|
|
517
|
-
designRoot,
|
|
518
|
-
hubUrl: 'https://x.example',
|
|
519
|
-
token: () => 't',
|
|
520
|
-
fetchImpl,
|
|
521
|
-
log: { log: () => {}, warn: () => {} },
|
|
522
|
-
sleep: async () => {},
|
|
523
|
-
});
|
|
524
|
-
expect(r.failed).toEqual([]);
|
|
525
|
-
expect(r.pushed.sort()).toEqual(['assets/there.png', 'system/ds/assets/logos/missing.svg']);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
test('a hostile probe answer cannot make the sweep skip a file it never named', async () => {
|
|
529
|
-
const designRoot = twoAssets();
|
|
530
|
-
const fetchImpl = (async (input: RequestInfo | URL) => {
|
|
531
|
-
if (String(input).endsWith('/_asset-probe')) {
|
|
532
|
-
return new Response(
|
|
533
|
-
JSON.stringify({ present: ['../../etc/passwd', 42, 'assets/never-asked.png'] }),
|
|
534
|
-
{ status: 200 }
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
return new Response(null, { status: 200 });
|
|
538
|
-
}) as typeof fetch;
|
|
539
|
-
const r = await pushAssets({
|
|
540
|
-
designRoot,
|
|
541
|
-
hubUrl: 'https://x.example',
|
|
542
|
-
token: () => 't',
|
|
543
|
-
fetchImpl,
|
|
544
|
-
log: { log: () => {}, warn: () => {} },
|
|
545
|
-
sleep: async () => {},
|
|
546
|
-
});
|
|
547
|
-
expect(r.skipped).toBe(0);
|
|
548
|
-
expect(r.pushed.sort()).toEqual(['assets/there.png', 'system/ds/assets/logos/missing.svg']);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test('a probe that hangs or errors degrades to the per-file path', async () => {
|
|
552
|
-
const designRoot = twoAssets();
|
|
553
|
-
let heads = 0;
|
|
554
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
555
|
-
if (String(input).endsWith('/_asset-probe')) throw new Error('connection reset');
|
|
556
|
-
if (init?.method === 'HEAD') {
|
|
557
|
-
heads += 1;
|
|
558
|
-
return new Response(null, { status: 404 });
|
|
559
|
-
}
|
|
560
|
-
return new Response(null, { status: 200 });
|
|
561
|
-
}) as typeof fetch;
|
|
562
|
-
const r = await pushAssets({
|
|
563
|
-
designRoot,
|
|
564
|
-
hubUrl: 'https://x.example',
|
|
565
|
-
token: () => 't',
|
|
566
|
-
fetchImpl,
|
|
567
|
-
log: { log: () => {}, warn: () => {} },
|
|
568
|
-
sleep: async () => {},
|
|
569
|
-
});
|
|
570
|
-
expect(heads).toBe(2);
|
|
571
|
-
expect(r.pushed).toHaveLength(2);
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
describe('pushAssets — a refused upload must not wedge the sweep (2026-08-11, second pass)', () => {
|
|
576
|
-
test('every PUT closes its connection — the keep-alive desync that killed the sidecar', async () => {
|
|
577
|
-
// A peer that refuses a PUT before reading the body leaves unread bytes in
|
|
578
|
-
// the socket; the next request over that pooled connection never gets an
|
|
579
|
-
// answer. Measured: `connection: close` on the retry alone does NOT help —
|
|
580
|
-
// it must be on the request that may be refused. Pin it.
|
|
581
|
-
const designRoot = scratchDesignRoot();
|
|
582
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
583
|
-
writeFileSync(join(designRoot, 'assets/one.png'), 'x');
|
|
584
|
-
const seen: Array<Record<string, string>> = [];
|
|
585
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
586
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
587
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
588
|
-
seen.push((init?.headers ?? {}) as Record<string, string>);
|
|
589
|
-
return new Response(null, { status: 503 }); // refused → the retry re-sends
|
|
590
|
-
}) as typeof fetch;
|
|
591
|
-
await pushAssets({
|
|
592
|
-
designRoot,
|
|
593
|
-
hubUrl: 'https://x.example',
|
|
594
|
-
token: () => 't',
|
|
595
|
-
fetchImpl,
|
|
596
|
-
log: { log: () => {}, warn: () => {} },
|
|
597
|
-
sleep: async () => {},
|
|
598
|
-
});
|
|
599
|
-
expect(seen).toHaveLength(2); // first attempt + the 5xx retry
|
|
600
|
-
expect(seen.every((h) => h.connection === 'close')).toBe(true);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
test('a request that never answers is abandoned, named, and the sweep goes on', async () => {
|
|
604
|
-
const designRoot = scratchDesignRoot();
|
|
605
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
606
|
-
writeFileSync(join(designRoot, 'assets/a-hangs.png'), 'x');
|
|
607
|
-
writeFileSync(join(designRoot, 'assets/b-fine.png'), 'x');
|
|
608
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
609
|
-
if (String(input).endsWith('/_asset-probe')) return new Response(null, { status: 404 });
|
|
610
|
-
if (init?.method === 'HEAD') return new Response(null, { status: 404 });
|
|
611
|
-
if (!String(input).includes('a-hangs')) return new Response(null, { status: 200 });
|
|
612
|
-
// Wedged: answers only when the caller's own budget aborts it.
|
|
613
|
-
return new Promise<Response>((_res, rej) => {
|
|
614
|
-
init?.signal?.addEventListener('abort', () => rej(init.signal?.reason));
|
|
615
|
-
});
|
|
616
|
-
}) as typeof fetch;
|
|
617
|
-
const r = await pushAssets({
|
|
618
|
-
designRoot,
|
|
619
|
-
hubUrl: 'https://x.example',
|
|
620
|
-
token: () => 't',
|
|
621
|
-
fetchImpl,
|
|
622
|
-
log: { log: () => {}, warn: () => {} },
|
|
623
|
-
sleep: async () => {},
|
|
624
|
-
timeoutFor: () => 25,
|
|
625
|
-
});
|
|
626
|
-
expect(r.failed).toEqual([
|
|
627
|
-
{ key: 'assets/a-hangs.png', reason: 'timed out — the hub stopped answering' },
|
|
628
|
-
]);
|
|
629
|
-
expect(r.pushed).toEqual(['assets/b-fine.png']);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
test('a hub that refuses the PROBE never gets the body streamed at it', async () => {
|
|
633
|
-
// The deployed cloud door answers 401 for a route it does not have yet —
|
|
634
|
-
// once per DS asset. Uploading anyway is pure waste on every boot.
|
|
635
|
-
const designRoot = scratchDesignRoot();
|
|
636
|
-
mkdirSync(join(designRoot, 'system/ds/assets/logos'), { recursive: true });
|
|
637
|
-
writeFileSync(join(designRoot, 'system/ds/assets/logos/mark.svg'), '<svg/>');
|
|
638
|
-
const methods: string[] = [];
|
|
639
|
-
const fetchImpl = (async (_i: RequestInfo | URL, init?: RequestInit) => {
|
|
640
|
-
methods.push(init?.method ?? 'GET');
|
|
641
|
-
return new Response('{"error":"sign in to open this project"}', { status: 401 });
|
|
642
|
-
}) as typeof fetch;
|
|
643
|
-
const r = await pushAssets({
|
|
644
|
-
designRoot,
|
|
645
|
-
hubUrl: 'https://x.example',
|
|
646
|
-
token: () => 't',
|
|
647
|
-
fetchImpl,
|
|
648
|
-
log: { log: () => {}, warn: () => {} },
|
|
649
|
-
sleep: async () => {},
|
|
650
|
-
});
|
|
651
|
-
expect(methods.filter((m) => m === 'PUT')).toEqual([]); // no body streamed at it
|
|
652
|
-
expect(methods).toContain('HEAD');
|
|
653
|
-
expect(r.failed).toEqual([
|
|
654
|
-
{
|
|
655
|
-
key: 'system/ds/assets/logos/mark.svg',
|
|
656
|
-
reason: 'HTTP 401 — {"error":"sign in to open this project"}',
|
|
657
|
-
},
|
|
658
|
-
]);
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
test('the upload budget has a floor, scales with bytes, and is capped', () => {
|
|
662
|
-
expect(putTimeoutMs(0)).toBe(60_000);
|
|
663
|
-
expect(putTimeoutMs(30 * 1024 * 1024)).toBeGreaterThan(5 * 60_000); // the 29.7 MB .mov
|
|
664
|
-
expect(putTimeoutMs(10 * 1024 * 1024 * 1024)).toBe(10 * 60_000);
|
|
665
|
-
});
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
describe('pushAssets — incremental progress (feature-sync-progress-modal)', () => {
|
|
669
|
-
test('emits a first, per-failure and final progress; the final has finished:true', async () => {
|
|
670
|
-
const designRoot = scratchDesignRoot();
|
|
671
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
672
|
-
writeFileSync(join(designRoot, 'assets/ok.png'), 'x');
|
|
673
|
-
writeFileSync(join(designRoot, 'assets/skipme.png'), 'x');
|
|
674
|
-
writeFileSync(join(designRoot, 'assets/zz-bad.png'), 'x');
|
|
675
|
-
// HEAD: only skipme.png is already there. PUT: zz-bad.png fails.
|
|
676
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
677
|
-
const url = String(input);
|
|
678
|
-
if (init?.method === 'HEAD')
|
|
679
|
-
return new Response(null, { status: url.includes('skipme') ? 200 : 404 });
|
|
680
|
-
return new Response(null, { status: url.includes('zz-bad') ? 500 : 200 });
|
|
681
|
-
}) as typeof fetch;
|
|
682
|
-
const emits: import('../sync/asset-push.ts').AssetPushProgress[] = [];
|
|
683
|
-
let t = 0;
|
|
684
|
-
const r = await pushAssets({
|
|
685
|
-
designRoot,
|
|
686
|
-
hubUrl: 'https://x.example',
|
|
687
|
-
token: () => 't',
|
|
688
|
-
fetchImpl,
|
|
689
|
-
log: { log: () => {}, warn: () => {} },
|
|
690
|
-
onProgress: (p) => emits.push(p),
|
|
691
|
-
now: () => (t += 1), // 1ms apart — inside the throttle window
|
|
692
|
-
});
|
|
693
|
-
expect(r.pushed).toEqual(['assets/ok.png']);
|
|
694
|
-
// First emit fires before anything settles (total known, push visible).
|
|
695
|
-
expect(emits[0]).toMatchObject({ total: 3, done: 0, finished: false });
|
|
696
|
-
// The failure force-emits through the throttle.
|
|
697
|
-
expect(emits.some((p) => p.failedCount === 1 && !p.finished)).toBe(true);
|
|
698
|
-
// Final emit always fires, with the full tally and no active file.
|
|
699
|
-
const last = emits[emits.length - 1];
|
|
700
|
-
expect(last).toEqual({
|
|
701
|
-
total: 3,
|
|
702
|
-
done: 3,
|
|
703
|
-
pushed: 1,
|
|
704
|
-
skipped: 1,
|
|
705
|
-
failedCount: 1,
|
|
706
|
-
failures: [{ key: 'assets/zz-bad.png', reason: 'HTTP 500' }],
|
|
707
|
-
active: null,
|
|
708
|
-
finished: true,
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
test('mid-flight emits are throttled; a slow clock lets them through', async () => {
|
|
713
|
-
const designRoot = scratchDesignRoot();
|
|
714
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
715
|
-
for (let i = 0; i < 5; i++) writeFileSync(join(designRoot, `assets/a${i}.png`), 'x');
|
|
716
|
-
const fetchImpl = (async (_i: RequestInfo | URL, init?: RequestInit) =>
|
|
717
|
-
new Response(null, { status: init?.method === 'HEAD' ? 404 : 200 })) as typeof fetch;
|
|
718
|
-
// Fast clock: everything inside 200ms → first + final only.
|
|
719
|
-
const fast: number[] = [];
|
|
720
|
-
await pushAssets({
|
|
721
|
-
designRoot,
|
|
722
|
-
hubUrl: 'https://x.example',
|
|
723
|
-
token: () => 't',
|
|
724
|
-
fetchImpl,
|
|
725
|
-
log: { log: () => {}, warn: () => {} },
|
|
726
|
-
onProgress: (p) => fast.push(p.done),
|
|
727
|
-
now: () => 1,
|
|
728
|
-
});
|
|
729
|
-
expect(fast).toHaveLength(2);
|
|
730
|
-
// Slow clock: 300ms between files → every per-file emit passes.
|
|
731
|
-
let t = 0;
|
|
732
|
-
const slow: number[] = [];
|
|
733
|
-
await pushAssets({
|
|
734
|
-
designRoot,
|
|
735
|
-
hubUrl: 'https://x.example',
|
|
736
|
-
token: () => 't',
|
|
737
|
-
fetchImpl,
|
|
738
|
-
log: { log: () => {}, warn: () => {} },
|
|
739
|
-
onProgress: (p) => slow.push(p.done),
|
|
740
|
-
now: () => (t += 300),
|
|
741
|
-
});
|
|
742
|
-
expect(slow.length).toBeGreaterThanOrEqual(6);
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
test('no assets → no progress emits at all (no empty section in the panel)', async () => {
|
|
746
|
-
const designRoot = scratchDesignRoot();
|
|
747
|
-
const emits: unknown[] = [];
|
|
748
|
-
await pushAssets({
|
|
749
|
-
designRoot,
|
|
750
|
-
hubUrl: 'https://x.example',
|
|
751
|
-
token: () => 't',
|
|
752
|
-
fetchImpl: (async () => new Response(null, { status: 200 })) as typeof fetch,
|
|
753
|
-
log: { log: () => {}, warn: () => {} },
|
|
754
|
-
onProgress: (p) => emits.push(p),
|
|
755
|
-
});
|
|
756
|
-
expect(emits).toEqual([]);
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
test('a throwing progress listener never breaks the push', async () => {
|
|
760
|
-
const designRoot = scratchDesignRoot();
|
|
761
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
762
|
-
writeFileSync(join(designRoot, 'assets/ok.png'), 'x');
|
|
763
|
-
const fetchImpl = (async (_i: RequestInfo | URL, init?: RequestInit) =>
|
|
764
|
-
new Response(null, { status: init?.method === 'HEAD' ? 404 : 200 })) as typeof fetch;
|
|
765
|
-
const r = await pushAssets({
|
|
766
|
-
designRoot,
|
|
767
|
-
hubUrl: 'https://x.example',
|
|
768
|
-
token: () => 't',
|
|
769
|
-
fetchImpl,
|
|
770
|
-
log: { log: () => {}, warn: () => {} },
|
|
771
|
-
onProgress: () => {
|
|
772
|
-
throw new Error('boom');
|
|
773
|
-
},
|
|
774
|
-
});
|
|
775
|
-
expect(r.pushed).toEqual(['assets/ok.png']);
|
|
776
|
-
});
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// An asset that appears AFTER boot has to go up now, not next launch.
|
|
780
|
-
//
|
|
781
|
-
// The sweep fired from exactly one place — `start()` — so a picture pasted into
|
|
782
|
-
// an annotation reached the cloud only on the next boot or Resync, while the
|
|
783
|
-
// annotation itself synced through the doc in milliseconds. The other side
|
|
784
|
-
// rendered an `<image>` pointing at bytes nobody had sent: a permanent empty
|
|
785
|
-
// frame that reads as a broken path. `isPushableAssetRel` is what decides an
|
|
786
|
-
// `fs:any` event is worth a sweep, so it has to agree with the walk that
|
|
787
|
-
// actually collects them.
|
|
788
|
-
describe('isPushableAssetRel — the on-change trigger', () => {
|
|
789
|
-
test('it agrees with listPushableAssets on a real tree', () => {
|
|
790
|
-
const root = mkdtempSync(join(tmpdir(), 'pushable-'));
|
|
791
|
-
mkdirSync(join(root, 'assets'), { recursive: true });
|
|
792
|
-
mkdirSync(join(root, 'system/ds/assets/logos'), { recursive: true });
|
|
793
|
-
mkdirSync(join(root, '_history/ui-home/assets'), { recursive: true });
|
|
794
|
-
mkdirSync(join(root, 'ui'), { recursive: true });
|
|
795
|
-
writeFileSync(join(root, 'assets/5f809613.png'), 'x'); // the reported case
|
|
796
|
-
writeFileSync(join(root, 'system/ds/assets/logos/mark.svg'), 'x');
|
|
797
|
-
writeFileSync(join(root, '_history/ui-home/assets/shot.png'), 'x');
|
|
798
|
-
writeFileSync(join(root, 'ui/Home.tsx'), 'x');
|
|
799
|
-
writeFileSync(join(root, 'assets/notes.txt'), 'x');
|
|
800
|
-
|
|
801
|
-
const walked = new Set(listPushableAssets(root));
|
|
802
|
-
for (const rel of [
|
|
803
|
-
'assets/5f809613.png',
|
|
804
|
-
'system/ds/assets/logos/mark.svg',
|
|
805
|
-
'_history/ui-home/assets/shot.png',
|
|
806
|
-
'ui/Home.tsx',
|
|
807
|
-
'assets/notes.txt',
|
|
808
|
-
]) {
|
|
809
|
-
expect(isPushableAssetRel(rel)).toBe(walked.has(rel));
|
|
810
|
-
}
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
test('the pasted-annotation image is what fires it', () => {
|
|
814
|
-
expect(isPushableAssetRel('assets/5f809613.png')).toBe(true);
|
|
815
|
-
expect(isPushableAssetRel('assets/9fb5bab5.jpg')).toBe(true);
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
test('runtime state and non-assets never fire a sweep', () => {
|
|
819
|
-
for (const rel of [
|
|
820
|
-
'_history/ui-home/assets/shot.png',
|
|
821
|
-
'_comments/assets/pasted.png',
|
|
822
|
-
'ui/Home.tsx',
|
|
823
|
-
'config.json',
|
|
824
|
-
'system/ds/preview/logo.tsx',
|
|
825
|
-
'assets/notes.txt',
|
|
826
|
-
'assets',
|
|
827
|
-
'',
|
|
828
|
-
]) {
|
|
829
|
-
expect(isPushableAssetRel(rel)).toBe(false);
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
test('a windows-shaped path is normalized, not refused', () => {
|
|
834
|
-
expect(isPushableAssetRel('system\\ds\\assets\\logos\\mark.svg')).toBe(true);
|
|
835
|
-
});
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
// The WIRING, pinned at source (the sync-panel-surface precedent).
|
|
839
|
-
//
|
|
840
|
-
// `isPushableAssetRel` above is exercised directly; what a unit test cannot
|
|
841
|
-
// reach here is that the runtime actually CALLS it from the fs watcher, which
|
|
842
|
-
// is the half that was missing. A behavioural test needs a live runtime plus a
|
|
843
|
-
// 1.5 s debounce and destabilises `sync-runtime.test.ts`'s shared fixtures, so
|
|
844
|
-
// the invariant is pinned where it is cheap and unambiguous instead.
|
|
845
|
-
describe('the runtime schedules the legacy push on change, not only at boot', () => {
|
|
846
|
-
const SYNC = readFileSync(join(import.meta.dir, '..', 'sync', 'index.ts'), 'utf8');
|
|
847
|
-
|
|
848
|
-
test('the fs watcher decides with isPushableAssetRel and schedules a pass', () => {
|
|
849
|
-
// The predicate gets the project's OWN canvas groups — with the default
|
|
850
|
-
// set a custom-group project's shared module would answer canvas-owned
|
|
851
|
-
// and silently never schedule the pass that uploads it.
|
|
852
|
-
expect(SYNC).toContain('if (isPushableAssetRel(rel, ctx.cfg.canvasGroups)) {');
|
|
853
|
-
expect(SYNC).toContain('scheduleLegacyPush(linkedHub.url);');
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
test('bursts coalesce, and a change DURING a pass is not lost', () => {
|
|
857
|
-
// Dragging six images on is six events; each pass costs a probe over the
|
|
858
|
-
// wire. And a file written while a pass runs was not in that pass's list.
|
|
859
|
-
expect(SYNC).toMatch(/ASSET_SWEEP_DEBOUNCE_MS = [\d_]+/);
|
|
860
|
-
expect(SYNC).toContain('legacyPushAgain');
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
test('the lane is decided once by the capability probe, and a journal hub never sees the legacy client', () => {
|
|
864
|
-
// Sync v2 Increment 5 — the burn-down's one invariant: exactly ONE push
|
|
865
|
-
// lane per boot. A journal hub gets the plane; only a journal-less hub
|
|
866
|
-
// (or an unreachable/opted-out probe) gets the legacy client.
|
|
867
|
-
expect(SYNC).toContain('decidePushLane(filePlane === null);');
|
|
868
|
-
expect(SYNC).toMatch(
|
|
869
|
-
/if \(stopped \|\| cellPairing \|\| legacyLane !== true\) return; \/\/ the cell never pushes/
|
|
870
|
-
);
|
|
871
|
-
expect(SYNC).toContain(
|
|
872
|
-
'if (legacyLane === false) return; // the plane owns pushes on this hub'
|
|
873
|
-
);
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
test('stop() clears the pending pass and cancels a running one', () => {
|
|
877
|
-
expect(SYNC).toContain('if (legacyPushTimer !== null) clearTimeout(legacyPushTimer);');
|
|
878
|
-
expect(SYNC).toContain('legacyPushCancel = true;');
|
|
879
|
-
});
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
describe('pushAssets — the cancel hook (the Sync panel button, legacy window)', () => {
|
|
883
|
-
test('cancelled() abandons the rest of the pass', async () => {
|
|
884
|
-
const designRoot = scratchDesignRoot();
|
|
885
|
-
mkdirSync(join(designRoot, 'assets'), { recursive: true });
|
|
886
|
-
writeFileSync(join(designRoot, 'assets/aaaaaaaa.png'), 'a');
|
|
887
|
-
writeFileSync(join(designRoot, 'assets/bbbbbbbb.png'), 'b');
|
|
888
|
-
writeFileSync(join(designRoot, 'assets/cccccccc.png'), 'c');
|
|
889
|
-
|
|
890
|
-
let puts = 0;
|
|
891
|
-
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
892
|
-
if (init?.method === 'PUT') puts += 1;
|
|
893
|
-
return new Response('not found', { status: 404 });
|
|
894
|
-
}) as unknown as typeof fetch;
|
|
895
|
-
|
|
896
|
-
const out = await pushAssets({
|
|
897
|
-
designRoot,
|
|
898
|
-
hubUrl: 'https://hub.example.com',
|
|
899
|
-
token: () => 't',
|
|
900
|
-
fetchImpl,
|
|
901
|
-
log: { log: () => {}, warn: () => {} },
|
|
902
|
-
// Cancel after the first upload settles.
|
|
903
|
-
cancelled: () => puts >= 1,
|
|
904
|
-
});
|
|
905
|
-
expect(puts).toBe(1);
|
|
906
|
-
// The rest of the set was neither pushed nor reported failed — it is the
|
|
907
|
-
// next pass's work, exactly like a budget wall.
|
|
908
|
-
expect(out.pushed.length + out.failed.length + out.skipped).toBeLessThan(3);
|
|
909
|
-
});
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
describe('the syncFiles default — Increment 4 flipped it ON', () => {
|
|
913
|
-
// The flip is one boolean in two places (`sync/index.ts` and here), and a
|
|
914
|
-
// drift between them means the sweep and the plane disagree about which
|
|
915
|
-
// files exist — the jurisdiction overlap Sync v2 exists to end. So the
|
|
916
|
-
// default is asserted, not assumed.
|
|
917
|
-
const designRoot = () => {
|
|
918
|
-
const root = scratchDesignRoot();
|
|
919
|
-
mkdirSync(join(root, 'system/ds'), { recursive: true });
|
|
920
|
-
writeFileSync(join(root, 'system/ds/brand.css'), 'x');
|
|
921
|
-
return root;
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
test('absent config ⇒ the plane is on', () => {
|
|
925
|
-
expect(listPushableAssets(designRoot())).toEqual(['system/ds/brand.css']);
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
test('`linkedHub.syncFiles: false` is the per-project rollback and still works', () => {
|
|
929
|
-
expect(listPushableAssets(designRoot(), { syncFiles: false })).toEqual([]);
|
|
930
|
-
});
|
|
931
|
-
});
|