@vellumai/assistant 0.4.17 → 0.4.18
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/eslint.config.mjs +2 -2
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +128 -120
- package/src/__tests__/account-registry.test.ts +121 -110
- package/src/__tests__/active-skill-tools.test.ts +200 -172
- package/src/__tests__/actor-token-service.test.ts +341 -274
- package/src/__tests__/agent-loop-thinking.test.ts +28 -19
- package/src/__tests__/agent-loop.test.ts +798 -378
- package/src/__tests__/anthropic-provider.test.ts +405 -247
- package/src/__tests__/app-builder-tool-scripts.test.ts +97 -97
- package/src/__tests__/app-bundler.test.ts +112 -79
- package/src/__tests__/app-executors.test.ts +205 -178
- package/src/__tests__/app-git-history.test.ts +90 -73
- package/src/__tests__/app-git-service.test.ts +67 -53
- package/src/__tests__/app-open-proxy.test.ts +29 -25
- package/src/__tests__/approval-conversation-turn.test.ts +100 -81
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +45 -17
- package/src/__tests__/approval-message-composer.test.ts +119 -119
- package/src/__tests__/approval-primitive.test.ts +264 -233
- package/src/__tests__/approval-routes-http.test.ts +4 -3
- package/src/__tests__/asset-materialize-tool.test.ts +250 -178
- package/src/__tests__/asset-search-tool.test.ts +251 -191
- package/src/__tests__/assistant-attachment-directive.test.ts +187 -142
- package/src/__tests__/assistant-attachments.test.ts +254 -186
- package/src/__tests__/assistant-event-hub.test.ts +105 -63
- package/src/__tests__/assistant-event.test.ts +66 -58
- package/src/__tests__/assistant-events-sse-hardening.test.ts +113 -73
- package/src/__tests__/assistant-feature-flag-guard.test.ts +78 -52
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +48 -45
- package/src/__tests__/assistant-feature-flags-integration.test.ts +118 -77
- package/src/__tests__/assistant-id-boundary-guard.test.ts +158 -104
- package/src/__tests__/attachments-store.test.ts +240 -183
- package/src/__tests__/attachments.test.ts +70 -62
- package/src/__tests__/audit-log-rotation.test.ts +50 -35
- package/src/__tests__/browser-fill-credential.test.ts +169 -101
- package/src/__tests__/browser-manager.test.ts +97 -75
- package/src/__tests__/browser-runtime-check.test.ts +16 -15
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +12 -10
- package/src/__tests__/browser-skill-endstate.test.ts +97 -72
- package/src/__tests__/bundle-scanner.test.ts +47 -22
- package/src/__tests__/bundled-asset.test.ts +74 -47
- package/src/__tests__/call-constants.test.ts +19 -19
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-conversation-messages.test.ts +90 -65
- package/src/__tests__/call-domain.test.ts +149 -121
- package/src/__tests__/call-pointer-message-composer.test.ts +113 -83
- package/src/__tests__/call-pointer-messages.test.ts +213 -154
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +9 -10
- package/src/__tests__/call-recovery.test.ts +232 -212
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/call-start-guardian-guard.test.ts +32 -30
- package/src/__tests__/call-state-machine.test.ts +62 -51
- package/src/__tests__/call-state.test.ts +89 -75
- package/src/__tests__/call-store.test.ts +387 -316
- package/src/__tests__/callback-handoff-copy.test.ts +84 -82
- package/src/__tests__/canonical-guardian-store.test.ts +331 -280
- package/src/__tests__/channel-approval-routes.test.ts +1643 -1115
- package/src/__tests__/channel-approval.test.ts +139 -137
- package/src/__tests__/channel-approvals.test.ts +0 -1
- package/src/__tests__/channel-delivery-store.test.ts +232 -194
- package/src/__tests__/channel-guardian.test.ts +5 -3
- package/src/__tests__/channel-invite-transport.test.ts +107 -92
- package/src/__tests__/channel-policy.test.ts +42 -38
- package/src/__tests__/channel-readiness-service.test.ts +119 -102
- package/src/__tests__/channel-reply-delivery.test.ts +147 -118
- package/src/__tests__/channel-retry-sweep.test.ts +153 -110
- package/src/__tests__/checker.test.ts +3309 -1850
- package/src/__tests__/clarification-resolver.test.ts +91 -79
- package/src/__tests__/classifier.test.ts +64 -54
- package/src/__tests__/claude-code-skill-regression.test.ts +42 -37
- package/src/__tests__/claude-code-tool-profiles.test.ts +31 -29
- package/src/__tests__/clawhub.test.ts +92 -82
- package/src/__tests__/cli.test.ts +30 -30
- package/src/__tests__/clipboard.test.ts +53 -46
- package/src/__tests__/commit-guarantee.test.ts +59 -52
- package/src/__tests__/commit-message-enrichment-service.test.ts +203 -75
- package/src/__tests__/compaction.benchmark.test.ts +33 -31
- package/src/__tests__/computer-use-session-compaction.test.ts +60 -50
- package/src/__tests__/computer-use-session-lifecycle.test.ts +145 -117
- package/src/__tests__/computer-use-session-working-dir.test.ts +62 -48
- package/src/__tests__/computer-use-skill-baseline.test.ts +22 -19
- package/src/__tests__/computer-use-skill-endstate.test.ts +45 -31
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +121 -88
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +65 -42
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +33 -18
- package/src/__tests__/computer-use-tools.test.ts +121 -98
- package/src/__tests__/config-schema.test.ts +443 -347
- package/src/__tests__/config-watcher.test.ts +96 -81
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +148 -133
- package/src/__tests__/conflict-intent-tokenization.test.ts +96 -78
- package/src/__tests__/conflict-policy.test.ts +151 -80
- package/src/__tests__/conflict-store.test.ts +203 -157
- package/src/__tests__/connection-policy.test.ts +89 -59
- package/src/__tests__/contacts-tools.test.ts +247 -178
- package/src/__tests__/context-memory-e2e.test.ts +306 -214
- package/src/__tests__/context-token-estimator.test.ts +114 -74
- package/src/__tests__/context-window-manager.test.ts +269 -167
- package/src/__tests__/contradiction-checker.test.ts +161 -135
- package/src/__tests__/conversation-attention-store.test.ts +350 -290
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-pairing.test.ts +220 -113
- package/src/__tests__/conversation-store.test.ts +390 -235
- package/src/__tests__/credential-broker-browser-fill.test.ts +325 -250
- package/src/__tests__/credential-broker-server-use.test.ts +283 -243
- package/src/__tests__/credential-broker.test.ts +128 -74
- package/src/__tests__/credential-host-pattern-match.test.ts +64 -44
- package/src/__tests__/credential-metadata-store.test.ts +360 -311
- package/src/__tests__/credential-policy-validate.test.ts +81 -65
- package/src/__tests__/credential-resolve.test.ts +212 -145
- package/src/__tests__/credential-security-e2e.test.ts +144 -103
- package/src/__tests__/credential-security-invariants.test.ts +253 -208
- package/src/__tests__/credential-selection.test.ts +254 -146
- package/src/__tests__/credential-vault-unit.test.ts +531 -341
- package/src/__tests__/credential-vault.test.ts +761 -484
- package/src/__tests__/daemon-assistant-events.test.ts +91 -66
- package/src/__tests__/daemon-lifecycle.test.ts +258 -190
- package/src/__tests__/daemon-server-session-init.test.ts +0 -1
- package/src/__tests__/date-context.test.ts +314 -249
- package/src/__tests__/db-migration-rollback.test.ts +259 -130
- package/src/__tests__/db-schedule-syntax-migration.test.ts +78 -41
- package/src/__tests__/delete-managed-skill-tool.test.ts +77 -53
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/dictation-mode-detection.test.ts +77 -55
- package/src/__tests__/dictation-profile-store.test.ts +70 -56
- package/src/__tests__/dictation-text-processing.test.ts +53 -35
- package/src/__tests__/diff.test.ts +102 -98
- package/src/__tests__/domain-normalize.test.ts +54 -54
- package/src/__tests__/domain-policy.test.ts +71 -55
- package/src/__tests__/dynamic-page-surface.test.ts +31 -33
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +69 -69
- package/src/__tests__/edit-engine.test.ts +56 -56
- package/src/__tests__/elevenlabs-client.test.ts +117 -91
- package/src/__tests__/elevenlabs-config.test.ts +32 -31
- package/src/__tests__/email-classifier.test.ts +15 -12
- package/src/__tests__/email-cli.test.ts +121 -108
- package/src/__tests__/emit-signal-routing-intent.test.ts +76 -69
- package/src/__tests__/encrypted-store.test.ts +180 -154
- package/src/__tests__/entity-extractor.test.ts +108 -87
- package/src/__tests__/entity-search.test.ts +664 -258
- package/src/__tests__/ephemeral-permissions.test.ts +224 -188
- package/src/__tests__/event-bus.test.ts +81 -77
- package/src/__tests__/extract-email.test.ts +29 -20
- package/src/__tests__/file-edit-tool.test.ts +62 -44
- package/src/__tests__/file-ops-service.test.ts +131 -114
- package/src/__tests__/file-read-tool.test.ts +48 -31
- package/src/__tests__/file-write-tool.test.ts +43 -37
- package/src/__tests__/filesystem-tools.test.ts +238 -209
- package/src/__tests__/followup-tools.test.ts +237 -162
- package/src/__tests__/forbidden-legacy-symbols.test.ts +19 -20
- package/src/__tests__/frontmatter.test.ts +96 -81
- package/src/__tests__/fuzzy-match-property.test.ts +75 -81
- package/src/__tests__/fuzzy-match.test.ts +71 -65
- package/src/__tests__/gateway-client-managed-outbound.test.ts +76 -57
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-image-service.test.ts +113 -100
- package/src/__tests__/gemini-provider.test.ts +297 -220
- package/src/__tests__/get-weather.test.ts +188 -114
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-action-conversation-turn.test.ts +226 -171
- package/src/__tests__/guardian-action-copy-generator.test.ts +111 -93
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-action-followup-store.test.ts +199 -167
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +297 -250
- package/src/__tests__/guardian-action-late-reply.test.ts +462 -316
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +23 -18
- package/src/__tests__/guardian-action-store.test.ts +158 -109
- package/src/__tests__/guardian-action-sweep.test.ts +114 -100
- package/src/__tests__/guardian-actions-endpoint.test.ts +440 -256
- package/src/__tests__/guardian-control-plane-policy.test.ts +497 -331
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +217 -215
- package/src/__tests__/guardian-dispatch.test.ts +316 -256
- package/src/__tests__/guardian-grant-minting.test.ts +247 -178
- package/src/__tests__/guardian-outbound-http.test.ts +5 -3
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +99 -96
- package/src/__tests__/guardian-question-copy.test.ts +17 -17
- package/src/__tests__/guardian-question-mode.test.ts +134 -100
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/guardian-verification-intent-routing.test.ts +94 -88
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +0 -1
- package/src/__tests__/handle-user-message-secret-resume.test.ts +0 -1
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +92 -76
- package/src/__tests__/handlers-cu-observation-blob.test.ts +103 -70
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +77 -51
- package/src/__tests__/handlers-slack-config.test.ts +63 -54
- package/src/__tests__/handlers-task-submit-slash.test.ts +18 -18
- package/src/__tests__/handlers-telegram-config.test.ts +662 -329
- package/src/__tests__/handlers-twitter-config.test.ts +525 -298
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -2
- package/src/__tests__/headless-browser-interactions.test.ts +444 -280
- package/src/__tests__/headless-browser-navigate.test.ts +116 -79
- package/src/__tests__/headless-browser-read-tools.test.ts +123 -86
- package/src/__tests__/headless-browser-snapshot.test.ts +71 -56
- package/src/__tests__/heartbeat-service.test.ts +76 -58
- package/src/__tests__/history-repair-observability.test.ts +14 -14
- package/src/__tests__/history-repair.test.ts +171 -167
- package/src/__tests__/home-base-bootstrap.test.ts +30 -27
- package/src/__tests__/hooks-blocking.test.ts +86 -37
- package/src/__tests__/hooks-cli.test.ts +104 -68
- package/src/__tests__/hooks-config.test.ts +81 -43
- package/src/__tests__/hooks-discovery.test.ts +106 -96
- package/src/__tests__/hooks-integration.test.ts +78 -72
- package/src/__tests__/hooks-manager.test.ts +99 -61
- package/src/__tests__/hooks-runner.test.ts +94 -71
- package/src/__tests__/hooks-settings.test.ts +69 -64
- package/src/__tests__/hooks-templates.test.ts +85 -54
- package/src/__tests__/hooks-ts-runner.test.ts +82 -45
- package/src/__tests__/hooks-watch.test.ts +32 -22
- package/src/__tests__/host-file-edit-tool.test.ts +190 -148
- package/src/__tests__/host-file-read-tool.test.ts +86 -63
- package/src/__tests__/host-file-write-tool.test.ts +98 -64
- package/src/__tests__/host-shell-tool.test.ts +342 -233
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-member-store.test.ts +163 -159
- package/src/__tests__/ingress-reconcile.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +441 -356
- package/src/__tests__/ingress-url-consistency.test.ts +125 -64
- package/src/__tests__/integration-status.test.ts +93 -73
- package/src/__tests__/intent-routing.test.ts +148 -118
- package/src/__tests__/invite-redemption-service.test.ts +163 -121
- package/src/__tests__/ipc-blob-store.test.ts +104 -91
- package/src/__tests__/ipc-contract-inventory.test.ts +27 -15
- package/src/__tests__/ipc-contract.test.ts +24 -23
- package/src/__tests__/ipc-protocol.test.ts +52 -46
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +61 -50
- package/src/__tests__/ipc-snapshot.test.ts +1135 -1056
- package/src/__tests__/ipc-validate.test.ts +240 -179
- package/src/__tests__/key-migration.test.ts +123 -90
- package/src/__tests__/keychain.test.ts +150 -123
- package/src/__tests__/lifecycle-docs-guard.test.ts +65 -64
- package/src/__tests__/llm-usage-store.test.ts +112 -87
- package/src/__tests__/managed-skill-lifecycle.test.ts +147 -108
- package/src/__tests__/managed-store.test.ts +411 -360
- package/src/__tests__/mcp-cli.test.ts +189 -123
- package/src/__tests__/mcp-health-check.test.ts +26 -21
- package/src/__tests__/media-generate-image.test.ts +122 -99
- package/src/__tests__/media-reuse-story.e2e.test.ts +282 -214
- package/src/__tests__/media-visibility-policy.test.ts +86 -38
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +146 -100
- package/src/__tests__/memory-lifecycle-e2e.test.ts +385 -297
- package/src/__tests__/memory-query-builder.test.ts +32 -33
- package/src/__tests__/memory-recall-quality.test.ts +761 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +443 -380
- package/src/__tests__/memory-regressions.test.ts +3725 -2642
- package/src/__tests__/memory-retrieval-budget.test.ts +7 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +144 -109
- package/src/__tests__/memory-upsert-concurrency.test.ts +292 -201
- package/src/__tests__/messaging-send-tool.test.ts +36 -29
- package/src/__tests__/migration-cli-flows.test.ts +69 -53
- package/src/__tests__/migration-ordering.test.ts +103 -86
- package/src/__tests__/mime-builder.test.ts +55 -32
- package/src/__tests__/mock-signup-server.test.ts +384 -246
- package/src/__tests__/model-intents.test.ts +61 -37
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +9 -12
- package/src/__tests__/no-is-trusted-guard.test.ts +24 -21
- package/src/__tests__/non-member-access-request.test.ts +3 -2
- package/src/__tests__/notification-broadcaster.test.ts +99 -81
- package/src/__tests__/notification-decision-fallback.test.ts +223 -178
- package/src/__tests__/notification-decision-strategy.test.ts +375 -337
- package/src/__tests__/notification-deep-link.test.ts +67 -61
- package/src/__tests__/notification-guardian-path.test.ts +248 -206
- package/src/__tests__/notification-routing-intent.test.ts +166 -93
- package/src/__tests__/notification-thread-candidate-validation.test.ts +78 -75
- package/src/__tests__/notification-thread-candidates.test.ts +64 -61
- package/src/__tests__/oauth-callback-registry.test.ts +40 -30
- package/src/__tests__/oauth-connect-handler.test.ts +109 -89
- package/src/__tests__/oauth-scope-policy.test.ts +63 -55
- package/src/__tests__/oauth2-gateway-transport.test.ts +252 -174
- package/src/__tests__/onboarding-starter-tasks.test.ts +93 -89
- package/src/__tests__/onboarding-template-contract.test.ts +93 -94
- package/src/__tests__/openai-provider.test.ts +366 -274
- package/src/__tests__/pairing-concurrent.test.ts +18 -12
- package/src/__tests__/pairing-routes.test.ts +45 -41
- package/src/__tests__/parallel-tool.benchmark.test.ts +108 -58
- package/src/__tests__/parser.test.ts +316 -226
- package/src/__tests__/path-classifier.test.ts +24 -25
- package/src/__tests__/path-policy.test.ts +187 -147
- package/src/__tests__/phone.test.ts +36 -36
- package/src/__tests__/platform-move-helper.test.ts +48 -40
- package/src/__tests__/platform-socket-path.test.ts +23 -24
- package/src/__tests__/platform-workspace-migration.test.ts +464 -414
- package/src/__tests__/platform.test.ts +61 -53
- package/src/__tests__/playbook-execution.test.ts +397 -265
- package/src/__tests__/playbook-tools.test.ts +267 -196
- package/src/__tests__/prebuilt-home-base-seed.test.ts +30 -27
- package/src/__tests__/pricing.test.ts +316 -136
- package/src/__tests__/profile-compiler.test.ts +206 -188
- package/src/__tests__/provider-commit-message-generator.test.ts +114 -106
- package/src/__tests__/provider-error-scenarios.test.ts +212 -158
- package/src/__tests__/provider-fail-open-selection.test.ts +51 -44
- package/src/__tests__/provider-registry-ollama.test.ts +13 -9
- package/src/__tests__/provider-streaming.benchmark.test.ts +232 -183
- package/src/__tests__/proxy-approval-callback.test.ts +180 -119
- package/src/__tests__/public-ingress-urls.test.ts +112 -94
- package/src/__tests__/qdrant-manager.test.ts +147 -98
- package/src/__tests__/ratelimit.test.ts +152 -82
- package/src/__tests__/recording-handler.test.ts +273 -151
- package/src/__tests__/recording-intent-fallback.test.ts +94 -75
- package/src/__tests__/recording-intent-handler.test.ts +0 -1
- package/src/__tests__/recording-intent.test.ts +578 -379
- package/src/__tests__/recording-state-machine.test.ts +530 -316
- package/src/__tests__/recurrence-engine-rruleset.test.ts +150 -92
- package/src/__tests__/recurrence-engine.test.ts +81 -41
- package/src/__tests__/recurrence-types.test.ts +63 -44
- package/src/__tests__/relay-server.test.ts +2131 -1602
- package/src/__tests__/reminder-store.test.ts +158 -80
- package/src/__tests__/reminder.test.ts +113 -109
- package/src/__tests__/remote-skill-policy.test.ts +96 -72
- package/src/__tests__/request-file-tool.test.ts +74 -67
- package/src/__tests__/response-tier.test.ts +131 -74
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/runtime-events-sse-parity.test.ts +167 -145
- package/src/__tests__/runtime-events-sse.test.ts +0 -1
- package/src/__tests__/sandbox-diagnostics.test.ts +66 -56
- package/src/__tests__/sandbox-host-parity.test.ts +377 -301
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +213 -161
- package/src/__tests__/schedule-store.test.ts +268 -205
- package/src/__tests__/schedule-tools.test.ts +702 -524
- package/src/__tests__/scheduler-recurrence.test.ts +240 -130
- package/src/__tests__/scoped-approval-grants.test.ts +258 -168
- package/src/__tests__/scoped-grant-security-matrix.test.ts +160 -146
- package/src/__tests__/script-proxy-certs.test.ts +38 -35
- package/src/__tests__/script-proxy-connect-tunnel.test.ts +71 -46
- package/src/__tests__/script-proxy-decision-trace.test.ts +161 -84
- package/src/__tests__/script-proxy-http-forwarder.test.ts +146 -129
- package/src/__tests__/script-proxy-injection-runtime.test.ts +139 -113
- package/src/__tests__/script-proxy-mitm-handler.test.ts +226 -142
- package/src/__tests__/script-proxy-policy-runtime.test.ts +126 -86
- package/src/__tests__/script-proxy-policy.test.ts +308 -153
- package/src/__tests__/script-proxy-rewrite-specificity.test.ts +74 -62
- package/src/__tests__/script-proxy-router.test.ts +111 -77
- package/src/__tests__/script-proxy-session-manager.test.ts +156 -113
- package/src/__tests__/script-proxy-session-runtime.test.ts +28 -24
- package/src/__tests__/secret-allowlist.test.ts +105 -90
- package/src/__tests__/secret-ingress-handler.test.ts +41 -30
- package/src/__tests__/secret-onetime-send.test.ts +67 -50
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +35 -31
- package/src/__tests__/secret-response-routing.test.ts +50 -41
- package/src/__tests__/secret-scanner-executor.test.ts +152 -111
- package/src/__tests__/secret-scanner.test.ts +495 -413
- package/src/__tests__/secure-keys.test.ts +132 -121
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/send-notification-tool.test.ts +43 -42
- package/src/__tests__/sensitive-output-placeholders.test.ts +72 -64
- package/src/__tests__/sequence-store.test.ts +335 -167
- package/src/__tests__/server-history-render.test.ts +341 -202
- package/src/__tests__/session-abort-tool-results.test.ts +133 -70
- package/src/__tests__/session-confirmation-signals.test.ts +252 -160
- package/src/__tests__/session-conflict-gate.test.ts +775 -585
- package/src/__tests__/session-error.test.ts +222 -191
- package/src/__tests__/session-evictor.test.ts +79 -62
- package/src/__tests__/session-init.benchmark.test.ts +170 -108
- package/src/__tests__/session-load-history-repair.test.ts +273 -139
- package/src/__tests__/session-messaging-secret-redirect.test.ts +130 -90
- package/src/__tests__/session-pre-run-repair.test.ts +106 -59
- package/src/__tests__/session-profile-injection.test.ts +198 -130
- package/src/__tests__/session-provider-retry-repair.test.ts +223 -141
- package/src/__tests__/session-queue.test.ts +624 -321
- package/src/__tests__/session-runtime-assembly.test.ts +425 -329
- package/src/__tests__/session-runtime-workspace.test.ts +69 -61
- package/src/__tests__/session-skill-tools.test.ts +973 -678
- package/src/__tests__/session-slash-known.test.ts +185 -133
- package/src/__tests__/session-slash-queue.test.ts +147 -81
- package/src/__tests__/session-slash-unknown.test.ts +135 -90
- package/src/__tests__/session-surfaces-task-progress.test.ts +122 -87
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +338 -177
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +63 -40
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +60 -37
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +28 -26
- package/src/__tests__/session-undo.test.ts +43 -30
- package/src/__tests__/session-workspace-cache-state.test.ts +108 -67
- package/src/__tests__/session-workspace-injection.test.ts +245 -117
- package/src/__tests__/session-workspace-tool-tracking.test.ts +260 -93
- package/src/__tests__/shared-filesystem-errors.test.ts +47 -47
- package/src/__tests__/shell-credential-ref.test.ts +126 -90
- package/src/__tests__/shell-identity.test.ts +134 -111
- package/src/__tests__/shell-parser-fuzz.test.ts +263 -179
- package/src/__tests__/shell-parser-property.test.ts +435 -288
- package/src/__tests__/shell-tool-proxy-mode.test.ts +142 -70
- package/src/__tests__/size-guard.test.ts +42 -44
- package/src/__tests__/skill-feature-flags-integration.test.ts +79 -52
- package/src/__tests__/skill-feature-flags.test.ts +75 -47
- package/src/__tests__/skill-include-graph.test.ts +143 -148
- package/src/__tests__/skill-load-feature-flag.test.ts +94 -59
- package/src/__tests__/skill-load-tool.test.ts +371 -199
- package/src/__tests__/skill-projection-feature-flag.test.ts +131 -88
- package/src/__tests__/skill-projection.benchmark.test.ts +93 -65
- package/src/__tests__/skill-script-runner-host.test.ts +460 -250
- package/src/__tests__/skill-script-runner-sandbox.test.ts +168 -108
- package/src/__tests__/skill-script-runner.test.ts +115 -74
- package/src/__tests__/skill-tool-factory.test.ts +140 -96
- package/src/__tests__/skill-tool-manifest.test.ts +306 -210
- package/src/__tests__/skill-version-hash.test.ts +70 -56
- package/src/__tests__/skills.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +127 -84
- package/src/__tests__/slack-skill.test.ts +60 -47
- package/src/__tests__/slash-commands-catalog.test.ts +37 -31
- package/src/__tests__/slash-commands-parser.test.ts +71 -64
- package/src/__tests__/slash-commands-resolver.test.ts +143 -107
- package/src/__tests__/slash-commands-rewrite.test.ts +22 -22
- package/src/__tests__/speaker-identification.test.ts +28 -25
- package/src/__tests__/starter-bundle.test.ts +27 -23
- package/src/__tests__/starter-task-flow.test.ts +67 -52
- package/src/__tests__/subagent-manager-notify.test.ts +154 -108
- package/src/__tests__/subagent-tools.test.ts +311 -270
- package/src/__tests__/subagent-types.test.ts +40 -40
- package/src/__tests__/surface-mutex-cleanup.test.ts +42 -30
- package/src/__tests__/swarm-dag-pathological.test.ts +122 -111
- package/src/__tests__/swarm-orchestrator.test.ts +135 -101
- package/src/__tests__/swarm-plan-validator.test.ts +125 -73
- package/src/__tests__/swarm-recursion.test.ts +58 -46
- package/src/__tests__/swarm-router-planner.test.ts +99 -74
- package/src/__tests__/swarm-session-integration.test.ts +148 -91
- package/src/__tests__/swarm-tool.test.ts +65 -45
- package/src/__tests__/swarm-worker-backend.test.ts +59 -45
- package/src/__tests__/swarm-worker-runner.test.ts +133 -118
- package/src/__tests__/system-prompt.test.ts +290 -256
- package/src/__tests__/task-compiler.test.ts +176 -120
- package/src/__tests__/task-management-tools.test.ts +561 -456
- package/src/__tests__/task-memory-cleanup.test.ts +627 -362
- package/src/__tests__/task-runner.test.ts +117 -94
- package/src/__tests__/task-scheduler.test.ts +113 -84
- package/src/__tests__/task-tools.test.ts +349 -264
- package/src/__tests__/terminal-sandbox.test.ts +138 -108
- package/src/__tests__/terminal-tools.test.ts +350 -305
- package/src/__tests__/thread-seed-composer.test.ts +307 -180
- package/src/__tests__/tool-approval-handler.test.ts +238 -137
- package/src/__tests__/tool-audit-listener.test.ts +69 -69
- package/src/__tests__/tool-domain-event-publisher.test.ts +142 -132
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +153 -146
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +136 -105
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +355 -239
- package/src/__tests__/tool-executor-redaction.test.ts +112 -109
- package/src/__tests__/tool-executor-shell-integration.test.ts +130 -79
- package/src/__tests__/tool-executor.test.ts +1274 -674
- package/src/__tests__/tool-grant-request-escalation.test.ts +401 -283
- package/src/__tests__/tool-metrics-listener.test.ts +97 -85
- package/src/__tests__/tool-notification-listener.test.ts +42 -25
- package/src/__tests__/tool-permission-simulate-handler.test.ts +137 -113
- package/src/__tests__/tool-policy.test.ts +44 -25
- package/src/__tests__/tool-profiling-listener.test.ts +99 -93
- package/src/__tests__/tool-result-truncation.test.ts +5 -4
- package/src/__tests__/tool-trace-listener.test.ts +131 -111
- package/src/__tests__/top-level-renderer.test.ts +62 -58
- package/src/__tests__/top-level-scanner.test.ts +68 -64
- package/src/__tests__/trace-emitter.test.ts +56 -56
- package/src/__tests__/trust-context-guards.test.ts +65 -65
- package/src/__tests__/trust-store.test.ts +1239 -806
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +3 -2
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -2
- package/src/__tests__/trusted-contact-verification.test.ts +251 -231
- package/src/__tests__/turn-commit.test.ts +259 -200
- package/src/__tests__/twilio-provider.test.ts +140 -126
- package/src/__tests__/twilio-rest.test.ts +22 -18
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -1
- package/src/__tests__/twilio-routes-twiml.test.ts +55 -55
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/twitter-auth-handler.test.ts +184 -139
- package/src/__tests__/twitter-cli-error-shaping.test.ts +88 -73
- package/src/__tests__/twitter-cli-routing.test.ts +146 -99
- package/src/__tests__/twitter-oauth-client.test.ts +82 -65
- package/src/__tests__/update-bulletin-format.test.ts +69 -66
- package/src/__tests__/update-bulletin-state.test.ts +66 -60
- package/src/__tests__/update-bulletin.test.ts +150 -114
- package/src/__tests__/update-template-contract.test.ts +15 -10
- package/src/__tests__/url-safety.test.ts +288 -265
- package/src/__tests__/user-reference.test.ts +32 -32
- package/src/__tests__/view-image-tool.test.ts +118 -96
- package/src/__tests__/voice-invite-redemption.test.ts +111 -106
- package/src/__tests__/voice-quality.test.ts +117 -102
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +204 -146
- package/src/__tests__/voice-session-bridge.test.ts +351 -216
- package/src/__tests__/weather-skill-regression.test.ts +170 -120
- package/src/__tests__/web-fetch.test.ts +664 -526
- package/src/__tests__/web-search.test.ts +379 -213
- package/src/__tests__/work-item-output.test.ts +90 -53
- package/src/__tests__/workspace-git-service.test.ts +437 -356
- package/src/__tests__/workspace-heartbeat-service.test.ts +125 -91
- package/src/__tests__/workspace-lifecycle.test.ts +98 -64
- package/src/__tests__/workspace-policy.test.ts +139 -71
- package/src/commands/__tests__/cc-command-registry.test.ts +142 -134
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +48 -39
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +25 -10
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +4 -3
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +15 -5
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +34 -32
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/env.ts +3 -4
- package/src/memory/db-connection.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +10 -3
- package/src/messaging/providers/gmail/client.ts +280 -72
- package/src/runtime/auth/__tests__/context.test.ts +75 -65
- package/src/runtime/auth/__tests__/credential-service.test.ts +137 -114
- package/src/runtime/auth/__tests__/guard-tests.test.ts +84 -90
- package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +40 -40
- package/src/runtime/auth/__tests__/middleware.test.ts +80 -74
- package/src/runtime/auth/__tests__/policy.test.ts +9 -9
- package/src/runtime/auth/__tests__/route-policy.test.ts +76 -65
- package/src/runtime/auth/__tests__/scopes.test.ts +68 -60
- package/src/runtime/auth/__tests__/subject.test.ts +54 -54
- package/src/runtime/auth/__tests__/token-service.test.ts +115 -108
- package/src/runtime/auth/scopes.ts +3 -0
- package/src/runtime/auth/token-service.ts +4 -1
- package/src/runtime/auth/types.ts +2 -1
- package/src/runtime/http-server.ts +2 -1
- package/src/security/secure-keys.ts +103 -53
- package/src/tools/browser/__tests__/auth-cache.test.ts +69 -63
- package/src/tools/browser/__tests__/auth-detector.test.ts +218 -157
- package/src/tools/browser/__tests__/jit-auth.test.ts +83 -99
|
@@ -27,14 +27,13 @@ import {
|
|
|
27
27
|
realpathSync,
|
|
28
28
|
rmSync,
|
|
29
29
|
writeFileSync,
|
|
30
|
-
} from
|
|
31
|
-
import { tmpdir } from
|
|
32
|
-
import { join } from
|
|
33
|
-
|
|
34
|
-
import { afterEach, describe, expect, mock, test } from 'bun:test';
|
|
30
|
+
} from "node:fs";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
35
34
|
|
|
36
35
|
// Mock the logger before any transitive imports that depend on pino
|
|
37
|
-
mock.module(
|
|
36
|
+
mock.module("../util/logger.js", () => ({
|
|
38
37
|
getLogger: () => ({
|
|
39
38
|
error: () => {},
|
|
40
39
|
warn: () => {},
|
|
@@ -43,15 +42,24 @@ mock.module('../util/logger.js', () => ({
|
|
|
43
42
|
}),
|
|
44
43
|
}));
|
|
45
44
|
|
|
46
|
-
import { applyEdit } from
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
import { applyEdit } from "../tools/shared/filesystem/edit-engine.js";
|
|
46
|
+
import {
|
|
47
|
+
FileSystemOps,
|
|
48
|
+
type PathPolicy,
|
|
49
|
+
} from "../tools/shared/filesystem/file-ops-service.js";
|
|
50
|
+
import {
|
|
51
|
+
hostPolicy,
|
|
52
|
+
sandboxPolicy,
|
|
53
|
+
} from "../tools/shared/filesystem/path-policy.js";
|
|
54
|
+
import {
|
|
55
|
+
formatShellOutput,
|
|
56
|
+
MAX_OUTPUT_LENGTH,
|
|
57
|
+
} from "../tools/shared/shell-output.js";
|
|
50
58
|
|
|
51
59
|
// Dynamically import modules that depend on the mocked logger
|
|
52
|
-
const { NativeBackend } = await import(
|
|
53
|
-
const { wrapCommand } = await import(
|
|
54
|
-
const { ToolError } = await import(
|
|
60
|
+
const { NativeBackend } = await import("../tools/terminal/backends/native.js");
|
|
61
|
+
const { wrapCommand } = await import("../tools/terminal/sandbox.js");
|
|
62
|
+
const { ToolError } = await import("../util/errors.js");
|
|
55
63
|
|
|
56
64
|
// ---------------------------------------------------------------------------
|
|
57
65
|
// Helpers
|
|
@@ -60,7 +68,7 @@ const { ToolError } = await import('../util/errors.js');
|
|
|
60
68
|
const testDirs: string[] = [];
|
|
61
69
|
|
|
62
70
|
function makeTempDir(): string {
|
|
63
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(),
|
|
71
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "parity-test-")));
|
|
64
72
|
testDirs.push(dir);
|
|
65
73
|
return dir;
|
|
66
74
|
}
|
|
@@ -85,7 +93,10 @@ function hostPolicyFn(): PathPolicy {
|
|
|
85
93
|
* Run the same operation against both sandbox and host FileSystemOps
|
|
86
94
|
* and return both results for comparison.
|
|
87
95
|
*/
|
|
88
|
-
function dualOps(boundary: string): {
|
|
96
|
+
function dualOps(boundary: string): {
|
|
97
|
+
sandbox: FileSystemOps;
|
|
98
|
+
host: FileSystemOps;
|
|
99
|
+
} {
|
|
89
100
|
return {
|
|
90
101
|
sandbox: new FileSystemOps(sandboxPolicyFor(boundary)),
|
|
91
102
|
host: new FileSystemOps(hostPolicyFn()),
|
|
@@ -96,16 +107,16 @@ function dualOps(boundary: string): { sandbox: FileSystemOps; host: FileSystemOp
|
|
|
96
107
|
// 1. File read parity
|
|
97
108
|
// ===========================================================================
|
|
98
109
|
|
|
99
|
-
describe(
|
|
100
|
-
test(
|
|
110
|
+
describe("Read parity: sandbox vs host produce identical content", () => {
|
|
111
|
+
test("simple file read returns same numbered content", () => {
|
|
101
112
|
const dir = makeTempDir();
|
|
102
|
-
const content =
|
|
103
|
-
writeFileSync(join(dir,
|
|
113
|
+
const content = "line one\nline two\nline three\n";
|
|
114
|
+
writeFileSync(join(dir, "data.txt"), content);
|
|
104
115
|
|
|
105
116
|
const { sandbox, host } = dualOps(dir);
|
|
106
117
|
|
|
107
|
-
const sandboxResult = sandbox.readFileSafe({ path:
|
|
108
|
-
const hostResult = host.readFileSafe({ path: join(dir,
|
|
118
|
+
const sandboxResult = sandbox.readFileSafe({ path: "data.txt" });
|
|
119
|
+
const hostResult = host.readFileSafe({ path: join(dir, "data.txt") });
|
|
109
120
|
|
|
110
121
|
expect(sandboxResult.ok).toBe(true);
|
|
111
122
|
expect(hostResult.ok).toBe(true);
|
|
@@ -115,14 +126,22 @@ describe('Read parity: sandbox vs host produce identical content', () => {
|
|
|
115
126
|
expect(sandboxResult.value.content).toBe(hostResult.value.content);
|
|
116
127
|
});
|
|
117
128
|
|
|
118
|
-
test(
|
|
129
|
+
test("read with offset and limit returns same slice", () => {
|
|
119
130
|
const dir = makeTempDir();
|
|
120
|
-
writeFileSync(join(dir,
|
|
131
|
+
writeFileSync(join(dir, "lines.txt"), "a\nb\nc\nd\ne\nf\n");
|
|
121
132
|
|
|
122
133
|
const { sandbox, host } = dualOps(dir);
|
|
123
134
|
|
|
124
|
-
const sandboxResult = sandbox.readFileSafe({
|
|
125
|
-
|
|
135
|
+
const sandboxResult = sandbox.readFileSafe({
|
|
136
|
+
path: "lines.txt",
|
|
137
|
+
offset: 2,
|
|
138
|
+
limit: 3,
|
|
139
|
+
});
|
|
140
|
+
const hostResult = host.readFileSafe({
|
|
141
|
+
path: join(dir, "lines.txt"),
|
|
142
|
+
offset: 2,
|
|
143
|
+
limit: 3,
|
|
144
|
+
});
|
|
126
145
|
|
|
127
146
|
expect(sandboxResult.ok).toBe(true);
|
|
128
147
|
expect(hostResult.ok).toBe(true);
|
|
@@ -131,47 +150,49 @@ describe('Read parity: sandbox vs host produce identical content', () => {
|
|
|
131
150
|
expect(sandboxResult.value.content).toBe(hostResult.value.content);
|
|
132
151
|
});
|
|
133
152
|
|
|
134
|
-
test(
|
|
153
|
+
test("reading a missing file returns NOT_FOUND from both", () => {
|
|
135
154
|
const dir = makeTempDir();
|
|
136
155
|
|
|
137
156
|
const { sandbox, host } = dualOps(dir);
|
|
138
157
|
|
|
139
|
-
const sandboxResult = sandbox.readFileSafe({ path:
|
|
140
|
-
const hostResult = host.readFileSafe({
|
|
158
|
+
const sandboxResult = sandbox.readFileSafe({ path: "nonexistent.txt" });
|
|
159
|
+
const hostResult = host.readFileSafe({
|
|
160
|
+
path: join(dir, "nonexistent.txt"),
|
|
161
|
+
});
|
|
141
162
|
|
|
142
163
|
expect(sandboxResult.ok).toBe(false);
|
|
143
164
|
expect(hostResult.ok).toBe(false);
|
|
144
165
|
if (sandboxResult.ok || hostResult.ok) return;
|
|
145
166
|
|
|
146
|
-
expect(sandboxResult.error.code).toBe(
|
|
147
|
-
expect(hostResult.error.code).toBe(
|
|
167
|
+
expect(sandboxResult.error.code).toBe("NOT_FOUND");
|
|
168
|
+
expect(hostResult.error.code).toBe("NOT_FOUND");
|
|
148
169
|
});
|
|
149
170
|
|
|
150
|
-
test(
|
|
171
|
+
test("reading a directory returns NOT_A_FILE from both", () => {
|
|
151
172
|
const dir = makeTempDir();
|
|
152
|
-
mkdirSync(join(dir,
|
|
173
|
+
mkdirSync(join(dir, "subdir"));
|
|
153
174
|
|
|
154
175
|
const { sandbox, host } = dualOps(dir);
|
|
155
176
|
|
|
156
|
-
const sandboxResult = sandbox.readFileSafe({ path:
|
|
157
|
-
const hostResult = host.readFileSafe({ path: join(dir,
|
|
177
|
+
const sandboxResult = sandbox.readFileSafe({ path: "subdir" });
|
|
178
|
+
const hostResult = host.readFileSafe({ path: join(dir, "subdir") });
|
|
158
179
|
|
|
159
180
|
expect(sandboxResult.ok).toBe(false);
|
|
160
181
|
expect(hostResult.ok).toBe(false);
|
|
161
182
|
if (sandboxResult.ok || hostResult.ok) return;
|
|
162
183
|
|
|
163
|
-
expect(sandboxResult.error.code).toBe(
|
|
164
|
-
expect(hostResult.error.code).toBe(
|
|
184
|
+
expect(sandboxResult.error.code).toBe("NOT_A_FILE");
|
|
185
|
+
expect(hostResult.error.code).toBe("NOT_A_FILE");
|
|
165
186
|
});
|
|
166
187
|
|
|
167
|
-
test(
|
|
188
|
+
test("empty file read returns same content from both", () => {
|
|
168
189
|
const dir = makeTempDir();
|
|
169
|
-
writeFileSync(join(dir,
|
|
190
|
+
writeFileSync(join(dir, "empty.txt"), "");
|
|
170
191
|
|
|
171
192
|
const { sandbox, host } = dualOps(dir);
|
|
172
193
|
|
|
173
|
-
const sandboxResult = sandbox.readFileSafe({ path:
|
|
174
|
-
const hostResult = host.readFileSafe({ path: join(dir,
|
|
194
|
+
const sandboxResult = sandbox.readFileSafe({ path: "empty.txt" });
|
|
195
|
+
const hostResult = host.readFileSafe({ path: join(dir, "empty.txt") });
|
|
175
196
|
|
|
176
197
|
expect(sandboxResult.ok).toBe(true);
|
|
177
198
|
expect(hostResult.ok).toBe(true);
|
|
@@ -180,15 +201,16 @@ describe('Read parity: sandbox vs host produce identical content', () => {
|
|
|
180
201
|
expect(sandboxResult.value.content).toBe(hostResult.value.content);
|
|
181
202
|
});
|
|
182
203
|
|
|
183
|
-
test(
|
|
204
|
+
test("file with unicode content returns same from both", () => {
|
|
184
205
|
const dir = makeTempDir();
|
|
185
|
-
const unicode =
|
|
186
|
-
|
|
206
|
+
const unicode =
|
|
207
|
+
"Hello\nEmoji: \u{1F600}\nCJK: \u4F60\u597D\nAccent: caf\u00E9\n";
|
|
208
|
+
writeFileSync(join(dir, "unicode.txt"), unicode);
|
|
187
209
|
|
|
188
210
|
const { sandbox, host } = dualOps(dir);
|
|
189
211
|
|
|
190
|
-
const sandboxResult = sandbox.readFileSafe({ path:
|
|
191
|
-
const hostResult = host.readFileSafe({ path: join(dir,
|
|
212
|
+
const sandboxResult = sandbox.readFileSafe({ path: "unicode.txt" });
|
|
213
|
+
const hostResult = host.readFileSafe({ path: join(dir, "unicode.txt") });
|
|
192
214
|
|
|
193
215
|
expect(sandboxResult.ok).toBe(true);
|
|
194
216
|
expect(hostResult.ok).toBe(true);
|
|
@@ -202,14 +224,20 @@ describe('Read parity: sandbox vs host produce identical content', () => {
|
|
|
202
224
|
// 2. File write parity
|
|
203
225
|
// ===========================================================================
|
|
204
226
|
|
|
205
|
-
describe(
|
|
206
|
-
test(
|
|
227
|
+
describe("Write parity: sandbox vs host produce identical results", () => {
|
|
228
|
+
test("writing a new file returns same shape from both", () => {
|
|
207
229
|
const dir = makeTempDir();
|
|
208
230
|
|
|
209
231
|
const { sandbox, host } = dualOps(dir);
|
|
210
232
|
|
|
211
|
-
const sandboxResult = sandbox.writeFileSafe({
|
|
212
|
-
|
|
233
|
+
const sandboxResult = sandbox.writeFileSafe({
|
|
234
|
+
path: "new-s.txt",
|
|
235
|
+
content: "hello",
|
|
236
|
+
});
|
|
237
|
+
const hostResult = host.writeFileSafe({
|
|
238
|
+
path: join(dir, "new-h.txt"),
|
|
239
|
+
content: "hello",
|
|
240
|
+
});
|
|
213
241
|
|
|
214
242
|
expect(sandboxResult.ok).toBe(true);
|
|
215
243
|
expect(hostResult.ok).toBe(true);
|
|
@@ -221,15 +249,21 @@ describe('Write parity: sandbox vs host produce identical results', () => {
|
|
|
221
249
|
expect(sandboxResult.value.oldContent).toBe(hostResult.value.oldContent);
|
|
222
250
|
});
|
|
223
251
|
|
|
224
|
-
test(
|
|
252
|
+
test("overwriting an existing file returns old content from both", () => {
|
|
225
253
|
const dir = makeTempDir();
|
|
226
|
-
writeFileSync(join(dir,
|
|
227
|
-
writeFileSync(join(dir,
|
|
254
|
+
writeFileSync(join(dir, "existing-s.txt"), "old");
|
|
255
|
+
writeFileSync(join(dir, "existing-h.txt"), "old");
|
|
228
256
|
|
|
229
257
|
const { sandbox, host } = dualOps(dir);
|
|
230
258
|
|
|
231
|
-
const sandboxResult = sandbox.writeFileSafe({
|
|
232
|
-
|
|
259
|
+
const sandboxResult = sandbox.writeFileSafe({
|
|
260
|
+
path: "existing-s.txt",
|
|
261
|
+
content: "new",
|
|
262
|
+
});
|
|
263
|
+
const hostResult = host.writeFileSafe({
|
|
264
|
+
path: join(dir, "existing-h.txt"),
|
|
265
|
+
content: "new",
|
|
266
|
+
});
|
|
233
267
|
|
|
234
268
|
expect(sandboxResult.ok).toBe(true);
|
|
235
269
|
expect(hostResult.ok).toBe(true);
|
|
@@ -237,30 +271,36 @@ describe('Write parity: sandbox vs host produce identical results', () => {
|
|
|
237
271
|
|
|
238
272
|
expect(sandboxResult.value.isNewFile).toBe(false);
|
|
239
273
|
expect(hostResult.value.isNewFile).toBe(false);
|
|
240
|
-
expect(sandboxResult.value.oldContent).toBe(
|
|
241
|
-
expect(hostResult.value.oldContent).toBe(
|
|
242
|
-
expect(sandboxResult.value.newContent).toBe(
|
|
243
|
-
expect(hostResult.value.newContent).toBe(
|
|
274
|
+
expect(sandboxResult.value.oldContent).toBe("old");
|
|
275
|
+
expect(hostResult.value.oldContent).toBe("old");
|
|
276
|
+
expect(sandboxResult.value.newContent).toBe("new");
|
|
277
|
+
expect(hostResult.value.newContent).toBe("new");
|
|
244
278
|
|
|
245
279
|
// Verify actual files on disk
|
|
246
|
-
expect(readFileSync(join(dir,
|
|
247
|
-
expect(readFileSync(join(dir,
|
|
280
|
+
expect(readFileSync(join(dir, "existing-s.txt"), "utf-8")).toBe("new");
|
|
281
|
+
expect(readFileSync(join(dir, "existing-h.txt"), "utf-8")).toBe("new");
|
|
248
282
|
});
|
|
249
283
|
|
|
250
|
-
test(
|
|
284
|
+
test("creating nested directories works from both", () => {
|
|
251
285
|
const dir = makeTempDir();
|
|
252
286
|
|
|
253
287
|
const { sandbox, host } = dualOps(dir);
|
|
254
288
|
|
|
255
|
-
const sandboxResult = sandbox.writeFileSafe({
|
|
256
|
-
|
|
289
|
+
const sandboxResult = sandbox.writeFileSafe({
|
|
290
|
+
path: "a/b/deep-s.txt",
|
|
291
|
+
content: "deep",
|
|
292
|
+
});
|
|
293
|
+
const hostResult = host.writeFileSafe({
|
|
294
|
+
path: join(dir, "c/d/deep-h.txt"),
|
|
295
|
+
content: "deep",
|
|
296
|
+
});
|
|
257
297
|
|
|
258
298
|
expect(sandboxResult.ok).toBe(true);
|
|
259
299
|
expect(hostResult.ok).toBe(true);
|
|
260
300
|
if (!sandboxResult.ok || !hostResult.ok) return;
|
|
261
301
|
|
|
262
|
-
expect(existsSync(join(dir,
|
|
263
|
-
expect(existsSync(join(dir,
|
|
302
|
+
expect(existsSync(join(dir, "a/b/deep-s.txt"))).toBe(true);
|
|
303
|
+
expect(existsSync(join(dir, "c/d/deep-h.txt"))).toBe(true);
|
|
264
304
|
expect(sandboxResult.value.isNewFile).toBe(true);
|
|
265
305
|
expect(hostResult.value.isNewFile).toBe(true);
|
|
266
306
|
});
|
|
@@ -270,24 +310,24 @@ describe('Write parity: sandbox vs host produce identical results', () => {
|
|
|
270
310
|
// 3. File edit parity
|
|
271
311
|
// ===========================================================================
|
|
272
312
|
|
|
273
|
-
describe(
|
|
274
|
-
test(
|
|
313
|
+
describe("Edit parity: sandbox vs host produce identical edits", () => {
|
|
314
|
+
test("unique match edit produces same result from both", () => {
|
|
275
315
|
const dir = makeTempDir();
|
|
276
|
-
writeFileSync(join(dir,
|
|
277
|
-
writeFileSync(join(dir,
|
|
316
|
+
writeFileSync(join(dir, "edit-s.txt"), "one two three");
|
|
317
|
+
writeFileSync(join(dir, "edit-h.txt"), "one two three");
|
|
278
318
|
|
|
279
319
|
const { sandbox, host } = dualOps(dir);
|
|
280
320
|
|
|
281
321
|
const sandboxResult = sandbox.editFileSafe({
|
|
282
|
-
path:
|
|
283
|
-
oldString:
|
|
284
|
-
newString:
|
|
322
|
+
path: "edit-s.txt",
|
|
323
|
+
oldString: "two",
|
|
324
|
+
newString: "TWO",
|
|
285
325
|
replaceAll: false,
|
|
286
326
|
});
|
|
287
327
|
const hostResult = host.editFileSafe({
|
|
288
|
-
path: join(dir,
|
|
289
|
-
oldString:
|
|
290
|
-
newString:
|
|
328
|
+
path: join(dir, "edit-h.txt"),
|
|
329
|
+
oldString: "two",
|
|
330
|
+
newString: "TWO",
|
|
291
331
|
replaceAll: false,
|
|
292
332
|
});
|
|
293
333
|
|
|
@@ -297,29 +337,29 @@ describe('Edit parity: sandbox vs host produce identical edits', () => {
|
|
|
297
337
|
|
|
298
338
|
expect(sandboxResult.value.matchCount).toBe(1);
|
|
299
339
|
expect(hostResult.value.matchCount).toBe(1);
|
|
300
|
-
expect(sandboxResult.value.newContent).toBe(
|
|
301
|
-
expect(hostResult.value.newContent).toBe(
|
|
340
|
+
expect(sandboxResult.value.newContent).toBe("one TWO three");
|
|
341
|
+
expect(hostResult.value.newContent).toBe("one TWO three");
|
|
302
342
|
expect(sandboxResult.value.matchMethod).toBe(hostResult.value.matchMethod);
|
|
303
343
|
});
|
|
304
344
|
|
|
305
|
-
test(
|
|
345
|
+
test("replaceAll edit produces same result from both", () => {
|
|
306
346
|
const dir = makeTempDir();
|
|
307
|
-
const original =
|
|
308
|
-
writeFileSync(join(dir,
|
|
309
|
-
writeFileSync(join(dir,
|
|
347
|
+
const original = "foo bar foo baz foo";
|
|
348
|
+
writeFileSync(join(dir, "ra-s.txt"), original);
|
|
349
|
+
writeFileSync(join(dir, "ra-h.txt"), original);
|
|
310
350
|
|
|
311
351
|
const { sandbox, host } = dualOps(dir);
|
|
312
352
|
|
|
313
353
|
const sandboxResult = sandbox.editFileSafe({
|
|
314
|
-
path:
|
|
315
|
-
oldString:
|
|
316
|
-
newString:
|
|
354
|
+
path: "ra-s.txt",
|
|
355
|
+
oldString: "foo",
|
|
356
|
+
newString: "qux",
|
|
317
357
|
replaceAll: true,
|
|
318
358
|
});
|
|
319
359
|
const hostResult = host.editFileSafe({
|
|
320
|
-
path: join(dir,
|
|
321
|
-
oldString:
|
|
322
|
-
newString:
|
|
360
|
+
path: join(dir, "ra-h.txt"),
|
|
361
|
+
oldString: "foo",
|
|
362
|
+
newString: "qux",
|
|
323
363
|
replaceAll: true,
|
|
324
364
|
});
|
|
325
365
|
|
|
@@ -330,26 +370,26 @@ describe('Edit parity: sandbox vs host produce identical edits', () => {
|
|
|
330
370
|
expect(sandboxResult.value.matchCount).toBe(3);
|
|
331
371
|
expect(hostResult.value.matchCount).toBe(3);
|
|
332
372
|
expect(sandboxResult.value.newContent).toBe(hostResult.value.newContent);
|
|
333
|
-
expect(sandboxResult.value.newContent).toBe(
|
|
373
|
+
expect(sandboxResult.value.newContent).toBe("qux bar qux baz qux");
|
|
334
374
|
});
|
|
335
375
|
|
|
336
|
-
test(
|
|
376
|
+
test("missing old_string returns MATCH_NOT_FOUND from both", () => {
|
|
337
377
|
const dir = makeTempDir();
|
|
338
|
-
writeFileSync(join(dir,
|
|
339
|
-
writeFileSync(join(dir,
|
|
378
|
+
writeFileSync(join(dir, "mnf-s.txt"), "hello world");
|
|
379
|
+
writeFileSync(join(dir, "mnf-h.txt"), "hello world");
|
|
340
380
|
|
|
341
381
|
const { sandbox, host } = dualOps(dir);
|
|
342
382
|
|
|
343
383
|
const sandboxResult = sandbox.editFileSafe({
|
|
344
|
-
path:
|
|
345
|
-
oldString:
|
|
346
|
-
newString:
|
|
384
|
+
path: "mnf-s.txt",
|
|
385
|
+
oldString: "xyz",
|
|
386
|
+
newString: "abc",
|
|
347
387
|
replaceAll: false,
|
|
348
388
|
});
|
|
349
389
|
const hostResult = host.editFileSafe({
|
|
350
|
-
path: join(dir,
|
|
351
|
-
oldString:
|
|
352
|
-
newString:
|
|
390
|
+
path: join(dir, "mnf-h.txt"),
|
|
391
|
+
oldString: "xyz",
|
|
392
|
+
newString: "abc",
|
|
353
393
|
replaceAll: false,
|
|
354
394
|
});
|
|
355
395
|
|
|
@@ -357,28 +397,28 @@ describe('Edit parity: sandbox vs host produce identical edits', () => {
|
|
|
357
397
|
expect(hostResult.ok).toBe(false);
|
|
358
398
|
if (sandboxResult.ok || hostResult.ok) return;
|
|
359
399
|
|
|
360
|
-
expect(sandboxResult.error.code).toBe(
|
|
361
|
-
expect(hostResult.error.code).toBe(
|
|
400
|
+
expect(sandboxResult.error.code).toBe("MATCH_NOT_FOUND");
|
|
401
|
+
expect(hostResult.error.code).toBe("MATCH_NOT_FOUND");
|
|
362
402
|
});
|
|
363
403
|
|
|
364
|
-
test(
|
|
404
|
+
test("ambiguous match returns MATCH_AMBIGUOUS from both", () => {
|
|
365
405
|
const dir = makeTempDir();
|
|
366
|
-
const content =
|
|
367
|
-
writeFileSync(join(dir,
|
|
368
|
-
writeFileSync(join(dir,
|
|
406
|
+
const content = "repeat\nrepeat\n";
|
|
407
|
+
writeFileSync(join(dir, "amb-s.txt"), content);
|
|
408
|
+
writeFileSync(join(dir, "amb-h.txt"), content);
|
|
369
409
|
|
|
370
410
|
const { sandbox, host } = dualOps(dir);
|
|
371
411
|
|
|
372
412
|
const sandboxResult = sandbox.editFileSafe({
|
|
373
|
-
path:
|
|
374
|
-
oldString:
|
|
375
|
-
newString:
|
|
413
|
+
path: "amb-s.txt",
|
|
414
|
+
oldString: "repeat",
|
|
415
|
+
newString: "unique",
|
|
376
416
|
replaceAll: false,
|
|
377
417
|
});
|
|
378
418
|
const hostResult = host.editFileSafe({
|
|
379
|
-
path: join(dir,
|
|
380
|
-
oldString:
|
|
381
|
-
newString:
|
|
419
|
+
path: join(dir, "amb-h.txt"),
|
|
420
|
+
oldString: "repeat",
|
|
421
|
+
newString: "unique",
|
|
382
422
|
replaceAll: false,
|
|
383
423
|
});
|
|
384
424
|
|
|
@@ -386,25 +426,25 @@ describe('Edit parity: sandbox vs host produce identical edits', () => {
|
|
|
386
426
|
expect(hostResult.ok).toBe(false);
|
|
387
427
|
if (sandboxResult.ok || hostResult.ok) return;
|
|
388
428
|
|
|
389
|
-
expect(sandboxResult.error.code).toBe(
|
|
390
|
-
expect(hostResult.error.code).toBe(
|
|
429
|
+
expect(sandboxResult.error.code).toBe("MATCH_AMBIGUOUS");
|
|
430
|
+
expect(hostResult.error.code).toBe("MATCH_AMBIGUOUS");
|
|
391
431
|
});
|
|
392
432
|
|
|
393
|
-
test(
|
|
433
|
+
test("editing a nonexistent file returns NOT_FOUND from both", () => {
|
|
394
434
|
const dir = makeTempDir();
|
|
395
435
|
|
|
396
436
|
const { sandbox, host } = dualOps(dir);
|
|
397
437
|
|
|
398
438
|
const sandboxResult = sandbox.editFileSafe({
|
|
399
|
-
path:
|
|
400
|
-
oldString:
|
|
401
|
-
newString:
|
|
439
|
+
path: "nope.txt",
|
|
440
|
+
oldString: "a",
|
|
441
|
+
newString: "b",
|
|
402
442
|
replaceAll: false,
|
|
403
443
|
});
|
|
404
444
|
const hostResult = host.editFileSafe({
|
|
405
|
-
path: join(dir,
|
|
406
|
-
oldString:
|
|
407
|
-
newString:
|
|
445
|
+
path: join(dir, "nope.txt"),
|
|
446
|
+
oldString: "a",
|
|
447
|
+
newString: "b",
|
|
408
448
|
replaceAll: false,
|
|
409
449
|
});
|
|
410
450
|
|
|
@@ -412,8 +452,8 @@ describe('Edit parity: sandbox vs host produce identical edits', () => {
|
|
|
412
452
|
expect(hostResult.ok).toBe(false);
|
|
413
453
|
if (sandboxResult.ok || hostResult.ok) return;
|
|
414
454
|
|
|
415
|
-
expect(sandboxResult.error.code).toBe(
|
|
416
|
-
expect(hostResult.error.code).toBe(
|
|
455
|
+
expect(sandboxResult.error.code).toBe("NOT_FOUND");
|
|
456
|
+
expect(hostResult.error.code).toBe("NOT_FOUND");
|
|
417
457
|
});
|
|
418
458
|
});
|
|
419
459
|
|
|
@@ -421,22 +461,22 @@ describe('Edit parity: sandbox vs host produce identical edits', () => {
|
|
|
421
461
|
// 4. Edit engine consistency (pure function)
|
|
422
462
|
// ===========================================================================
|
|
423
463
|
|
|
424
|
-
describe(
|
|
425
|
-
test(
|
|
426
|
-
const content =
|
|
427
|
-
const r1 = applyEdit(content,
|
|
428
|
-
const r2 = applyEdit(content,
|
|
464
|
+
describe("applyEdit engine: deterministic across invocations", () => {
|
|
465
|
+
test("exact single match is idempotent", () => {
|
|
466
|
+
const content = "alpha beta gamma";
|
|
467
|
+
const r1 = applyEdit(content, "beta", "BETA", false);
|
|
468
|
+
const r2 = applyEdit(content, "beta", "BETA", false);
|
|
429
469
|
|
|
430
470
|
expect(r1).toEqual(r2);
|
|
431
471
|
expect(r1.ok).toBe(true);
|
|
432
472
|
if (!r1.ok) return;
|
|
433
|
-
expect(r1.updatedContent).toBe(
|
|
473
|
+
expect(r1.updatedContent).toBe("alpha BETA gamma");
|
|
434
474
|
});
|
|
435
475
|
|
|
436
|
-
test(
|
|
437
|
-
const content =
|
|
438
|
-
const r1 = applyEdit(content,
|
|
439
|
-
const r2 = applyEdit(content,
|
|
476
|
+
test("replaceAll is idempotent", () => {
|
|
477
|
+
const content = "x y x z x";
|
|
478
|
+
const r1 = applyEdit(content, "x", "X", true);
|
|
479
|
+
const r2 = applyEdit(content, "x", "X", true);
|
|
440
480
|
|
|
441
481
|
expect(r1).toEqual(r2);
|
|
442
482
|
expect(r1.ok).toBe(true);
|
|
@@ -444,56 +484,56 @@ describe('applyEdit engine: deterministic across invocations', () => {
|
|
|
444
484
|
expect(r1.matchCount).toBe(3);
|
|
445
485
|
});
|
|
446
486
|
|
|
447
|
-
test(
|
|
448
|
-
const content =
|
|
449
|
-
const r1 = applyEdit(content,
|
|
450
|
-
const r2 = applyEdit(content,
|
|
487
|
+
test("not-found is consistent", () => {
|
|
488
|
+
const content = "hello";
|
|
489
|
+
const r1 = applyEdit(content, "missing", "found", false);
|
|
490
|
+
const r2 = applyEdit(content, "missing", "found", false);
|
|
451
491
|
|
|
452
492
|
expect(r1).toEqual(r2);
|
|
453
493
|
expect(r1.ok).toBe(false);
|
|
454
494
|
if (r1.ok) return;
|
|
455
|
-
expect(r1.reason).toBe(
|
|
495
|
+
expect(r1.reason).toBe("not_found");
|
|
456
496
|
});
|
|
457
497
|
|
|
458
|
-
test(
|
|
459
|
-
const content =
|
|
460
|
-
const r1 = applyEdit(content,
|
|
461
|
-
const r2 = applyEdit(content,
|
|
498
|
+
test("ambiguous is consistent", () => {
|
|
499
|
+
const content = "dup dup dup";
|
|
500
|
+
const r1 = applyEdit(content, "dup", "uniq", false);
|
|
501
|
+
const r2 = applyEdit(content, "dup", "uniq", false);
|
|
462
502
|
|
|
463
503
|
expect(r1).toEqual(r2);
|
|
464
504
|
expect(r1.ok).toBe(false);
|
|
465
505
|
if (r1.ok) return;
|
|
466
|
-
expect(r1.reason).toBe(
|
|
467
|
-
if (r1.reason !==
|
|
506
|
+
expect(r1.reason).toBe("ambiguous");
|
|
507
|
+
if (r1.reason !== "ambiguous") return;
|
|
468
508
|
expect(r1.matchCount).toBe(3);
|
|
469
509
|
});
|
|
470
510
|
|
|
471
|
-
test(
|
|
472
|
-
const content =
|
|
473
|
-
const result = applyEdit(content,
|
|
511
|
+
test("multiline content is handled correctly", () => {
|
|
512
|
+
const content = "line1\nline2\nline3\nline4\n";
|
|
513
|
+
const result = applyEdit(content, "line2\nline3", "replaced", false);
|
|
474
514
|
|
|
475
515
|
expect(result.ok).toBe(true);
|
|
476
516
|
if (!result.ok) return;
|
|
477
|
-
expect(result.updatedContent).toBe(
|
|
517
|
+
expect(result.updatedContent).toBe("line1\nreplaced\nline4\n");
|
|
478
518
|
expect(result.matchCount).toBe(1);
|
|
479
519
|
});
|
|
480
520
|
|
|
481
|
-
test(
|
|
482
|
-
const content =
|
|
483
|
-
const result = applyEdit(content,
|
|
521
|
+
test("replacing with empty string works", () => {
|
|
522
|
+
const content = "keep remove keep";
|
|
523
|
+
const result = applyEdit(content, " remove", "", false);
|
|
484
524
|
|
|
485
525
|
expect(result.ok).toBe(true);
|
|
486
526
|
if (!result.ok) return;
|
|
487
|
-
expect(result.updatedContent).toBe(
|
|
527
|
+
expect(result.updatedContent).toBe("keep keep");
|
|
488
528
|
});
|
|
489
529
|
|
|
490
|
-
test(
|
|
491
|
-
const content =
|
|
492
|
-
const result = applyEdit(content,
|
|
530
|
+
test("special regex characters in old_string are treated as literals", () => {
|
|
531
|
+
const content = "price is $100.00 (USD)";
|
|
532
|
+
const result = applyEdit(content, "$100.00", "$200.00", false);
|
|
493
533
|
|
|
494
534
|
expect(result.ok).toBe(true);
|
|
495
535
|
if (!result.ok) return;
|
|
496
|
-
expect(result.updatedContent).toBe(
|
|
536
|
+
expect(result.updatedContent).toBe("price is $200.00 (USD)");
|
|
497
537
|
});
|
|
498
538
|
});
|
|
499
539
|
|
|
@@ -501,57 +541,62 @@ describe('applyEdit engine: deterministic across invocations', () => {
|
|
|
501
541
|
// 5. Path policy divergence — expected differences
|
|
502
542
|
// ===========================================================================
|
|
503
543
|
|
|
504
|
-
describe(
|
|
505
|
-
test(
|
|
544
|
+
describe("Path policy divergence: sandbox blocks escapes, host requires absolute", () => {
|
|
545
|
+
test("sandbox blocks path traversal (../), host allows any absolute path", () => {
|
|
506
546
|
const dir = makeTempDir();
|
|
507
|
-
writeFileSync(join(dir,
|
|
547
|
+
writeFileSync(join(dir, "inside.txt"), "safe content");
|
|
508
548
|
|
|
509
549
|
const sandboxOps = new FileSystemOps(sandboxPolicyFor(dir));
|
|
510
550
|
const hostOps = new FileSystemOps(hostPolicyFn());
|
|
511
551
|
|
|
512
552
|
// Sandbox: path traversal should be rejected
|
|
513
|
-
const sandboxResult = sandboxOps.readFileSafe({
|
|
553
|
+
const sandboxResult = sandboxOps.readFileSafe({
|
|
554
|
+
path: "../../../etc/hostname",
|
|
555
|
+
});
|
|
514
556
|
expect(sandboxResult.ok).toBe(false);
|
|
515
557
|
if (!sandboxResult.ok) {
|
|
516
|
-
expect(sandboxResult.error.code).toBe(
|
|
558
|
+
expect(sandboxResult.error.code).toBe("PATH_OUT_OF_BOUNDS");
|
|
517
559
|
}
|
|
518
560
|
|
|
519
561
|
// Host: relative paths are rejected (requires absolute)
|
|
520
|
-
const hostResult = hostOps.readFileSafe({ path:
|
|
562
|
+
const hostResult = hostOps.readFileSafe({ path: "relative.txt" });
|
|
521
563
|
expect(hostResult.ok).toBe(false);
|
|
522
564
|
if (!hostResult.ok) {
|
|
523
|
-
expect(hostResult.error.code).toBe(
|
|
565
|
+
expect(hostResult.error.code).toBe("PATH_NOT_ABSOLUTE");
|
|
524
566
|
}
|
|
525
567
|
});
|
|
526
568
|
|
|
527
|
-
test(
|
|
569
|
+
test("sandbox allows relative paths within boundary", () => {
|
|
528
570
|
const dir = makeTempDir();
|
|
529
|
-
writeFileSync(join(dir,
|
|
571
|
+
writeFileSync(join(dir, "valid.txt"), "data");
|
|
530
572
|
|
|
531
573
|
const sandboxOps = new FileSystemOps(sandboxPolicyFor(dir));
|
|
532
574
|
|
|
533
|
-
const result = sandboxOps.readFileSafe({ path:
|
|
575
|
+
const result = sandboxOps.readFileSafe({ path: "valid.txt" });
|
|
534
576
|
expect(result.ok).toBe(true);
|
|
535
577
|
});
|
|
536
578
|
|
|
537
|
-
test(
|
|
579
|
+
test("host requires absolute path even for simple filenames", () => {
|
|
538
580
|
const hostOps = new FileSystemOps(hostPolicyFn());
|
|
539
581
|
|
|
540
|
-
const result = hostOps.readFileSafe({ path:
|
|
582
|
+
const result = hostOps.readFileSafe({ path: "just-a-name.txt" });
|
|
541
583
|
expect(result.ok).toBe(false);
|
|
542
584
|
if (!result.ok) {
|
|
543
|
-
expect(result.error.code).toBe(
|
|
585
|
+
expect(result.error.code).toBe("PATH_NOT_ABSOLUTE");
|
|
544
586
|
}
|
|
545
587
|
});
|
|
546
588
|
|
|
547
|
-
test(
|
|
589
|
+
test("sandbox rejects absolute paths outside boundary", () => {
|
|
548
590
|
const dir = makeTempDir();
|
|
549
591
|
const sandboxOps = new FileSystemOps(sandboxPolicyFor(dir));
|
|
550
592
|
|
|
551
|
-
const result = sandboxOps.writeFileSafe({
|
|
593
|
+
const result = sandboxOps.writeFileSafe({
|
|
594
|
+
path: "/tmp/somewhere-else.txt",
|
|
595
|
+
content: "bad",
|
|
596
|
+
});
|
|
552
597
|
expect(result.ok).toBe(false);
|
|
553
598
|
if (!result.ok) {
|
|
554
|
-
expect(result.error.code).toBe(
|
|
599
|
+
expect(result.error.code).toBe("PATH_OUT_OF_BOUNDS");
|
|
555
600
|
}
|
|
556
601
|
});
|
|
557
602
|
});
|
|
@@ -560,21 +605,21 @@ describe('Path policy divergence: sandbox blocks escapes, host requires absolute
|
|
|
560
605
|
// 6. Sandbox backend parity — NativeBackend & DockerBackend SandboxResult shape
|
|
561
606
|
// ===========================================================================
|
|
562
607
|
|
|
563
|
-
describe(
|
|
564
|
-
test(
|
|
608
|
+
describe("SandboxResult shape consistency across backends", () => {
|
|
609
|
+
test("NativeBackend.wrap returns required fields", () => {
|
|
565
610
|
const native = new NativeBackend();
|
|
566
611
|
|
|
567
612
|
// On macOS this will succeed; on other platforms it will throw ToolError
|
|
568
613
|
try {
|
|
569
|
-
const result = native.wrap(
|
|
570
|
-
expect(typeof result.command).toBe(
|
|
614
|
+
const result = native.wrap("echo test", "/tmp");
|
|
615
|
+
expect(typeof result.command).toBe("string");
|
|
571
616
|
expect(Array.isArray(result.args)).toBe(true);
|
|
572
|
-
expect(typeof result.sandboxed).toBe(
|
|
617
|
+
expect(typeof result.sandboxed).toBe("boolean");
|
|
573
618
|
expect(result.sandboxed).toBe(true);
|
|
574
619
|
|
|
575
620
|
// All args must be strings
|
|
576
621
|
for (const arg of result.args) {
|
|
577
|
-
expect(typeof arg).toBe(
|
|
622
|
+
expect(typeof arg).toBe("string");
|
|
578
623
|
}
|
|
579
624
|
} catch (err) {
|
|
580
625
|
// NativeBackend explicitly throws ToolError on unsupported platforms or
|
|
@@ -582,29 +627,32 @@ describe('SandboxResult shape consistency across backends', () => {
|
|
|
582
627
|
// throw system errors (ErrnoException). Both are legitimate — but
|
|
583
628
|
// programming errors like TypeError/ReferenceError should still fail.
|
|
584
629
|
const isToolError = err instanceof ToolError;
|
|
585
|
-
const isSystemError =
|
|
630
|
+
const isSystemError =
|
|
631
|
+
err instanceof Error &&
|
|
632
|
+
"syscall" in err &&
|
|
633
|
+
typeof (err as NodeJS.ErrnoException).code === "string";
|
|
586
634
|
expect(isToolError || isSystemError).toBe(true);
|
|
587
635
|
}
|
|
588
636
|
});
|
|
589
637
|
|
|
590
|
-
test(
|
|
591
|
-
const result = wrapCommand(
|
|
638
|
+
test("wrapCommand disabled returns bash with sandboxed=false", () => {
|
|
639
|
+
const result = wrapCommand("echo hi", "/tmp", { enabled: false });
|
|
592
640
|
|
|
593
|
-
expect(result.command).toBe(
|
|
594
|
-
expect(result.args).toEqual([
|
|
641
|
+
expect(result.command).toBe("bash");
|
|
642
|
+
expect(result.args).toEqual(["-c", "--", "echo hi"]);
|
|
595
643
|
expect(result.sandboxed).toBe(false);
|
|
596
644
|
});
|
|
597
645
|
|
|
598
|
-
test(
|
|
599
|
-
const disabled = wrapCommand(
|
|
646
|
+
test("wrapCommand disabled result has same shape as enabled result", () => {
|
|
647
|
+
const disabled = wrapCommand("echo hi", "/tmp", { enabled: false });
|
|
600
648
|
|
|
601
649
|
// Both must have: command (string), args (string[]), sandboxed (boolean)
|
|
602
|
-
expect(typeof disabled.command).toBe(
|
|
650
|
+
expect(typeof disabled.command).toBe("string");
|
|
603
651
|
expect(Array.isArray(disabled.args)).toBe(true);
|
|
604
|
-
expect(typeof disabled.sandboxed).toBe(
|
|
652
|
+
expect(typeof disabled.sandboxed).toBe("boolean");
|
|
605
653
|
|
|
606
654
|
for (const arg of disabled.args) {
|
|
607
|
-
expect(typeof arg).toBe(
|
|
655
|
+
expect(typeof arg).toBe("string");
|
|
608
656
|
}
|
|
609
657
|
});
|
|
610
658
|
});
|
|
@@ -613,58 +661,60 @@ describe('SandboxResult shape consistency across backends', () => {
|
|
|
613
661
|
// 7. Terminal output format consistency
|
|
614
662
|
// ===========================================================================
|
|
615
663
|
|
|
616
|
-
describe(
|
|
617
|
-
test(
|
|
618
|
-
const result = formatShellOutput(
|
|
664
|
+
describe("Terminal output format: formatShellOutput shared by sandbox and host", () => {
|
|
665
|
+
test("successful command output has no XML status tags", () => {
|
|
666
|
+
const result = formatShellOutput("hello world", "", 0, false, 120);
|
|
619
667
|
|
|
620
|
-
expect(result.content).toBe(
|
|
621
|
-
expect(result.content).not.toContain(
|
|
622
|
-
expect(result.content).not.toContain(
|
|
668
|
+
expect(result.content).toBe("hello world");
|
|
669
|
+
expect(result.content).not.toContain("<command_exit");
|
|
670
|
+
expect(result.content).not.toContain("<command_completed");
|
|
623
671
|
expect(result.isError).toBe(false);
|
|
624
672
|
expect(result.status).toBeUndefined();
|
|
625
673
|
});
|
|
626
674
|
|
|
627
|
-
test(
|
|
628
|
-
const result = formatShellOutput(
|
|
675
|
+
test("empty output on success produces <command_completed /> tag", () => {
|
|
676
|
+
const result = formatShellOutput("", "", 0, false, 120);
|
|
629
677
|
|
|
630
|
-
expect(result.content).toBe(
|
|
678
|
+
expect(result.content).toBe("<command_completed />");
|
|
631
679
|
expect(result.isError).toBe(false);
|
|
632
680
|
});
|
|
633
681
|
|
|
634
|
-
test(
|
|
635
|
-
const result = formatShellOutput(
|
|
682
|
+
test("non-zero exit code with empty output produces <command_exit /> tag", () => {
|
|
683
|
+
const result = formatShellOutput("", "", 42, false, 120);
|
|
636
684
|
|
|
637
685
|
expect(result.content).toBe('<command_exit code="42" />');
|
|
638
686
|
expect(result.isError).toBe(true);
|
|
639
687
|
});
|
|
640
688
|
|
|
641
|
-
test(
|
|
642
|
-
const result = formatShellOutput(
|
|
689
|
+
test("stderr is appended to stdout with a newline separator", () => {
|
|
690
|
+
const result = formatShellOutput("out", "err", 0, false, 120);
|
|
643
691
|
|
|
644
|
-
expect(result.content).toBe(
|
|
692
|
+
expect(result.content).toBe("out\nerr");
|
|
645
693
|
});
|
|
646
694
|
|
|
647
|
-
test(
|
|
648
|
-
const result = formatShellOutput(
|
|
695
|
+
test("stderr-only output uses stderr as the output", () => {
|
|
696
|
+
const result = formatShellOutput("", "error message", 0, false, 120);
|
|
649
697
|
|
|
650
|
-
expect(result.content).toBe(
|
|
698
|
+
expect(result.content).toBe("error message");
|
|
651
699
|
});
|
|
652
700
|
|
|
653
|
-
test(
|
|
654
|
-
const longOutput =
|
|
655
|
-
const result = formatShellOutput(longOutput,
|
|
701
|
+
test("output truncation uses the shared MAX_OUTPUT_LENGTH constant", () => {
|
|
702
|
+
const longOutput = "x".repeat(MAX_OUTPUT_LENGTH + 100);
|
|
703
|
+
const result = formatShellOutput(longOutput, "", 0, false, 120);
|
|
656
704
|
|
|
657
|
-
expect(result.content.length).toBe(
|
|
658
|
-
|
|
705
|
+
expect(result.content.length).toBe(
|
|
706
|
+
MAX_OUTPUT_LENGTH + 1 + '<output_truncated limit="50K" />'.length,
|
|
707
|
+
);
|
|
708
|
+
expect(result.content).toContain("<output_truncated");
|
|
659
709
|
});
|
|
660
710
|
|
|
661
|
-
test(
|
|
662
|
-
const result = formatShellOutput(
|
|
711
|
+
test("timed-out command appends timeout tag and sets isError", () => {
|
|
712
|
+
const result = formatShellOutput("partial", "", 137, true, 30);
|
|
663
713
|
|
|
664
|
-
expect(result.content).toContain(
|
|
714
|
+
expect(result.content).toContain("partial");
|
|
665
715
|
expect(result.content).toContain('<command_timeout seconds="30" />');
|
|
666
716
|
expect(result.isError).toBe(true);
|
|
667
|
-
expect(result.status).toContain(
|
|
717
|
+
expect(result.status).toContain("<command_timeout");
|
|
668
718
|
});
|
|
669
719
|
});
|
|
670
720
|
|
|
@@ -672,39 +722,45 @@ describe('Terminal output format: formatShellOutput shared by sandbox and host',
|
|
|
672
722
|
// 8. Regression tests for edge cases found during migration
|
|
673
723
|
// ===========================================================================
|
|
674
724
|
|
|
675
|
-
describe(
|
|
676
|
-
test(
|
|
725
|
+
describe("Regression: edge cases in shared FileSystemOps", () => {
|
|
726
|
+
test("writing empty content creates a file with empty content", () => {
|
|
677
727
|
const dir = makeTempDir();
|
|
678
728
|
|
|
679
729
|
const { sandbox, host } = dualOps(dir);
|
|
680
730
|
|
|
681
|
-
const sandboxResult = sandbox.writeFileSafe({
|
|
682
|
-
|
|
731
|
+
const sandboxResult = sandbox.writeFileSafe({
|
|
732
|
+
path: "empty-s.txt",
|
|
733
|
+
content: "",
|
|
734
|
+
});
|
|
735
|
+
const hostResult = host.writeFileSafe({
|
|
736
|
+
path: join(dir, "empty-h.txt"),
|
|
737
|
+
content: "",
|
|
738
|
+
});
|
|
683
739
|
|
|
684
740
|
expect(sandboxResult.ok).toBe(true);
|
|
685
741
|
expect(hostResult.ok).toBe(true);
|
|
686
742
|
|
|
687
|
-
expect(readFileSync(join(dir,
|
|
688
|
-
expect(readFileSync(join(dir,
|
|
743
|
+
expect(readFileSync(join(dir, "empty-s.txt"), "utf-8")).toBe("");
|
|
744
|
+
expect(readFileSync(join(dir, "empty-h.txt"), "utf-8")).toBe("");
|
|
689
745
|
});
|
|
690
746
|
|
|
691
|
-
test(
|
|
747
|
+
test("editing a file with only whitespace works correctly", () => {
|
|
692
748
|
const dir = makeTempDir();
|
|
693
|
-
writeFileSync(join(dir,
|
|
694
|
-
writeFileSync(join(dir,
|
|
749
|
+
writeFileSync(join(dir, "ws-s.txt"), " \n \n ");
|
|
750
|
+
writeFileSync(join(dir, "ws-h.txt"), " \n \n ");
|
|
695
751
|
|
|
696
752
|
const { sandbox, host } = dualOps(dir);
|
|
697
753
|
|
|
698
754
|
const sandboxResult = sandbox.editFileSafe({
|
|
699
|
-
path:
|
|
700
|
-
oldString:
|
|
701
|
-
newString:
|
|
755
|
+
path: "ws-s.txt",
|
|
756
|
+
oldString: " \n \n ",
|
|
757
|
+
newString: "replaced",
|
|
702
758
|
replaceAll: false,
|
|
703
759
|
});
|
|
704
760
|
const hostResult = host.editFileSafe({
|
|
705
|
-
path: join(dir,
|
|
706
|
-
oldString:
|
|
707
|
-
newString:
|
|
761
|
+
path: join(dir, "ws-h.txt"),
|
|
762
|
+
oldString: " \n \n ",
|
|
763
|
+
newString: "replaced",
|
|
708
764
|
replaceAll: false,
|
|
709
765
|
});
|
|
710
766
|
|
|
@@ -712,21 +768,21 @@ describe('Regression: edge cases in shared FileSystemOps', () => {
|
|
|
712
768
|
expect(hostResult.ok).toBe(true);
|
|
713
769
|
if (!sandboxResult.ok || !hostResult.ok) return;
|
|
714
770
|
|
|
715
|
-
expect(sandboxResult.value.newContent).toBe(
|
|
716
|
-
expect(hostResult.value.newContent).toBe(
|
|
771
|
+
expect(sandboxResult.value.newContent).toBe("replaced");
|
|
772
|
+
expect(hostResult.value.newContent).toBe("replaced");
|
|
717
773
|
});
|
|
718
774
|
|
|
719
|
-
test(
|
|
775
|
+
test("write then read roundtrip produces consistent content", () => {
|
|
720
776
|
const dir = makeTempDir();
|
|
721
|
-
const content =
|
|
777
|
+
const content = "line 1\nline 2\nline 3";
|
|
722
778
|
|
|
723
779
|
const { sandbox, host } = dualOps(dir);
|
|
724
780
|
|
|
725
781
|
// Write via sandbox, read via both
|
|
726
|
-
sandbox.writeFileSafe({ path:
|
|
782
|
+
sandbox.writeFileSafe({ path: "roundtrip.txt", content });
|
|
727
783
|
|
|
728
|
-
const sandboxRead = sandbox.readFileSafe({ path:
|
|
729
|
-
const hostRead = host.readFileSafe({ path: join(dir,
|
|
784
|
+
const sandboxRead = sandbox.readFileSafe({ path: "roundtrip.txt" });
|
|
785
|
+
const hostRead = host.readFileSafe({ path: join(dir, "roundtrip.txt") });
|
|
730
786
|
|
|
731
787
|
expect(sandboxRead.ok).toBe(true);
|
|
732
788
|
expect(hostRead.ok).toBe(true);
|
|
@@ -735,43 +791,43 @@ describe('Regression: edge cases in shared FileSystemOps', () => {
|
|
|
735
791
|
expect(sandboxRead.value.content).toBe(hostRead.value.content);
|
|
736
792
|
});
|
|
737
793
|
|
|
738
|
-
test(
|
|
794
|
+
test("write then edit then read roundtrip is consistent", () => {
|
|
739
795
|
const dir = makeTempDir();
|
|
740
|
-
const initial =
|
|
796
|
+
const initial = "const x = 1;\nconst y = 2;\nconst z = 3;";
|
|
741
797
|
|
|
742
798
|
const { sandbox, host } = dualOps(dir);
|
|
743
799
|
|
|
744
|
-
sandbox.writeFileSafe({ path:
|
|
800
|
+
sandbox.writeFileSafe({ path: "code.ts", content: initial });
|
|
745
801
|
|
|
746
802
|
sandbox.editFileSafe({
|
|
747
|
-
path:
|
|
748
|
-
oldString:
|
|
749
|
-
newString:
|
|
803
|
+
path: "code.ts",
|
|
804
|
+
oldString: "const y = 2;",
|
|
805
|
+
newString: "const y = 42;",
|
|
750
806
|
replaceAll: false,
|
|
751
807
|
});
|
|
752
808
|
|
|
753
|
-
const sandboxRead = sandbox.readFileSafe({ path:
|
|
754
|
-
const hostRead = host.readFileSafe({ path: join(dir,
|
|
809
|
+
const sandboxRead = sandbox.readFileSafe({ path: "code.ts" });
|
|
810
|
+
const hostRead = host.readFileSafe({ path: join(dir, "code.ts") });
|
|
755
811
|
|
|
756
812
|
expect(sandboxRead.ok).toBe(true);
|
|
757
813
|
expect(hostRead.ok).toBe(true);
|
|
758
814
|
if (!sandboxRead.ok || !hostRead.ok) return;
|
|
759
815
|
|
|
760
816
|
expect(sandboxRead.value.content).toBe(hostRead.value.content);
|
|
761
|
-
expect(sandboxRead.value.content).toContain(
|
|
762
|
-
expect(sandboxRead.value.content).not.toContain(
|
|
817
|
+
expect(sandboxRead.value.content).toContain("const y = 42;");
|
|
818
|
+
expect(sandboxRead.value.content).not.toContain("const y = 2;");
|
|
763
819
|
});
|
|
764
820
|
|
|
765
|
-
test(
|
|
821
|
+
test("file with very long lines is handled identically", () => {
|
|
766
822
|
const dir = makeTempDir();
|
|
767
|
-
const longLine =
|
|
768
|
-
writeFileSync(join(dir,
|
|
769
|
-
writeFileSync(join(dir,
|
|
823
|
+
const longLine = "a".repeat(10_000);
|
|
824
|
+
writeFileSync(join(dir, "long-s.txt"), longLine);
|
|
825
|
+
writeFileSync(join(dir, "long-h.txt"), longLine);
|
|
770
826
|
|
|
771
827
|
const { sandbox, host } = dualOps(dir);
|
|
772
828
|
|
|
773
|
-
const sandboxResult = sandbox.readFileSafe({ path:
|
|
774
|
-
const hostResult = host.readFileSafe({ path: join(dir,
|
|
829
|
+
const sandboxResult = sandbox.readFileSafe({ path: "long-s.txt" });
|
|
830
|
+
const hostResult = host.readFileSafe({ path: join(dir, "long-h.txt") });
|
|
775
831
|
|
|
776
832
|
expect(sandboxResult.ok).toBe(true);
|
|
777
833
|
expect(hostResult.ok).toBe(true);
|
|
@@ -780,80 +836,82 @@ describe('Regression: edge cases in shared FileSystemOps', () => {
|
|
|
780
836
|
expect(sandboxResult.value.content).toBe(hostResult.value.content);
|
|
781
837
|
});
|
|
782
838
|
|
|
783
|
-
test(
|
|
839
|
+
test("concurrent writes to different files in same dir are isolated", () => {
|
|
784
840
|
const dir = makeTempDir();
|
|
785
841
|
|
|
786
842
|
const { sandbox } = dualOps(dir);
|
|
787
843
|
|
|
788
844
|
// Simulate two "concurrent" writes (sequential here, but tests isolation)
|
|
789
|
-
const r1 = sandbox.writeFileSafe({ path:
|
|
790
|
-
const r2 = sandbox.writeFileSafe({ path:
|
|
845
|
+
const r1 = sandbox.writeFileSafe({ path: "a.txt", content: "content-a" });
|
|
846
|
+
const r2 = sandbox.writeFileSafe({ path: "b.txt", content: "content-b" });
|
|
791
847
|
|
|
792
848
|
expect(r1.ok).toBe(true);
|
|
793
849
|
expect(r2.ok).toBe(true);
|
|
794
850
|
|
|
795
|
-
expect(readFileSync(join(dir,
|
|
796
|
-
expect(readFileSync(join(dir,
|
|
851
|
+
expect(readFileSync(join(dir, "a.txt"), "utf-8")).toBe("content-a");
|
|
852
|
+
expect(readFileSync(join(dir, "b.txt"), "utf-8")).toBe("content-b");
|
|
797
853
|
});
|
|
798
854
|
|
|
799
|
-
test(
|
|
855
|
+
test("edit preserves file content exactly except for the replacement", () => {
|
|
800
856
|
const dir = makeTempDir();
|
|
801
857
|
// Content with trailing newline, tabs, and special chars
|
|
802
|
-
const content =
|
|
803
|
-
writeFileSync(join(dir,
|
|
858
|
+
const content = "\tfirst line\n\tsecond line\n\tthird line\n";
|
|
859
|
+
writeFileSync(join(dir, "precise.txt"), content);
|
|
804
860
|
|
|
805
861
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
806
862
|
const result = ops.editFileSafe({
|
|
807
|
-
path:
|
|
808
|
-
oldString:
|
|
809
|
-
newString:
|
|
863
|
+
path: "precise.txt",
|
|
864
|
+
oldString: "\tsecond line",
|
|
865
|
+
newString: "\treplaced line",
|
|
810
866
|
replaceAll: false,
|
|
811
867
|
});
|
|
812
868
|
|
|
813
869
|
expect(result.ok).toBe(true);
|
|
814
870
|
if (!result.ok) return;
|
|
815
871
|
|
|
816
|
-
expect(result.value.newContent).toBe(
|
|
817
|
-
|
|
818
|
-
|
|
872
|
+
expect(result.value.newContent).toBe(
|
|
873
|
+
"\tfirst line\n\treplaced line\n\tthird line\n",
|
|
874
|
+
);
|
|
875
|
+
expect(readFileSync(join(dir, "precise.txt"), "utf-8")).toBe(
|
|
876
|
+
"\tfirst line\n\treplaced line\n\tthird line\n",
|
|
819
877
|
);
|
|
820
878
|
});
|
|
821
879
|
|
|
822
|
-
test(
|
|
880
|
+
test("read with offset=1 and no limit returns all lines", () => {
|
|
823
881
|
const dir = makeTempDir();
|
|
824
|
-
writeFileSync(join(dir,
|
|
882
|
+
writeFileSync(join(dir, "full.txt"), "one\ntwo\nthree\n");
|
|
825
883
|
|
|
826
884
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
827
|
-
const result = ops.readFileSafe({ path:
|
|
885
|
+
const result = ops.readFileSafe({ path: "full.txt", offset: 1 });
|
|
828
886
|
|
|
829
887
|
expect(result.ok).toBe(true);
|
|
830
888
|
if (!result.ok) return;
|
|
831
889
|
|
|
832
|
-
expect(result.value.content).toContain(
|
|
833
|
-
expect(result.value.content).toContain(
|
|
834
|
-
expect(result.value.content).toContain(
|
|
890
|
+
expect(result.value.content).toContain("one");
|
|
891
|
+
expect(result.value.content).toContain("two");
|
|
892
|
+
expect(result.value.content).toContain("three");
|
|
835
893
|
});
|
|
836
894
|
|
|
837
|
-
test(
|
|
895
|
+
test("symlink within sandbox boundary is readable", () => {
|
|
838
896
|
const dir = makeTempDir();
|
|
839
|
-
const targetFile = join(dir,
|
|
840
|
-
writeFileSync(targetFile,
|
|
897
|
+
const targetFile = join(dir, "target.txt");
|
|
898
|
+
writeFileSync(targetFile, "linked content");
|
|
841
899
|
|
|
842
|
-
const linkPath = join(dir,
|
|
900
|
+
const linkPath = join(dir, "link.txt");
|
|
843
901
|
try {
|
|
844
902
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
845
|
-
require(
|
|
903
|
+
require("node:fs").symlinkSync(targetFile, linkPath);
|
|
846
904
|
} catch {
|
|
847
905
|
// Symlink creation may fail on some systems — skip gracefully
|
|
848
906
|
return;
|
|
849
907
|
}
|
|
850
908
|
|
|
851
909
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
852
|
-
const result = ops.readFileSafe({ path:
|
|
910
|
+
const result = ops.readFileSafe({ path: "link.txt" });
|
|
853
911
|
|
|
854
912
|
expect(result.ok).toBe(true);
|
|
855
913
|
if (!result.ok) return;
|
|
856
|
-
expect(result.value.content).toContain(
|
|
914
|
+
expect(result.value.content).toContain("linked content");
|
|
857
915
|
});
|
|
858
916
|
});
|
|
859
917
|
|
|
@@ -861,20 +919,25 @@ describe('Regression: edge cases in shared FileSystemOps', () => {
|
|
|
861
919
|
// 9. NativeBackend shape verification
|
|
862
920
|
// ===========================================================================
|
|
863
921
|
|
|
864
|
-
describe(
|
|
865
|
-
test(
|
|
922
|
+
describe("NativeBackend: SandboxResult shape", () => {
|
|
923
|
+
test("NativeBackend has a wrap method", () => {
|
|
866
924
|
const native = new NativeBackend();
|
|
867
|
-
expect(typeof native.wrap).toBe(
|
|
925
|
+
expect(typeof native.wrap).toBe("function");
|
|
868
926
|
});
|
|
869
927
|
|
|
870
|
-
test(
|
|
928
|
+
test("disabled sandbox returns consistent bash -c -- invocation", () => {
|
|
871
929
|
// Various commands should all be wrapped consistently when disabled
|
|
872
|
-
const commands = [
|
|
930
|
+
const commands = [
|
|
931
|
+
"echo hello",
|
|
932
|
+
"ls -la",
|
|
933
|
+
"cat /etc/hosts",
|
|
934
|
+
"true && false",
|
|
935
|
+
];
|
|
873
936
|
for (const cmd of commands) {
|
|
874
|
-
const result = wrapCommand(cmd,
|
|
875
|
-
expect(result.command).toBe(
|
|
876
|
-
expect(result.args[0]).toBe(
|
|
877
|
-
expect(result.args[1]).toBe(
|
|
937
|
+
const result = wrapCommand(cmd, "/tmp", { enabled: false });
|
|
938
|
+
expect(result.command).toBe("bash");
|
|
939
|
+
expect(result.args[0]).toBe("-c");
|
|
940
|
+
expect(result.args[1]).toBe("--");
|
|
878
941
|
expect(result.args[2]).toBe(cmd);
|
|
879
942
|
expect(result.sandboxed).toBe(false);
|
|
880
943
|
}
|
|
@@ -885,15 +948,15 @@ describe('NativeBackend: SandboxResult shape', () => {
|
|
|
885
948
|
// 10. Error handling consistency
|
|
886
949
|
// ===========================================================================
|
|
887
950
|
|
|
888
|
-
describe(
|
|
889
|
-
test(
|
|
951
|
+
describe("Error handling consistency across code paths", () => {
|
|
952
|
+
test("FsError codes are consistent between sandbox and host for same conditions", () => {
|
|
890
953
|
const dir = makeTempDir();
|
|
891
954
|
|
|
892
955
|
const { sandbox, host } = dualOps(dir);
|
|
893
956
|
|
|
894
957
|
// NOT_FOUND
|
|
895
|
-
const sfNotFound = sandbox.readFileSafe({ path:
|
|
896
|
-
const hfNotFound = host.readFileSafe({ path: join(dir,
|
|
958
|
+
const sfNotFound = sandbox.readFileSafe({ path: "missing.txt" });
|
|
959
|
+
const hfNotFound = host.readFileSafe({ path: join(dir, "missing.txt") });
|
|
897
960
|
expect(sfNotFound.ok).toBe(false);
|
|
898
961
|
expect(hfNotFound.ok).toBe(false);
|
|
899
962
|
if (!sfNotFound.ok && !hfNotFound.ok) {
|
|
@@ -901,9 +964,9 @@ describe('Error handling consistency across code paths', () => {
|
|
|
901
964
|
}
|
|
902
965
|
|
|
903
966
|
// NOT_A_FILE
|
|
904
|
-
mkdirSync(join(dir,
|
|
905
|
-
const sfNotFile = sandbox.readFileSafe({ path:
|
|
906
|
-
const hfNotFile = host.readFileSafe({ path: join(dir,
|
|
967
|
+
mkdirSync(join(dir, "dirA"));
|
|
968
|
+
const sfNotFile = sandbox.readFileSafe({ path: "dirA" });
|
|
969
|
+
const hfNotFile = host.readFileSafe({ path: join(dir, "dirA") });
|
|
907
970
|
expect(sfNotFile.ok).toBe(false);
|
|
908
971
|
expect(hfNotFile.ok).toBe(false);
|
|
909
972
|
if (!sfNotFile.ok && !hfNotFile.ok) {
|
|
@@ -911,34 +974,47 @@ describe('Error handling consistency across code paths', () => {
|
|
|
911
974
|
}
|
|
912
975
|
});
|
|
913
976
|
|
|
914
|
-
test(
|
|
977
|
+
test("write error codes match between sandbox and host for same conditions", () => {
|
|
915
978
|
const dir = makeTempDir();
|
|
916
979
|
|
|
917
980
|
const { sandbox, host } = dualOps(dir);
|
|
918
981
|
|
|
919
982
|
// Both should succeed for valid operations
|
|
920
|
-
const sfWrite = sandbox.writeFileSafe({ path:
|
|
921
|
-
const hfWrite = host.writeFileSafe({
|
|
983
|
+
const sfWrite = sandbox.writeFileSafe({ path: "ok-s.txt", content: "ok" });
|
|
984
|
+
const hfWrite = host.writeFileSafe({
|
|
985
|
+
path: join(dir, "ok-h.txt"),
|
|
986
|
+
content: "ok",
|
|
987
|
+
});
|
|
922
988
|
|
|
923
989
|
expect(sfWrite.ok).toBe(true);
|
|
924
990
|
expect(hfWrite.ok).toBe(true);
|
|
925
991
|
});
|
|
926
992
|
|
|
927
|
-
test(
|
|
993
|
+
test("edit MATCH_NOT_FOUND vs MATCH_AMBIGUOUS error codes match between paths", () => {
|
|
928
994
|
const dir = makeTempDir();
|
|
929
|
-
writeFileSync(join(dir,
|
|
930
|
-
writeFileSync(join(dir,
|
|
995
|
+
writeFileSync(join(dir, "err-s.txt"), "unique text");
|
|
996
|
+
writeFileSync(join(dir, "err-h.txt"), "unique text");
|
|
931
997
|
|
|
932
998
|
const { sandbox, host } = dualOps(dir);
|
|
933
999
|
|
|
934
1000
|
// MATCH_NOT_FOUND
|
|
935
|
-
const sfMnf = sandbox.editFileSafe({
|
|
936
|
-
|
|
1001
|
+
const sfMnf = sandbox.editFileSafe({
|
|
1002
|
+
path: "err-s.txt",
|
|
1003
|
+
oldString: "nope",
|
|
1004
|
+
newString: "x",
|
|
1005
|
+
replaceAll: false,
|
|
1006
|
+
});
|
|
1007
|
+
const hfMnf = host.editFileSafe({
|
|
1008
|
+
path: join(dir, "err-h.txt"),
|
|
1009
|
+
oldString: "nope",
|
|
1010
|
+
newString: "x",
|
|
1011
|
+
replaceAll: false,
|
|
1012
|
+
});
|
|
937
1013
|
expect(sfMnf.ok).toBe(false);
|
|
938
1014
|
expect(hfMnf.ok).toBe(false);
|
|
939
1015
|
if (!sfMnf.ok && !hfMnf.ok) {
|
|
940
1016
|
expect(sfMnf.error.code).toBe(hfMnf.error.code);
|
|
941
|
-
expect(sfMnf.error.code).toBe(
|
|
1017
|
+
expect(sfMnf.error.code).toBe("MATCH_NOT_FOUND");
|
|
942
1018
|
}
|
|
943
1019
|
});
|
|
944
1020
|
});
|