@vellumai/assistant 0.6.4 → 0.6.6
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/.prettierignore +5 -0
- package/AGENTS.md +9 -1
- package/ARCHITECTURE.md +43 -49
- package/Dockerfile +17 -3
- package/README.md +3 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/bun.lock +8 -3
- package/docs/architecture/integrations.md +33 -59
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +19 -18
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/error-handling.md +111 -0
- package/docs/plugins.md +761 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +2 -1
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/knip.json +9 -2
- package/node_modules/@vellumai/ces-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
- package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
- package/node_modules/@vellumai/credential-storage/package.json +2 -2
- package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
- package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
- package/node_modules/@vellumai/egress-proxy/package.json +2 -2
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +334 -78
- package/package.json +6 -3
- package/scripts/generate-openapi.ts +50 -11
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
- package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
- package/src/__tests__/agent-loop.test.ts +112 -1
- package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
- package/src/__tests__/anthropic-provider.test.ts +171 -2
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +36 -10
- package/src/__tests__/approval-routes-http.test.ts +134 -10
- package/src/__tests__/assistant-attachments.test.ts +44 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
- package/src/__tests__/browser-skill-endstate.test.ts +51 -182
- package/src/__tests__/btw-routes.test.ts +47 -1
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/call-controller.test.ts +1 -2
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +96 -4
- package/src/__tests__/channel-approval-routes.test.ts +4 -4
- package/src/__tests__/channel-reply-delivery.test.ts +300 -2
- package/src/__tests__/checker.test.ts +870 -655
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/config-analysis.test.ts +11 -28
- package/src/__tests__/config-loader-backfill.test.ts +174 -0
- package/src/__tests__/config-loader-corrupt.test.ts +183 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +440 -114
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +72 -73
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/contacts-write.test.ts +4 -4
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +883 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
- package/src/__tests__/conversation-agent-loop.test.ts +435 -216
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
- package/src/__tests__/conversation-error.test.ts +37 -6
- package/src/__tests__/conversation-history-web-search.test.ts +7 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
- package/src/__tests__/conversation-process-callsite.test.ts +309 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
- package/src/__tests__/conversation-queue.test.ts +68 -38
- package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
- package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-skill-tools.test.ts +12 -146
- package/src/__tests__/conversation-slash-queue.test.ts +39 -19
- package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
- package/src/__tests__/conversation-speed-override.test.ts +36 -12
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
- package/src/__tests__/conversation-title-service.test.ts +118 -2
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +4 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
- package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +5 -2
- package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
- package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
- package/src/__tests__/credential-vault-unit.test.ts +135 -19
- package/src/__tests__/credentials-cli.test.ts +1 -9
- package/src/__tests__/cross-provider-web-search.test.ts +84 -0
- package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/delete-propagation.test.ts +437 -0
- package/src/__tests__/dm-backfill.test.ts +417 -0
- package/src/__tests__/dm-persistence.test.ts +227 -0
- package/src/__tests__/edit-propagation.test.ts +280 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/ephemeral-permissions.test.ts +93 -3
- package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
- package/src/__tests__/estimator-calibration.test.ts +213 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/gemini-provider.test.ts +0 -3
- package/src/__tests__/guardian-grant-minting.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/heartbeat-service.test.ts +96 -15
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +1 -66
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-catalog-parity.test.ts +174 -0
- package/src/__tests__/llm-context-normalization.test.ts +121 -0
- package/src/__tests__/llm-resolver.test.ts +214 -0
- package/src/__tests__/llm-schema.test.ts +223 -0
- package/src/__tests__/managed-proxy-context.test.ts +6 -2
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-import-from-url.test.ts +621 -0
- package/src/__tests__/model-intents.test.ts +11 -83
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-fallback.test.ts +0 -10
- package/src/__tests__/notification-decision-identity.test.ts +0 -9
- package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +46 -78
- package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/openai-provider.test.ts +7 -0
- package/src/__tests__/openai-responses-provider.test.ts +396 -0
- package/src/__tests__/openrouter-provider-only.test.ts +135 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/persona-resolver.test.ts +13 -13
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +93 -14
- package/src/__tests__/profiler-routes.test.ts +1 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
- package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
- package/src/__tests__/provider-error-scenarios.test.ts +135 -6
- package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
- package/src/__tests__/provider-registry-ollama.test.ts +1 -2
- package/src/__tests__/proxy-approval-callback.test.ts +69 -9
- package/src/__tests__/reaction-persistence.test.ts +561 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
- package/src/__tests__/risk-classifier-parity.test.ts +230 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/secret-ingress-http.test.ts +28 -0
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
- package/src/__tests__/secret-scanner-executor.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +29 -1
- package/src/__tests__/server-history-render.test.ts +31 -0
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/shell-parser-property.test.ts +13 -13
- package/src/__tests__/skill-cache-store.test.ts +182 -0
- package/src/__tests__/skills.test.ts +19 -33
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-skill.test.ts +3 -8
- package/src/__tests__/starter-bundle.test.ts +35 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
- package/src/__tests__/suggestion-routes.test.ts +259 -3
- package/src/__tests__/system-prompt.test.ts +22 -35
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +201 -94
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/trust-store.test.ts +442 -109
- package/src/__tests__/update-bulletin-job.test.ts +389 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
- package/src/__tests__/voice-session-bridge.test.ts +39 -0
- package/src/__tests__/volume-security-guard.test.ts +3 -2
- package/src/__tests__/web-search-history.test.ts +337 -0
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
- package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
- package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +22 -16
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/loop.ts +545 -115
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/avatar/resvg-lazy.test.ts +136 -0
- package/src/avatar/resvg-lazy.ts +82 -9
- package/src/avatar/traits-png-sync.ts +21 -1
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/browser/__tests__/operations.test.ts +163 -0
- package/src/browser/identifiers.ts +51 -0
- package/src/browser/operations.ts +660 -0
- package/src/browser/types.ts +81 -0
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/calls/guardian-question-copy.ts +2 -2
- package/src/calls/telephony-stt-routing.ts +1 -1
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/browser.test.ts +554 -0
- package/src/cli/commands/__tests__/cache.test.ts +623 -0
- package/src/cli/commands/__tests__/email-list.test.ts +6 -0
- package/src/cli/commands/__tests__/email-send.test.ts +93 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
- package/src/cli/commands/__tests__/task.test.ts +913 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
- package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
- package/src/cli/commands/__tests__/ui.test.ts +1215 -0
- package/src/cli/commands/__tests__/watchers.test.ts +716 -0
- package/src/cli/commands/attachment.ts +182 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -12
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +69 -8
- package/src/cli/commands/email.ts +234 -194
- package/src/cli/commands/image-generation.ts +299 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/commands/stt.ts +339 -0
- package/src/cli/commands/task.ts +795 -0
- package/src/cli/commands/trust.ts +50 -19
- package/src/cli/commands/tts.ts +273 -0
- package/src/cli/commands/ui.ts +670 -0
- package/src/cli/commands/watchers.ts +509 -0
- package/src/cli/lib/daemon-credential-client.ts +0 -19
- package/src/cli/program.ts +39 -24
- package/src/cli.ts +0 -37
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-tool-registry.ts +0 -190
- package/src/config/env.ts +7 -2
- package/src/config/feature-flag-registry.json +42 -10
- package/src/config/llm-resolver.ts +128 -0
- package/src/config/loader.ts +194 -10
- package/src/config/raw-config-utils.ts +30 -2
- package/src/config/sanitize-for-transfer.ts +35 -0
- package/src/config/schema.ts +49 -41
- package/src/config/schemas/analysis.ts +3 -22
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/calls.ts +0 -4
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/filing.ts +2 -7
- package/src/config/schemas/heartbeat.ts +0 -5
- package/src/config/schemas/inference.ts +3 -23
- package/src/config/schemas/llm.ts +317 -0
- package/src/config/schemas/memory-processing.ts +1 -9
- package/src/config/schemas/notifications.ts +4 -11
- package/src/config/schemas/platform.ts +3 -9
- package/src/config/schemas/security.ts +33 -0
- package/src/config/schemas/services.ts +9 -4
- package/src/config/schemas/stt.ts +1 -0
- package/src/config/schemas/tts.ts +64 -0
- package/src/config/schemas/updates.ts +1 -1
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +96 -7
- package/src/context/__tests__/compact-prompt.test.ts +63 -0
- package/src/context/__tests__/microcompact.test.ts +805 -0
- package/src/context/estimator-calibration.ts +136 -0
- package/src/context/microcompact.ts +443 -0
- package/src/context/prompts/compact.md +26 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +417 -39
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/executable-discovery.ts +19 -8
- package/src/credential-execution/process-manager.test.ts +109 -0
- package/src/credential-execution/process-manager.ts +65 -2
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/approval-generators.ts +29 -4
- package/src/daemon/assistant-attachments.ts +24 -13
- package/src/daemon/classifier.ts +2 -2
- package/src/daemon/config-watcher.ts +0 -3
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
- package/src/daemon/conversation-agent-loop.ts +1282 -599
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +36 -1
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +59 -17
- package/src/daemon/conversation-messaging.ts +73 -4
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +24 -11
- package/src/daemon/conversation-queue-manager.ts +3 -0
- package/src/daemon/conversation-runtime-assembly.ts +1063 -211
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +389 -1
- package/src/daemon/conversation-tool-setup.ts +51 -9
- package/src/daemon/conversation-usage.ts +1 -1
- package/src/daemon/conversation.ts +197 -64
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/guardian-action-generators.ts +34 -14
- package/src/daemon/handlers/config-model.test.ts +86 -0
- package/src/daemon/handlers/config-model.ts +65 -12
- package/src/daemon/handlers/conversations.ts +9 -2
- package/src/daemon/handlers/shared.ts +39 -11
- package/src/daemon/handlers/skills.ts +7 -3
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/lifecycle.ts +109 -82
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +63 -0
- package/src/daemon/message-types/messages.ts +21 -1
- package/src/daemon/message-types/trust.ts +0 -2
- package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
- package/src/daemon/pkb-context-tracker.test.ts +169 -0
- package/src/daemon/pkb-context-tracker.ts +125 -0
- package/src/daemon/pkb-reminder-builder.test.ts +70 -0
- package/src/daemon/pkb-reminder-builder.ts +31 -0
- package/src/daemon/providers-setup.ts +6 -0
- package/src/daemon/server.ts +122 -12
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -65
- package/src/daemon/web-search-history.ts +126 -0
- package/src/events/domain-events.ts +0 -1
- package/src/filing/filing-service.ts +9 -10
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +99 -28
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/__tests__/feed-scheduler.test.ts +39 -11
- package/src/home/__tests__/rollup-producer.test.ts +44 -0
- package/src/home/assistant-feed-authoring.ts +4 -0
- package/src/home/emit-feed-event.ts +11 -0
- package/src/home/feed-scheduler.ts +20 -4
- package/src/home/feed-types.ts +97 -4
- package/src/home/relationship-state-writer.ts +2 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/home/rollup-producer.ts +34 -5
- package/src/home/suggested-prompts.ts +101 -0
- package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
- package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
- package/src/ipc/__tests__/socket-path.test.ts +34 -0
- package/src/ipc/__tests__/task-ipc.test.ts +577 -0
- package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
- package/src/ipc/cli-client.ts +2 -1
- package/src/ipc/cli-server.ts +26 -8
- package/src/ipc/gateway-client.ts +6 -3
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +63 -0
- package/src/ipc/routes/browser.ts +97 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +31 -1
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/task-queue.ts +226 -0
- package/src/ipc/routes/task.ts +173 -0
- package/src/ipc/routes/ui-request.ts +50 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/routes/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +76 -0
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
- package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
- package/src/memory/admin.ts +18 -0
- package/src/memory/conversation-analyze-job.ts +14 -13
- package/src/memory/conversation-attention-store.ts +13 -6
- package/src/memory/conversation-crud.ts +133 -3
- package/src/memory/conversation-group-migration.ts +38 -6
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +32 -4
- package/src/memory/db-init.ts +10 -0
- package/src/memory/embedding-backend.ts +1 -1
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/compaction.ts +299 -0
- package/src/memory/graph/consolidation.ts +4 -4
- package/src/memory/graph/conversation-graph-memory.ts +89 -29
- package/src/memory/graph/extraction.test.ts +272 -2
- package/src/memory/graph/extraction.ts +183 -53
- package/src/memory/graph/graph-search.test.ts +93 -0
- package/src/memory/graph/graph-search.ts +4 -1
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +459 -0
- package/src/memory/graph/retriever.ts +237 -48
- package/src/memory/graph/store.ts +41 -0
- package/src/memory/graph/tool-handlers.ts +27 -0
- package/src/memory/graph/tools.ts +6 -1
- package/src/memory/indexer.ts +5 -5
- package/src/memory/job-handlers/conversation-starters.ts +23 -20
- package/src/memory/job-handlers/summarization.ts +2 -2
- package/src/memory/job-utils.ts +7 -1
- package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
- package/src/memory/jobs/embed-pkb-file.ts +54 -0
- package/src/memory/jobs-store.ts +44 -3
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/pkb/pkb-index.test.ts +369 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +499 -0
- package/src/memory/pkb/pkb-search.ts +159 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +147 -1
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/memory/slack-thread-store.ts +37 -0
- package/src/messaging/providers/gmail/adapter.ts +6 -16
- package/src/messaging/providers/gmail/client.ts +22 -0
- package/src/messaging/providers/gmail/types.ts +7 -0
- package/src/messaging/providers/slack/adapter.ts +14 -2
- package/src/messaging/providers/slack/backfill.test.ts +257 -0
- package/src/messaging/providers/slack/backfill.ts +101 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
- package/src/messaging/providers/slack/message-metadata.ts +123 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
- package/src/messaging/providers/slack/render-transcript.ts +501 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/decision-engine.ts +3 -9
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +31 -14
- package/src/oauth/platform-connection.test.ts +47 -0
- package/src/oauth/platform-connection.ts +15 -5
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -106
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +1223 -0
- package/src/permissions/approval-policy.ts +309 -0
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +1620 -0
- package/src/permissions/bash-risk-classifier.ts +950 -0
- package/src/permissions/checker.ts +348 -711
- package/src/permissions/command-registry.test.ts +774 -0
- package/src/permissions/command-registry.ts +1005 -0
- package/src/permissions/defaults.ts +28 -79
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +262 -0
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/secret-prompter.ts +53 -2
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/skill-risk-classifier.test.ts +311 -0
- package/src/permissions/skill-risk-classifier.ts +214 -0
- package/src/permissions/trust-client.ts +52 -25
- package/src/permissions/trust-store-interface.ts +1 -6
- package/src/permissions/trust-store.ts +161 -62
- package/src/permissions/types.ts +25 -14
- package/src/permissions/web-risk-classifier.test.ts +170 -0
- package/src/permissions/web-risk-classifier.ts +89 -0
- package/src/permissions/workspace-policy.ts +9 -19
- package/src/platform/client.ts +19 -1
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/persona-resolver.ts +3 -3
- package/src/prompts/system-prompt.ts +19 -20
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/prompts/templates/SOUL.md +2 -2
- package/src/prompts/update-bulletin-job.ts +190 -0
- package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
- package/src/providers/__tests__/retry-callsite.test.ts +424 -0
- package/src/providers/anthropic/client.ts +183 -14
- package/src/providers/call-site-routing.ts +71 -0
- package/src/providers/gemini/client.ts +65 -2
- package/src/providers/managed-proxy/constants.ts +2 -1
- package/src/providers/model-catalog.ts +524 -33
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +57 -1
- package/src/providers/openai/responses-provider.ts +86 -9
- package/src/providers/openrouter/client.ts +80 -9
- package/src/providers/provider-env-vars.ts +56 -0
- package/src/providers/provider-send-message.ts +22 -5
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/registry.ts +19 -8
- package/src/providers/retry.ts +174 -39
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
- package/src/providers/speech-to-text/provider-catalog.ts +17 -0
- package/src/providers/speech-to-text/resolve.ts +7 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
- package/src/providers/speech-to-text/xai-realtime.ts +821 -0
- package/src/providers/speech-to-text/xai.test.ts +155 -0
- package/src/providers/speech-to-text/xai.ts +97 -0
- package/src/providers/types.ts +93 -3
- package/src/runtime/AGENTS.md +27 -18
- package/src/runtime/__tests__/agent-wake.test.ts +43 -2
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
- package/src/runtime/agent-wake.ts +63 -22
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/btw-sidechain.ts +13 -3
- package/src/runtime/channel-reply-delivery.ts +106 -2
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/decision-token.ts +116 -0
- package/src/runtime/gateway-client.ts +2 -2
- package/src/runtime/http-router.ts +32 -0
- package/src/runtime/http-server.ts +129 -9
- package/src/runtime/http-types.ts +23 -3
- package/src/runtime/interactive-ui.ts +362 -0
- package/src/runtime/invite-instruction-generator.ts +2 -2
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
- package/src/runtime/migrations/gcs-signed-url.ts +162 -0
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/migrations/vbundle-importer.ts +154 -9
- package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
- package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
- package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
- package/src/runtime/migrations/vbundle-validator.ts +15 -6
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
- package/src/runtime/routes/approval-routes.ts +29 -17
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
- package/src/runtime/routes/avatar-routes.ts +20 -4
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/btw-routes.ts +1 -4
- package/src/runtime/routes/conversation-management-routes.ts +20 -2
- package/src/runtime/routes/conversation-routes.ts +351 -138
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +6 -4
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/guardian-approval-interception.ts +33 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
- package/src/runtime/routes/home-feed-routes.ts +120 -2
- package/src/runtime/routes/inbound-message-handler.ts +987 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
- package/src/runtime/routes/integrations/slack/channel.ts +25 -3
- package/src/runtime/routes/llm-context-normalization.ts +23 -1
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +720 -127
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/routes/settings-routes.ts +4 -2
- package/src/runtime/routes/trust-rules-routes.ts +30 -14
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +3 -2
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
- package/src/runtime/services/analyze-conversation.ts +12 -16
- package/src/runtime/skill-route-registry.ts +97 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +56 -8
- package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
- package/src/security/__tests__/untrusted-content.test.ts +109 -0
- package/src/security/oauth2.ts +98 -35
- package/src/security/secure-keys.ts +7 -8
- package/src/security/token-manager.ts +27 -13
- package/src/security/untrusted-content.ts +102 -0
- package/src/skills/catalog-cache.ts +35 -9
- package/src/skills/catalog-install.ts +31 -3
- package/src/skills/skill-cache-store.ts +97 -0
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
- package/src/stt/daemon-batch-transcriber.ts +33 -0
- package/src/stt/stt-stream-session.ts +8 -1
- package/src/stt/types.ts +5 -1
- package/src/subagent/manager.ts +41 -13
- package/src/tasks/ephemeral-permissions.ts +9 -4
- package/src/telemetry/usage-telemetry-reporter.ts +27 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
- package/src/tools/browser/browser-execution.ts +150 -54
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +9 -4
- package/src/tools/executor.ts +129 -73
- package/src/tools/filesystem/write.ts +52 -0
- package/src/tools/host-terminal/host-shell.ts +45 -5
- package/src/tools/memory/register.test.ts +185 -0
- package/src/tools/memory/register.ts +3 -1
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/network/web-fetch.ts +20 -10
- package/src/tools/network/web-search.ts +19 -4
- package/src/tools/permission-checker.ts +116 -46
- package/src/tools/policy-context.ts +29 -8
- package/src/tools/registry.ts +195 -6
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/side-effects.ts +0 -11
- package/src/tools/skills/execute.ts +2 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/terminal/backends/native.ts +51 -2
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +1 -0
- package/src/tools/tool-manifest.ts +6 -21
- package/src/tools/types.ts +40 -5
- package/src/tools/verification-control-plane-policy.ts +1 -1
- package/src/tts/__tests__/provider-adapters.test.ts +240 -13
- package/src/tts/provider-catalog.ts +18 -0
- package/src/tts/providers/index.ts +2 -0
- package/src/tts/providers/xai-provider.ts +224 -0
- package/src/tts/types.ts +46 -0
- package/src/types/tar-stream.d.ts +66 -0
- package/src/util/json.ts +17 -0
- package/src/util/platform.ts +9 -4
- package/src/util/pricing.ts +41 -8
- package/src/watcher/engine.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +134 -8
- package/src/watcher/providers/outlook-calendar.ts +42 -2
- package/src/workspace/git-service.ts +23 -4
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
- package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
- package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
- package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
- package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
- package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +28 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
- package/src/__tests__/gmail-archive-gate.test.ts +0 -246
- package/src/__tests__/gmail-preferences.test.ts +0 -117
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/outlook-attachments.test.ts +0 -301
- package/src/__tests__/outlook-automation-tools.test.ts +0 -425
- package/src/__tests__/outlook-categories.test.ts +0 -212
- package/src/__tests__/outlook-compose-tools.test.ts +0 -325
- package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
- package/src/__tests__/outlook-follow-up.test.ts +0 -196
- package/src/__tests__/outlook-trash.test.ts +0 -77
- package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/__tests__/update-bulletin-format.test.ts +0 -181
- package/src/__tests__/update-bulletin-state.test.ts +0 -135
- package/src/__tests__/update-bulletin.test.ts +0 -478
- package/src/__tests__/update-template-contract.test.ts +0 -29
- package/src/cli/commands/doctor.ts +0 -341
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/browser/SKILL.md +0 -88
- package/src/config/bundled-skills/browser/TOOLS.json +0 -516
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
- package/src/config/bundled-skills/gmail/SKILL.md +0 -221
- package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
- package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
- package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
- package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/google-calendar/types.ts +0 -97
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/outlook/SKILL.md +0 -196
- package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
- package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/config/bundled-skills/slack/SKILL.md +0 -108
- package/src/config/bundled-skills/tasks/SKILL.md +0 -37
- package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
- package/src/config/bundled-skills/tasks/icon.svg +0 -34
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
- package/src/config/bundled-skills/watcher/SKILL.md +0 -31
- package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/prompts/templates/UPDATES.md +0 -50
- package/src/prompts/update-bulletin-format.ts +0 -85
- package/src/prompts/update-bulletin-state.ts +0 -58
- package/src/prompts/update-bulletin-template-path.ts +0 -13
- package/src/prompts/update-bulletin.ts +0 -139
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/shared/provider-env-vars.ts +0 -19
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
- package/src/tools/watcher/create.ts +0 -86
- package/src/tools/watcher/delete.ts +0 -36
- package/src/tools/watcher/digest.ts +0 -54
- package/src/tools/watcher/list.ts +0 -83
- package/src/tools/watcher/update.ts +0 -71
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `streamCommitImport` — the streaming `.vbundle` importer.
|
|
3
|
+
*
|
|
4
|
+
* Covered:
|
|
5
|
+
* - Happy path: multi-file bundle lands in workspace; report shape matches
|
|
6
|
+
* buffer-based `commitImport`.
|
|
7
|
+
* - Manifest-first-failure: non-manifest first entry → validation_failed,
|
|
8
|
+
* temp dir cleaned, real workspace untouched.
|
|
9
|
+
* - Mid-stream hash failure: tampered manifest sha → validation_failed,
|
|
10
|
+
* temp dir cleaned, real workspace untouched.
|
|
11
|
+
* - Missing entry: manifest declares a file that's absent from the tar →
|
|
12
|
+
* validation_failed with offending path surfaced.
|
|
13
|
+
* - Extra entry (manifest_mismatch): tar carries a file the manifest does
|
|
14
|
+
* not declare → validation_failed.
|
|
15
|
+
* - Memory ceiling: 100 MB fixture streams through without pushing heap
|
|
16
|
+
* past ~64 MB, proving we're not buffering the whole bundle.
|
|
17
|
+
* - Sanity parity: buffer-based `commitImport` and `streamCommitImport`
|
|
18
|
+
* produce report objects with the same field shape for the same input.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import {
|
|
23
|
+
createReadStream,
|
|
24
|
+
existsSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
mkdtempSync,
|
|
27
|
+
readdirSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
realpathSync,
|
|
30
|
+
rmSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
} from "node:fs";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { Readable } from "node:stream";
|
|
36
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
37
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
38
|
+
|
|
39
|
+
import { buildVBundle } from "../vbundle-builder.js";
|
|
40
|
+
import { DefaultPathResolver } from "../vbundle-import-analyzer.js";
|
|
41
|
+
import { commitImport } from "../vbundle-importer.js";
|
|
42
|
+
import { streamCommitImport } from "../vbundle-streaming-importer.js";
|
|
43
|
+
import { canonicalizeJson } from "../vbundle-validator.js";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fixed "customized" guardian persona content used by the USER.md-skip
|
|
47
|
+
* test. Has user-authored content past the bare scaffold, so
|
|
48
|
+
* `isGuardianPersonaCustomized` returns true.
|
|
49
|
+
*/
|
|
50
|
+
const CUSTOMIZED_PERSONA_FIXTURE = `_ Lines starting with _ are comments - they won't appear in the system prompt
|
|
51
|
+
|
|
52
|
+
# User Profile
|
|
53
|
+
|
|
54
|
+
- Preferred name/reference: Real User
|
|
55
|
+
- Pronouns: she/her
|
|
56
|
+
- Locale: en-US
|
|
57
|
+
- Work role: Staff Engineer
|
|
58
|
+
- Goals: Ship drop-user-md
|
|
59
|
+
- Hobbies/fun: Reading papers
|
|
60
|
+
- Daily tools: Terminal, Vellum
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Fixture helpers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a temp workspace dir whose parent we own, so the atomic-swap rename
|
|
69
|
+
* (`workspaceDir` → `workspaceDir.pre-import-<ts>`) stays inside the test
|
|
70
|
+
* sandbox instead of polluting $TMPDIR with stale siblings.
|
|
71
|
+
*/
|
|
72
|
+
function freshWorkspace(): string {
|
|
73
|
+
const parent = realpathSync(
|
|
74
|
+
mkdtempSync(join(tmpdir(), "vbundle-stream-import-")),
|
|
75
|
+
);
|
|
76
|
+
const workspaceDir = join(parent, "workspace");
|
|
77
|
+
// Don't mkdir — leaving it absent lets us verify "real workspace untouched"
|
|
78
|
+
// semantics clearly. Individual tests that need an existing workspace
|
|
79
|
+
// create it themselves.
|
|
80
|
+
return workspaceDir;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readableFrom(buf: Uint8Array): Readable {
|
|
84
|
+
return Readable.from([Buffer.from(buf)]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sha256Hex(data: Uint8Array | string): string {
|
|
88
|
+
return createHash("sha256").update(data).digest("hex");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Strip a specific ustar entry from an already-built archive. Keeps the
|
|
93
|
+
* manifest (first entry) intact and drops the entry whose name matches
|
|
94
|
+
* `entryName`. Assumes no PAX/longname entries precede the target.
|
|
95
|
+
*/
|
|
96
|
+
function removeEntry(archive: Uint8Array, entryName: string): Uint8Array {
|
|
97
|
+
const raw = gunzipSync(archive);
|
|
98
|
+
|
|
99
|
+
let offset = 0;
|
|
100
|
+
while (offset + 512 <= raw.length) {
|
|
101
|
+
const block = raw.subarray(offset, offset + 512);
|
|
102
|
+
if (block.every((b) => b === 0)) break;
|
|
103
|
+
|
|
104
|
+
// Entry name is at offset 0..100 of the header, null-terminated.
|
|
105
|
+
let nameEnd = 0;
|
|
106
|
+
while (nameEnd < 100 && block[nameEnd] !== 0) nameEnd += 1;
|
|
107
|
+
const name = new TextDecoder().decode(block.subarray(0, nameEnd));
|
|
108
|
+
|
|
109
|
+
const sizeStr = new TextDecoder()
|
|
110
|
+
.decode(block.subarray(124, 136))
|
|
111
|
+
.replace(/\0.*$/, "")
|
|
112
|
+
.trim();
|
|
113
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
114
|
+
const dataBlocks = Math.ceil(size / 512);
|
|
115
|
+
const entryLen = 512 + dataBlocks * 512;
|
|
116
|
+
|
|
117
|
+
if (name === entryName) {
|
|
118
|
+
const out = new Uint8Array(raw.length - entryLen);
|
|
119
|
+
out.set(raw.subarray(0, offset), 0);
|
|
120
|
+
out.set(raw.subarray(offset + entryLen), offset);
|
|
121
|
+
return gzipSync(out);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
offset += entryLen;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(
|
|
128
|
+
`removeEntry: test helper could not find entry "${entryName}" in archive`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Update manifest.json in place to drop the entry with the given archive
|
|
134
|
+
* path AND recompute manifest_sha256 so the manifest itself stays valid.
|
|
135
|
+
* Used to craft the "extra entry" (manifest_mismatch) fixture — the tar
|
|
136
|
+
* has the file, but the manifest does not.
|
|
137
|
+
*/
|
|
138
|
+
function dropFromManifestAndRepack(
|
|
139
|
+
archive: Uint8Array,
|
|
140
|
+
pathToDrop: string,
|
|
141
|
+
): Uint8Array {
|
|
142
|
+
const raw = gunzipSync(archive);
|
|
143
|
+
const sizeStr = new TextDecoder()
|
|
144
|
+
.decode(raw.subarray(124, 136))
|
|
145
|
+
.replace(/\0.*$/, "")
|
|
146
|
+
.trim();
|
|
147
|
+
const origSize = parseInt(sizeStr, 8);
|
|
148
|
+
const manifestJson = new TextDecoder().decode(
|
|
149
|
+
raw.subarray(512, 512 + origSize),
|
|
150
|
+
);
|
|
151
|
+
const manifest = JSON.parse(manifestJson) as {
|
|
152
|
+
files: Array<{ path: string; sha256: string; size: number }>;
|
|
153
|
+
manifest_sha256: string;
|
|
154
|
+
[k: string]: unknown;
|
|
155
|
+
};
|
|
156
|
+
manifest.files = manifest.files.filter((f) => f.path !== pathToDrop);
|
|
157
|
+
// Recompute manifest_sha256.
|
|
158
|
+
const withoutChecksum: Record<string, unknown> = { ...manifest };
|
|
159
|
+
delete withoutChecksum.manifest_sha256;
|
|
160
|
+
manifest.manifest_sha256 = sha256Hex(canonicalizeJson(withoutChecksum));
|
|
161
|
+
|
|
162
|
+
const newJson = JSON.stringify(manifest);
|
|
163
|
+
const newBytes = new TextEncoder().encode(newJson);
|
|
164
|
+
|
|
165
|
+
// The manifest has almost certainly changed length — rebuild the tar.
|
|
166
|
+
// Rewrite the first entry's size field and pad the body to the next
|
|
167
|
+
// 512-byte boundary, then concatenate everything after the old manifest.
|
|
168
|
+
const header = new Uint8Array(512);
|
|
169
|
+
header.set(raw.subarray(0, 512), 0);
|
|
170
|
+
const newSizeOctal = newBytes.length.toString(8).padStart(11, "0");
|
|
171
|
+
for (let i = 0; i < 11; i++) {
|
|
172
|
+
header[124 + i] = newSizeOctal.charCodeAt(i);
|
|
173
|
+
}
|
|
174
|
+
header[135] = 0;
|
|
175
|
+
// Zero out the old checksum field before recomputing.
|
|
176
|
+
for (let i = 148; i < 156; i++) header[i] = 0x20;
|
|
177
|
+
let sum = 0;
|
|
178
|
+
for (let i = 0; i < 512; i++) sum += header[i];
|
|
179
|
+
const cksum = sum.toString(8).padStart(6, "0");
|
|
180
|
+
for (let i = 0; i < 6; i++) header[148 + i] = cksum.charCodeAt(i);
|
|
181
|
+
header[154] = 0;
|
|
182
|
+
header[155] = 0x20;
|
|
183
|
+
|
|
184
|
+
const oldPaddedLen = 512 + Math.ceil(origSize / 512) * 512;
|
|
185
|
+
const newPadded = Math.ceil(newBytes.length / 512) * 512;
|
|
186
|
+
const out = new Uint8Array(
|
|
187
|
+
header.length + newPadded + (raw.length - oldPaddedLen),
|
|
188
|
+
);
|
|
189
|
+
out.set(header, 0);
|
|
190
|
+
out.set(newBytes, 512);
|
|
191
|
+
out.set(raw.subarray(oldPaddedLen), 512 + newPadded);
|
|
192
|
+
return gzipSync(out);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Happy path
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
describe("streamCommitImport — happy path", () => {
|
|
200
|
+
let workspaceDir: string;
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
workspaceDir = freshWorkspace();
|
|
203
|
+
});
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
// Clean up any sibling temp/backup dirs left under the workspace parent.
|
|
206
|
+
const parent = join(workspaceDir, "..");
|
|
207
|
+
try {
|
|
208
|
+
rmSync(parent, { recursive: true, force: true });
|
|
209
|
+
} catch {
|
|
210
|
+
// best-effort
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("writes every file into the workspace and returns a report with the expected shape", async () => {
|
|
215
|
+
const fileA = new TextEncoder().encode("alpha alpha alpha\n");
|
|
216
|
+
const fileB = new TextEncoder().encode("beta beta\n");
|
|
217
|
+
const fileC = new TextEncoder().encode("gamma payload\n");
|
|
218
|
+
|
|
219
|
+
const { archive } = buildVBundle({
|
|
220
|
+
files: [
|
|
221
|
+
{ path: "workspace/a.txt", data: fileA },
|
|
222
|
+
{ path: "workspace/sub/b.txt", data: fileB },
|
|
223
|
+
{ path: "workspace/sub/c.txt", data: fileC },
|
|
224
|
+
],
|
|
225
|
+
source: "test-happy-path",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const result = await streamCommitImport({
|
|
229
|
+
source: readableFrom(archive),
|
|
230
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
231
|
+
workspaceDir,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.ok).toBe(true);
|
|
235
|
+
if (!result.ok) throw new Error("unreachable");
|
|
236
|
+
|
|
237
|
+
expect(existsSync(join(workspaceDir, "a.txt"))).toBe(true);
|
|
238
|
+
expect(readFileSync(join(workspaceDir, "a.txt"))).toEqual(
|
|
239
|
+
Buffer.from(fileA),
|
|
240
|
+
);
|
|
241
|
+
expect(readFileSync(join(workspaceDir, "sub/b.txt"))).toEqual(
|
|
242
|
+
Buffer.from(fileB),
|
|
243
|
+
);
|
|
244
|
+
expect(readFileSync(join(workspaceDir, "sub/c.txt"))).toEqual(
|
|
245
|
+
Buffer.from(fileC),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(result.report.success).toBe(true);
|
|
249
|
+
expect(result.report.summary.total_files).toBe(3);
|
|
250
|
+
expect(result.report.summary.files_created).toBe(3);
|
|
251
|
+
expect(result.report.manifest.files).toHaveLength(3);
|
|
252
|
+
for (const f of result.report.files) {
|
|
253
|
+
expect(f.action).toBe("created");
|
|
254
|
+
expect(f.backup_path).toBeNull();
|
|
255
|
+
expect(typeof f.sha256).toBe("string");
|
|
256
|
+
expect(f.disk_path.startsWith(workspaceDir)).toBe(true);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("invokes onProgress after each file entry finishes", async () => {
|
|
261
|
+
const { archive } = buildVBundle({
|
|
262
|
+
files: [
|
|
263
|
+
{
|
|
264
|
+
path: "workspace/a.txt",
|
|
265
|
+
data: new TextEncoder().encode("one"),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
path: "workspace/b.txt",
|
|
269
|
+
data: new TextEncoder().encode("two!"),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const events: Array<{
|
|
275
|
+
archivePath: string;
|
|
276
|
+
bytesWritten: number;
|
|
277
|
+
entryIndex: number;
|
|
278
|
+
}> = [];
|
|
279
|
+
const result = await streamCommitImport({
|
|
280
|
+
source: readableFrom(archive),
|
|
281
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
282
|
+
workspaceDir,
|
|
283
|
+
onProgress: (e) => events.push(e),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result.ok).toBe(true);
|
|
287
|
+
expect(events.map((e) => e.archivePath)).toEqual([
|
|
288
|
+
"workspace/a.txt",
|
|
289
|
+
"workspace/b.txt",
|
|
290
|
+
]);
|
|
291
|
+
expect(events[0]?.bytesWritten).toBe(3);
|
|
292
|
+
expect(events[1]?.bytesWritten).toBe(4);
|
|
293
|
+
expect(events[0]?.entryIndex).toBeLessThan(events[1]?.entryIndex ?? -1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("forwards credentials to importCredentials callback but never writes them to disk", async () => {
|
|
297
|
+
const { archive } = buildVBundle({
|
|
298
|
+
files: [
|
|
299
|
+
{
|
|
300
|
+
path: "workspace/config.json",
|
|
301
|
+
data: new TextEncoder().encode("{}"),
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
path: "credentials/openai-key",
|
|
305
|
+
data: new TextEncoder().encode("sk-test-1"),
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
path: "credentials/anthropic-key",
|
|
309
|
+
data: new TextEncoder().encode("sk-ant-2"),
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const received: Array<{ account: string; value: string }> = [];
|
|
315
|
+
const result = await streamCommitImport({
|
|
316
|
+
source: readableFrom(archive),
|
|
317
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
318
|
+
workspaceDir,
|
|
319
|
+
importCredentials: async (creds) => {
|
|
320
|
+
received.push(...creds);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(result.ok).toBe(true);
|
|
325
|
+
expect(received).toHaveLength(2);
|
|
326
|
+
expect(received).toContainEqual({
|
|
327
|
+
account: "openai-key",
|
|
328
|
+
value: "sk-test-1",
|
|
329
|
+
});
|
|
330
|
+
expect(received).toContainEqual({
|
|
331
|
+
account: "anthropic-key",
|
|
332
|
+
value: "sk-ant-2",
|
|
333
|
+
});
|
|
334
|
+
// Credentials must NOT appear on disk.
|
|
335
|
+
expect(existsSync(join(workspaceDir, "credentials"))).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Failure modes — every one must leave the real workspace untouched and
|
|
341
|
+
// clean up the sibling temp dir.
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
describe("streamCommitImport — failure modes", () => {
|
|
345
|
+
let workspaceDir: string;
|
|
346
|
+
beforeEach(() => {
|
|
347
|
+
workspaceDir = freshWorkspace();
|
|
348
|
+
});
|
|
349
|
+
afterEach(() => {
|
|
350
|
+
const parent = join(workspaceDir, "..");
|
|
351
|
+
try {
|
|
352
|
+
rmSync(parent, { recursive: true, force: true });
|
|
353
|
+
} catch {
|
|
354
|
+
// best-effort
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
/** Ensure no sibling temp/backup dirs for this workspace remain. */
|
|
359
|
+
function assertNoLeftoverTempDirs(): void {
|
|
360
|
+
const parent = join(workspaceDir, "..");
|
|
361
|
+
const base = workspaceDir.split("/").pop()!;
|
|
362
|
+
const siblings = readdirSync(parent);
|
|
363
|
+
const leftover = siblings.filter(
|
|
364
|
+
(name) =>
|
|
365
|
+
name.startsWith(`${base}.import-`) ||
|
|
366
|
+
name.startsWith(`${base}.pre-import-`),
|
|
367
|
+
);
|
|
368
|
+
expect(leftover).toEqual([]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
test("manifest-first failure: non-manifest first entry → validation_failed, real workspace untouched", async () => {
|
|
372
|
+
// Seed the real workspace with a marker file so we can verify it's
|
|
373
|
+
// untouched after the failed import.
|
|
374
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
375
|
+
writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
|
|
376
|
+
|
|
377
|
+
// Hand-roll a gzipped tar whose first entry is NOT manifest.json.
|
|
378
|
+
// Reuse buildVBundle for a valid archive, then strip the manifest
|
|
379
|
+
// entry using removeEntry — the remaining archive opens with a
|
|
380
|
+
// workspace/ file as entry #1.
|
|
381
|
+
const { archive } = buildVBundle({
|
|
382
|
+
files: [
|
|
383
|
+
{
|
|
384
|
+
path: "workspace/a.txt",
|
|
385
|
+
data: new TextEncoder().encode("hello"),
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
});
|
|
389
|
+
const noManifest = removeEntry(archive, "manifest.json");
|
|
390
|
+
|
|
391
|
+
const result = await streamCommitImport({
|
|
392
|
+
source: readableFrom(noManifest),
|
|
393
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
394
|
+
workspaceDir,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(result.ok).toBe(false);
|
|
398
|
+
if (result.ok) throw new Error("unreachable");
|
|
399
|
+
expect(result.reason).toBe("validation_failed");
|
|
400
|
+
|
|
401
|
+
// Real workspace's pre-existing content is still there, unmodified.
|
|
402
|
+
expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
|
|
403
|
+
"keep me\n",
|
|
404
|
+
);
|
|
405
|
+
assertNoLeftoverTempDirs();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("mid-stream hash failure: tampered manifest sha → validation_failed, cleanup intact", async () => {
|
|
409
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
410
|
+
writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
|
|
411
|
+
|
|
412
|
+
// Build a valid bundle with one file whose data is 32 bytes long.
|
|
413
|
+
const body = new TextEncoder().encode("x".repeat(32));
|
|
414
|
+
const { archive } = buildVBundle({
|
|
415
|
+
files: [{ path: "workspace/victim.txt", data: body }],
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Tamper the manifest sha256 for workspace/victim.txt by substituting
|
|
419
|
+
// one hex character. Keeps the manifest valid (the substitution is
|
|
420
|
+
// same-length) — but because manifest_sha256 is recomputed over the
|
|
421
|
+
// declared data, we ALSO need to tamper manifest_sha256 to keep the
|
|
422
|
+
// manifest itself valid. Otherwise the manifest will fail its
|
|
423
|
+
// self-checksum and the test exercises the wrong path.
|
|
424
|
+
//
|
|
425
|
+
// Easier approach: build a NEW valid manifest that declares the wrong
|
|
426
|
+
// hash for victim.txt. We hand-rebuild the archive via
|
|
427
|
+
// `dropFromManifestAndRepack`-style logic: replace the existing entry
|
|
428
|
+
// in manifest.files with a different sha256, recompute manifest_sha256.
|
|
429
|
+
const raw = gunzipSync(archive);
|
|
430
|
+
const sizeStr = new TextDecoder()
|
|
431
|
+
.decode(raw.subarray(124, 136))
|
|
432
|
+
.replace(/\0.*$/, "")
|
|
433
|
+
.trim();
|
|
434
|
+
const origSize = parseInt(sizeStr, 8);
|
|
435
|
+
const manifestJson = new TextDecoder().decode(
|
|
436
|
+
raw.subarray(512, 512 + origSize),
|
|
437
|
+
);
|
|
438
|
+
const manifest = JSON.parse(manifestJson) as {
|
|
439
|
+
files: Array<{ path: string; sha256: string; size: number }>;
|
|
440
|
+
manifest_sha256: string;
|
|
441
|
+
[k: string]: unknown;
|
|
442
|
+
};
|
|
443
|
+
manifest.files = manifest.files.map((f) =>
|
|
444
|
+
f.path === "workspace/victim.txt"
|
|
445
|
+
? {
|
|
446
|
+
...f,
|
|
447
|
+
// Deterministic-but-wrong sha: flip the high bit of char 0.
|
|
448
|
+
sha256: "0" + f.sha256.slice(1),
|
|
449
|
+
}
|
|
450
|
+
: f,
|
|
451
|
+
);
|
|
452
|
+
const withoutChecksum: Record<string, unknown> = { ...manifest };
|
|
453
|
+
delete withoutChecksum.manifest_sha256;
|
|
454
|
+
manifest.manifest_sha256 = sha256Hex(canonicalizeJson(withoutChecksum));
|
|
455
|
+
|
|
456
|
+
const newJson = JSON.stringify(manifest);
|
|
457
|
+
const newBytes = new TextEncoder().encode(newJson);
|
|
458
|
+
if (newBytes.length !== origSize) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`hash-failure test fixture: manifest length drifted (${newBytes.length} vs ${origSize})`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const tampered = new Uint8Array(raw.length);
|
|
464
|
+
tampered.set(raw);
|
|
465
|
+
tampered.set(newBytes, 512);
|
|
466
|
+
const tamperedArchive = gzipSync(tampered);
|
|
467
|
+
|
|
468
|
+
const result = await streamCommitImport({
|
|
469
|
+
source: readableFrom(tamperedArchive),
|
|
470
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
471
|
+
workspaceDir,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(result.ok).toBe(false);
|
|
475
|
+
if (result.ok) throw new Error("unreachable");
|
|
476
|
+
expect(result.reason).toBe("validation_failed");
|
|
477
|
+
|
|
478
|
+
// Existing workspace content preserved, no temp dir hanging around.
|
|
479
|
+
expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
|
|
480
|
+
"keep me\n",
|
|
481
|
+
);
|
|
482
|
+
assertNoLeftoverTempDirs();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("missing entry: manifest declares a path absent from the tar → validation_failed", async () => {
|
|
486
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
487
|
+
writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
|
|
488
|
+
|
|
489
|
+
const { archive } = buildVBundle({
|
|
490
|
+
files: [
|
|
491
|
+
{
|
|
492
|
+
path: "workspace/present.txt",
|
|
493
|
+
data: new TextEncoder().encode("here"),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
path: "workspace/missing.txt",
|
|
497
|
+
data: new TextEncoder().encode("gone"),
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
const stripped = removeEntry(archive, "workspace/missing.txt");
|
|
502
|
+
|
|
503
|
+
const result = await streamCommitImport({
|
|
504
|
+
source: readableFrom(stripped),
|
|
505
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
506
|
+
workspaceDir,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(result.ok).toBe(false);
|
|
510
|
+
if (result.ok) throw new Error("unreachable");
|
|
511
|
+
expect(result.reason).toBe("validation_failed");
|
|
512
|
+
// The error payload should surface the missing path.
|
|
513
|
+
const combined = JSON.stringify(result);
|
|
514
|
+
expect(combined).toContain("workspace/missing.txt");
|
|
515
|
+
|
|
516
|
+
expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
|
|
517
|
+
"keep me\n",
|
|
518
|
+
);
|
|
519
|
+
assertNoLeftoverTempDirs();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("extra entry: tar contains a file the manifest does not declare → validation_failed", async () => {
|
|
523
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
524
|
+
writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
|
|
525
|
+
|
|
526
|
+
const { archive } = buildVBundle({
|
|
527
|
+
files: [
|
|
528
|
+
{
|
|
529
|
+
path: "workspace/declared.txt",
|
|
530
|
+
data: new TextEncoder().encode("fine"),
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
path: "workspace/extra.txt",
|
|
534
|
+
data: new TextEncoder().encode("surprise"),
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
});
|
|
538
|
+
const extraPresent = dropFromManifestAndRepack(
|
|
539
|
+
archive,
|
|
540
|
+
"workspace/extra.txt",
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const result = await streamCommitImport({
|
|
544
|
+
source: readableFrom(extraPresent),
|
|
545
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
546
|
+
workspaceDir,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(result.ok).toBe(false);
|
|
550
|
+
if (result.ok) throw new Error("unreachable");
|
|
551
|
+
expect(result.reason).toBe("validation_failed");
|
|
552
|
+
|
|
553
|
+
expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
|
|
554
|
+
"keep me\n",
|
|
555
|
+
);
|
|
556
|
+
assertNoLeftoverTempDirs();
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
// Memory ceiling — the point of the streaming path.
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Materialize a ~100 MB .vbundle fixture on disk and return its path.
|
|
566
|
+
* Wrapping the build in its own function lets the intermediate Uint8Arrays
|
|
567
|
+
* go out of scope before we start measuring heap — the fixture itself
|
|
568
|
+
* must not count against the importer's working-set budget.
|
|
569
|
+
*/
|
|
570
|
+
function writeLargeFixtureToDisk(archivePath: string): void {
|
|
571
|
+
const CHUNK = 25 * 1024 * 1024;
|
|
572
|
+
const files = [0, 1, 2, 3].map((i) => ({
|
|
573
|
+
path: `workspace/big-${i}.bin`,
|
|
574
|
+
data: new Uint8Array(CHUNK).fill(0x41 + i),
|
|
575
|
+
}));
|
|
576
|
+
const { archive } = buildVBundle({ files });
|
|
577
|
+
writeFileSync(archivePath, archive);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
describe("streamCommitImport — memory ceiling", () => {
|
|
581
|
+
test("100 MB fixture streams in without pushing RSS past ~64 MB over baseline", async () => {
|
|
582
|
+
const workspaceDir = freshWorkspace();
|
|
583
|
+
const parent = join(workspaceDir, "..");
|
|
584
|
+
const archivePath = join(parent, "fixture.vbundle");
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
// Build the fixture in an isolated scope so intermediate buffers go
|
|
588
|
+
// out of scope before we start measuring.
|
|
589
|
+
writeLargeFixtureToDisk(archivePath);
|
|
590
|
+
|
|
591
|
+
// Bun's `process.memoryUsage().heapUsed` can include accounting for
|
|
592
|
+
// off-heap Buffer backing stores, so a strict heap ceiling is noisy
|
|
593
|
+
// across engines. Use RSS instead — that's the actual "did the
|
|
594
|
+
// process grow" signal. If the importer were buffering the full 100
|
|
595
|
+
// MB archive, RSS would spike by at least 100 MB; a streaming
|
|
596
|
+
// importer's per-entry working set is bounded by ~one tar entry's
|
|
597
|
+
// internal buffers (a few MB).
|
|
598
|
+
const baselineRss = process.memoryUsage().rss;
|
|
599
|
+
let peakRss = baselineRss;
|
|
600
|
+
let progressCount = 0;
|
|
601
|
+
|
|
602
|
+
const result = await streamCommitImport({
|
|
603
|
+
source: createReadStream(archivePath),
|
|
604
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
605
|
+
workspaceDir,
|
|
606
|
+
onProgress: () => {
|
|
607
|
+
progressCount += 1;
|
|
608
|
+
const cur = process.memoryUsage().rss;
|
|
609
|
+
if (cur > peakRss) peakRss = cur;
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(result.ok).toBe(true);
|
|
614
|
+
// We expect onProgress to fire at least 4 times (one per big file) —
|
|
615
|
+
// spot-check that we actually sampled during import.
|
|
616
|
+
expect(progressCount).toBeGreaterThanOrEqual(4);
|
|
617
|
+
|
|
618
|
+
// The 64 MB delta bound is a rough guard proving "it doesn't buffer
|
|
619
|
+
// the whole bundle" — if the importer were accumulating the 100 MB
|
|
620
|
+
// archive in memory, RSS would jump well past this threshold.
|
|
621
|
+
const delta = peakRss - baselineRss;
|
|
622
|
+
expect(delta).toBeLessThan(64 * 1024 * 1024);
|
|
623
|
+
} finally {
|
|
624
|
+
try {
|
|
625
|
+
rmSync(parent, { recursive: true, force: true });
|
|
626
|
+
} catch {
|
|
627
|
+
// best-effort
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}, 60_000);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// Sanity parity with buffer-based commitImport.
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
|
|
637
|
+
describe("streamCommitImport — report parity with commitImport", () => {
|
|
638
|
+
test("buffer-based and streaming importer produce report objects with the same field shape", async () => {
|
|
639
|
+
const bufferWorkspace = freshWorkspace();
|
|
640
|
+
const streamWorkspace = freshWorkspace();
|
|
641
|
+
|
|
642
|
+
const files = [
|
|
643
|
+
{
|
|
644
|
+
path: "workspace/a.txt",
|
|
645
|
+
data: new TextEncoder().encode("alpha"),
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
path: "workspace/sub/b.txt",
|
|
649
|
+
data: new TextEncoder().encode("beta beta"),
|
|
650
|
+
},
|
|
651
|
+
];
|
|
652
|
+
const { archive } = buildVBundle({ files });
|
|
653
|
+
|
|
654
|
+
// Buffer-based path.
|
|
655
|
+
mkdirSync(bufferWorkspace, { recursive: true });
|
|
656
|
+
const bufferResult = commitImport({
|
|
657
|
+
archiveData: archive,
|
|
658
|
+
pathResolver: new DefaultPathResolver(bufferWorkspace),
|
|
659
|
+
workspaceDir: bufferWorkspace,
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// Streaming path.
|
|
663
|
+
const streamResult = await streamCommitImport({
|
|
664
|
+
source: readableFrom(archive),
|
|
665
|
+
pathResolver: new DefaultPathResolver(streamWorkspace),
|
|
666
|
+
workspaceDir: streamWorkspace,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
expect(bufferResult.ok).toBe(true);
|
|
671
|
+
expect(streamResult.ok).toBe(true);
|
|
672
|
+
if (!bufferResult.ok || !streamResult.ok) throw new Error("unreachable");
|
|
673
|
+
|
|
674
|
+
// The shapes must match key-for-key.
|
|
675
|
+
expect(Object.keys(streamResult.report).sort()).toEqual(
|
|
676
|
+
Object.keys(bufferResult.report).sort(),
|
|
677
|
+
);
|
|
678
|
+
expect(Object.keys(streamResult.report.summary).sort()).toEqual(
|
|
679
|
+
Object.keys(bufferResult.report.summary).sort(),
|
|
680
|
+
);
|
|
681
|
+
expect(streamResult.report.files.length).toBe(
|
|
682
|
+
bufferResult.report.files.length,
|
|
683
|
+
);
|
|
684
|
+
for (let i = 0; i < streamResult.report.files.length; i++) {
|
|
685
|
+
expect(Object.keys(streamResult.report.files[i]).sort()).toEqual(
|
|
686
|
+
Object.keys(bufferResult.report.files[i]).sort(),
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Manifest payload itself should match — the streaming path parses it
|
|
691
|
+
// directly from the same bytes.
|
|
692
|
+
expect(streamResult.report.manifest.manifest_sha256).toBe(
|
|
693
|
+
bufferResult.report.manifest.manifest_sha256,
|
|
694
|
+
);
|
|
695
|
+
} finally {
|
|
696
|
+
for (const ws of [bufferWorkspace, streamWorkspace]) {
|
|
697
|
+
const parent = join(ws, "..");
|
|
698
|
+
try {
|
|
699
|
+
rmSync(parent, { recursive: true, force: true });
|
|
700
|
+
} catch {
|
|
701
|
+
// best-effort
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
// Parity with commitImport: workspace-swap gating, config sanitization,
|
|
710
|
+
// legacy USER.md skip when persona is customized. Each of these regressed
|
|
711
|
+
// when the streaming path was first introduced.
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
|
|
714
|
+
describe("streamCommitImport — no workspace entries means no swap", () => {
|
|
715
|
+
let workspaceDir: string;
|
|
716
|
+
beforeEach(() => {
|
|
717
|
+
workspaceDir = freshWorkspace();
|
|
718
|
+
});
|
|
719
|
+
afterEach(() => {
|
|
720
|
+
const parent = join(workspaceDir, "..");
|
|
721
|
+
try {
|
|
722
|
+
rmSync(parent, { recursive: true, force: true });
|
|
723
|
+
} catch {
|
|
724
|
+
// best-effort
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("bundle with only credentials leaves the real workspace untouched", async () => {
|
|
729
|
+
// Seed the real workspace with a marker file. A successful import that
|
|
730
|
+
// had workspace entries would wipe-and-swap this file out of existence,
|
|
731
|
+
// so its survival post-import proves we skipped the rename pair.
|
|
732
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
733
|
+
writeFileSync(
|
|
734
|
+
join(workspaceDir, "marker-please-preserve.txt"),
|
|
735
|
+
"do not touch\n",
|
|
736
|
+
);
|
|
737
|
+
mkdirSync(join(workspaceDir, "deep", "nested"), { recursive: true });
|
|
738
|
+
writeFileSync(
|
|
739
|
+
join(workspaceDir, "deep", "nested", "file.txt"),
|
|
740
|
+
"nested content\n",
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
// Credential entries resolve to null via DefaultPathResolver — they are
|
|
744
|
+
// buffered in memory for CES, never land on disk. A bundle consisting
|
|
745
|
+
// entirely of credentials therefore has zero workspace-targeted writes.
|
|
746
|
+
const { archive } = buildVBundle({
|
|
747
|
+
files: [
|
|
748
|
+
{
|
|
749
|
+
path: "credentials/openai-key",
|
|
750
|
+
data: new TextEncoder().encode("sk-test-creds-only"),
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const received: Array<{ account: string; value: string }> = [];
|
|
756
|
+
const result = await streamCommitImport({
|
|
757
|
+
source: readableFrom(archive),
|
|
758
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
759
|
+
workspaceDir,
|
|
760
|
+
importCredentials: async (creds) => {
|
|
761
|
+
received.push(...creds);
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
expect(result.ok).toBe(true);
|
|
766
|
+
if (!result.ok) throw new Error("unreachable");
|
|
767
|
+
|
|
768
|
+
// Real workspace's pre-existing files are STILL THERE — the temp tree
|
|
769
|
+
// was not swapped in.
|
|
770
|
+
expect(
|
|
771
|
+
readFileSync(join(workspaceDir, "marker-please-preserve.txt"), "utf8"),
|
|
772
|
+
).toBe("do not touch\n");
|
|
773
|
+
expect(
|
|
774
|
+
readFileSync(join(workspaceDir, "deep", "nested", "file.txt"), "utf8"),
|
|
775
|
+
).toBe("nested content\n");
|
|
776
|
+
|
|
777
|
+
// Credentials still flowed through post-commit as they should.
|
|
778
|
+
expect(received).toEqual([
|
|
779
|
+
{ account: "openai-key", value: "sk-test-creds-only" },
|
|
780
|
+
]);
|
|
781
|
+
|
|
782
|
+
// Report should reflect "nothing imported into the workspace".
|
|
783
|
+
expect(result.report.summary.files_created).toBe(0);
|
|
784
|
+
expect(result.report.summary.files_overwritten).toBe(0);
|
|
785
|
+
|
|
786
|
+
// Cleanup removed the temp dir — no sibling left behind.
|
|
787
|
+
const parent = join(workspaceDir, "..");
|
|
788
|
+
const base = workspaceDir.split("/").pop()!;
|
|
789
|
+
const siblings = readdirSync(parent);
|
|
790
|
+
const leftover = siblings.filter(
|
|
791
|
+
(name) =>
|
|
792
|
+
name.startsWith(`${base}.import-`) ||
|
|
793
|
+
name.startsWith(`${base}.pre-import-`),
|
|
794
|
+
);
|
|
795
|
+
expect(leftover).toEqual([]);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("bundle with only out-of-workspace resolved targets leaves real workspace untouched", async () => {
|
|
799
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
800
|
+
writeFileSync(
|
|
801
|
+
join(workspaceDir, "marker-please-preserve.txt"),
|
|
802
|
+
"survive\n",
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
// A resolver that resolves paths OUTSIDE the workspace dir — the
|
|
806
|
+
// streaming importer drains these through the verifier and records
|
|
807
|
+
// them as skipped, so no writes land in the temp workspace.
|
|
808
|
+
const outOfWorkspaceDir = realpathSync(
|
|
809
|
+
mkdtempSync(join(tmpdir(), "oow-target-")),
|
|
810
|
+
);
|
|
811
|
+
const externalResolver = {
|
|
812
|
+
resolve(archivePath: string): string | null {
|
|
813
|
+
if (archivePath.startsWith("credentials/")) return null;
|
|
814
|
+
return join(outOfWorkspaceDir, archivePath.replace(/\//g, "_"));
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const { archive } = buildVBundle({
|
|
819
|
+
files: [
|
|
820
|
+
{
|
|
821
|
+
path: "workspace/something.txt",
|
|
822
|
+
data: new TextEncoder().encode("ignored"),
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
const result = await streamCommitImport({
|
|
829
|
+
source: readableFrom(archive),
|
|
830
|
+
pathResolver: externalResolver,
|
|
831
|
+
workspaceDir,
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
expect(result.ok).toBe(true);
|
|
835
|
+
if (!result.ok) throw new Error("unreachable");
|
|
836
|
+
|
|
837
|
+
// Everything was skipped as "outside workspace".
|
|
838
|
+
expect(result.report.summary.files_skipped).toBeGreaterThanOrEqual(1);
|
|
839
|
+
expect(result.report.summary.files_created).toBe(0);
|
|
840
|
+
|
|
841
|
+
// Real workspace is still intact.
|
|
842
|
+
expect(
|
|
843
|
+
readFileSync(join(workspaceDir, "marker-please-preserve.txt"), "utf8"),
|
|
844
|
+
).toBe("survive\n");
|
|
845
|
+
} finally {
|
|
846
|
+
try {
|
|
847
|
+
rmSync(outOfWorkspaceDir, { recursive: true, force: true });
|
|
848
|
+
} catch {
|
|
849
|
+
// best-effort
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
describe("streamCommitImport — config sanitization parity", () => {
|
|
856
|
+
let workspaceDir: string;
|
|
857
|
+
beforeEach(() => {
|
|
858
|
+
workspaceDir = freshWorkspace();
|
|
859
|
+
});
|
|
860
|
+
afterEach(() => {
|
|
861
|
+
const parent = join(workspaceDir, "..");
|
|
862
|
+
try {
|
|
863
|
+
rmSync(parent, { recursive: true, force: true });
|
|
864
|
+
} catch {
|
|
865
|
+
// best-effort
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("workspace/config.json is sanitized before being written", async () => {
|
|
870
|
+
// sanitizeConfigForTransfer strips `daemon` entirely, clears
|
|
871
|
+
// `ingress.publicBaseUrl`, deletes `ingress.enabled`, and zeros
|
|
872
|
+
// `skills.load.extraDirs`. We plant all of these in the archived
|
|
873
|
+
// config and assert they're gone on disk.
|
|
874
|
+
const tainted = JSON.stringify({
|
|
875
|
+
daemon: { pid: 1234, host: "private.example.com" },
|
|
876
|
+
ingress: { publicBaseUrl: "https://leaky.example", enabled: true },
|
|
877
|
+
skills: { load: { extraDirs: ["/tmp/leak-a", "/tmp/leak-b"] } },
|
|
878
|
+
unrelated: "keep-me",
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
const { archive } = buildVBundle({
|
|
882
|
+
files: [
|
|
883
|
+
{
|
|
884
|
+
path: "workspace/config.json",
|
|
885
|
+
data: new TextEncoder().encode(tainted),
|
|
886
|
+
},
|
|
887
|
+
],
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const result = await streamCommitImport({
|
|
891
|
+
source: readableFrom(archive),
|
|
892
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
893
|
+
workspaceDir,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
expect(result.ok).toBe(true);
|
|
897
|
+
if (!result.ok) throw new Error("unreachable");
|
|
898
|
+
|
|
899
|
+
const writtenPath = join(workspaceDir, "config.json");
|
|
900
|
+
expect(existsSync(writtenPath)).toBe(true);
|
|
901
|
+
const writtenJson = JSON.parse(readFileSync(writtenPath, "utf8")) as Record<
|
|
902
|
+
string,
|
|
903
|
+
unknown
|
|
904
|
+
>;
|
|
905
|
+
|
|
906
|
+
// Environment-specific fields have been stripped or reset.
|
|
907
|
+
expect(writtenJson.daemon).toBeUndefined();
|
|
908
|
+
expect((writtenJson.ingress as Record<string, unknown>).publicBaseUrl).toBe(
|
|
909
|
+
"",
|
|
910
|
+
);
|
|
911
|
+
expect(
|
|
912
|
+
(writtenJson.ingress as Record<string, unknown>).enabled,
|
|
913
|
+
).toBeUndefined();
|
|
914
|
+
expect(
|
|
915
|
+
(
|
|
916
|
+
(writtenJson.skills as Record<string, unknown>).load as Record<
|
|
917
|
+
string,
|
|
918
|
+
unknown
|
|
919
|
+
>
|
|
920
|
+
).extraDirs,
|
|
921
|
+
).toEqual([]);
|
|
922
|
+
|
|
923
|
+
// Unrelated content is preserved verbatim.
|
|
924
|
+
expect(writtenJson.unrelated).toBe("keep-me");
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
describe("streamCommitImport — legacy USER.md skip on customized persona", () => {
|
|
929
|
+
let workspaceDir: string;
|
|
930
|
+
beforeEach(() => {
|
|
931
|
+
workspaceDir = freshWorkspace();
|
|
932
|
+
});
|
|
933
|
+
afterEach(() => {
|
|
934
|
+
const parent = join(workspaceDir, "..");
|
|
935
|
+
try {
|
|
936
|
+
rmSync(parent, { recursive: true, force: true });
|
|
937
|
+
} catch {
|
|
938
|
+
// best-effort
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("skips writing prompts/USER.md when guardian persona is customized", async () => {
|
|
943
|
+
// Seed the live workspace with a customized guardian persona file.
|
|
944
|
+
// `isGuardianPersonaCustomized` inspects its content vs the bare
|
|
945
|
+
// scaffold template; customized content must prevent the write.
|
|
946
|
+
//
|
|
947
|
+
// NOTE: the streaming importer uses an atomic temp-dir swap. The swap
|
|
948
|
+
// REPLACES the entire live workspace with the temp workspace — so the
|
|
949
|
+
// customized file at users/captain.md is expected to be gone after
|
|
950
|
+
// import regardless (its contents are not re-materialized into the
|
|
951
|
+
// temp tree). The behavior under test here is narrower: we verify
|
|
952
|
+
// the legacy bundle's USER.md content was NEVER written into the
|
|
953
|
+
// temp workspace's guardian path, and that the entry is reported as
|
|
954
|
+
// `"skipped"` with a warning. This matches commitImport's semantics
|
|
955
|
+
// for the legacy entry itself.
|
|
956
|
+
const guardianPath = join(workspaceDir, "users", "captain.md");
|
|
957
|
+
mkdirSync(join(workspaceDir, "users"), { recursive: true });
|
|
958
|
+
writeFileSync(guardianPath, CUSTOMIZED_PERSONA_FIXTURE, "utf8");
|
|
959
|
+
|
|
960
|
+
const legacyContent = new TextEncoder().encode(
|
|
961
|
+
"# Legacy bundle persona — should NOT be written over a customized file\n",
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
const resolver = new DefaultPathResolver(
|
|
965
|
+
workspaceDir,
|
|
966
|
+
undefined,
|
|
967
|
+
() => guardianPath,
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
const { archive } = buildVBundle({
|
|
971
|
+
files: [
|
|
972
|
+
{
|
|
973
|
+
path: "prompts/USER.md",
|
|
974
|
+
data: legacyContent,
|
|
975
|
+
},
|
|
976
|
+
// Second entry ensures there's at least one workspace-targeted
|
|
977
|
+
// write, so the atomic swap runs and we're exercising the skip
|
|
978
|
+
// branch on the full flow rather than the no-swap short circuit.
|
|
979
|
+
{
|
|
980
|
+
path: "workspace/other.txt",
|
|
981
|
+
data: new TextEncoder().encode("other content"),
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const result = await streamCommitImport({
|
|
987
|
+
source: readableFrom(archive),
|
|
988
|
+
pathResolver: resolver,
|
|
989
|
+
workspaceDir,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
expect(result.ok).toBe(true);
|
|
993
|
+
if (!result.ok) throw new Error("unreachable");
|
|
994
|
+
|
|
995
|
+
// The other file in the bundle was written normally (proves the swap
|
|
996
|
+
// happened — we're not accidentally hitting the no-swap short circuit).
|
|
997
|
+
expect(readFileSync(join(workspaceDir, "other.txt"), "utf8")).toBe(
|
|
998
|
+
"other content",
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
// Legacy content was NEVER written to the guardian target path. Because
|
|
1002
|
+
// the USER.md entry was skipped (not written into the temp tree) and
|
|
1003
|
+
// the swap replaces the entire workspace with the temp tree, the
|
|
1004
|
+
// guardian path should simply not exist after import.
|
|
1005
|
+
if (existsSync(guardianPath)) {
|
|
1006
|
+
// If something did write there, it must not be the bundle's legacy
|
|
1007
|
+
// content — that's the crucial regression to prevent.
|
|
1008
|
+
expect(readFileSync(guardianPath, "utf8")).not.toBe(
|
|
1009
|
+
new TextDecoder().decode(legacyContent),
|
|
1010
|
+
);
|
|
1011
|
+
} else {
|
|
1012
|
+
expect(existsSync(guardianPath)).toBe(false);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// The legacy entry is present in the report as "skipped".
|
|
1016
|
+
const legacyEntry = result.report.files.find(
|
|
1017
|
+
(f) => f.path === "prompts/USER.md",
|
|
1018
|
+
);
|
|
1019
|
+
expect(legacyEntry).toBeDefined();
|
|
1020
|
+
expect(legacyEntry!.action).toBe("skipped");
|
|
1021
|
+
|
|
1022
|
+
// A warning surfaces the skip reason.
|
|
1023
|
+
expect(
|
|
1024
|
+
result.report.warnings.some((w) => w.includes("prompts/USER.md")),
|
|
1025
|
+
).toBe(true);
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// ---------------------------------------------------------------------------
|
|
1030
|
+
// Carry-over parity: live workspace paths that the buffer-based importer
|
|
1031
|
+
// preserves (data/db, data/qdrant, embedding-models, deprecated) must also
|
|
1032
|
+
// survive the streaming importer's temp-dir atomic swap when the bundle
|
|
1033
|
+
// does not carry them. Without this behavior, a partial bundle (e.g. one
|
|
1034
|
+
// that only ships prompts + config) would wipe the user's SQLite DB and
|
|
1035
|
+
// Qdrant vector store when imported through the streaming path.
|
|
1036
|
+
// ---------------------------------------------------------------------------
|
|
1037
|
+
|
|
1038
|
+
describe("streamCommitImport — preserves live workspace paths when bundle omits them", () => {
|
|
1039
|
+
let workspaceDir: string;
|
|
1040
|
+
beforeEach(() => {
|
|
1041
|
+
workspaceDir = freshWorkspace();
|
|
1042
|
+
});
|
|
1043
|
+
afterEach(() => {
|
|
1044
|
+
const parent = join(workspaceDir, "..");
|
|
1045
|
+
try {
|
|
1046
|
+
rmSync(parent, { recursive: true, force: true });
|
|
1047
|
+
} catch {
|
|
1048
|
+
// best-effort
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("keeps the live data/db/assistant.db when the bundle omits data/db/*", async () => {
|
|
1053
|
+
// Seed the live workspace with a fake SQLite DB whose contents we
|
|
1054
|
+
// can identify post-import.
|
|
1055
|
+
mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
|
|
1056
|
+
const dbContent = Buffer.from("SQLite-format-3\0live-db-payload");
|
|
1057
|
+
writeFileSync(join(workspaceDir, "data", "db", "assistant.db"), dbContent);
|
|
1058
|
+
|
|
1059
|
+
// A bundle that writes a config file but carries nothing under
|
|
1060
|
+
// workspace/data/db/.
|
|
1061
|
+
const { archive } = buildVBundle({
|
|
1062
|
+
files: [
|
|
1063
|
+
{
|
|
1064
|
+
path: "workspace/skills/example.md",
|
|
1065
|
+
data: new TextEncoder().encode("# skill\n"),
|
|
1066
|
+
},
|
|
1067
|
+
],
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const result = await streamCommitImport({
|
|
1071
|
+
source: readableFrom(archive),
|
|
1072
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1073
|
+
workspaceDir,
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
expect(result.ok).toBe(true);
|
|
1077
|
+
if (!result.ok) throw new Error("unreachable");
|
|
1078
|
+
|
|
1079
|
+
// Live DB survived the atomic swap with its exact original bytes.
|
|
1080
|
+
const postDbPath = join(workspaceDir, "data", "db", "assistant.db");
|
|
1081
|
+
expect(existsSync(postDbPath)).toBe(true);
|
|
1082
|
+
expect(readFileSync(postDbPath)).toEqual(dbContent);
|
|
1083
|
+
|
|
1084
|
+
// The bundle-provided file also landed.
|
|
1085
|
+
expect(
|
|
1086
|
+
readFileSync(join(workspaceDir, "skills", "example.md"), "utf8"),
|
|
1087
|
+
).toBe("# skill\n");
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("keeps the live data/qdrant/ directory when the bundle omits qdrant entries", async () => {
|
|
1091
|
+
// Populate a fake qdrant store with a nested file.
|
|
1092
|
+
mkdirSync(join(workspaceDir, "data", "qdrant", "segments"), {
|
|
1093
|
+
recursive: true,
|
|
1094
|
+
});
|
|
1095
|
+
const segmentBytes = Buffer.from("qdrant-segment-bytes");
|
|
1096
|
+
writeFileSync(
|
|
1097
|
+
join(workspaceDir, "data", "qdrant", "segments", "0.seg"),
|
|
1098
|
+
segmentBytes,
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
const { archive } = buildVBundle({
|
|
1102
|
+
files: [
|
|
1103
|
+
{
|
|
1104
|
+
path: "workspace/config.json",
|
|
1105
|
+
data: new TextEncoder().encode("{}"),
|
|
1106
|
+
},
|
|
1107
|
+
],
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const result = await streamCommitImport({
|
|
1111
|
+
source: readableFrom(archive),
|
|
1112
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1113
|
+
workspaceDir,
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
expect(result.ok).toBe(true);
|
|
1117
|
+
if (!result.ok) throw new Error("unreachable");
|
|
1118
|
+
|
|
1119
|
+
const postSegPath = join(
|
|
1120
|
+
workspaceDir,
|
|
1121
|
+
"data",
|
|
1122
|
+
"qdrant",
|
|
1123
|
+
"segments",
|
|
1124
|
+
"0.seg",
|
|
1125
|
+
);
|
|
1126
|
+
expect(existsSync(postSegPath)).toBe(true);
|
|
1127
|
+
expect(readFileSync(postSegPath)).toEqual(segmentBytes);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test("lets the bundle overwrite data/db when it does carry an assistant.db entry", async () => {
|
|
1131
|
+
// Seed the live workspace with OLD content so we can tell whether the
|
|
1132
|
+
// carry-over logic accidentally kept it instead of honoring the
|
|
1133
|
+
// bundle's new DB file.
|
|
1134
|
+
mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
|
|
1135
|
+
writeFileSync(
|
|
1136
|
+
join(workspaceDir, "data", "db", "assistant.db"),
|
|
1137
|
+
"OLD-LIVE-DB",
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
const newDbBytes = new TextEncoder().encode("NEW-BUNDLE-DB");
|
|
1141
|
+
const { archive } = buildVBundle({
|
|
1142
|
+
files: [
|
|
1143
|
+
{
|
|
1144
|
+
path: "workspace/data/db/assistant.db",
|
|
1145
|
+
data: newDbBytes,
|
|
1146
|
+
},
|
|
1147
|
+
],
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
const result = await streamCommitImport({
|
|
1151
|
+
source: readableFrom(archive),
|
|
1152
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1153
|
+
workspaceDir,
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
expect(result.ok).toBe(true);
|
|
1157
|
+
if (!result.ok) throw new Error("unreachable");
|
|
1158
|
+
|
|
1159
|
+
const postDbPath = join(workspaceDir, "data", "db", "assistant.db");
|
|
1160
|
+
expect(readFileSync(postDbPath)).toEqual(Buffer.from(newDbBytes));
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
// ---------------------------------------------------------------------------
|
|
1165
|
+
// Gap A — legacy-only bundle (no `workspace/*` entries) writes each file in
|
|
1166
|
+
// place WITHOUT triggering the atomic workspace swap. This matches
|
|
1167
|
+
// commitImport's legacy branch: a `data/db/assistant.db`- or
|
|
1168
|
+
// `config/settings.json`- style bundle preserves arbitrary live files
|
|
1169
|
+
// outside WORKSPACE_PRESERVE_PATHS (where the swap path would have wiped
|
|
1170
|
+
// them). Regression test for the self-review Gap A finding.
|
|
1171
|
+
// ---------------------------------------------------------------------------
|
|
1172
|
+
|
|
1173
|
+
describe("streamCommitImport — legacy-only bundle writes in place", () => {
|
|
1174
|
+
let workspaceDir: string;
|
|
1175
|
+
beforeEach(() => {
|
|
1176
|
+
workspaceDir = freshWorkspace();
|
|
1177
|
+
});
|
|
1178
|
+
afterEach(() => {
|
|
1179
|
+
const parent = join(workspaceDir, "..");
|
|
1180
|
+
try {
|
|
1181
|
+
rmSync(parent, { recursive: true, force: true });
|
|
1182
|
+
} catch {
|
|
1183
|
+
// best-effort
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("bundle carrying only `data/db/assistant.db` writes the DB in place and leaves unrelated live files untouched", async () => {
|
|
1188
|
+
// Seed the live workspace with BOTH a pre-existing `data/db/assistant.db`
|
|
1189
|
+
// (which the bundle will overwrite) AND an unrelated marker file at
|
|
1190
|
+
// top-level that is not in WORKSPACE_PRESERVE_PATHS — proving the old
|
|
1191
|
+
// atomic-swap path would have wiped it. Under the legacy-only branch
|
|
1192
|
+
// that file must survive.
|
|
1193
|
+
mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
|
|
1194
|
+
writeFileSync(
|
|
1195
|
+
join(workspaceDir, "data", "db", "assistant.db"),
|
|
1196
|
+
"OLD-LIVE-DB",
|
|
1197
|
+
);
|
|
1198
|
+
writeFileSync(
|
|
1199
|
+
join(workspaceDir, "unrelated-top-level.txt"),
|
|
1200
|
+
"must-survive-legacy-import\n",
|
|
1201
|
+
);
|
|
1202
|
+
// A sibling directory — also outside preserve paths — that the atomic
|
|
1203
|
+
// swap would have wiped.
|
|
1204
|
+
mkdirSync(join(workspaceDir, "custom-user-dir"), { recursive: true });
|
|
1205
|
+
writeFileSync(
|
|
1206
|
+
join(workspaceDir, "custom-user-dir", "note.md"),
|
|
1207
|
+
"custom note\n",
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
const newDbBytes = new TextEncoder().encode("NEW-LEGACY-BUNDLE-DB");
|
|
1211
|
+
const { archive } = buildVBundle({
|
|
1212
|
+
files: [
|
|
1213
|
+
{
|
|
1214
|
+
// Legacy archive path — NO `workspace/` prefix. This is what
|
|
1215
|
+
// older bundle exports produce.
|
|
1216
|
+
path: "data/db/assistant.db",
|
|
1217
|
+
data: newDbBytes,
|
|
1218
|
+
},
|
|
1219
|
+
],
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
const result = await streamCommitImport({
|
|
1223
|
+
source: readableFrom(archive),
|
|
1224
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1225
|
+
workspaceDir,
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
expect(result.ok).toBe(true);
|
|
1229
|
+
if (!result.ok) throw new Error("unreachable");
|
|
1230
|
+
|
|
1231
|
+
// Bundle's DB landed at the right live location with the new bytes.
|
|
1232
|
+
const postDbPath = join(workspaceDir, "data", "db", "assistant.db");
|
|
1233
|
+
expect(readFileSync(postDbPath)).toEqual(Buffer.from(newDbBytes));
|
|
1234
|
+
|
|
1235
|
+
// Unrelated live files still present — the atomic-swap path would
|
|
1236
|
+
// have wiped them. Their survival here proves we took the in-place
|
|
1237
|
+
// legacy branch.
|
|
1238
|
+
expect(
|
|
1239
|
+
readFileSync(join(workspaceDir, "unrelated-top-level.txt"), "utf8"),
|
|
1240
|
+
).toBe("must-survive-legacy-import\n");
|
|
1241
|
+
expect(
|
|
1242
|
+
readFileSync(join(workspaceDir, "custom-user-dir", "note.md"), "utf8"),
|
|
1243
|
+
).toBe("custom note\n");
|
|
1244
|
+
|
|
1245
|
+
// The legacy entry is reported as "overwritten" (pre-existing DB),
|
|
1246
|
+
// with a backup path captured alongside.
|
|
1247
|
+
const dbEntry = result.report.files.find(
|
|
1248
|
+
(f) => f.path === "data/db/assistant.db",
|
|
1249
|
+
);
|
|
1250
|
+
expect(dbEntry).toBeDefined();
|
|
1251
|
+
expect(dbEntry!.action).toBe("overwritten");
|
|
1252
|
+
expect(dbEntry!.backup_path).not.toBeNull();
|
|
1253
|
+
// And the report summary reflects the same.
|
|
1254
|
+
expect(result.report.summary.files_overwritten).toBe(1);
|
|
1255
|
+
expect(result.report.summary.backups_created).toBe(1);
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// ---------------------------------------------------------------------------
|
|
1260
|
+
// Gap B — carry-over does per-file merge inside preserved directories.
|
|
1261
|
+
// A bundle that writes ONE file under `workspace/data/qdrant/` must not
|
|
1262
|
+
// cause the rest of the live `data/qdrant/` tree to be wiped by the
|
|
1263
|
+
// atomic swap. Regression test for the self-review Gap B finding.
|
|
1264
|
+
// ---------------------------------------------------------------------------
|
|
1265
|
+
|
|
1266
|
+
describe("streamCommitImport — preserved-path carry-over is per-file", () => {
|
|
1267
|
+
let workspaceDir: string;
|
|
1268
|
+
beforeEach(() => {
|
|
1269
|
+
workspaceDir = freshWorkspace();
|
|
1270
|
+
});
|
|
1271
|
+
afterEach(() => {
|
|
1272
|
+
const parent = join(workspaceDir, "..");
|
|
1273
|
+
try {
|
|
1274
|
+
rmSync(parent, { recursive: true, force: true });
|
|
1275
|
+
} catch {
|
|
1276
|
+
// best-effort
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
test("bundle touching one file in data/qdrant/ does not wipe other live files in the same preserved dir", async () => {
|
|
1281
|
+
// Populate the live qdrant store with several nested files / dirs.
|
|
1282
|
+
// Only one of these paths overlaps with what the bundle will write;
|
|
1283
|
+
// the rest must survive the atomic swap via per-file carry-over.
|
|
1284
|
+
mkdirSync(join(workspaceDir, "data", "qdrant", "segments"), {
|
|
1285
|
+
recursive: true,
|
|
1286
|
+
});
|
|
1287
|
+
writeFileSync(
|
|
1288
|
+
join(workspaceDir, "data", "qdrant", "segments", "0.seg"),
|
|
1289
|
+
"seg-0-live",
|
|
1290
|
+
);
|
|
1291
|
+
writeFileSync(
|
|
1292
|
+
join(workspaceDir, "data", "qdrant", "segments", "1.seg"),
|
|
1293
|
+
"seg-1-live",
|
|
1294
|
+
);
|
|
1295
|
+
// A top-level file inside the preserved dir that the bundle will
|
|
1296
|
+
// overwrite.
|
|
1297
|
+
writeFileSync(
|
|
1298
|
+
join(workspaceDir, "data", "qdrant", "meta.json"),
|
|
1299
|
+
'{"stale":true}',
|
|
1300
|
+
);
|
|
1301
|
+
// A sibling directory the bundle never touches — must survive.
|
|
1302
|
+
mkdirSync(join(workspaceDir, "data", "qdrant", "wal"), {
|
|
1303
|
+
recursive: true,
|
|
1304
|
+
});
|
|
1305
|
+
writeFileSync(
|
|
1306
|
+
join(workspaceDir, "data", "qdrant", "wal", "wal-000"),
|
|
1307
|
+
"wal-entry",
|
|
1308
|
+
);
|
|
1309
|
+
|
|
1310
|
+
// Bundle carries exactly ONE file under `data/qdrant/` AND at least
|
|
1311
|
+
// one other `workspace/*` entry so the atomic-swap path fires (not
|
|
1312
|
+
// the legacy in-place path).
|
|
1313
|
+
const newMeta = new TextEncoder().encode('{"fresh":true}');
|
|
1314
|
+
const { archive } = buildVBundle({
|
|
1315
|
+
files: [
|
|
1316
|
+
{
|
|
1317
|
+
path: "workspace/data/qdrant/meta.json",
|
|
1318
|
+
data: newMeta,
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
path: "workspace/marker.txt",
|
|
1322
|
+
data: new TextEncoder().encode("marker\n"),
|
|
1323
|
+
},
|
|
1324
|
+
],
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
const result = await streamCommitImport({
|
|
1328
|
+
source: readableFrom(archive),
|
|
1329
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1330
|
+
workspaceDir,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
expect(result.ok).toBe(true);
|
|
1334
|
+
if (!result.ok) throw new Error("unreachable");
|
|
1335
|
+
|
|
1336
|
+
// Bundle's overwrite landed.
|
|
1337
|
+
expect(
|
|
1338
|
+
readFileSync(join(workspaceDir, "data", "qdrant", "meta.json")),
|
|
1339
|
+
).toEqual(Buffer.from(newMeta));
|
|
1340
|
+
|
|
1341
|
+
// All the other live files in the preserved dir survived.
|
|
1342
|
+
expect(
|
|
1343
|
+
readFileSync(
|
|
1344
|
+
join(workspaceDir, "data", "qdrant", "segments", "0.seg"),
|
|
1345
|
+
"utf8",
|
|
1346
|
+
),
|
|
1347
|
+
).toBe("seg-0-live");
|
|
1348
|
+
expect(
|
|
1349
|
+
readFileSync(
|
|
1350
|
+
join(workspaceDir, "data", "qdrant", "segments", "1.seg"),
|
|
1351
|
+
"utf8",
|
|
1352
|
+
),
|
|
1353
|
+
).toBe("seg-1-live");
|
|
1354
|
+
expect(
|
|
1355
|
+
readFileSync(
|
|
1356
|
+
join(workspaceDir, "data", "qdrant", "wal", "wal-000"),
|
|
1357
|
+
"utf8",
|
|
1358
|
+
),
|
|
1359
|
+
).toBe("wal-entry");
|
|
1360
|
+
|
|
1361
|
+
// And the workspace-targeted file outside the preserved dir landed.
|
|
1362
|
+
expect(readFileSync(join(workspaceDir, "marker.txt"), "utf8")).toBe(
|
|
1363
|
+
"marker\n",
|
|
1364
|
+
);
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
// ---------------------------------------------------------------------------
|
|
1369
|
+
// Gap C — resource ceilings. Bundles declaring too many entries or too
|
|
1370
|
+
// many bytes of data should abort with validation_failed before we commit
|
|
1371
|
+
// anything to disk. Uses the test-only `maxBundleEntries` /
|
|
1372
|
+
// `maxBundleBytes` knobs so we can exercise the abort with tiny
|
|
1373
|
+
// fixtures. Regression test for the self-review Gap C finding.
|
|
1374
|
+
// ---------------------------------------------------------------------------
|
|
1375
|
+
|
|
1376
|
+
describe("streamCommitImport — bundle resource ceilings", () => {
|
|
1377
|
+
let workspaceDir: string;
|
|
1378
|
+
beforeEach(() => {
|
|
1379
|
+
workspaceDir = freshWorkspace();
|
|
1380
|
+
});
|
|
1381
|
+
afterEach(() => {
|
|
1382
|
+
const parent = join(workspaceDir, "..");
|
|
1383
|
+
try {
|
|
1384
|
+
rmSync(parent, { recursive: true, force: true });
|
|
1385
|
+
} catch {
|
|
1386
|
+
// best-effort
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
/** Assert no sibling temp/backup dirs remain. */
|
|
1391
|
+
function assertNoLeftoverTempDirs(): void {
|
|
1392
|
+
const parent = join(workspaceDir, "..");
|
|
1393
|
+
const base = workspaceDir.split("/").pop()!;
|
|
1394
|
+
const siblings = readdirSync(parent);
|
|
1395
|
+
const leftover = siblings.filter(
|
|
1396
|
+
(name) =>
|
|
1397
|
+
name.startsWith(`${base}.import-`) ||
|
|
1398
|
+
name.startsWith(`${base}.pre-import-`),
|
|
1399
|
+
);
|
|
1400
|
+
expect(leftover).toEqual([]);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
test("bundle declaring more entries than maxBundleEntries → validation_failed with bundle_too_many_entries", async () => {
|
|
1404
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
1405
|
+
writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
|
|
1406
|
+
|
|
1407
|
+
// Three small files — we'll set the cap to 2 so the manifest's
|
|
1408
|
+
// declared-count check trips before any tar entry is processed.
|
|
1409
|
+
const { archive } = buildVBundle({
|
|
1410
|
+
files: [
|
|
1411
|
+
{ path: "workspace/a.txt", data: new TextEncoder().encode("a") },
|
|
1412
|
+
{ path: "workspace/b.txt", data: new TextEncoder().encode("b") },
|
|
1413
|
+
{ path: "workspace/c.txt", data: new TextEncoder().encode("c") },
|
|
1414
|
+
],
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
const result = await streamCommitImport({
|
|
1418
|
+
source: readableFrom(archive),
|
|
1419
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1420
|
+
workspaceDir,
|
|
1421
|
+
maxBundleEntries: 2,
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
expect(result.ok).toBe(false);
|
|
1425
|
+
if (result.ok) throw new Error("unreachable");
|
|
1426
|
+
expect(result.reason).toBe("validation_failed");
|
|
1427
|
+
if (result.reason !== "validation_failed") throw new Error("unreachable");
|
|
1428
|
+
expect(result.errors[0]!.code).toBe("bundle_too_many_entries");
|
|
1429
|
+
|
|
1430
|
+
// Real workspace untouched, temp dir cleaned up.
|
|
1431
|
+
expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
|
|
1432
|
+
"keep me\n",
|
|
1433
|
+
);
|
|
1434
|
+
assertNoLeftoverTempDirs();
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
test("bundle exceeding maxBundleBytes → validation_failed with bundle_too_large", async () => {
|
|
1438
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
1439
|
+
writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
|
|
1440
|
+
|
|
1441
|
+
// Two files totaling 200 bytes of user data. Cap at 128 bytes so the
|
|
1442
|
+
// second file's streamed size pushes totalBytesStreamed over.
|
|
1443
|
+
const big = new Uint8Array(100).fill(0x41);
|
|
1444
|
+
const { archive } = buildVBundle({
|
|
1445
|
+
files: [
|
|
1446
|
+
{ path: "workspace/big1.bin", data: big },
|
|
1447
|
+
{ path: "workspace/big2.bin", data: big },
|
|
1448
|
+
],
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
const result = await streamCommitImport({
|
|
1452
|
+
source: readableFrom(archive),
|
|
1453
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1454
|
+
workspaceDir,
|
|
1455
|
+
maxBundleBytes: 128,
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
expect(result.ok).toBe(false);
|
|
1459
|
+
if (result.ok) throw new Error("unreachable");
|
|
1460
|
+
expect(result.reason).toBe("validation_failed");
|
|
1461
|
+
if (result.reason !== "validation_failed") throw new Error("unreachable");
|
|
1462
|
+
expect(result.errors[0]!.code).toBe("bundle_too_large");
|
|
1463
|
+
|
|
1464
|
+
expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
|
|
1465
|
+
"keep me\n",
|
|
1466
|
+
);
|
|
1467
|
+
assertNoLeftoverTempDirs();
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
// ---------------------------------------------------------------------------
|
|
1472
|
+
// Gap D — sanitized config reports sha256 of the BYTES ACTUALLY WRITTEN,
|
|
1473
|
+
// not the manifest-declared sha of the raw archive content. commitImport
|
|
1474
|
+
// already does this; the streaming importer regressed to reporting the
|
|
1475
|
+
// manifest sha, so downstream integrity re-checks failed. Regression test
|
|
1476
|
+
// for the self-review Gap D finding.
|
|
1477
|
+
// ---------------------------------------------------------------------------
|
|
1478
|
+
|
|
1479
|
+
describe("streamCommitImport — report.sha256 reflects post-sanitization bytes", () => {
|
|
1480
|
+
let workspaceDir: string;
|
|
1481
|
+
beforeEach(() => {
|
|
1482
|
+
workspaceDir = freshWorkspace();
|
|
1483
|
+
});
|
|
1484
|
+
afterEach(() => {
|
|
1485
|
+
const parent = join(workspaceDir, "..");
|
|
1486
|
+
try {
|
|
1487
|
+
rmSync(parent, { recursive: true, force: true });
|
|
1488
|
+
} catch {
|
|
1489
|
+
// best-effort
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
test("workspace/config.json report.sha256 equals sha256 of the on-disk sanitized bytes, not the raw archive sha", async () => {
|
|
1494
|
+
// Plant content that sanitizeConfigForTransfer will demonstrably
|
|
1495
|
+
// change (`daemon` is deleted, `ingress.publicBaseUrl` cleared, etc).
|
|
1496
|
+
// The raw archive bytes and the post-sanitization bytes therefore
|
|
1497
|
+
// differ — a correctly implemented importer reports the latter's sha.
|
|
1498
|
+
const tainted = JSON.stringify({
|
|
1499
|
+
daemon: { pid: 9999, host: "private.example.com" },
|
|
1500
|
+
ingress: { publicBaseUrl: "https://leak.example", enabled: true },
|
|
1501
|
+
unrelated: "keep-me",
|
|
1502
|
+
});
|
|
1503
|
+
const rawArchiveBytes = new TextEncoder().encode(tainted);
|
|
1504
|
+
const rawArchiveSha = sha256Hex(rawArchiveBytes);
|
|
1505
|
+
|
|
1506
|
+
const { archive } = buildVBundle({
|
|
1507
|
+
files: [
|
|
1508
|
+
{
|
|
1509
|
+
path: "workspace/config.json",
|
|
1510
|
+
data: rawArchiveBytes,
|
|
1511
|
+
},
|
|
1512
|
+
],
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
const result = await streamCommitImport({
|
|
1516
|
+
source: readableFrom(archive),
|
|
1517
|
+
pathResolver: new DefaultPathResolver(workspaceDir),
|
|
1518
|
+
workspaceDir,
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
expect(result.ok).toBe(true);
|
|
1522
|
+
if (!result.ok) throw new Error("unreachable");
|
|
1523
|
+
|
|
1524
|
+
const writtenPath = join(workspaceDir, "config.json");
|
|
1525
|
+
const onDiskBytes = readFileSync(writtenPath);
|
|
1526
|
+
const onDiskSha = sha256Hex(onDiskBytes);
|
|
1527
|
+
|
|
1528
|
+
// Sanity: sanitization actually changed the bytes (otherwise the test
|
|
1529
|
+
// would be tautologically true).
|
|
1530
|
+
expect(onDiskSha).not.toBe(rawArchiveSha);
|
|
1531
|
+
|
|
1532
|
+
const configEntry = result.report.files.find(
|
|
1533
|
+
(f) => f.path === "workspace/config.json",
|
|
1534
|
+
);
|
|
1535
|
+
expect(configEntry).toBeDefined();
|
|
1536
|
+
expect(configEntry!.sha256).toBe(onDiskSha);
|
|
1537
|
+
// Reported size is also the post-sanitization size, not raw.
|
|
1538
|
+
expect(configEntry!.size).toBe(onDiskBytes.length);
|
|
1539
|
+
});
|
|
1540
|
+
});
|