@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,118 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
import { buildDdrBatch } from './ddr-to-kgai.mjs';
|
|
7
|
-
|
|
8
|
-
function fixtureDir(files) {
|
|
9
|
-
const dir = mkdtempSync(join(tmpdir(), 'ddr-'));
|
|
10
|
-
mkdirSync(dir, { recursive: true });
|
|
11
|
-
for (const [name, body] of Object.entries(files)) writeFileSync(join(dir, name), body);
|
|
12
|
-
return dir;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DDR1 = `# DDR-001: First decision
|
|
16
|
-
|
|
17
|
-
**Status:** Accepted
|
|
18
|
-
**Date:** 2026-01-01
|
|
19
|
-
**Tags:** infra, monorepo, ci
|
|
20
|
-
|
|
21
|
-
## Context
|
|
22
|
-
The problem is X.
|
|
23
|
-
|
|
24
|
-
## Decision
|
|
25
|
-
We pick A because it is simplest and safest.
|
|
26
|
-
`;
|
|
27
|
-
|
|
28
|
-
const DDR2 = `# DDR-002: Second decision
|
|
29
|
-
|
|
30
|
-
**Status:** Accepted
|
|
31
|
-
**Date:** 2026-02-02
|
|
32
|
-
**Tags:** auth
|
|
33
|
-
|
|
34
|
-
**Supersedes:** DDR-001
|
|
35
|
-
**Related:** DDR-003
|
|
36
|
-
|
|
37
|
-
## Decision
|
|
38
|
-
We replace the earlier approach. See DDR-001 for history and DDR-003 alongside.
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
test('each DDR yields one decision with area + title + date + rationale', () => {
|
|
42
|
-
const { batch, stats } = buildDdrBatch(fixtureDir({ 'DDR-001-x.md': DDR1 }), {});
|
|
43
|
-
assert.equal(batch.decisions.length, 1);
|
|
44
|
-
const d = batch.decisions[0];
|
|
45
|
-
assert.equal(d.title, 'First decision');
|
|
46
|
-
assert.equal(d.date, '2026-01-01');
|
|
47
|
-
assert.match(d.rationale, /simplest and safest/);
|
|
48
|
-
// area = primary tag, decision element, ABOUT link
|
|
49
|
-
const kinds = d.mutations.map((m) => `${m.op}:${m.kind || m.link || ''}`);
|
|
50
|
-
assert.ok(kinds.includes('upsert_element:area'));
|
|
51
|
-
assert.ok(kinds.includes('upsert_element:decision'));
|
|
52
|
-
assert.ok(kinds.includes('add_link:ABOUT'));
|
|
53
|
-
assert.equal(stats.files, 1);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('secondary tags become topic elements + TOUCHES links', () => {
|
|
57
|
-
const { batch } = buildDdrBatch(fixtureDir({ 'DDR-001-x.md': DDR1 }), {});
|
|
58
|
-
const muts = batch.decisions[0].mutations;
|
|
59
|
-
const topics = muts
|
|
60
|
-
.filter((m) => m.op === 'upsert_element' && m.kind === 'topic')
|
|
61
|
-
.map((m) => m.name);
|
|
62
|
-
assert.deepEqual(topics.sort(), ['ci', 'monorepo']); // primary 'infra' is the area, not a topic
|
|
63
|
-
assert.ok(muts.some((m) => m.link === 'TOUCHES'));
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('typed Supersedes marker wins; bare mention does not duplicate it', () => {
|
|
67
|
-
const { batch } = buildDdrBatch(fixtureDir({ 'DDR-002-y.md': DDR2 }), {});
|
|
68
|
-
const links = batch.decisions[0].mutations.filter((m) => m.op === 'add_link');
|
|
69
|
-
const sup = links.filter((l) => l.link === 'SUPERSEDES' && l.to === 'decision:DDR-001');
|
|
70
|
-
assert.equal(
|
|
71
|
-
sup.length,
|
|
72
|
-
1,
|
|
73
|
-
'exactly one SUPERSEDES → DDR-001 (typed marker, not duplicated by the bare mention)'
|
|
74
|
-
);
|
|
75
|
-
// DDR-003 came from **Related:** → REFERENCES
|
|
76
|
-
assert.ok(links.some((l) => l.link === 'REFERENCES' && l.to === 'decision:DDR-003'));
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('scope tags are added per decision when scope is set (model A)', () => {
|
|
80
|
-
const { batch } = buildDdrBatch(fixtureDir({ 'DDR-001-x.md': DDR1 }), {
|
|
81
|
-
repo: 'maude',
|
|
82
|
-
dept: 'dev',
|
|
83
|
-
});
|
|
84
|
-
const muts = batch.decisions[0].mutations;
|
|
85
|
-
assert.ok(muts.some((m) => m.op === 'upsert_element' && m.kind === 'repo' && m.name === 'maude'));
|
|
86
|
-
assert.ok(muts.some((m) => m.link === 'IN_REPO' && m.to === 'repo:maude'));
|
|
87
|
-
assert.ok(muts.some((m) => m.link === 'IN_DEPT' && m.to === 'dept:dev'));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('no scope ⇒ no scope mutations', () => {
|
|
91
|
-
const { batch } = buildDdrBatch(fixtureDir({ 'DDR-001-x.md': DDR1 }), {});
|
|
92
|
-
const muts = batch.decisions[0].mutations;
|
|
93
|
-
assert.ok(!muts.some((m) => m.link === 'IN_REPO' || m.link === 'IN_DEPT'));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('non-DDR files are ignored', () => {
|
|
97
|
-
const { batch } = buildDdrBatch(fixtureDir({ 'DDR-001-x.md': DDR1, 'README.md': '# index' }), {});
|
|
98
|
-
assert.equal(batch.decisions.length, 1);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('decision names are namespaced by repo so DDR numbers do not collide cross-repo', () => {
|
|
102
|
-
const files = { 'DDR-001-a.md': '# DDR-001: Alpha\n\n**Tags:** infra\n' };
|
|
103
|
-
const a = buildDdrBatch(fixtureDir(files), { repo: 'vantage', dept: 'marketing' });
|
|
104
|
-
const b = buildDdrBatch(fixtureDir(files), { repo: 'AI-StudyMate', dept: 'dev' });
|
|
105
|
-
const nameOf = (batch) => batch.decisions[0].mutations.find((m) => m.kind === 'decision').name;
|
|
106
|
-
assert.equal(nameOf(a.batch), 'vantage/DDR-001');
|
|
107
|
-
assert.equal(nameOf(b.batch), 'AI-StudyMate/DDR-001');
|
|
108
|
-
assert.notEqual(nameOf(a.batch), nameOf(b.batch));
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('area elements are NOT namespaced — concepts must converge across repos', () => {
|
|
112
|
-
const { batch } = buildDdrBatch(
|
|
113
|
-
fixtureDir({ 'DDR-001-a.md': '# DDR-001: Alpha\n\n**Tags:** security\n' }),
|
|
114
|
-
{ repo: 'vantage', dept: 'marketing' }
|
|
115
|
-
);
|
|
116
|
-
const area = batch.decisions[0].mutations.find((m) => m.kind === 'area');
|
|
117
|
-
assert.equal(area.name, 'security');
|
|
118
|
-
});
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
// Two modes, no hybrid — DDR-228.
|
|
2
|
-
//
|
|
3
|
-
// What is pinned here is mostly the SAFETY of the transitions rather than
|
|
4
|
-
// their mechanics: a mode switch runs against whatever state a person's tree
|
|
5
|
-
// happens to be in, so the properties that matter are "nothing on disk moved",
|
|
6
|
-
// "nothing unrelated got staged", and "the reverse puts it back".
|
|
7
|
-
|
|
8
|
-
import assert from 'node:assert/strict';
|
|
9
|
-
import { execFileSync } from 'node:child_process';
|
|
10
|
-
import {
|
|
11
|
-
existsSync,
|
|
12
|
-
mkdirSync,
|
|
13
|
-
mkdtempSync,
|
|
14
|
-
readFileSync,
|
|
15
|
-
rmSync,
|
|
16
|
-
symlinkSync,
|
|
17
|
-
writeFileSync,
|
|
18
|
-
} from 'node:fs';
|
|
19
|
-
import { tmpdir } from 'node:os';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
import { after, describe, it } from 'node:test';
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
adoptToHub,
|
|
25
|
-
applyOwnershipBlock,
|
|
26
|
-
buildOwnershipBlock,
|
|
27
|
-
detachToRepo,
|
|
28
|
-
findOwnershipBlock,
|
|
29
|
-
isGitRepo,
|
|
30
|
-
isHubOwned,
|
|
31
|
-
ownershipState,
|
|
32
|
-
removeOwnershipBlock,
|
|
33
|
-
stignoreLineFor,
|
|
34
|
-
syncthingFolderRoot,
|
|
35
|
-
trackedDesignPaths,
|
|
36
|
-
} from './design-ownership.mjs';
|
|
37
|
-
|
|
38
|
-
const made = [];
|
|
39
|
-
after(() => {
|
|
40
|
-
for (const d of made) rmSync(d, { recursive: true, force: true });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
function gitAvailable() {
|
|
44
|
-
try {
|
|
45
|
-
execFileSync('git', ['--version'], { stdio: 'ignore' });
|
|
46
|
-
return true;
|
|
47
|
-
} catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const gitOk = gitAvailable();
|
|
52
|
-
|
|
53
|
-
/** A real repo with a committed `.design/` — the Mode-A starting point. */
|
|
54
|
-
function repoWithDesign() {
|
|
55
|
-
const root = mkdtempSync(join(tmpdir(), 'ownership-'));
|
|
56
|
-
made.push(root);
|
|
57
|
-
const git = (...a) => execFileSync('git', a, { cwd: root, stdio: 'ignore' });
|
|
58
|
-
git('init', '-q');
|
|
59
|
-
git('config', 'user.email', 't@t.test');
|
|
60
|
-
git('config', 'user.name', 'T');
|
|
61
|
-
mkdirSync(join(root, '.design/system/ds'), { recursive: true });
|
|
62
|
-
writeFileSync(join(root, '.design/config.json'), '{}');
|
|
63
|
-
writeFileSync(join(root, '.design/system/ds/brand.css'), ':root{}');
|
|
64
|
-
writeFileSync(join(root, 'README.md'), '# repo');
|
|
65
|
-
git('add', '-A');
|
|
66
|
-
git('commit', '-qm', 'seed');
|
|
67
|
-
return root;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe('the ignore block', () => {
|
|
71
|
-
it('is one rule, and it is idempotent', () => {
|
|
72
|
-
const first = applyOwnershipBlock('', '.design');
|
|
73
|
-
assert.equal(first.action, 'added');
|
|
74
|
-
assert.ok(isHubOwned(first.contents));
|
|
75
|
-
assert.match(first.contents, /^\/\.design\/$/m);
|
|
76
|
-
|
|
77
|
-
const second = applyOwnershipBlock(first.contents, '.design');
|
|
78
|
-
assert.equal(second.action, 'updated');
|
|
79
|
-
assert.equal(second.contents.match(/maude:hub-owned:begin/g).length, 1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('goes at the END — gitignore is last-match-wins', () => {
|
|
83
|
-
const existing = 'node_modules/\n!.design/keep-me\n';
|
|
84
|
-
const { contents } = applyOwnershipBlock(existing, '.design');
|
|
85
|
-
assert.ok(
|
|
86
|
-
contents.indexOf('/.design/') > contents.indexOf('!.design/keep-me'),
|
|
87
|
-
'a rule that has to hold cannot sit above one that re-includes the path'
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('removes cleanly, leaving the rest of the file alone', () => {
|
|
92
|
-
const existing = 'node_modules/\ndist/\n';
|
|
93
|
-
const { contents: withBlock } = applyOwnershipBlock(existing, '.design');
|
|
94
|
-
const { contents: back, action } = removeOwnershipBlock(withBlock);
|
|
95
|
-
assert.equal(action, 'removed');
|
|
96
|
-
assert.ok(!isHubOwned(back));
|
|
97
|
-
assert.match(back, /node_modules\//);
|
|
98
|
-
assert.match(back, /dist\//);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('removing when absent is not an error', () => {
|
|
102
|
-
assert.equal(removeOwnershipBlock('node_modules/\n').action, 'absent');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('honours a non-default design root', () => {
|
|
106
|
-
assert.match(buildOwnershipBlock('design'), /^\/design\/$/m);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe('adopt — A to B', { skip: gitOk ? false : 'git not available' }, () => {
|
|
111
|
-
it('untracks the folder and leaves every byte on disk', () => {
|
|
112
|
-
const root = repoWithDesign();
|
|
113
|
-
assert.equal(trackedDesignPaths(root).length, 2);
|
|
114
|
-
|
|
115
|
-
const res = adoptToHub(root);
|
|
116
|
-
assert.equal(res.untracked, 2);
|
|
117
|
-
|
|
118
|
-
// THE safety property: `--cached` touches the index, never the tree.
|
|
119
|
-
assert.equal(readFileSync(join(root, '.design/system/ds/brand.css'), 'utf8'), ':root{}');
|
|
120
|
-
assert.equal(existsSync(join(root, '.design/config.json')), true);
|
|
121
|
-
assert.equal(trackedDesignPaths(root).length, 0);
|
|
122
|
-
assert.ok(isHubOwned(readFileSync(join(root, '.gitignore'), 'utf8')));
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('stages the removals and .gitignore — and nothing else', () => {
|
|
126
|
-
const root = repoWithDesign();
|
|
127
|
-
// An unrelated edit, dirty in the tree exactly as a real person's would be.
|
|
128
|
-
writeFileSync(join(root, 'README.md'), '# repo, mid-edit');
|
|
129
|
-
writeFileSync(join(root, 'notes.txt'), 'scratch');
|
|
130
|
-
|
|
131
|
-
adoptToHub(root);
|
|
132
|
-
|
|
133
|
-
const staged = execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
134
|
-
cwd: root,
|
|
135
|
-
encoding: 'utf8',
|
|
136
|
-
})
|
|
137
|
-
.split('\n')
|
|
138
|
-
.filter(Boolean)
|
|
139
|
-
.sort();
|
|
140
|
-
assert.deepEqual(staged, ['.design/config.json', '.design/system/ds/brand.css', '.gitignore']);
|
|
141
|
-
// Their work is untouched, staged or otherwise.
|
|
142
|
-
assert.equal(readFileSync(join(root, 'README.md'), 'utf8'), '# repo, mid-edit');
|
|
143
|
-
assert.equal(existsSync(join(root, 'notes.txt')), true);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('is safe to run twice', () => {
|
|
147
|
-
const root = repoWithDesign();
|
|
148
|
-
adoptToHub(root);
|
|
149
|
-
const again = adoptToHub(root);
|
|
150
|
-
assert.equal(again.untracked, 0);
|
|
151
|
-
assert.equal(
|
|
152
|
-
readFileSync(join(root, '.gitignore'), 'utf8').match(/maude:hub-owned:begin/g).length,
|
|
153
|
-
1
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('--dry-run says what it would do and does none of it', () => {
|
|
158
|
-
const root = repoWithDesign();
|
|
159
|
-
const res = adoptToHub(root, { dryRun: true });
|
|
160
|
-
assert.equal(res.untracked, 2);
|
|
161
|
-
assert.equal(existsSync(join(root, '.gitignore')), false);
|
|
162
|
-
assert.equal(trackedDesignPaths(root).length, 2);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('detach — B back to A', { skip: gitOk ? false : 'git not available' }, () => {
|
|
167
|
-
it('un-ignores the folder and leaves committing to the person', () => {
|
|
168
|
-
const root = repoWithDesign();
|
|
169
|
-
adoptToHub(root);
|
|
170
|
-
execFileSync('git', ['commit', '-qm', 'adopt'], { cwd: root, stdio: 'ignore' });
|
|
171
|
-
|
|
172
|
-
const res = detachToRepo(root);
|
|
173
|
-
assert.equal(res.action, 'removed');
|
|
174
|
-
assert.ok(!isHubOwned(readFileSync(join(root, '.gitignore'), 'utf8')));
|
|
175
|
-
|
|
176
|
-
// Not committed, and not even staged — theirs to decide. But git can SEE
|
|
177
|
-
// them again, which is the whole point of the transition.
|
|
178
|
-
const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
|
|
179
|
-
cwd: root,
|
|
180
|
-
encoding: 'utf8',
|
|
181
|
-
});
|
|
182
|
-
assert.match(untracked, /\.design\/system\/ds\/brand\.css/);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('round-trips: adopt then detach restores what git can see', () => {
|
|
186
|
-
const root = repoWithDesign();
|
|
187
|
-
const before = trackedDesignPaths(root).sort();
|
|
188
|
-
adoptToHub(root);
|
|
189
|
-
execFileSync('git', ['commit', '-qm', 'adopt'], { cwd: root, stdio: 'ignore' });
|
|
190
|
-
detachToRepo(root);
|
|
191
|
-
execFileSync('git', ['add', '--', '.design'], { cwd: root, stdio: 'ignore' });
|
|
192
|
-
execFileSync('git', ['commit', '-qm', 'detach'], { cwd: root, stdio: 'ignore' });
|
|
193
|
-
assert.deepEqual(trackedDesignPaths(root).sort(), before);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('the hybrid is a NAMED state, not an accident', { skip: gitOk ? false : 'no git' }, () => {
|
|
198
|
-
it('linked + still tracked reads as hybrid', () => {
|
|
199
|
-
const root = repoWithDesign();
|
|
200
|
-
const st = ownershipState(root, { linked: true });
|
|
201
|
-
assert.equal(st.mode, 'hybrid');
|
|
202
|
-
assert.equal(st.ignored, false);
|
|
203
|
-
assert.equal(st.trackedCount, 2);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('linked + ignored + untracked reads as hub-owned', () => {
|
|
207
|
-
const root = repoWithDesign();
|
|
208
|
-
adoptToHub(root);
|
|
209
|
-
assert.equal(ownershipState(root, { linked: true }).mode, 'hub-owned');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('unlinked is repo-owned whatever the gitignore says', () => {
|
|
213
|
-
const root = repoWithDesign();
|
|
214
|
-
assert.equal(ownershipState(root, { linked: false }).mode, 'repo-owned');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('Syncthing rides a lane gitignore cannot close', () => {
|
|
219
|
-
it('finds the managed folder root from a nested repo', () => {
|
|
220
|
-
const st = mkdtempSync(join(tmpdir(), 'stfolder-'));
|
|
221
|
-
made.push(st);
|
|
222
|
-
mkdirSync(join(st, '.stfolder'), { recursive: true });
|
|
223
|
-
mkdirSync(join(st, 'personal/maude'), { recursive: true });
|
|
224
|
-
assert.equal(syncthingFolderRoot(join(st, 'personal/maude')), st);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('is null outside one', () => {
|
|
228
|
-
const plain = mkdtempSync(join(tmpdir(), 'plain-'));
|
|
229
|
-
made.push(plain);
|
|
230
|
-
assert.equal(syncthingFolderRoot(plain), null);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('names the .stignore line relative to the managed root', () => {
|
|
234
|
-
assert.equal(
|
|
235
|
-
stignoreLineFor('/tree/personal/maude', '.design', '/tree'),
|
|
236
|
-
'/personal/maude/.design'
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('no git repo — the question does not arise', () => {
|
|
242
|
-
it('reads as hub-owned when linked, and every transition is a no-op', () => {
|
|
243
|
-
const plain = mkdtempSync(join(tmpdir(), 'nogit-'));
|
|
244
|
-
made.push(plain);
|
|
245
|
-
mkdirSync(join(plain, '.design'), { recursive: true });
|
|
246
|
-
writeFileSync(join(plain, '.design/config.json'), '{}');
|
|
247
|
-
|
|
248
|
-
assert.equal(isGitRepo(plain), false);
|
|
249
|
-
assert.equal(ownershipState(plain, { linked: true }).mode, 'hub-owned');
|
|
250
|
-
assert.equal(adoptToHub(plain).action, 'no-git');
|
|
251
|
-
assert.equal(detachToRepo(plain).action, 'no-git');
|
|
252
|
-
// And nothing was written — Maude does not require git, so it does not
|
|
253
|
-
// start leaving gitignores in projects that have no repo.
|
|
254
|
-
assert.equal(existsSync(join(plain, '.gitignore')), false);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('a .gitignore is a shared, untrusted file', () => {
|
|
259
|
-
// The repo is committed and DDR-054 says peers can write it. The first
|
|
260
|
-
// version matched the markers as a bare substring and replaced everything
|
|
261
|
-
// between the first BEGIN and the first END — so a peer could wrap a
|
|
262
|
-
// victim's real rules in our markers and have `adopt` delete them.
|
|
263
|
-
const hostile = [
|
|
264
|
-
'node_modules/',
|
|
265
|
-
'# maude:hub-owned:begin',
|
|
266
|
-
'.env',
|
|
267
|
-
'*.pem',
|
|
268
|
-
'secrets/',
|
|
269
|
-
'# maude:hub-owned:end',
|
|
270
|
-
].join('\n');
|
|
271
|
-
|
|
272
|
-
it('refuses to rewrite a block it did not write', () => {
|
|
273
|
-
const res = applyOwnershipBlock(hostile, '.design');
|
|
274
|
-
assert.equal(res.action, 'refused-malformed');
|
|
275
|
-
assert.equal(res.contents, hostile, 'not one rule touched');
|
|
276
|
-
assert.match(res.contents, /\.env/);
|
|
277
|
-
assert.match(res.contents, /secrets\//);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('and refuses to remove one either', () => {
|
|
281
|
-
assert.equal(removeOwnershipBlock(hostile).action, 'refused-malformed');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('a lone marker is malformed, not "already hub-owned"', () => {
|
|
285
|
-
// The cheaper half of the same finding: `ignored` derived from one marker
|
|
286
|
-
// made a hybrid report itself as settled, so the state DDR-228 exists to
|
|
287
|
-
// end would persist undetected.
|
|
288
|
-
assert.equal(isHubOwned('# maude:hub-owned:begin\nnode_modules/\n'), false);
|
|
289
|
-
assert.equal(findOwnershipBlock('# maude:hub-owned:begin\n').reason, 'malformed');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('a marker inside a comment is not a marker', () => {
|
|
293
|
-
assert.equal(isHubOwned('# see # maude:hub-owned:begin for details\n'), false);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('our own block still round-trips', () => {
|
|
297
|
-
const { contents } = applyOwnershipBlock('node_modules/\n', '.design');
|
|
298
|
-
assert.equal(isHubOwned(contents), true);
|
|
299
|
-
const back = removeOwnershipBlock(contents);
|
|
300
|
-
assert.equal(back.action, 'removed');
|
|
301
|
-
assert.match(back.contents, /node_modules\//);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('adopt refuses outright rather than untracking against a file it cannot read', {
|
|
305
|
-
skip: gitOk ? false : 'git not available',
|
|
306
|
-
}, () => {
|
|
307
|
-
const root = repoWithDesign();
|
|
308
|
-
writeFileSync(join(root, '.gitignore'), hostile);
|
|
309
|
-
const res = adoptToHub(root);
|
|
310
|
-
assert.equal(res.action, 'refused-malformed');
|
|
311
|
-
assert.equal(res.untracked, 0);
|
|
312
|
-
assert.equal(trackedDesignPaths(root).length, 2, 'the design root is still tracked');
|
|
313
|
-
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), hostile);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('refuses to write through a symlinked .gitignore', {
|
|
317
|
-
skip: gitOk ? false : 'git not available',
|
|
318
|
-
}, () => {
|
|
319
|
-
const root = repoWithDesign();
|
|
320
|
-
const outside = mkdtempSync(join(tmpdir(), 'gi-target-'));
|
|
321
|
-
made.push(outside);
|
|
322
|
-
const victim = join(outside, 'important.txt');
|
|
323
|
-
writeFileSync(victim, 'do not overwrite me');
|
|
324
|
-
symlinkSync(victim, join(root, '.gitignore'));
|
|
325
|
-
|
|
326
|
-
assert.throws(() => adoptToHub(root), /not a regular file/);
|
|
327
|
-
assert.equal(readFileSync(victim, 'utf8'), 'do not overwrite me');
|
|
328
|
-
});
|
|
329
|
-
});
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// DDR-147 § Security follow-up item 1 — the hardened `maude design fetch-asset`
|
|
2
|
-
// helper (apps/studio/bin/_fetch-asset.mjs). This suite locks the SECURITY CORE
|
|
3
|
-
// that the untrusted-fetch rails promise: the resolved-IP SSRF classifier
|
|
4
|
-
// (reject loopback / link-local / RFC-1918 / CGNAT / multicast / reserved, v4 +
|
|
5
|
-
// v6 incl. IPv4-mapped + NAT64), https-only URL parsing, the png/jpg/gif/webp
|
|
6
|
-
// magic sniff (SVG/HTML rejected), the content-addressed name contract, and the
|
|
7
|
-
// realpath containment assertion. Runs under `pnpm test` (node --test cli/**).
|
|
8
|
-
//
|
|
9
|
-
// The network path (curl) is exercised by the manual smoke in the RCA/PR; these
|
|
10
|
-
// are the offline, deterministic guards a regression would trip.
|
|
11
|
-
|
|
12
|
-
import assert from 'node:assert/strict';
|
|
13
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
-
import { dirname, resolve } from 'node:path';
|
|
15
|
-
import { test } from 'node:test';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
|
|
18
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const REPO = resolve(HERE, '..', '..');
|
|
20
|
-
const MOD = resolve(REPO, 'apps', 'studio', 'bin', '_fetch-asset.mjs');
|
|
21
|
-
|
|
22
|
-
const {
|
|
23
|
-
classifyAddress,
|
|
24
|
-
parseIPv4,
|
|
25
|
-
parseIPv6,
|
|
26
|
-
parseHttpsTarget,
|
|
27
|
-
sniffImageExt,
|
|
28
|
-
assetName,
|
|
29
|
-
containedAssetPath,
|
|
30
|
-
FetchAssetError,
|
|
31
|
-
} = await import(MOD);
|
|
32
|
-
|
|
33
|
-
// ── SSRF classifier: BLOCKED addresses ───────────────────────────────────────
|
|
34
|
-
test('classifyAddress blocks private / loopback / link-local / reserved IPv4', () => {
|
|
35
|
-
const blocked = [
|
|
36
|
-
'127.0.0.1',
|
|
37
|
-
'127.9.9.9',
|
|
38
|
-
'10.0.0.1',
|
|
39
|
-
'10.255.255.255',
|
|
40
|
-
'172.16.0.1',
|
|
41
|
-
'172.31.255.255',
|
|
42
|
-
'192.168.1.1',
|
|
43
|
-
'169.254.169.254', // AWS/GCP IMDS
|
|
44
|
-
'169.254.0.1',
|
|
45
|
-
'100.64.0.1', // CGNAT
|
|
46
|
-
'100.127.255.255',
|
|
47
|
-
'0.0.0.0',
|
|
48
|
-
'224.0.0.1', // multicast
|
|
49
|
-
'239.255.255.255',
|
|
50
|
-
'240.0.0.1', // reserved
|
|
51
|
-
'255.255.255.255',
|
|
52
|
-
'198.18.0.1', // benchmark
|
|
53
|
-
'192.0.2.5', // TEST-NET-1
|
|
54
|
-
'192.0.0.1', // IETF
|
|
55
|
-
];
|
|
56
|
-
for (const ip of blocked) {
|
|
57
|
-
assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('classifyAddress allows genuine public IPv4 (incl. just-outside-range boundaries)', () => {
|
|
62
|
-
const allowed = [
|
|
63
|
-
'8.8.8.8',
|
|
64
|
-
'1.1.1.1',
|
|
65
|
-
'93.184.216.34', // example.com
|
|
66
|
-
'172.15.255.255', // just below 172.16/12
|
|
67
|
-
'172.32.0.1', // just above 172.31
|
|
68
|
-
'100.63.255.255', // just below CGNAT
|
|
69
|
-
'100.128.0.1', // just above CGNAT
|
|
70
|
-
'169.253.255.255', // just below link-local
|
|
71
|
-
'11.0.0.1',
|
|
72
|
-
'192.167.255.255',
|
|
73
|
-
'192.169.0.1',
|
|
74
|
-
'223.255.255.255', // just below multicast
|
|
75
|
-
];
|
|
76
|
-
for (const ip of allowed) {
|
|
77
|
-
assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('classifyAddress blocks loopback / link-local / ULA / multicast + embedded-v4 IPv6', () => {
|
|
82
|
-
const blocked = [
|
|
83
|
-
'::1', // loopback
|
|
84
|
-
'::', // unspecified
|
|
85
|
-
'fe80::1', // link-local
|
|
86
|
-
'fe80::abcd:1234',
|
|
87
|
-
'fc00::1', // ULA
|
|
88
|
-
'fd12:3456::1', // ULA
|
|
89
|
-
'ff02::1', // multicast
|
|
90
|
-
'::ffff:127.0.0.1', // IPv4-mapped loopback
|
|
91
|
-
'::ffff:10.0.0.1', // IPv4-mapped private
|
|
92
|
-
'::ffff:169.254.169.254', // IPv4-mapped IMDS
|
|
93
|
-
'64:ff9b::127.0.0.1', // NAT64 → loopback
|
|
94
|
-
'64:ff9b::192.168.0.1', // NAT64 → private
|
|
95
|
-
];
|
|
96
|
-
for (const ip of blocked) {
|
|
97
|
-
assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('classifyAddress allows genuine public IPv6 (incl. IPv4-mapped public)', () => {
|
|
102
|
-
const allowed = [
|
|
103
|
-
'2001:4860:4860::8888', // Google DNS
|
|
104
|
-
'2606:4700:4700::1111', // Cloudflare DNS
|
|
105
|
-
'::ffff:8.8.8.8', // IPv4-mapped PUBLIC → allowed via embedded classify
|
|
106
|
-
'2a00:1450:4001:81b::200e',
|
|
107
|
-
];
|
|
108
|
-
for (const ip of allowed) {
|
|
109
|
-
assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('classifyAddress fail-closes on non-IP / garbage input', () => {
|
|
114
|
-
for (const bad of ['not-an-ip', 'example.com', '', '999.999.999.999', 'gggg::1']) {
|
|
115
|
-
assert.ok(classifyAddress(bad), `expected ${JSON.stringify(bad)} to be treated as blocked`);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ── IP parsers ───────────────────────────────────────────────────────────────
|
|
120
|
-
test('parseIPv4 rejects malformed dotted-quads', () => {
|
|
121
|
-
for (const bad of ['1.2.3', '1.2.3.4.5', '256.1.1.1', '01.2.3.4', '1.2.3.a', '', '-1.2.3.4']) {
|
|
122
|
-
assert.equal(parseIPv4(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
|
|
123
|
-
}
|
|
124
|
-
assert.deepEqual([...parseIPv4('1.2.3.4')], [1, 2, 3, 4]);
|
|
125
|
-
assert.deepEqual([...parseIPv4('255.255.255.255')], [255, 255, 255, 255]);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('parseIPv6 handles ::, embedded IPv4, zone id; rejects malformed', () => {
|
|
129
|
-
assert.ok(parseIPv6('::1'));
|
|
130
|
-
assert.ok(parseIPv6('::'));
|
|
131
|
-
assert.ok(parseIPv6('fe80::1%en0')); // zone id stripped
|
|
132
|
-
assert.ok(parseIPv6('2001:db8::1'));
|
|
133
|
-
assert.ok(parseIPv6('::ffff:1.2.3.4'));
|
|
134
|
-
for (const bad of ['gggg::', '1::2::3', '12345::', '1.2.3.4', '']) {
|
|
135
|
-
assert.equal(parseIPv6(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// ── URL target ───────────────────────────────────────────────────────────────
|
|
140
|
-
test('parseHttpsTarget accepts https and extracts host/port', () => {
|
|
141
|
-
assert.deepEqual(parseHttpsTarget('https://example.com/a.png'), {
|
|
142
|
-
host: 'example.com',
|
|
143
|
-
port: 443,
|
|
144
|
-
});
|
|
145
|
-
assert.deepEqual(parseHttpsTarget('https://cdn.example.com:8443/x'), {
|
|
146
|
-
host: 'cdn.example.com',
|
|
147
|
-
port: 8443,
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('parseHttpsTarget rejects non-https, credentials, and hostless URLs', () => {
|
|
152
|
-
const bad = [
|
|
153
|
-
'http://example.com/a.png',
|
|
154
|
-
'ftp://example.com/a.png',
|
|
155
|
-
'file:///etc/passwd',
|
|
156
|
-
'https://user:pass@example.com/a.png',
|
|
157
|
-
'not a url',
|
|
158
|
-
];
|
|
159
|
-
for (const u of bad) {
|
|
160
|
-
assert.throws(() => parseHttpsTarget(u), FetchAssetError, `expected ${u} to throw`);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// ── image sniff ──────────────────────────────────────────────────────────────
|
|
165
|
-
test('sniffImageExt recognises png/jpg/gif/webp and rejects SVG/HTML/empty', () => {
|
|
166
|
-
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0]);
|
|
167
|
-
const jpg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0]);
|
|
168
|
-
const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0, 0]);
|
|
169
|
-
const webp = Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50]);
|
|
170
|
-
assert.equal(sniffImageExt(png), 'png');
|
|
171
|
-
assert.equal(sniffImageExt(jpg), 'jpg');
|
|
172
|
-
assert.equal(sniffImageExt(gif), 'gif');
|
|
173
|
-
assert.equal(sniffImageExt(webp), 'webp');
|
|
174
|
-
assert.equal(sniffImageExt(Buffer.from('<svg xmlns="...')), null);
|
|
175
|
-
assert.equal(sniffImageExt(Buffer.from('<!DOCTYPE html>')), null);
|
|
176
|
-
assert.equal(sniffImageExt(Buffer.from([])), null);
|
|
177
|
-
// RIFF that is NOT webp (e.g. wav) must not sniff as an image.
|
|
178
|
-
assert.equal(
|
|
179
|
-
sniffImageExt(Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x41, 0x56, 0x45])),
|
|
180
|
-
null
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// ── naming + containment ─────────────────────────────────────────────────────
|
|
185
|
-
test('assetName is content-addressed <sha8>.<ext> and deterministic', () => {
|
|
186
|
-
const bytes = Buffer.from('hello world');
|
|
187
|
-
const n1 = assetName(bytes, 'png');
|
|
188
|
-
const n2 = assetName(bytes, 'png');
|
|
189
|
-
assert.equal(n1, n2);
|
|
190
|
-
assert.match(n1, /^[a-f0-9]{8}\.png$/);
|
|
191
|
-
assert.notEqual(assetName(Buffer.from('other'), 'jpg'), n1);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('containedAssetPath resolves under <root>/<designRoot>/assets and rejects escape', () => {
|
|
195
|
-
const { assetsDir, fileAbs } = containedAssetPath('/repo', '.design', 'a1b2c3d4.png');
|
|
196
|
-
assert.equal(assetsDir, resolve('/repo/.design/assets'));
|
|
197
|
-
assert.equal(fileAbs, resolve('/repo/.design/assets/a1b2c3d4.png'));
|
|
198
|
-
// A traversal name escapes → throws.
|
|
199
|
-
assert.throws(() => containedAssetPath('/repo', '.design', '../../etc/passwd'), FetchAssetError);
|
|
200
|
-
// A design-root that climbs out of the repo → throws.
|
|
201
|
-
assert.throws(() => containedAssetPath('/repo', '../../etc', 'a1b2c3d4.png'), FetchAssetError);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// ── wiring: verb registered + helper files ship ──────────────────────────────
|
|
205
|
-
test('fetch-asset is a registered `maude design` bin verb with both helper files present', () => {
|
|
206
|
-
assert.ok(existsSync(resolve(REPO, 'apps/studio/bin/fetch-asset.sh')), 'fetch-asset.sh missing');
|
|
207
|
-
assert.ok(
|
|
208
|
-
existsSync(resolve(REPO, 'apps/studio/bin/_fetch-asset.mjs')),
|
|
209
|
-
'_fetch-asset.mjs missing'
|
|
210
|
-
);
|
|
211
|
-
const design = readFileSync(resolve(REPO, 'cli/commands/design.mjs'), 'utf8');
|
|
212
|
-
assert.match(design, /'fetch-asset'/, "'fetch-asset' not in design.mjs BIN_VERBS");
|
|
213
|
-
});
|