@vellumai/assistant 0.7.1 → 0.7.2
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/ARCHITECTURE.md +32 -49
- package/Dockerfile +1 -0
- package/README.md +1 -2
- package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
- package/bun.lock +26 -26
- package/docs/architecture/security.md +20 -0
- package/docs/plugins.md +7 -9
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
- package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
- package/node_modules/@vellumai/twilio-client/package.json +18 -0
- package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
- package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
- package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
- package/openapi.yaml +565 -12
- package/package.json +6 -3
- package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
- package/src/__tests__/app-bundler.test.ts +170 -1
- package/src/__tests__/app-control-flow.test.ts +374 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
- package/src/__tests__/app-executors.test.ts +30 -43
- package/src/__tests__/approval-routes-http.test.ts +23 -6
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
- package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
- package/src/__tests__/assistant-event-hub.test.ts +109 -2
- package/src/__tests__/assistant-event.test.ts +10 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
- package/src/__tests__/background-shell-host-bash.test.ts +14 -15
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
- package/src/__tests__/btw-routes.test.ts +13 -4
- package/src/__tests__/call-controller.test.ts +49 -1
- package/src/__tests__/call-domain.test.ts +0 -2
- package/src/__tests__/call-routes-http.test.ts +0 -2
- package/src/__tests__/channel-readiness-service.test.ts +59 -1
- package/src/__tests__/checker.test.ts +3 -4
- package/src/__tests__/config-loader-backfill.test.ts +90 -155
- package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-set-platform-guard.test.ts +48 -4
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-lifecycle.test.ts +36 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-slash-commands.test.ts +0 -4
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
- package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
- package/src/__tests__/credentials-cli.test.ts +5 -12
- package/src/__tests__/cu-unified-flow.test.ts +185 -23
- package/src/__tests__/daemon-credential-client.test.ts +101 -19
- package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +718 -1
- package/src/__tests__/helpers/call-route-handler.ts +7 -1
- package/src/__tests__/host-app-control-proxy.test.ts +602 -0
- package/src/__tests__/host-app-control-routes.test.ts +263 -0
- package/src/__tests__/host-bash-proxy.test.ts +246 -47
- package/src/__tests__/host-bash-routes.test.ts +294 -0
- package/src/__tests__/host-browser-proxy.test.ts +24 -22
- package/src/__tests__/host-browser-routes.test.ts +39 -13
- package/src/__tests__/host-cu-proxy.test.ts +41 -52
- package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
- package/src/__tests__/host-file-edit-tool.test.ts +47 -1
- package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
- package/src/__tests__/host-file-proxy.test.ts +37 -43
- package/src/__tests__/host-file-read-tool.test.ts +17 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
- package/src/__tests__/host-file-write-tool.test.ts +42 -1
- package/src/__tests__/host-proxy-base.test.ts +312 -0
- package/src/__tests__/host-shell-tool.test.ts +22 -4
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
- package/src/__tests__/host-transfer-proxy.test.ts +121 -22
- package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +29 -0
- package/src/__tests__/identity-routes.test.ts +103 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
- package/src/__tests__/inline-command-runner.test.ts +0 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
- package/src/__tests__/integration-status.test.ts +85 -5
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/mcp-auth-routes.test.ts +197 -0
- package/src/__tests__/mcp-cli.test.ts +338 -2
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
- package/src/__tests__/migration-import-commit-http.test.ts +108 -2
- package/src/__tests__/mock-gateway-ipc.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +0 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
- package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +97 -0
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/retry-backoff.test.ts +87 -0
- package/src/__tests__/runtime-events-sse.test.ts +10 -6
- package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
- package/src/__tests__/schedule-retry.test.ts +715 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +43 -41
- package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
- package/src/__tests__/skill-load-inline-command.test.ts +0 -51
- package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +9 -14
- package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-config.test.ts +0 -1
- package/src/__tests__/test-preload.ts +8 -0
- package/src/__tests__/tool-approval-handler.test.ts +3 -4
- package/src/__tests__/tool-audit-listener.test.ts +48 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +3 -5
- package/src/__tests__/twilio-validation.test.ts +93 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
- package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
- package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
- package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
- package/src/backup/__tests__/paths.test.ts +0 -22
- package/src/backup/__tests__/restore.test.ts +51 -151
- package/src/backup/paths.ts +2 -18
- package/src/backup/restore.ts +107 -231
- package/src/bundler/app-bundler.ts +51 -3
- package/src/calls/relay-server.ts +4 -44
- package/src/calls/twilio-config.ts +2 -17
- package/src/calls/twilio-rest.ts +33 -105
- package/src/calls/twilio-routes.ts +11 -12
- package/src/channels/types.ts +8 -7
- package/src/cli/commands/__tests__/backup.test.ts +6 -277
- package/src/cli/commands/__tests__/gateway.test.ts +288 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
- package/src/cli/commands/backup.ts +6 -331
- package/src/cli/commands/clients.ts +36 -37
- package/src/cli/commands/contacts.ts +73 -0
- package/src/cli/commands/conversations.ts +2 -5
- package/src/cli/commands/credentials.ts +15 -7
- package/src/cli/commands/domain.ts +66 -15
- package/src/cli/commands/gateway.ts +183 -0
- package/src/cli/commands/keys.ts +9 -6
- package/src/cli/commands/mcp.ts +116 -156
- package/src/cli/commands/memory-v2.ts +296 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
- package/src/cli/commands/platform/disconnect.ts +5 -4
- package/src/cli/commands/platform/index.ts +0 -18
- package/src/cli/lib/daemon-credential-client.ts +110 -28
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +67 -10
- package/src/config/bundled-skills/acp/SKILL.md +6 -0
- package/src/config/bundled-skills/acp/TOOLS.json +1 -22
- package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
- package/src/config/bundled-skills/app-control/SKILL.md +75 -0
- package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
- package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
- package/src/config/bundled-skills/document/TOOLS.json +0 -8
- package/src/config/bundled-skills/followups/TOOLS.json +0 -12
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
- package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
- package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
- package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
- package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
- package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
- package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
- package/src/config/bundled-skills/settings/SKILL.md +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +0 -12
- package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
- package/src/config/bundled-skills/subagent/SKILL.md +6 -2
- package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
- package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
- package/src/config/bundled-tool-registry.ts +21 -0
- package/src/config/env-registry.ts +0 -2
- package/src/config/env.ts +19 -12
- package/src/config/feature-flag-registry.json +21 -133
- package/src/config/loader.ts +73 -99
- package/src/config/sanitize-for-transfer.ts +2 -0
- package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
- package/src/config/schemas/calls.ts +0 -9
- package/src/config/schemas/heartbeat.ts +63 -0
- package/src/config/schemas/ingress.ts +10 -6
- package/src/config/schemas/llm.ts +5 -10
- package/src/config/schemas/memory-lifecycle.ts +77 -24
- package/src/config/schemas/memory-v2.ts +48 -4
- package/src/config/schemas/platform.ts +6 -0
- package/src/config/schemas/services.ts +1 -15
- package/src/config/schemas/skills.ts +0 -6
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/contacts/contact-store.ts +0 -30
- package/src/contacts/contacts-write.ts +0 -27
- package/src/context/window-manager.ts +1 -2
- package/src/credential-execution/feature-gates.ts +10 -10
- package/src/credential-execution/process-manager.ts +12 -41
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
- package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
- package/src/daemon/config-watcher.ts +4 -3
- package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
- package/src/daemon/conversation-agent-loop.ts +32 -28
- package/src/daemon/conversation-lifecycle.ts +8 -1
- package/src/daemon/conversation-process.ts +16 -11
- package/src/daemon/conversation-runtime-assembly.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +125 -4
- package/src/daemon/conversation-tool-setup.ts +16 -55
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/doordash-steps.ts +1 -1
- package/src/daemon/handlers/shared.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +293 -0
- package/src/daemon/host-bash-proxy.ts +84 -74
- package/src/daemon/host-browser-proxy.ts +67 -82
- package/src/daemon/host-cu-proxy.ts +81 -86
- package/src/daemon/host-file-proxy.ts +93 -69
- package/src/daemon/host-proxy-base.ts +294 -0
- package/src/daemon/host-proxy-preactivation.ts +82 -0
- package/src/daemon/host-transfer-proxy.ts +247 -129
- package/src/daemon/lifecycle.ts +115 -117
- package/src/daemon/message-protocol.ts +3 -8
- package/src/daemon/message-types/contacts.ts +23 -1
- package/src/daemon/message-types/conversations.ts +11 -8
- package/src/daemon/message-types/host-app-control.ts +150 -0
- package/src/daemon/message-types/host-bash.ts +4 -0
- package/src/daemon/message-types/host-cu.ts +2 -0
- package/src/daemon/message-types/host-file.ts +4 -0
- package/src/daemon/message-types/host-transfer.ts +3 -0
- package/src/daemon/message-types/schedules.ts +8 -3
- package/src/daemon/message-types/skills.ts +2 -2
- package/src/daemon/process-message.ts +18 -1
- package/src/daemon/shutdown-handlers.ts +0 -3
- package/src/daemon/tool-setup-types.ts +51 -0
- package/src/daemon/tool-side-effects.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -1
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
- package/src/heartbeat/heartbeat-run-store.ts +236 -0
- package/src/heartbeat/heartbeat-service.ts +280 -49
- package/src/home/__tests__/post-connect-feed.test.ts +99 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
- package/src/home/__tests__/suggested-prompts.test.ts +89 -0
- package/src/home/post-connect-feed.ts +68 -0
- package/src/home/relationship-state-writer.ts +17 -92
- package/src/home/suggested-prompts.ts +46 -10
- package/src/inbound/public-ingress-urls.ts +32 -34
- package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
- package/src/ipc/assistant-server.ts +14 -1
- package/src/ipc/cli-client.ts +32 -1
- package/src/live-voice/live-voice-metrics.ts +10 -10
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
- package/src/mcp/mcp-auth-orchestrator.ts +213 -0
- package/src/mcp/mcp-auth-state.ts +133 -0
- package/src/mcp/mcp-oauth-provider.ts +19 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
- package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
- package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
- package/src/memory/anisotropy.test.ts +247 -0
- package/src/memory/anisotropy.ts +443 -0
- package/src/memory/auto-analysis-constants.ts +17 -0
- package/src/memory/auto-analysis-guard.ts +5 -15
- package/src/memory/canonical-guardian-store.ts +7 -7
- package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
- package/src/memory/context-search/agent-protocol.ts +6 -6
- package/src/memory/context-search/agent-runner.ts +32 -7
- package/src/memory/context-search/sources/memory-v2.ts +17 -5
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-key-store.ts +2 -15
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-backend.ts +9 -21
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
- package/src/memory/graph/conversation-graph-memory.ts +1 -24
- package/src/memory/graph/graph-search.ts +8 -0
- package/src/memory/graph/retriever.ts +28 -0
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
- package/src/memory/jobs/embed-concept-page.ts +28 -2
- package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
- package/src/memory/jobs-store.ts +66 -22
- package/src/memory/jobs-worker.ts +112 -63
- package/src/memory/memory-v2-activation-log-store.ts +1 -1
- package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
- package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/pkb/pkb-search.ts +7 -0
- package/src/memory/qdrant-client.ts +50 -20
- package/src/memory/schema/infrastructure.ts +15 -0
- package/src/memory/search/semantic.ts +7 -0
- package/src/memory/sparse-tokenize.ts +49 -0
- package/src/memory/v2/__tests__/activation.test.ts +77 -95
- package/src/memory/v2/__tests__/injection.test.ts +43 -21
- package/src/memory/v2/__tests__/sim.test.ts +166 -6
- package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
- package/src/memory/v2/__tests__/static-context.test.ts +0 -1
- package/src/memory/v2/activation.ts +69 -88
- package/src/memory/v2/consolidation-job.ts +3 -5
- package/src/memory/v2/constants.ts +7 -0
- package/src/memory/v2/injection.ts +86 -53
- package/src/memory/v2/prompts/consolidation.ts +312 -91
- package/src/memory/v2/qdrant.ts +99 -1
- package/src/memory/v2/sim.ts +126 -16
- package/src/memory/v2/skill-qdrant.ts +12 -3
- package/src/memory/v2/skill-store.ts +16 -1
- package/src/memory/v2/sparse-bm25.ts +245 -0
- package/src/memory/v2/static-context.ts +6 -5
- package/src/messaging/providers/gmail/types.ts +0 -49
- package/src/messaging/providers/slack/adapter.ts +1 -31
- package/src/messaging/providers/slack/types.ts +0 -32
- package/src/notifications/README.md +10 -10
- package/src/notifications/broadcaster.ts +1 -1
- package/src/notifications/guardian-question-mode.ts +5 -5
- package/src/oauth/connect-orchestrator.ts +4 -0
- package/src/oauth/credential-token-resolver.ts +1 -3
- package/src/oauth/manual-token-connection.ts +0 -4
- package/src/outbound-proxy/index.ts +1 -37
- package/src/outbound-proxy/logging.ts +1 -1
- package/src/outbound-proxy/policy.ts +6 -5
- package/src/outbound-proxy/router.ts +2 -1
- package/src/permissions/approval-policy.test.ts +6 -275
- package/src/permissions/approval-policy.ts +0 -51
- package/src/permissions/checker.test.ts +0 -1
- package/src/permissions/checker.ts +3 -17
- package/src/permissions/gateway-threshold-reader.ts +2 -0
- package/src/permissions/prompter.ts +34 -1
- package/src/permissions/secret-prompter.ts +6 -2
- package/src/prompts/bootstrap-cleanup.ts +27 -0
- package/src/prompts/system-prompt.ts +3 -18
- package/src/prompts/templates/SOUL.md +13 -1
- package/src/providers/speech-to-text/provider-catalog.ts +7 -8
- package/src/runtime/assistant-event-hub.ts +118 -96
- package/src/runtime/assistant-event.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
- package/src/runtime/auth/middleware.ts +0 -96
- package/src/runtime/auth/route-policy.ts +19 -0
- package/src/runtime/btw-sidechain.ts +2 -3
- package/src/runtime/channel-invite-transport.ts +2 -48
- package/src/runtime/channel-invite-transports/email.ts +1 -1
- package/src/runtime/channel-invite-transports/slack.ts +1 -1
- package/src/runtime/channel-invite-transports/telegram.ts +1 -1
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
- package/src/runtime/channel-invite-types.ts +54 -0
- package/src/runtime/channel-readiness-service.ts +32 -13
- package/src/runtime/http-server.ts +3 -329
- package/src/runtime/http-types.ts +0 -5
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
- package/src/runtime/migrations/migration-transport.ts +7 -7
- package/src/runtime/migrations/vbundle-builder.ts +327 -60
- package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
- package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
- package/src/runtime/migrations/vbundle-importer.ts +245 -68
- package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
- package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
- package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
- package/src/runtime/migrations/vbundle-validator.ts +114 -0
- package/src/runtime/pending-interactions.ts +35 -9
- package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
- package/src/runtime/routes/approval-interception-types.ts +13 -0
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
- package/src/runtime/routes/backup-routes.ts +15 -38
- package/src/runtime/routes/btw-routes.ts +14 -37
- package/src/runtime/routes/client-routes.ts +1 -0
- package/src/runtime/routes/contact-prompt-routes.ts +183 -0
- package/src/runtime/routes/conversation-query-routes.ts +36 -1
- package/src/runtime/routes/conversation-routes.ts +30 -13
- package/src/runtime/routes/document-pdf-renderer.ts +165 -0
- package/src/runtime/routes/documents-routes.ts +30 -0
- package/src/runtime/routes/errors.ts +19 -4
- package/src/runtime/routes/events-routes.ts +12 -6
- package/src/runtime/routes/gateway-log-routes.ts +79 -0
- package/src/runtime/routes/guardian-approval-interception.ts +2 -8
- package/src/runtime/routes/heartbeat-routes.ts +103 -38
- package/src/runtime/routes/host-app-control-routes.ts +134 -0
- package/src/runtime/routes/host-bash-routes.ts +36 -6
- package/src/runtime/routes/host-browser-routes.ts +108 -13
- package/src/runtime/routes/host-cu-routes.ts +44 -14
- package/src/runtime/routes/host-file-routes.ts +33 -10
- package/src/runtime/routes/host-transfer-routes.ts +64 -24
- package/src/runtime/routes/http-adapter.ts +1 -0
- package/src/runtime/routes/identity-intro-cache.ts +30 -0
- package/src/runtime/routes/identity-routes.ts +15 -43
- package/src/runtime/routes/inbound-message-handler.ts +1 -9
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/mcp-auth-routes.ts +132 -0
- package/src/runtime/routes/memory-item-routes.ts +10 -12
- package/src/runtime/routes/memory-v2-routes.ts +441 -1
- package/src/runtime/routes/migration-routes.ts +96 -0
- package/src/runtime/routes/schedule-routes.ts +7 -0
- package/src/runtime/verification-templates.ts +4 -7
- package/src/schedule/integration-status.ts +66 -2
- package/src/schedule/recurrence-engine.ts +4 -1
- package/src/schedule/retry-backoff.ts +18 -0
- package/src/schedule/retry-policy.ts +82 -0
- package/src/schedule/schedule-recovery.ts +64 -0
- package/src/schedule/schedule-store.ts +106 -2
- package/src/schedule/scheduler-types.ts +25 -0
- package/src/schedule/scheduler.ts +63 -38
- package/src/security/oauth-callback-registry.ts +8 -0
- package/src/sequence/analytics.ts +5 -5
- package/src/sequence/engine.ts +1 -1
- package/src/skills/catalog-files.ts +2 -8
- package/src/skills/include-graph.ts +5 -5
- package/src/skills/remote-skill-policy.ts +5 -5
- package/src/skills/skill-file-provider.ts +1 -1
- package/src/skills/skill-file-types.ts +13 -0
- package/src/skills/skillssh-audit-types.ts +28 -0
- package/src/skills/skillssh-registry.ts +8 -21
- package/src/telemetry/types.ts +2 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
- package/src/tools/apps/executors.ts +56 -69
- package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
- package/src/tools/browser/cdp-client/factory.ts +23 -24
- package/src/tools/browser/cdp-client/index.ts +1 -14
- package/src/tools/computer-use/definitions.ts +42 -20
- package/src/tools/executor.ts +2 -0
- package/src/tools/host-filesystem/edit.ts +26 -0
- package/src/tools/host-filesystem/read.ts +26 -0
- package/src/tools/host-filesystem/transfer.ts +31 -1
- package/src/tools/host-filesystem/write.ts +26 -0
- package/src/tools/host-terminal/host-shell.ts +58 -0
- package/src/tools/schedule/create.ts +6 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/schedule/update.ts +10 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
- package/src/tools/shared/filesystem/path-policy.ts +25 -1
- package/src/tools/skills/load.ts +0 -32
- package/src/tools/tool-approval-handler.ts +1 -5
- package/src/tools/types.ts +4 -0
- package/src/usage/pricing.ts +1 -1
- package/src/workspace/hatched-date.ts +86 -0
- package/src/workspace/migrations/003-seed-device-id.ts +1 -1
- package/src/workspace/migrations/006-services-config.ts +8 -5
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
- package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
- package/src/workspace/migrations/utils.ts +21 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
- package/src/__tests__/twilio-rest.test.ts +0 -34
- package/src/backup/__tests__/backup-key.test.ts +0 -152
- package/src/backup/__tests__/backup-worker.test.ts +0 -782
- package/src/backup/__tests__/offsite-writer.test.ts +0 -641
- package/src/backup/__tests__/stream-crypt.test.ts +0 -228
- package/src/backup/backup-key.ts +0 -137
- package/src/backup/backup-worker.ts +0 -472
- package/src/backup/offsite-writer.ts +0 -222
- package/src/backup/stream-crypt.ts +0 -263
- package/src/daemon/message-types/pairing.ts +0 -58
- package/src/outbound-proxy/config.ts +0 -20
- package/src/outbound-proxy/health.ts +0 -18
- package/src/outbound-proxy/types.ts +0 -150
- package/src/runtime/capability-tokens.ts +0 -190
- package/src/signals/mcp-reload.ts +0 -18
|
@@ -1,782 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the periodic backup worker. Drives `runBackupTick` and
|
|
3
|
-
* `createSnapshotNow` directly with fake dependencies so the whole pipeline
|
|
4
|
-
* runs against a temp directory with an in-memory checkpoint store and
|
|
5
|
-
* no real database.
|
|
6
|
-
*
|
|
7
|
-
* `streamExportVBundle` is stubbed to write a tiny byte blob to a temp
|
|
8
|
-
* file — the worker never validates bundle contents, it just hands the
|
|
9
|
-
* path to `writeLocalSnapshot` which renames it into place.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
existsSync,
|
|
14
|
-
mkdirSync,
|
|
15
|
-
mkdtempSync,
|
|
16
|
-
readdirSync,
|
|
17
|
-
rmSync,
|
|
18
|
-
writeFileSync,
|
|
19
|
-
} from "node:fs";
|
|
20
|
-
import { unlink, writeFile } from "node:fs/promises";
|
|
21
|
-
import { tmpdir } from "node:os";
|
|
22
|
-
import { join } from "node:path";
|
|
23
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
24
|
-
|
|
25
|
-
import type { BackupConfig, BackupDestination } from "../../config/schema.js";
|
|
26
|
-
import { BackupConfigSchema } from "../../config/schema.js";
|
|
27
|
-
import type { StreamExportVBundleResult } from "../../runtime/migrations/vbundle-builder.js";
|
|
28
|
-
import type { BackupDeps } from "../backup-worker.js";
|
|
29
|
-
import { createSnapshotNow, runBackupTick } from "../backup-worker.js";
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Test fixtures
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
let ROOT: string;
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
ROOT = mkdtempSync(join(tmpdir(), "vellum-backup-worker-"));
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
try {
|
|
43
|
-
rmSync(ROOT, { recursive: true, force: true });
|
|
44
|
-
} catch {
|
|
45
|
-
// best-effort
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
/** Build a valid BackupConfig with overrides. Starts from schema defaults. */
|
|
50
|
-
function makeConfig(overrides?: {
|
|
51
|
-
enabled?: boolean;
|
|
52
|
-
intervalHours?: number;
|
|
53
|
-
retention?: number;
|
|
54
|
-
localDirectory?: string | null;
|
|
55
|
-
offsite?: {
|
|
56
|
-
enabled?: boolean;
|
|
57
|
-
destinations?: BackupDestination[] | null;
|
|
58
|
-
};
|
|
59
|
-
}): BackupConfig {
|
|
60
|
-
const base = BackupConfigSchema.parse({});
|
|
61
|
-
return {
|
|
62
|
-
...base,
|
|
63
|
-
enabled: overrides?.enabled ?? base.enabled,
|
|
64
|
-
intervalHours: overrides?.intervalHours ?? base.intervalHours,
|
|
65
|
-
retention: overrides?.retention ?? base.retention,
|
|
66
|
-
localDirectory: overrides?.localDirectory ?? base.localDirectory,
|
|
67
|
-
offsite: {
|
|
68
|
-
enabled: overrides?.offsite?.enabled ?? base.offsite.enabled,
|
|
69
|
-
destinations:
|
|
70
|
-
overrides?.offsite?.destinations === undefined
|
|
71
|
-
? base.offsite.destinations
|
|
72
|
-
: overrides.offsite.destinations,
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Build an in-memory checkpoint store. Returns fake getters/setters and a
|
|
79
|
-
* plain object so tests can inspect or preload entries.
|
|
80
|
-
*/
|
|
81
|
-
function makeCheckpointStore(initial: Record<string, string> = {}) {
|
|
82
|
-
const store: Record<string, string> = { ...initial };
|
|
83
|
-
return {
|
|
84
|
-
store,
|
|
85
|
-
get: (key: string): string | null => store[key] ?? null,
|
|
86
|
-
set: (key: string, value: string): void => {
|
|
87
|
-
store[key] = value;
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Build a stub `streamExportVBundle` that writes a tiny payload to a fresh
|
|
94
|
-
* temp file on every call. The worker only cares that `tempPath` exists and
|
|
95
|
-
* can be renamed — the bundle content is never introspected.
|
|
96
|
-
*/
|
|
97
|
-
function makeStreamExportStub(): {
|
|
98
|
-
fn: BackupDeps["streamExportVBundle"];
|
|
99
|
-
calls: Array<Parameters<NonNullable<BackupDeps["streamExportVBundle"]>>[0]>;
|
|
100
|
-
} {
|
|
101
|
-
const calls: Array<
|
|
102
|
-
Parameters<NonNullable<BackupDeps["streamExportVBundle"]>>[0]
|
|
103
|
-
> = [];
|
|
104
|
-
let counter = 0;
|
|
105
|
-
const fn: BackupDeps["streamExportVBundle"] = async (opts) => {
|
|
106
|
-
calls.push(opts);
|
|
107
|
-
// Deliberately do NOT fire opts.checkpoint?.() here — the real checkpoint
|
|
108
|
-
// callback opens a fresh DB handle at `getDbPath()` which does not exist
|
|
109
|
-
// in the test environment. Tests don't care about the WAL side-effect;
|
|
110
|
-
// they just need the stub to return a valid temp bundle path.
|
|
111
|
-
counter += 1;
|
|
112
|
-
const tempPath = join(ROOT, `stub-bundle-${counter}.tmp`);
|
|
113
|
-
await writeFile(tempPath, `fake bundle ${counter}`);
|
|
114
|
-
const result: StreamExportVBundleResult = {
|
|
115
|
-
tempPath,
|
|
116
|
-
size: 16,
|
|
117
|
-
manifest: {
|
|
118
|
-
schema_version: 1,
|
|
119
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
120
|
-
created_at: new Date().toISOString(),
|
|
121
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
122
|
-
origin: { mode: "self-hosted-local" },
|
|
123
|
-
compatibility: {
|
|
124
|
-
min_runtime_version: "0.0.0-test",
|
|
125
|
-
max_runtime_version: null,
|
|
126
|
-
},
|
|
127
|
-
contents: [],
|
|
128
|
-
checksum: "0".repeat(64),
|
|
129
|
-
secrets_redacted: false,
|
|
130
|
-
export_options: {
|
|
131
|
-
include_logs: false,
|
|
132
|
-
include_browser_state: false,
|
|
133
|
-
include_memory_vectors: false,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
cleanup: async () => {
|
|
137
|
-
try {
|
|
138
|
-
await unlink(tempPath);
|
|
139
|
-
} catch {
|
|
140
|
-
// best-effort
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
return result;
|
|
145
|
-
};
|
|
146
|
-
return { fn, calls };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// runBackupTick — gating
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
describe("runBackupTick — gating", () => {
|
|
154
|
-
test("returns null when config.enabled is false", async () => {
|
|
155
|
-
const checkpoints = makeCheckpointStore();
|
|
156
|
-
const streamStub = makeStreamExportStub();
|
|
157
|
-
const config = makeConfig({ enabled: false });
|
|
158
|
-
const localDir = join(ROOT, "local");
|
|
159
|
-
|
|
160
|
-
const result = await runBackupTick(config, new Date(), {
|
|
161
|
-
streamExportVBundle: streamStub.fn,
|
|
162
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
163
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
164
|
-
workspaceDir: ROOT,
|
|
165
|
-
localDir,
|
|
166
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
expect(result).toBeNull();
|
|
170
|
-
expect(streamStub.calls).toHaveLength(0);
|
|
171
|
-
expect(Object.keys(checkpoints.store)).toHaveLength(0);
|
|
172
|
-
expect(existsSync(localDir)).toBe(false);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("returns null when last_run_at is within the interval window", async () => {
|
|
176
|
-
const now = new Date("2026-04-11T10:00:00Z");
|
|
177
|
-
const oneHourAgoMs = now.getTime() - 1 * 3600 * 1000;
|
|
178
|
-
const checkpoints = makeCheckpointStore({
|
|
179
|
-
"backup:last_run_at": String(oneHourAgoMs),
|
|
180
|
-
});
|
|
181
|
-
const streamStub = makeStreamExportStub();
|
|
182
|
-
const config = makeConfig({ enabled: true, intervalHours: 6 });
|
|
183
|
-
const localDir = join(ROOT, "local");
|
|
184
|
-
|
|
185
|
-
const result = await runBackupTick(config, now, {
|
|
186
|
-
streamExportVBundle: streamStub.fn,
|
|
187
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
188
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
189
|
-
workspaceDir: ROOT,
|
|
190
|
-
localDir,
|
|
191
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
expect(result).toBeNull();
|
|
195
|
-
expect(streamStub.calls).toHaveLength(0);
|
|
196
|
-
// Checkpoint unchanged
|
|
197
|
-
expect(checkpoints.store["backup:last_run_at"]).toBe(String(oneHourAgoMs));
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("runs when last_run_at is older than the interval", async () => {
|
|
201
|
-
const now = new Date("2026-04-11T10:00:00Z");
|
|
202
|
-
const sevenHoursAgoMs = now.getTime() - 7 * 3600 * 1000;
|
|
203
|
-
const checkpoints = makeCheckpointStore({
|
|
204
|
-
"backup:last_run_at": String(sevenHoursAgoMs),
|
|
205
|
-
});
|
|
206
|
-
const streamStub = makeStreamExportStub();
|
|
207
|
-
const config = makeConfig({
|
|
208
|
-
enabled: true,
|
|
209
|
-
intervalHours: 6,
|
|
210
|
-
offsite: { enabled: false, destinations: null },
|
|
211
|
-
});
|
|
212
|
-
const localDir = join(ROOT, "local");
|
|
213
|
-
|
|
214
|
-
const result = await runBackupTick(config, now, {
|
|
215
|
-
streamExportVBundle: streamStub.fn,
|
|
216
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
217
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
218
|
-
workspaceDir: ROOT,
|
|
219
|
-
localDir,
|
|
220
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
expect(result).not.toBeNull();
|
|
224
|
-
expect(streamStub.calls).toHaveLength(1);
|
|
225
|
-
expect(checkpoints.store["backup:last_run_at"]).toBe(String(now.getTime()));
|
|
226
|
-
// Local snapshot file was created
|
|
227
|
-
expect(result!.local.path).toContain("backup-20260411-100000-000.vbundle");
|
|
228
|
-
expect(existsSync(result!.local.path)).toBe(true);
|
|
229
|
-
expect(result!.offsite).toEqual([]);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("runs when last_run_at checkpoint is missing (first-ever run)", async () => {
|
|
233
|
-
const now = new Date("2026-04-11T10:00:00Z");
|
|
234
|
-
const checkpoints = makeCheckpointStore();
|
|
235
|
-
const streamStub = makeStreamExportStub();
|
|
236
|
-
const config = makeConfig({
|
|
237
|
-
enabled: true,
|
|
238
|
-
offsite: { enabled: false, destinations: null },
|
|
239
|
-
});
|
|
240
|
-
const localDir = join(ROOT, "local");
|
|
241
|
-
|
|
242
|
-
const result = await runBackupTick(config, now, {
|
|
243
|
-
streamExportVBundle: streamStub.fn,
|
|
244
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
245
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
246
|
-
workspaceDir: ROOT,
|
|
247
|
-
localDir,
|
|
248
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
expect(result).not.toBeNull();
|
|
252
|
-
expect(checkpoints.store["backup:last_run_at"]).toBe(String(now.getTime()));
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// runBackupTick — offsite destinations
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
|
|
260
|
-
describe("runBackupTick — offsite destinations", () => {
|
|
261
|
-
test("config.offsite.enabled === false: offsite is empty and key is not loaded", async () => {
|
|
262
|
-
const checkpoints = makeCheckpointStore();
|
|
263
|
-
const streamStub = makeStreamExportStub();
|
|
264
|
-
const ensureKey = mock(async () => Buffer.alloc(32, 1));
|
|
265
|
-
const config = makeConfig({
|
|
266
|
-
enabled: true,
|
|
267
|
-
offsite: { enabled: false, destinations: null },
|
|
268
|
-
});
|
|
269
|
-
const localDir = join(ROOT, "local");
|
|
270
|
-
|
|
271
|
-
const result = await runBackupTick(config, new Date(), {
|
|
272
|
-
streamExportVBundle: streamStub.fn,
|
|
273
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
274
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
275
|
-
ensureBackupKey: ensureKey,
|
|
276
|
-
workspaceDir: ROOT,
|
|
277
|
-
localDir,
|
|
278
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
expect(result).not.toBeNull();
|
|
282
|
-
expect(result!.offsite).toEqual([]);
|
|
283
|
-
expect(ensureKey).not.toHaveBeenCalled();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("single plaintext destination: key is not loaded, file has .vbundle extension", async () => {
|
|
287
|
-
const checkpoints = makeCheckpointStore();
|
|
288
|
-
const streamStub = makeStreamExportStub();
|
|
289
|
-
const ensureKey = mock(async () => Buffer.alloc(32, 1));
|
|
290
|
-
const offsiteDir = join(ROOT, "offsite", "plain");
|
|
291
|
-
// Parent must exist — writer probes for parent before mkdir of dest.
|
|
292
|
-
mkdirSync(join(ROOT, "offsite"), { recursive: true });
|
|
293
|
-
const config = makeConfig({
|
|
294
|
-
enabled: true,
|
|
295
|
-
offsite: {
|
|
296
|
-
enabled: true,
|
|
297
|
-
destinations: [{ path: offsiteDir, encrypt: false }],
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
const localDir = join(ROOT, "local");
|
|
301
|
-
const now = new Date("2026-04-11T12:00:00Z");
|
|
302
|
-
|
|
303
|
-
const result = await runBackupTick(config, now, {
|
|
304
|
-
streamExportVBundle: streamStub.fn,
|
|
305
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
306
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
307
|
-
ensureBackupKey: ensureKey,
|
|
308
|
-
workspaceDir: ROOT,
|
|
309
|
-
localDir,
|
|
310
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
expect(result).not.toBeNull();
|
|
314
|
-
expect(ensureKey).not.toHaveBeenCalled();
|
|
315
|
-
expect(result!.offsite).toHaveLength(1);
|
|
316
|
-
expect(result!.offsite[0].entry).not.toBeNull();
|
|
317
|
-
expect(result!.offsite[0].entry!.filename).toBe(
|
|
318
|
-
"backup-20260411-120000-000.vbundle",
|
|
319
|
-
);
|
|
320
|
-
expect(result!.offsite[0].entry!.encrypted).toBe(false);
|
|
321
|
-
expect(existsSync(result!.offsite[0].entry!.path)).toBe(true);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
test("single encrypted destination: key is loaded, file has .vbundle.enc extension", async () => {
|
|
325
|
-
const checkpoints = makeCheckpointStore();
|
|
326
|
-
const streamStub = makeStreamExportStub();
|
|
327
|
-
const ensureKey = mock(async () => Buffer.alloc(32, 0xab));
|
|
328
|
-
const offsiteDir = join(ROOT, "offsite", "enc");
|
|
329
|
-
mkdirSync(join(ROOT, "offsite"), { recursive: true });
|
|
330
|
-
const keyPath = join(ROOT, "backup.key");
|
|
331
|
-
const config = makeConfig({
|
|
332
|
-
enabled: true,
|
|
333
|
-
offsite: {
|
|
334
|
-
enabled: true,
|
|
335
|
-
destinations: [{ path: offsiteDir, encrypt: true }],
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
const localDir = join(ROOT, "local");
|
|
339
|
-
const now = new Date("2026-04-11T13:00:00Z");
|
|
340
|
-
|
|
341
|
-
const result = await runBackupTick(config, now, {
|
|
342
|
-
streamExportVBundle: streamStub.fn,
|
|
343
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
344
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
345
|
-
ensureBackupKey: ensureKey,
|
|
346
|
-
backupKeyPath: keyPath,
|
|
347
|
-
workspaceDir: ROOT,
|
|
348
|
-
localDir,
|
|
349
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
expect(result).not.toBeNull();
|
|
353
|
-
expect(ensureKey).toHaveBeenCalledTimes(1);
|
|
354
|
-
expect(ensureKey).toHaveBeenCalledWith(keyPath);
|
|
355
|
-
expect(result!.offsite).toHaveLength(1);
|
|
356
|
-
expect(result!.offsite[0].entry).not.toBeNull();
|
|
357
|
-
expect(result!.offsite[0].entry!.filename).toBe(
|
|
358
|
-
"backup-20260411-130000-000.vbundle.enc",
|
|
359
|
-
);
|
|
360
|
-
expect(result!.offsite[0].entry!.encrypted).toBe(true);
|
|
361
|
-
expect(existsSync(result!.offsite[0].entry!.path)).toBe(true);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("mixed destinations: key is loaded once (because A needs it), both files written", async () => {
|
|
365
|
-
const checkpoints = makeCheckpointStore();
|
|
366
|
-
const streamStub = makeStreamExportStub();
|
|
367
|
-
const ensureKey = mock(async () => Buffer.alloc(32, 0xcd));
|
|
368
|
-
const encDir = join(ROOT, "offsite", "enc");
|
|
369
|
-
const plainDir = join(ROOT, "offsite", "plain");
|
|
370
|
-
mkdirSync(join(ROOT, "offsite"), { recursive: true });
|
|
371
|
-
const config = makeConfig({
|
|
372
|
-
enabled: true,
|
|
373
|
-
offsite: {
|
|
374
|
-
enabled: true,
|
|
375
|
-
destinations: [
|
|
376
|
-
{ path: encDir, encrypt: true },
|
|
377
|
-
{ path: plainDir, encrypt: false },
|
|
378
|
-
],
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
const localDir = join(ROOT, "local");
|
|
382
|
-
|
|
383
|
-
const result = await runBackupTick(config, new Date(), {
|
|
384
|
-
streamExportVBundle: streamStub.fn,
|
|
385
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
386
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
387
|
-
ensureBackupKey: ensureKey,
|
|
388
|
-
workspaceDir: ROOT,
|
|
389
|
-
localDir,
|
|
390
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
expect(result).not.toBeNull();
|
|
394
|
-
expect(ensureKey).toHaveBeenCalledTimes(1);
|
|
395
|
-
expect(result!.offsite).toHaveLength(2);
|
|
396
|
-
expect(result!.offsite[0].entry).not.toBeNull();
|
|
397
|
-
expect(result!.offsite[0].entry!.encrypted).toBe(true);
|
|
398
|
-
expect(result!.offsite[1].entry).not.toBeNull();
|
|
399
|
-
expect(result!.offsite[1].entry!.encrypted).toBe(false);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test("mixed reachability: one ok + one parent-missing skip, local succeeds, checkpoint updated", async () => {
|
|
403
|
-
const checkpoints = makeCheckpointStore();
|
|
404
|
-
const streamStub = makeStreamExportStub();
|
|
405
|
-
const reachableDir = join(ROOT, "offsite", "reachable");
|
|
406
|
-
// Nested parent-missing: the parent directory is /nope/deeper which is
|
|
407
|
-
// unreachable because /nope itself does not exist.
|
|
408
|
-
const unreachableDir = join(ROOT, "nope", "deeper", "backups");
|
|
409
|
-
mkdirSync(join(ROOT, "offsite"), { recursive: true });
|
|
410
|
-
const config = makeConfig({
|
|
411
|
-
enabled: true,
|
|
412
|
-
offsite: {
|
|
413
|
-
enabled: true,
|
|
414
|
-
destinations: [
|
|
415
|
-
{ path: reachableDir, encrypt: false },
|
|
416
|
-
{ path: unreachableDir, encrypt: false },
|
|
417
|
-
],
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
const localDir = join(ROOT, "local");
|
|
421
|
-
const now = new Date("2026-04-11T14:00:00Z");
|
|
422
|
-
|
|
423
|
-
const result = await runBackupTick(config, now, {
|
|
424
|
-
streamExportVBundle: streamStub.fn,
|
|
425
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
426
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
427
|
-
workspaceDir: ROOT,
|
|
428
|
-
localDir,
|
|
429
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
expect(result).not.toBeNull();
|
|
433
|
-
expect(result!.offsite).toHaveLength(2);
|
|
434
|
-
expect(result!.offsite[0].entry).not.toBeNull();
|
|
435
|
-
expect(result!.offsite[1].entry).toBeNull();
|
|
436
|
-
expect(result!.offsite[1].skipped).toBe("parent-missing");
|
|
437
|
-
// Local still succeeded
|
|
438
|
-
expect(existsSync(result!.local.path)).toBe(true);
|
|
439
|
-
// Checkpoint updated because performBackup returned successfully
|
|
440
|
-
expect(checkpoints.store["backup:last_run_at"]).toBe(String(now.getTime()));
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
// runBackupTick — error propagation
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
|
-
|
|
448
|
-
describe("runBackupTick — error propagation", () => {
|
|
449
|
-
test("throws when streamExportVBundle throws and leaves checkpoint untouched", async () => {
|
|
450
|
-
const checkpoints = makeCheckpointStore();
|
|
451
|
-
const throwingStream: BackupDeps["streamExportVBundle"] = async () => {
|
|
452
|
-
throw new Error("boom");
|
|
453
|
-
};
|
|
454
|
-
const config = makeConfig({
|
|
455
|
-
enabled: true,
|
|
456
|
-
offsite: { enabled: false, destinations: null },
|
|
457
|
-
});
|
|
458
|
-
const localDir = join(ROOT, "local");
|
|
459
|
-
|
|
460
|
-
await expect(
|
|
461
|
-
runBackupTick(config, new Date(), {
|
|
462
|
-
streamExportVBundle: throwingStream,
|
|
463
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
464
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
465
|
-
workspaceDir: ROOT,
|
|
466
|
-
localDir,
|
|
467
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
468
|
-
}),
|
|
469
|
-
).rejects.toThrow("boom");
|
|
470
|
-
expect(checkpoints.store["backup:last_run_at"]).toBeUndefined();
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// ---------------------------------------------------------------------------
|
|
475
|
-
// createSnapshotNow — manual trigger
|
|
476
|
-
// ---------------------------------------------------------------------------
|
|
477
|
-
|
|
478
|
-
describe("createSnapshotNow", () => {
|
|
479
|
-
test("bypasses enabled check (snapshot created even when enabled is false)", async () => {
|
|
480
|
-
const checkpoints = makeCheckpointStore();
|
|
481
|
-
const streamStub = makeStreamExportStub();
|
|
482
|
-
const config = makeConfig({
|
|
483
|
-
enabled: false,
|
|
484
|
-
offsite: { enabled: false, destinations: null },
|
|
485
|
-
});
|
|
486
|
-
const localDir = join(ROOT, "local");
|
|
487
|
-
|
|
488
|
-
const result = await createSnapshotNow(config, new Date(), {
|
|
489
|
-
streamExportVBundle: streamStub.fn,
|
|
490
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
491
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
492
|
-
workspaceDir: ROOT,
|
|
493
|
-
localDir,
|
|
494
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
expect(result).not.toBeNull();
|
|
498
|
-
expect(streamStub.calls).toHaveLength(1);
|
|
499
|
-
// Manual runs do NOT update the automatic cadence checkpoint
|
|
500
|
-
expect(checkpoints.store["backup:last_run_at"]).toBeUndefined();
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
test("bypasses interval check even when a recent run was recorded", async () => {
|
|
504
|
-
const now = new Date("2026-04-11T10:00:00Z");
|
|
505
|
-
const checkpoints = makeCheckpointStore({
|
|
506
|
-
"backup:last_run_at": String(now.getTime() - 60_000),
|
|
507
|
-
});
|
|
508
|
-
const streamStub = makeStreamExportStub();
|
|
509
|
-
const config = makeConfig({
|
|
510
|
-
enabled: true,
|
|
511
|
-
intervalHours: 6,
|
|
512
|
-
offsite: { enabled: false, destinations: null },
|
|
513
|
-
});
|
|
514
|
-
const localDir = join(ROOT, "local");
|
|
515
|
-
|
|
516
|
-
const result = await createSnapshotNow(config, now, {
|
|
517
|
-
streamExportVBundle: streamStub.fn,
|
|
518
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
519
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
520
|
-
workspaceDir: ROOT,
|
|
521
|
-
localDir,
|
|
522
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
expect(result).not.toBeNull();
|
|
526
|
-
expect(streamStub.calls).toHaveLength(1);
|
|
527
|
-
// The pre-existing checkpoint is preserved — manual runs do not touch it.
|
|
528
|
-
expect(checkpoints.store["backup:last_run_at"]).toBe(
|
|
529
|
-
String(now.getTime() - 60_000),
|
|
530
|
-
);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
test("two concurrent calls: second throws 'snapshot in progress'", async () => {
|
|
534
|
-
const checkpoints = makeCheckpointStore();
|
|
535
|
-
// Stub that holds the first caller indefinitely until we release it,
|
|
536
|
-
// giving the test a clean window to observe the mutex from a second call.
|
|
537
|
-
let release: () => void = () => {};
|
|
538
|
-
const holdPromise = new Promise<void>((resolve) => {
|
|
539
|
-
release = resolve;
|
|
540
|
-
});
|
|
541
|
-
let callCount = 0;
|
|
542
|
-
const holdingStream: BackupDeps["streamExportVBundle"] = async (_opts) => {
|
|
543
|
-
callCount += 1;
|
|
544
|
-
if (callCount === 1) {
|
|
545
|
-
await holdPromise;
|
|
546
|
-
}
|
|
547
|
-
const tempPath = join(ROOT, `hold-${callCount}.tmp`);
|
|
548
|
-
writeFileSync(tempPath, "payload");
|
|
549
|
-
return {
|
|
550
|
-
tempPath,
|
|
551
|
-
size: 7,
|
|
552
|
-
manifest: {
|
|
553
|
-
schema_version: 1,
|
|
554
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
555
|
-
created_at: new Date().toISOString(),
|
|
556
|
-
assistant: {
|
|
557
|
-
id: "self",
|
|
558
|
-
name: "Test",
|
|
559
|
-
runtime_version: "0.0.0-test",
|
|
560
|
-
},
|
|
561
|
-
origin: { mode: "self-hosted-local" },
|
|
562
|
-
compatibility: {
|
|
563
|
-
min_runtime_version: "0.0.0-test",
|
|
564
|
-
max_runtime_version: null,
|
|
565
|
-
},
|
|
566
|
-
contents: [],
|
|
567
|
-
checksum: "0".repeat(64),
|
|
568
|
-
secrets_redacted: false,
|
|
569
|
-
export_options: {
|
|
570
|
-
include_logs: false,
|
|
571
|
-
include_browser_state: false,
|
|
572
|
-
include_memory_vectors: false,
|
|
573
|
-
},
|
|
574
|
-
},
|
|
575
|
-
cleanup: async () => {
|
|
576
|
-
try {
|
|
577
|
-
await unlink(tempPath);
|
|
578
|
-
} catch {
|
|
579
|
-
// best-effort
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
|
-
};
|
|
583
|
-
};
|
|
584
|
-
const config = makeConfig({
|
|
585
|
-
enabled: true,
|
|
586
|
-
offsite: { enabled: false, destinations: null },
|
|
587
|
-
});
|
|
588
|
-
const localDir = join(ROOT, "local");
|
|
589
|
-
|
|
590
|
-
// Start the first call — it will park inside `streamExportVBundle`
|
|
591
|
-
// waiting on holdPromise.
|
|
592
|
-
const first = createSnapshotNow(config, new Date(), {
|
|
593
|
-
streamExportVBundle: holdingStream,
|
|
594
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
595
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
596
|
-
workspaceDir: ROOT,
|
|
597
|
-
localDir,
|
|
598
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
// Yield once so the first call has a chance to enter the mutex + the
|
|
602
|
-
// stream stub before we kick off the second call.
|
|
603
|
-
await Promise.resolve();
|
|
604
|
-
await Promise.resolve();
|
|
605
|
-
|
|
606
|
-
await expect(
|
|
607
|
-
createSnapshotNow(config, new Date(), {
|
|
608
|
-
streamExportVBundle: holdingStream,
|
|
609
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
610
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
611
|
-
workspaceDir: ROOT,
|
|
612
|
-
localDir,
|
|
613
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
614
|
-
}),
|
|
615
|
-
).rejects.toThrow("snapshot in progress");
|
|
616
|
-
|
|
617
|
-
release();
|
|
618
|
-
await first;
|
|
619
|
-
// Only the first call should have been executed by the stub.
|
|
620
|
-
expect(callCount).toBe(1);
|
|
621
|
-
});
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
// ---------------------------------------------------------------------------
|
|
625
|
-
// Cross-process lock (simulates a second process holding the lock)
|
|
626
|
-
// ---------------------------------------------------------------------------
|
|
627
|
-
|
|
628
|
-
describe("cross-process snapshot lock", () => {
|
|
629
|
-
test("after performBackup succeeds, the lock file no longer exists", async () => {
|
|
630
|
-
const checkpoints = makeCheckpointStore();
|
|
631
|
-
const streamStub = makeStreamExportStub();
|
|
632
|
-
const config = makeConfig({
|
|
633
|
-
enabled: true,
|
|
634
|
-
offsite: { enabled: false, destinations: null },
|
|
635
|
-
});
|
|
636
|
-
const localDir = join(ROOT, "local");
|
|
637
|
-
const lockPath = join(ROOT, ".snapshot.lock");
|
|
638
|
-
|
|
639
|
-
const result = await createSnapshotNow(config, new Date(), {
|
|
640
|
-
streamExportVBundle: streamStub.fn,
|
|
641
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
642
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
643
|
-
workspaceDir: ROOT,
|
|
644
|
-
localDir,
|
|
645
|
-
snapshotLockPath: lockPath,
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
expect(result).not.toBeNull();
|
|
649
|
-
// Lock file released on the finally path — must not linger on disk.
|
|
650
|
-
expect(existsSync(lockPath)).toBe(false);
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test("another process holds the lock: createSnapshotNow throws 'snapshot in progress'", async () => {
|
|
654
|
-
const checkpoints = makeCheckpointStore();
|
|
655
|
-
const streamStub = makeStreamExportStub();
|
|
656
|
-
const config = makeConfig({
|
|
657
|
-
enabled: true,
|
|
658
|
-
offsite: { enabled: false, destinations: null },
|
|
659
|
-
});
|
|
660
|
-
const localDir = join(ROOT, "local");
|
|
661
|
-
const lockPath = join(ROOT, ".snapshot.lock");
|
|
662
|
-
|
|
663
|
-
// Simulate a concurrent CLI invocation by writing a lock file with the
|
|
664
|
-
// CURRENT pid (which is definitely alive — it's us). Because the lock
|
|
665
|
-
// file pre-exists and the PID probes as alive, the in-process flag will
|
|
666
|
-
// pass (it's reset after the previous test) but the cross-process lock
|
|
667
|
-
// will reject with "snapshot in progress (locked by pid N)".
|
|
668
|
-
writeFileSync(lockPath, `${process.pid} ${Date.now()}\n`, { mode: 0o600 });
|
|
669
|
-
|
|
670
|
-
await expect(
|
|
671
|
-
createSnapshotNow(config, new Date(), {
|
|
672
|
-
streamExportVBundle: streamStub.fn,
|
|
673
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
674
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
675
|
-
workspaceDir: ROOT,
|
|
676
|
-
localDir,
|
|
677
|
-
snapshotLockPath: lockPath,
|
|
678
|
-
}),
|
|
679
|
-
).rejects.toThrow(/snapshot in progress/);
|
|
680
|
-
|
|
681
|
-
// The stream stub must not have been invoked because acquisition failed
|
|
682
|
-
// before performBackup ran.
|
|
683
|
-
expect(streamStub.calls).toHaveLength(0);
|
|
684
|
-
// The pre-existing lock file is preserved — we did not own it, so we
|
|
685
|
-
// must not have removed it on the failed-acquisition path.
|
|
686
|
-
expect(existsSync(lockPath)).toBe(true);
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
test("runBackupTick defers silently when another process holds the lock", async () => {
|
|
690
|
-
const now = new Date("2026-04-11T10:00:00Z");
|
|
691
|
-
const checkpoints = makeCheckpointStore();
|
|
692
|
-
const streamStub = makeStreamExportStub();
|
|
693
|
-
const config = makeConfig({
|
|
694
|
-
enabled: true,
|
|
695
|
-
offsite: { enabled: false, destinations: null },
|
|
696
|
-
});
|
|
697
|
-
const localDir = join(ROOT, "local");
|
|
698
|
-
const lockPath = join(ROOT, ".snapshot.lock");
|
|
699
|
-
|
|
700
|
-
// Pre-seed the lock file with the live PID so the worker observes a
|
|
701
|
-
// conflict on its cross-process check.
|
|
702
|
-
writeFileSync(lockPath, `${process.pid} ${Date.now()}\n`, { mode: 0o600 });
|
|
703
|
-
|
|
704
|
-
const result = await runBackupTick(config, now, {
|
|
705
|
-
streamExportVBundle: streamStub.fn,
|
|
706
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
707
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
708
|
-
workspaceDir: ROOT,
|
|
709
|
-
localDir,
|
|
710
|
-
snapshotLockPath: lockPath,
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// Scheduled tick defers silently on conflict rather than throwing — the
|
|
714
|
-
// next interval will retry.
|
|
715
|
-
expect(result).toBeNull();
|
|
716
|
-
expect(streamStub.calls).toHaveLength(0);
|
|
717
|
-
// Checkpoint must not advance when the tick defers.
|
|
718
|
-
expect(checkpoints.store["backup:last_run_at"]).toBeUndefined();
|
|
719
|
-
// Pre-existing lock file is preserved.
|
|
720
|
-
expect(existsSync(lockPath)).toBe(true);
|
|
721
|
-
});
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
// ---------------------------------------------------------------------------
|
|
725
|
-
// Retention — integration across multiple ticks
|
|
726
|
-
// ---------------------------------------------------------------------------
|
|
727
|
-
|
|
728
|
-
describe("retention across successive ticks", () => {
|
|
729
|
-
test("three ticks past the interval with retention=2 leaves 2 local + 2 offsite", async () => {
|
|
730
|
-
const checkpoints = makeCheckpointStore();
|
|
731
|
-
const streamStub = makeStreamExportStub();
|
|
732
|
-
const offsiteDir = join(ROOT, "offsite", "plain");
|
|
733
|
-
mkdirSync(join(ROOT, "offsite"), { recursive: true });
|
|
734
|
-
const config = makeConfig({
|
|
735
|
-
enabled: true,
|
|
736
|
-
intervalHours: 1,
|
|
737
|
-
retention: 2,
|
|
738
|
-
offsite: {
|
|
739
|
-
enabled: true,
|
|
740
|
-
destinations: [{ path: offsiteDir, encrypt: false }],
|
|
741
|
-
},
|
|
742
|
-
});
|
|
743
|
-
const localDir = join(ROOT, "local");
|
|
744
|
-
|
|
745
|
-
// Three successive runs, each 2 hours apart (past the 1-hour interval).
|
|
746
|
-
const t1 = new Date("2026-04-11T10:00:00Z");
|
|
747
|
-
const t2 = new Date("2026-04-11T12:00:00Z");
|
|
748
|
-
const t3 = new Date("2026-04-11T14:00:00Z");
|
|
749
|
-
|
|
750
|
-
for (const t of [t1, t2, t3]) {
|
|
751
|
-
const result = await runBackupTick(config, t, {
|
|
752
|
-
streamExportVBundle: streamStub.fn,
|
|
753
|
-
getMemoryCheckpoint: checkpoints.get,
|
|
754
|
-
setMemoryCheckpoint: checkpoints.set,
|
|
755
|
-
workspaceDir: ROOT,
|
|
756
|
-
localDir,
|
|
757
|
-
snapshotLockPath: join(ROOT, ".snapshot.lock"),
|
|
758
|
-
});
|
|
759
|
-
expect(result).not.toBeNull();
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// After three runs with retention=2, only the two newest survive in
|
|
763
|
-
// both local and offsite pools.
|
|
764
|
-
const localFiles = readdirSync(localDir)
|
|
765
|
-
.filter((f) => f.startsWith("backup-"))
|
|
766
|
-
.sort();
|
|
767
|
-
expect(localFiles).toHaveLength(2);
|
|
768
|
-
expect(localFiles).toEqual([
|
|
769
|
-
"backup-20260411-120000-000.vbundle",
|
|
770
|
-
"backup-20260411-140000-000.vbundle",
|
|
771
|
-
]);
|
|
772
|
-
|
|
773
|
-
const offsiteFiles = readdirSync(offsiteDir)
|
|
774
|
-
.filter((f) => f.startsWith("backup-"))
|
|
775
|
-
.sort();
|
|
776
|
-
expect(offsiteFiles).toHaveLength(2);
|
|
777
|
-
expect(offsiteFiles).toEqual([
|
|
778
|
-
"backup-20260411-120000-000.vbundle",
|
|
779
|
-
"backup-20260411-140000-000.vbundle",
|
|
780
|
-
]);
|
|
781
|
-
});
|
|
782
|
-
});
|