@vellumai/assistant 0.4.26 → 0.4.29
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/.env.example +2 -2
- package/AGENTS.md +5 -0
- package/ARCHITECTURE.md +169 -69
- package/Dockerfile +1 -1
- package/README.md +111 -112
- package/bun.lock +0 -3
- package/docs/architecture/integrations.md +0 -1
- package/docs/architecture/memory.md +100 -63
- package/docs/error-handling.md +71 -0
- package/docs/runbook-trusted-contacts.md +10 -9
- package/docs/trusted-contact-access.md +48 -46
- package/package.json +3 -3
- package/scripts/compare-benchmarks.sh +12 -5
- package/scripts/ipc/check-swift-decoder-drift.ts +3 -0
- package/scripts/test.sh +89 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +46 -0
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/account-registry.test.ts +1 -1
- package/src/__tests__/actor-token-service.test.ts +36 -23
- package/src/__tests__/agent-loop-thinking.test.ts +29 -13
- package/src/__tests__/agent-loop.test.ts +2 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +7 -7
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +217 -0
- package/src/__tests__/call-controller.test.ts +99 -69
- package/src/__tests__/call-start-guardian-guard.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +113 -70
- package/src/__tests__/channel-guardian.test.ts +173 -282
- package/src/__tests__/channel-readiness-service.test.ts +6 -2
- package/src/__tests__/channel-reply-delivery.test.ts +2 -2
- package/src/__tests__/channel-retry-sweep.test.ts +14 -14
- package/src/__tests__/checker.test.ts +12 -31
- package/src/__tests__/claude-code-tool-profiles.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +67 -59
- package/src/__tests__/compaction.benchmark.test.ts +6 -2
- package/src/__tests__/computer-use-tools.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +66 -7
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +29 -29
- package/src/__tests__/contacts-tools.test.ts +63 -2
- package/src/__tests__/context-overflow-approval.test.ts +141 -0
- package/src/__tests__/context-overflow-policy.test.ts +171 -0
- package/src/__tests__/context-overflow-reducer.test.ts +533 -0
- package/src/__tests__/context-window-manager.test.ts +97 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +38 -46
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +214 -10
- package/src/__tests__/conversation-routes.test.ts +4 -7
- package/src/__tests__/credential-broker-browser-fill.test.ts +13 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/credential-vault-unit.test.ts +1 -1
- package/src/__tests__/credential-vault.test.ts +11 -8
- package/src/__tests__/daemon-lifecycle.test.ts +2 -2
- package/src/__tests__/daemon-server-session-init.test.ts +6 -6
- package/src/__tests__/delete-managed-skill-tool.test.ts +1 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -2
- package/src/__tests__/emit-signal-routing-intent.test.ts +4 -0
- package/src/__tests__/encrypted-store.test.ts +10 -7
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/file-edit-tool.test.ts +1 -1
- package/src/__tests__/file-read-tool.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +1 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +87 -64
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +37 -31
- package/src/__tests__/fixtures/mock-signup-server.ts +171 -115
- package/src/__tests__/fixtures/proxy-fixtures.ts +39 -39
- package/src/__tests__/followup-tools.test.ts +1 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +543 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +15 -15
- package/src/__tests__/guardian-dispatch.test.ts +79 -1
- package/src/__tests__/guardian-grant-minting.test.ts +14 -14
- package/src/__tests__/guardian-outbound-http.test.ts +1 -2
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -41
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -5
- package/src/__tests__/guardian-routing-state.test.ts +36 -52
- package/src/__tests__/guardian-verification-intent-routing.test.ts +4 -6
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
- package/src/__tests__/handle-user-message-secret-resume.test.ts +39 -1
- package/src/__tests__/handlers-cu-observation-blob.test.ts +21 -10
- package/src/__tests__/handlers-telegram-config.test.ts +14 -14
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +23 -2
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/headless-browser-navigate.test.ts +1 -1
- package/src/__tests__/headless-browser-read-tools.test.ts +1 -1
- package/src/__tests__/headless-browser-snapshot.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +45 -2
- package/src/__tests__/host-file-edit-tool.test.ts +1 -1
- package/src/__tests__/host-file-read-tool.test.ts +1 -1
- package/src/__tests__/host-file-write-tool.test.ts +1 -1
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +16 -18
- package/src/__tests__/ingress-reconcile.test.ts +2 -2
- package/src/__tests__/ingress-routes-http.test.ts +2 -1
- package/src/__tests__/integrations-cli.test.ts +256 -0
- package/src/__tests__/intent-routing.test.ts +4 -5
- package/src/__tests__/invite-redemption-service.test.ts +4 -3
- package/src/__tests__/ipc-snapshot.test.ts +28 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +1 -1
- package/src/__tests__/mcp-cli.test.ts +136 -57
- package/src/__tests__/mcp-client-auth.test.ts +95 -0
- package/src/__tests__/media-generate-image.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +8 -8
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/messaging-send-tool.test.ts +1 -1
- package/src/__tests__/migration-cross-version-compatibility.test.ts +1855 -0
- package/src/__tests__/migration-export-http.test.ts +540 -0
- package/src/__tests__/migration-import-commit-http.test.ts +823 -0
- package/src/__tests__/migration-import-preflight-http.test.ts +755 -0
- package/src/__tests__/migration-parity-persistence.test.ts +1854 -0
- package/src/__tests__/migration-transport.test.ts +904 -0
- package/src/__tests__/migration-validate-http.test.ts +698 -0
- package/src/__tests__/migration-wizard.test.ts +1289 -0
- package/src/__tests__/non-member-access-request.test.ts +17 -17
- package/src/__tests__/notification-decision-strategy.test.ts +110 -2
- package/src/__tests__/notification-deep-link.test.ts +18 -0
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +1 -1
- package/src/__tests__/playbook-execution.test.ts +1 -1
- package/src/__tests__/playbook-tools.test.ts +1 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +3 -1
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/qdrant-manager.test.ts +40 -11
- package/src/__tests__/rebind-secrets-screen.test.ts +839 -0
- package/src/__tests__/recording-handler.test.ts +2 -2
- package/src/__tests__/recording-intent-handler.test.ts +3 -3
- package/src/__tests__/recording-state-machine.test.ts +2 -2
- package/src/__tests__/relay-server.test.ts +506 -227
- package/src/__tests__/reminder-store.test.ts +8 -0
- package/src/__tests__/reminder.test.ts +8 -0
- package/src/__tests__/{resolve-guardian-trust-class.test.ts → resolve-trust-class.test.ts} +11 -17
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -1
- package/src/__tests__/schedule-tools.test.ts +1 -1
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/script-proxy-connect-tunnel.test.ts +2 -3
- package/src/__tests__/script-proxy-decision-trace.test.ts +2 -2
- package/src/__tests__/script-proxy-http-forwarder.test.ts +1 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +5 -5
- package/src/__tests__/script-proxy-mitm-handler.test.ts +4 -4
- package/src/__tests__/script-proxy-policy-runtime.test.ts +2 -2
- package/src/__tests__/script-proxy-policy.test.ts +2 -2
- package/src/__tests__/script-proxy-session-manager.test.ts +4 -7
- package/src/__tests__/script-proxy-session-runtime.test.ts +1 -6
- package/src/__tests__/secret-onetime-send.test.ts +4 -4
- package/src/__tests__/secret-scanner-executor.test.ts +2 -2
- package/src/__tests__/send-endpoint-busy.test.ts +11 -9
- package/src/__tests__/send-notification-tool.test.ts +2 -2
- package/src/__tests__/session-abort-tool-results.test.ts +17 -2
- package/src/__tests__/session-agent-loop.test.ts +456 -35
- package/src/__tests__/session-confirmation-signals.test.ts +3 -2
- package/src/__tests__/session-conflict-gate.test.ts +20 -3
- package/src/__tests__/session-init.benchmark.test.ts +2 -2
- package/src/__tests__/session-load-history-repair.test.ts +7 -7
- package/src/__tests__/session-pre-run-repair.test.ts +17 -2
- package/src/__tests__/session-profile-injection.test.ts +20 -3
- package/src/__tests__/session-provider-retry-repair.test.ts +86 -6
- package/src/__tests__/session-queue.test.ts +33 -18
- package/src/__tests__/session-runtime-assembly.test.ts +147 -1
- package/src/__tests__/session-runtime-workspace.test.ts +40 -0
- package/src/__tests__/session-slash-known.test.ts +21 -3
- package/src/__tests__/session-slash-queue.test.ts +17 -2
- package/src/__tests__/session-slash-unknown.test.ts +17 -2
- package/src/__tests__/session-surfaces-deselection.test.ts +208 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +2 -2
- package/src/__tests__/session-workspace-injection.test.ts +17 -2
- package/src/__tests__/session-workspace-tool-tracking.test.ts +17 -2
- package/src/__tests__/shell-credential-ref.test.ts +1 -1
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
- package/src/__tests__/skill-load-tool.test.ts +1 -1
- package/src/__tests__/skill-script-runner-host.test.ts +1 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -1
- package/src/__tests__/skill-script-runner.test.ts +1 -1
- package/src/__tests__/skill-tool-factory.test.ts +1 -1
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/subagent-tools.test.ts +3 -3
- package/src/__tests__/swarm-recursion.test.ts +1 -1
- package/src/__tests__/swarm-session-integration.test.ts +1 -1
- package/src/__tests__/swarm-tool.test.ts +1 -1
- package/src/__tests__/task-management-tools.test.ts +1 -1
- package/src/__tests__/task-tools.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +1 -1
- package/src/__tests__/test-support/browser-skill-harness.ts +39 -27
- package/src/__tests__/test-support/computer-use-skill-harness.ts +14 -14
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor-shell-integration.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +23 -182
- package/src/__tests__/tool-grant-request-escalation.test.ts +11 -11
- package/src/__tests__/tool-permission-simulate-handler.test.ts +4 -4
- package/src/__tests__/transfer-progress-screen.test.ts +1180 -0
- package/src/__tests__/trust-context-guards.test.ts +25 -29
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +23 -21
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +37 -40
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +29 -25
- package/src/__tests__/trusted-contact-multichannel.test.ts +25 -24
- package/src/__tests__/trusted-contact-verification.test.ts +63 -77
- package/src/__tests__/turn-commit.test.ts +18 -18
- package/src/__tests__/twilio-provider.test.ts +7 -7
- package/src/__tests__/validation-results-screen.test.ts +1107 -0
- package/src/__tests__/view-image-tool.test.ts +1 -1
- package/src/__tests__/voice-invite-redemption.test.ts +3 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +12 -12
- package/src/__tests__/voice-session-bridge.test.ts +24 -24
- package/src/agent/attachments.ts +3 -1
- package/src/agent/loop.ts +13 -13
- package/src/agent/message-types.ts +13 -7
- package/src/amazon/cart.ts +59 -32
- package/src/amazon/checkout.ts +25 -14
- package/src/amazon/client.ts +68 -48
- package/src/amazon/product-details.ts +3 -3
- package/src/amazon/request-extractor.ts +46 -31
- package/src/amazon/search.ts +6 -4
- package/src/amazon/session.ts +33 -24
- package/src/approvals/AGENTS.md +26 -0
- package/src/approvals/approval-primitive.ts +87 -64
- package/src/approvals/guardian-decision-primitive.ts +172 -81
- package/src/approvals/guardian-request-resolvers.ts +262 -155
- package/src/autonomy/autonomy-resolver.ts +7 -5
- package/src/autonomy/autonomy-store.ts +34 -19
- package/src/autonomy/disposition-mapper.ts +5 -5
- package/src/autonomy/index.ts +6 -6
- package/src/autonomy/types.ts +7 -3
- package/src/browser-extension-relay/client.ts +50 -19
- package/src/browser-extension-relay/protocol.ts +11 -11
- package/src/browser-extension-relay/server.ts +45 -20
- package/src/bundler/app-bundler.ts +75 -50
- package/src/bundler/bundle-scanner.ts +145 -41
- package/src/bundler/bundle-signer.ts +16 -14
- package/src/bundler/signature-verifier.ts +36 -33
- package/src/calls/call-constants.ts +10 -3
- package/src/calls/call-controller.ts +473 -214
- package/src/calls/call-conversation-messages.ts +25 -15
- package/src/calls/call-domain.ts +401 -148
- package/src/calls/call-pointer-message-composer.ts +26 -21
- package/src/calls/call-pointer-messages.ts +52 -28
- package/src/calls/call-recovery.ts +53 -37
- package/src/calls/call-state-machine.ts +37 -7
- package/src/calls/call-state.ts +35 -13
- package/src/calls/call-store.ts +165 -77
- package/src/calls/elevenlabs-client.ts +39 -20
- package/src/calls/guardian-action-sweep.ts +42 -24
- package/src/calls/guardian-dispatch.ts +79 -56
- package/src/calls/guardian-question-copy.ts +28 -23
- package/src/calls/relay-server.ts +1121 -532
- package/src/calls/speaker-identification.ts +21 -15
- package/src/calls/twilio-config.ts +34 -17
- package/src/calls/twilio-provider.ts +108 -55
- package/src/calls/twilio-rest.ts +212 -100
- package/src/calls/twilio-routes.ts +165 -92
- package/src/calls/types.ts +55 -7
- package/src/calls/voice-quality.ts +6 -4
- package/src/calls/voice-session-bridge.ts +181 -133
- package/src/channels/config.ts +17 -13
- package/src/channels/types.ts +38 -10
- package/src/cli/amazon.ts +333 -227
- package/src/cli/config-commands.ts +236 -146
- package/src/cli/core-commands.ts +403 -329
- package/src/cli/email-guardrails.ts +38 -19
- package/src/cli/email.ts +207 -153
- package/src/cli/influencer.ts +58 -56
- package/src/cli/integrations.ts +362 -0
- package/src/cli/ipc-client.ts +24 -19
- package/src/cli/map.ts +176 -129
- package/src/cli/mcp.ts +260 -152
- package/src/cli/sequence.ts +165 -107
- package/src/cli/twitter.ts +302 -218
- package/src/cli.ts +418 -279
- package/src/commands/cc-command-registry.ts +52 -27
- package/src/config/agent-schema.ts +217 -134
- package/src/config/assistant-feature-flags.ts +23 -18
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +19 -0
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +7 -4
- package/src/config/bundled-skills/app-builder/tools/app-delete.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +7 -4
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +7 -4
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-query.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +6 -3
- package/src/config/bundled-skills/browser/tools/browser-click.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-close.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-type.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +13 -6
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +5 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +4 -0
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +5 -2
- package/src/config/bundled-skills/computer-use/SKILL.md +2 -2
- package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +6 -3
- package/src/config/bundled-skills/configure-settings/SKILL.md +28 -14
- package/src/config/bundled-skills/contacts/SKILL.md +446 -15
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +99 -20
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +74 -17
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +89 -26
- package/src/config/bundled-skills/document/tools/document-create.ts +5 -2
- package/src/config/bundled-skills/document/tools/document-update.ts +5 -2
- package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -7
- package/src/config/bundled-skills/email-setup/SKILL.md +9 -9
- package/src/config/bundled-skills/followups/tools/followup-create.ts +5 -2
- package/src/config/bundled-skills/followups/tools/followup-list.ts +5 -2
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +5 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +44 -32
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +11 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +13 -7
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +11 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +13 -7
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +28 -12
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +6 -4
- package/src/config/bundled-skills/google-calendar/types.ts +3 -3
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +46 -24
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +36 -19
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +60 -35
- package/src/config/bundled-skills/mcp-setup/SKILL.md +75 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +55 -15
- package/src/config/bundled-skills/media-processing/TOOLS.json +20 -2
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +12 -10
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +34 -19
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +82 -66
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +148 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +1 -1
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +8 -3
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +117 -53
- package/src/config/bundled-skills/media-processing/services/gemini-video.ts +273 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +185 -97
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +32 -27
- package/src/config/bundled-skills/media-processing/services/reduce.ts +101 -24
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +121 -55
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +58 -24
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +177 -91
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +98 -70
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +59 -19
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +26 -10
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +29 -14
- package/src/config/bundled-skills/messaging/SKILL.md +7 -5
- package/src/config/bundled-skills/messaging/TOOLS.json +7 -7
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +31 -13
- package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +16 -10
- package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +18 -9
- package/src/config/bundled-skills/messaging/tools/gmail-download-attachment.ts +23 -16
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +28 -12
- package/src/config/bundled-skills/messaging/tools/gmail-filters.ts +41 -21
- package/src/config/bundled-skills/messaging/tools/gmail-follow-up.ts +44 -23
- package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +73 -33
- package/src/config/bundled-skills/messaging/tools/gmail-label.ts +15 -9
- package/src/config/bundled-skills/messaging/tools/gmail-list-attachments.ts +22 -14
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +99 -50
- package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +14 -8
- package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +63 -44
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +90 -46
- package/src/config/bundled-skills/messaging/tools/gmail-summarize-thread.ts +43 -22
- package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +15 -9
- package/src/config/bundled-skills/messaging/tools/gmail-triage.ts +51 -22
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +62 -26
- package/src/config/bundled-skills/messaging/tools/gmail-vacation.ts +34 -19
- package/src/config/bundled-skills/messaging/tools/google-contacts.ts +32 -16
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +10 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +91 -47
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +21 -9
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +9 -3
- package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +30 -17
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +10 -4
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +14 -6
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +16 -5
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +63 -36
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +10 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +30 -12
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +48 -29
- package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +20 -6
- package/src/config/bundled-skills/messaging/tools/send-notification.ts +1 -1
- package/src/config/bundled-skills/messaging/tools/sequence-analytics.ts +59 -22
- package/src/config/bundled-skills/messaging/tools/sequence-cancel.ts +13 -7
- package/src/config/bundled-skills/messaging/tools/sequence-create.ts +27 -12
- package/src/config/bundled-skills/messaging/tools/sequence-delete.ts +14 -6
- package/src/config/bundled-skills/messaging/tools/sequence-enroll.ts +30 -11
- package/src/config/bundled-skills/messaging/tools/sequence-enrollment-list.ts +16 -8
- package/src/config/bundled-skills/messaging/tools/sequence-get.ts +31 -13
- package/src/config/bundled-skills/messaging/tools/sequence-import.ts +38 -22
- package/src/config/bundled-skills/messaging/tools/sequence-list.ts +16 -7
- package/src/config/bundled-skills/messaging/tools/sequence-pause.ts +29 -10
- package/src/config/bundled-skills/messaging/tools/sequence-resume.ts +16 -8
- package/src/config/bundled-skills/messaging/tools/sequence-update.ts +35 -16
- package/src/config/bundled-skills/messaging/tools/shared.ts +26 -12
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +69 -34
- package/src/config/bundled-skills/notifications/tools/shared.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +46 -48
- package/src/config/bundled-skills/phone-calls/tools/call-end.ts +1 -1
- package/src/config/bundled-skills/phone-calls/tools/call-start.ts +1 -1
- package/src/config/bundled-skills/phone-calls/tools/call-status.ts +1 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +91 -51
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +30 -16
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +66 -27
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +89 -42
- package/src/config/bundled-skills/public-ingress/SKILL.md +26 -19
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +5 -2
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +5 -2
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +5 -2
- package/src/config/bundled-skills/screen-recording/SKILL.md +11 -3
- package/src/config/bundled-skills/self-upgrade/SKILL.md +9 -8
- package/src/config/bundled-skills/slack/TOOLS.json +33 -15
- package/src/config/bundled-skills/slack/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +11 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +11 -5
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +46 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +11 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +28 -0
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +12 -6
- package/src/config/bundled-skills/sms-setup/SKILL.md +5 -8
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-run.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-save.ts +5 -2
- package/src/config/bundled-skills/telegram-setup/SKILL.md +7 -8
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +232 -127
- package/src/config/bundled-skills/twilio-setup/SKILL.md +7 -12
- package/src/config/bundled-skills/twitter/SKILL.md +19 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +5 -5
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +5 -2
- package/src/config/bundled-skills/weather/tools/get-weather.ts +5 -2
- package/src/config/calls-schema.ts +108 -63
- package/src/config/computer-use-prompt.ts +7 -7
- package/src/config/core-schema.ts +239 -155
- package/src/config/defaults.ts +2 -2
- package/src/config/elevenlabs-schema.ts +15 -15
- package/src/config/env-registry.ts +33 -33
- package/src/config/feature-flag-registry.json +31 -7
- package/src/config/loader.ts +118 -58
- package/src/config/mcp-schema.ts +29 -15
- package/src/config/memory-schema.ts +434 -229
- package/src/config/notifications-schema.ts +4 -4
- package/src/config/sandbox-schema.ts +2 -2
- package/src/config/schema.ts +12 -2
- package/src/config/skill-state.ts +27 -15
- package/src/config/skills-schema.ts +72 -23
- package/src/config/skills.ts +303 -143
- package/src/config/system-prompt.ts +25 -6
- package/src/config/types.ts +1 -1
- package/src/config/update-bulletin-format.ts +3 -3
- package/src/config/update-bulletin-state.ts +15 -6
- package/src/config/update-bulletin-template-path.ts +8 -4
- package/src/config/update-bulletin.ts +33 -14
- package/src/config/user-reference.ts +8 -8
- package/src/contacts/contact-events.ts +21 -0
- package/src/contacts/contact-store.ts +622 -100
- package/src/contacts/contacts-write.ts +287 -0
- package/src/contacts/index.ts +13 -4
- package/src/contacts/startup-migration.ts +21 -0
- package/src/contacts/types.ts +47 -2
- package/src/context/token-estimator.ts +54 -31
- package/src/context/tool-result-truncation.ts +41 -7
- package/src/context/window-manager.ts +225 -120
- package/src/daemon/approval-generators.ts +83 -55
- package/src/daemon/approved-devices-store.ts +33 -20
- package/src/daemon/assistant-attachments.ts +134 -98
- package/src/daemon/auth-manager.ts +17 -15
- package/src/daemon/classifier.ts +117 -46
- package/src/daemon/computer-use-session.ts +316 -187
- package/src/daemon/config-watcher.ts +91 -44
- package/src/daemon/connection-policy.ts +18 -10
- package/src/daemon/context-overflow-approval.ts +48 -0
- package/src/daemon/context-overflow-policy.ts +50 -0
- package/src/daemon/context-overflow-reducer.ts +300 -0
- package/src/daemon/daemon-control.ts +79 -51
- package/src/daemon/date-context.ts +119 -69
- package/src/daemon/dictation-profile-store.ts +94 -48
- package/src/daemon/dictation-text-processing.ts +33 -12
- package/src/daemon/doordash-steps.ts +92 -49
- package/src/daemon/guardian-action-generators.ts +62 -46
- package/src/daemon/guardian-verification-intent.ts +31 -18
- package/src/daemon/handlers/apps.ts +257 -111
- package/src/daemon/handlers/avatar.ts +20 -15
- package/src/daemon/handlers/computer-use.ts +82 -39
- package/src/daemon/handlers/config-channels.ts +146 -69
- package/src/daemon/handlers/config-heartbeat.ts +114 -59
- package/src/daemon/handlers/config-inbox.ts +277 -106
- package/src/daemon/handlers/config-ingress.ts +127 -55
- package/src/daemon/handlers/config-integrations.ts +145 -88
- package/src/daemon/handlers/config-model.ts +58 -22
- package/src/daemon/handlers/config-platform.ts +40 -16
- package/src/daemon/handlers/config-scheduling.ts +109 -48
- package/src/daemon/handlers/config-slack-channel.ts +67 -35
- package/src/daemon/handlers/config-slack.ts +21 -20
- package/src/daemon/handlers/config-telegram.ts +100 -70
- package/src/daemon/handlers/config-tools.ts +103 -55
- package/src/daemon/handlers/config-trust.ts +50 -20
- package/src/daemon/handlers/config.ts +72 -24
- package/src/daemon/handlers/contacts.ts +163 -0
- package/src/daemon/handlers/diagnostics.ts +90 -48
- package/src/daemon/handlers/documents.ts +74 -46
- package/src/daemon/handlers/guardian-actions.ts +118 -71
- package/src/daemon/handlers/home-base.ts +19 -16
- package/src/daemon/handlers/identity.ts +65 -45
- package/src/daemon/handlers/index.ts +78 -54
- package/src/daemon/handlers/misc.ts +664 -234
- package/src/daemon/handlers/navigate-settings.ts +14 -11
- package/src/daemon/handlers/oauth-connect.ts +48 -35
- package/src/daemon/handlers/open-bundle-handler.ts +31 -24
- package/src/daemon/handlers/pairing.ts +51 -25
- package/src/daemon/handlers/publish.ts +55 -33
- package/src/daemon/handlers/recording.ts +378 -162
- package/src/daemon/handlers/sessions.ts +923 -423
- package/src/daemon/handlers/shared.ts +202 -117
- package/src/daemon/handlers/signing.ts +25 -6
- package/src/daemon/handlers/subagents.ts +117 -56
- package/src/daemon/handlers/twitter-auth.ts +70 -49
- package/src/daemon/handlers/work-items.ts +264 -112
- package/src/daemon/handlers/workspace-files.ts +27 -20
- package/src/daemon/handlers.ts +2 -2
- package/src/daemon/history-repair.ts +16 -15
- package/src/daemon/identity-helpers.ts +4 -4
- package/src/daemon/install-cli-launchers.ts +33 -22
- package/src/daemon/ipc-blob-store.ts +38 -24
- package/src/daemon/ipc-contract/apps.ts +61 -49
- package/src/daemon/ipc-contract/computer-use.ts +47 -37
- package/src/daemon/ipc-contract/contacts.ts +69 -0
- package/src/daemon/ipc-contract/diagnostics.ts +14 -14
- package/src/daemon/ipc-contract/documents.ts +8 -8
- package/src/daemon/ipc-contract/guardian-actions.ts +4 -4
- package/src/daemon/ipc-contract/inbox.ts +16 -16
- package/src/daemon/ipc-contract/integrations.ts +57 -44
- package/src/daemon/ipc-contract/memory.ts +3 -5
- package/src/daemon/ipc-contract/messages.ts +95 -69
- package/src/daemon/ipc-contract/notifications.ts +10 -6
- package/src/daemon/ipc-contract/pairing.ts +8 -8
- package/src/daemon/ipc-contract/schedules.ts +20 -20
- package/src/daemon/ipc-contract/sessions.ts +88 -57
- package/src/daemon/ipc-contract/settings.ts +12 -7
- package/src/daemon/ipc-contract/shared.ts +9 -7
- package/src/daemon/ipc-contract/skills.ts +46 -26
- package/src/daemon/ipc-contract/subagents.ts +9 -9
- package/src/daemon/ipc-contract/trust.ts +11 -11
- package/src/daemon/ipc-contract/work-items.ts +33 -28
- package/src/daemon/ipc-contract/workspace.ts +28 -21
- package/src/daemon/ipc-contract-inventory.json +8 -0
- package/src/daemon/ipc-contract-inventory.ts +29 -26
- package/src/daemon/ipc-contract.ts +111 -44
- package/src/daemon/ipc-handler.ts +27 -19
- package/src/daemon/ipc-protocol.ts +22 -12
- package/src/daemon/ipc-validate.ts +91 -46
- package/src/daemon/lifecycle.ts +25 -1
- package/src/daemon/main.ts +10 -8
- package/src/daemon/media-visibility-policy.ts +3 -1
- package/src/daemon/pairing-store.ts +72 -40
- package/src/daemon/providers-setup.ts +35 -25
- package/src/daemon/recording-executor.ts +37 -30
- package/src/daemon/recording-intent-fallback.ts +58 -28
- package/src/daemon/recording-intent.ts +71 -61
- package/src/daemon/ride-shotgun-handler.ts +201 -121
- package/src/daemon/seed-files.ts +28 -17
- package/src/daemon/server.ts +23 -14
- package/src/daemon/session-agent-loop-handlers.ts +261 -135
- package/src/daemon/session-agent-loop.ts +795 -253
- package/src/daemon/session-attachments.ts +104 -39
- package/src/daemon/session-conflict-gate.ts +72 -28
- package/src/daemon/session-dynamic-profile.ts +36 -22
- package/src/daemon/session-error.ts +50 -45
- package/src/daemon/session-evictor.ts +17 -10
- package/src/daemon/session-history.ts +201 -89
- package/src/daemon/session-lifecycle.ts +79 -42
- package/src/daemon/session-media-retry.ts +89 -41
- package/src/daemon/session-memory.ts +77 -55
- package/src/daemon/session-messaging.ts +261 -111
- package/src/daemon/session-notifiers.ts +57 -45
- package/src/daemon/session-process.ts +370 -154
- package/src/daemon/session-queue-manager.ts +30 -13
- package/src/daemon/session-runtime-assembly.ts +61 -15
- package/src/daemon/session-skill-tools.ts +84 -36
- package/src/daemon/session-slash.ts +178 -113
- package/src/daemon/session-surfaces.ts +498 -211
- package/src/daemon/session-tool-setup.ts +22 -17
- package/src/daemon/session-usage.ts +26 -13
- package/src/daemon/session-workspace.ts +7 -4
- package/src/daemon/session.ts +18 -19
- package/src/daemon/shutdown-handlers.ts +36 -33
- package/src/daemon/tls-certs.ts +90 -57
- package/src/daemon/tool-side-effects.ts +97 -65
- package/src/daemon/trace-emitter.ts +8 -7
- package/src/daemon/video-thumbnail.ts +55 -25
- package/src/daemon/watch-handler.ts +164 -86
- package/src/email/provider.ts +1 -1
- package/src/email/providers/agentmail.ts +87 -45
- package/src/email/providers/index.ts +19 -14
- package/src/email/service.ts +52 -24
- package/src/email/types.ts +2 -2
- package/src/errors.ts +1 -1
- package/src/events/bus.ts +30 -10
- package/src/events/domain-events.ts +19 -13
- package/src/events/index.ts +6 -6
- package/src/events/tool-audit-listener.ts +34 -20
- package/src/events/tool-domain-event-publisher.ts +22 -20
- package/src/events/tool-metrics-listener.ts +26 -21
- package/src/events/tool-notification-listener.ts +5 -5
- package/src/events/tool-profiling-listener.ts +33 -23
- package/src/events/tool-trace-listener.ts +70 -46
- package/src/export/formatter.ts +38 -32
- package/src/followups/followup-store.ts +43 -36
- package/src/followups/index.ts +2 -2
- package/src/followups/types.ts +1 -1
- package/src/gallery/default-gallery.ts +37 -34
- package/src/gallery/gallery-manifest.ts +9 -9
- package/src/heartbeat/heartbeat-service.ts +59 -37
- package/src/home-base/app-link-store.ts +14 -12
- package/src/home-base/bootstrap.ts +14 -8
- package/src/home-base/prebuilt/seed.ts +35 -26
- package/src/home-base/prebuilt-home-base-updater.ts +14 -8
- package/src/hooks/cli.ts +56 -43
- package/src/hooks/config.ts +27 -14
- package/src/hooks/discovery.ts +53 -33
- package/src/hooks/manager.ts +50 -26
- package/src/hooks/runner.ts +35 -29
- package/src/hooks/templates.ts +38 -15
- package/src/hooks/types.ts +13 -13
- package/src/inbound/platform-callback-registration.ts +21 -15
- package/src/inbound/public-ingress-urls.ts +9 -6
- package/src/index.ts +20 -19
- package/src/influencer/client.ts +269 -108
- package/src/instrument.ts +3 -1
- package/src/logfire.ts +64 -39
- package/src/mcp/client.ts +107 -55
- package/src/mcp/manager.ts +45 -18
- package/src/mcp/mcp-oauth-provider.ts +114 -62
- package/src/media/gemini-image-service.ts +28 -21
- package/src/memory/account-store.ts +16 -9
- package/src/memory/admin.ts +87 -57
- package/src/memory/app-git-service.ts +77 -47
- package/src/memory/app-store.ts +151 -77
- package/src/memory/attachments-store.ts +123 -53
- package/src/memory/canonical-guardian-store.ts +190 -48
- package/src/memory/channel-delivery-store.ts +5 -5
- package/src/memory/channel-guardian-store.ts +31 -16
- package/src/memory/checkpoints.ts +14 -7
- package/src/memory/clarification-resolver.ts +219 -104
- package/src/memory/conflict-intent.ts +74 -23
- package/src/memory/conflict-policy.ts +20 -7
- package/src/memory/conflict-store.ts +144 -94
- package/src/memory/contradiction-checker.ts +257 -132
- package/src/memory/conversation-attention-store.ts +72 -32
- package/src/memory/conversation-bootstrap.ts +28 -0
- package/src/memory/conversation-crud.ts +12 -5
- package/src/memory/conversation-display-order-migration.ts +7 -7
- package/src/memory/conversation-key-store.ts +18 -13
- package/src/memory/conversation-queries.ts +130 -52
- package/src/memory/conversation-store.ts +43 -26
- package/src/memory/conversation-title-service.ts +89 -66
- package/src/memory/db-init.ts +90 -2
- package/src/memory/db.ts +10 -3
- package/src/memory/delivery-channels.ts +12 -6
- package/src/memory/delivery-crud.ts +26 -12
- package/src/memory/delivery-status.ts +19 -16
- package/src/memory/embedding-backend.ts +205 -77
- package/src/memory/embedding-gemini.ts +23 -10
- package/src/memory/embedding-local.ts +89 -44
- package/src/memory/embedding-ollama.ts +25 -13
- package/src/memory/embedding-openai.ts +20 -11
- package/src/memory/embedding-runtime-manager.ts +163 -90
- package/src/memory/entity-extractor.ts +185 -123
- package/src/memory/external-conversation-store.ts +30 -12
- package/src/memory/fingerprint.ts +2 -2
- package/src/memory/fts-reconciler.ts +57 -28
- package/src/memory/guardian-action-store.ts +162 -100
- package/src/memory/guardian-approvals.ts +63 -129
- package/src/memory/guardian-rate-limits.ts +20 -9
- package/src/memory/guardian-verification.ts +82 -35
- package/src/memory/indexer.ts +96 -55
- package/src/memory/ingress-invite-store.ts +28 -169
- package/src/memory/items-extractor.ts +313 -157
- package/src/memory/job-handlers/backfill.ts +116 -63
- package/src/memory/job-handlers/cleanup.ts +64 -41
- package/src/memory/job-handlers/conflict.ts +90 -49
- package/src/memory/job-handlers/embedding.ts +32 -17
- package/src/memory/job-handlers/extraction.ts +58 -33
- package/src/memory/job-handlers/index-maintenance.ts +31 -17
- package/src/memory/job-handlers/media-processing.ts +65 -24
- package/src/memory/job-handlers/summarization.ts +186 -128
- package/src/memory/job-utils.ts +100 -57
- package/src/memory/jobs-store.ts +235 -142
- package/src/memory/jobs-worker.ts +167 -83
- package/src/memory/llm-request-log-store.ts +13 -11
- package/src/memory/llm-usage-store.ts +35 -26
- package/src/memory/media-store.ts +151 -44
- package/src/memory/message-content.ts +28 -18
- package/src/memory/migrations/001-job-deferrals.ts +11 -5
- package/src/memory/migrations/002-tool-invocations-fk.ts +14 -6
- package/src/memory/migrations/003-memory-fts-backfill.ts +11 -5
- package/src/memory/migrations/004-entity-relation-dedup.ts +17 -11
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +36 -21
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +35 -20
- package/src/memory/migrations/007-assistant-id-to-self.ts +40 -27
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +58 -36
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +36 -22
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +21 -11
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +30 -15
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +4 -2
- package/src/memory/migrations/013-guardian-action-tables.ts +29 -11
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +35 -21
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -11
- package/src/memory/migrations/016-memory-segments-indexes.ts +7 -3
- package/src/memory/migrations/017-memory-items-indexes.ts +4 -2
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -5
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +34 -20
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +87 -53
- package/src/memory/migrations/021-conversation-status-indexes.ts +7 -3
- package/src/memory/migrations/022-add-origin-interface.ts +4 -2
- package/src/memory/migrations/023-memory-item-sources-indexes.ts +4 -2
- package/src/memory/migrations/024-embedding-vector-blob.ts +34 -18
- package/src/memory/migrations/025-messages-fts-backfill.ts +11 -5
- package/src/memory/migrations/026-guardian-verification-sessions.ts +80 -14
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +42 -26
- package/src/memory/migrations/027-notification-delivery-pairing-columns.ts +22 -8
- package/src/memory/migrations/027a-guardian-bootstrap-token.ts +11 -3
- package/src/memory/migrations/028-call-session-mode.ts +13 -3
- package/src/memory/migrations/028-notification-delivery-client-ack.ts +22 -8
- package/src/memory/migrations/029-channel-inbound-delivered-segments.ts +7 -3
- package/src/memory/migrations/030-guardian-action-followup.ts +46 -8
- package/src/memory/migrations/030-guardian-verification-purpose.ts +4 -2
- package/src/memory/migrations/031-conversations-thread-type-index.ts +4 -2
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +4 -2
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +22 -8
- package/src/memory/migrations/033-scoped-approval-grants.ts +1 -1
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +15 -3
- package/src/memory/migrations/035-guardian-action-supersession.ts +15 -3
- package/src/memory/migrations/036-normalize-phone-identities.ts +101 -87
- package/src/memory/migrations/037-voice-invite-columns.ts +22 -4
- package/src/memory/migrations/038-actor-token-records.ts +5 -9
- package/src/memory/migrations/039-actor-refresh-token-records.ts +7 -13
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +1 -1
- package/src/memory/migrations/103-complex-migrations.ts +9 -9
- package/src/memory/migrations/104-core-indexes.ts +188 -64
- package/src/memory/migrations/105-contacts-and-triage.ts +28 -10
- package/src/memory/migrations/106-call-sessions.ts +58 -16
- package/src/memory/migrations/107-followups.ts +16 -6
- package/src/memory/migrations/108-tasks-and-work-items.ts +43 -11
- package/src/memory/migrations/109-external-conversation-bindings.ts +11 -5
- package/src/memory/migrations/110-channel-guardian.ts +48 -10
- package/src/memory/migrations/111-media-assets.ts +52 -18
- package/src/memory/migrations/112-assistant-inbox.ts +32 -12
- package/src/memory/migrations/113-late-migrations.ts +12 -12
- package/src/memory/migrations/114-notifications.ts +28 -12
- package/src/memory/migrations/115-sequences.ts +10 -4
- package/src/memory/migrations/116-messages-fts.ts +1 -1
- package/src/memory/migrations/117-conversation-attention.ts +16 -6
- package/src/memory/migrations/118-reminder-routing-intent.ts +7 -3
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +35 -15
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +36 -17
- package/src/memory/migrations/121-canonical-guardian-requests.ts +25 -9
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +11 -3
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +4 -2
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +15 -3
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +22 -4
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +174 -126
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +58 -42
- package/src/memory/migrations/128-contacts-role-principal.ts +26 -0
- package/src/memory/migrations/129-contact-channels-access-fields.ts +105 -0
- package/src/memory/migrations/130-contact-channels-type-ext-chat-id-index.ts +15 -0
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +134 -0
- package/src/memory/migrations/132-contacts-assistant-id.ts +21 -0
- package/src/memory/migrations/index.ts +82 -73
- package/src/memory/migrations/registry.ts +53 -37
- package/src/memory/migrations/validate-migration-state.ts +73 -46
- package/src/memory/profile-compiler.ts +58 -24
- package/src/memory/published-pages-store.ts +12 -16
- package/src/memory/qdrant-circuit-breaker.ts +28 -20
- package/src/memory/qdrant-client.ts +99 -63
- package/src/memory/qdrant-manager.ts +89 -57
- package/src/memory/query-builder.ts +9 -7
- package/src/memory/raw-query.ts +63 -14
- package/src/memory/recall-cache.ts +15 -8
- package/src/memory/retrieval-budget.ts +0 -1
- package/src/memory/retriever.ts +385 -192
- package/src/memory/schema-migration.ts +1 -1
- package/src/memory/schema.ts +44 -56
- package/src/memory/scoped-approval-grants.ts +99 -45
- package/src/memory/search/entity.ts +102 -40
- package/src/memory/search/formatting.ts +70 -52
- package/src/memory/search/lexical.ts +82 -43
- package/src/memory/search/ranking.ts +103 -39
- package/src/memory/search/semantic.ts +59 -35
- package/src/memory/search/types.ts +8 -8
- package/src/memory/segmenter.ts +20 -12
- package/src/memory/shared-app-links-store.ts +21 -16
- package/src/memory/task-memory-cleanup.ts +18 -8
- package/src/memory/tool-usage-store.ts +27 -19
- package/src/memory/validation.ts +4 -2
- package/src/messaging/activity-analyzer.ts +7 -7
- package/src/messaging/draft-store.ts +13 -10
- package/src/messaging/email-classifier.ts +73 -37
- package/src/messaging/index.ts +3 -3
- package/src/messaging/outreach-classifier.ts +76 -38
- package/src/messaging/provider-types.ts +2 -4
- package/src/messaging/provider.ts +37 -8
- package/src/messaging/providers/gmail/adapter.ts +183 -66
- package/src/messaging/providers/gmail/client.ts +3 -1
- package/src/messaging/providers/gmail/mime-builder.ts +21 -19
- package/src/messaging/providers/gmail/people-client.ts +22 -9
- package/src/messaging/providers/gmail/types.ts +6 -6
- package/src/messaging/providers/slack/adapter.ts +93 -43
- package/src/messaging/providers/slack/client.ts +100 -41
- package/src/messaging/providers/slack/types.ts +6 -0
- package/src/messaging/providers/sms/adapter.ts +76 -40
- package/src/messaging/providers/sms/client.ts +4 -4
- package/src/messaging/providers/telegram-bot/adapter.ts +52 -30
- package/src/messaging/providers/telegram-bot/client.ts +7 -7
- package/src/messaging/providers/whatsapp/adapter.ts +58 -31
- package/src/messaging/providers/whatsapp/client.ts +4 -4
- package/src/messaging/registry.ts +9 -5
- package/src/messaging/style-analyzer.ts +69 -39
- package/src/messaging/thread-summarizer.ts +101 -53
- package/src/messaging/triage-engine.ts +111 -82
- package/src/messaging/types.ts +10 -10
- package/src/migrations/config-merge.ts +18 -10
- package/src/migrations/data-layout.ts +35 -22
- package/src/migrations/data-merge.ts +17 -7
- package/src/migrations/hooks-merge.ts +43 -16
- package/src/migrations/index.ts +6 -6
- package/src/migrations/log.ts +9 -5
- package/src/migrations/skills-merge.ts +17 -7
- package/src/migrations/workspace-layout.ts +39 -25
- package/src/notifications/AGENTS.md +5 -0
- package/src/notifications/adapters/macos.ts +21 -14
- package/src/notifications/adapters/sms.ts +28 -15
- package/src/notifications/adapters/telegram.ts +24 -15
- package/src/notifications/broadcaster.ts +108 -52
- package/src/notifications/conversation-pairing.ts +64 -29
- package/src/notifications/copy-composer.ts +165 -95
- package/src/notifications/decision-engine.ts +353 -147
- package/src/notifications/decisions-store.ts +26 -10
- package/src/notifications/deliveries-store.ts +23 -13
- package/src/notifications/destination-resolver.ts +42 -24
- package/src/notifications/deterministic-checks.ts +78 -27
- package/src/notifications/emit-signal.ts +83 -45
- package/src/notifications/events-store.ts +13 -7
- package/src/notifications/guardian-question-mode.ts +125 -75
- package/src/notifications/preference-extractor.ts +85 -53
- package/src/notifications/preference-summary.ts +31 -18
- package/src/notifications/preferences-store.ts +29 -18
- package/src/notifications/runtime-dispatch.ts +22 -12
- package/src/notifications/signal.ts +4 -4
- package/src/notifications/thread-candidates.ts +59 -23
- package/src/notifications/thread-seed-composer.ts +45 -27
- package/src/notifications/types.ts +19 -10
- package/src/oauth/connect-orchestrator.ts +105 -54
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/provider-profiles.ts +80 -59
- package/src/oauth/scope-policy.ts +5 -2
- package/src/oauth/token-persistence.ts +58 -24
- package/src/outbound-proxy/certs.ts +284 -0
- package/src/outbound-proxy/config.ts +94 -0
- package/src/outbound-proxy/connect-tunnel.ts +84 -0
- package/src/outbound-proxy/health.ts +62 -0
- package/src/outbound-proxy/host-pattern-match.ts +67 -0
- package/src/outbound-proxy/http-forwarder.ts +162 -0
- package/src/outbound-proxy/index.ts +80 -0
- package/src/outbound-proxy/logging.ts +193 -0
- package/src/outbound-proxy/mitm-handler.ts +292 -0
- package/src/outbound-proxy/policy.ts +172 -0
- package/src/outbound-proxy/router.ts +64 -0
- package/src/outbound-proxy/server.ts +145 -0
- package/src/outbound-proxy/types.ts +150 -0
- package/src/permissions/checker.ts +481 -189
- package/src/permissions/defaults.ts +135 -108
- package/src/permissions/prompter.ts +53 -27
- package/src/permissions/secret-prompter.ts +21 -15
- package/src/permissions/shell-identity.ts +47 -16
- package/src/permissions/trust-store.ts +185 -73
- package/src/permissions/types.ts +22 -12
- package/src/permissions/workspace-policy.ts +47 -38
- package/src/playbooks/index.ts +10 -2
- package/src/playbooks/playbook-compiler.ts +30 -24
- package/src/playbooks/types.ts +11 -8
- package/src/providers/anthropic/client.ts +325 -168
- package/src/providers/failover.ts +57 -22
- package/src/providers/fireworks/client.ts +9 -5
- package/src/providers/gemini/client.ts +61 -39
- package/src/providers/model-intents.ts +40 -33
- package/src/providers/ollama/client.ts +7 -7
- package/src/providers/openai/client.ts +106 -68
- package/src/providers/openrouter/client.ts +9 -5
- package/src/providers/provider-send-message.ts +59 -27
- package/src/providers/ratelimit.ts +25 -8
- package/src/providers/registry.ts +86 -38
- package/src/providers/retry.ts +84 -36
- package/src/providers/stream-timeout.ts +5 -3
- package/src/providers/types.ts +7 -6
- package/src/runtime/AGENTS.md +42 -0
- package/src/runtime/access-request-helper.ts +118 -68
- package/src/runtime/actor-refresh-token-store.ts +21 -16
- package/src/runtime/actor-token-store.ts +25 -18
- package/src/runtime/actor-trust-resolver.ts +183 -80
- package/src/runtime/approval-conversation-turn.ts +39 -26
- package/src/runtime/approval-message-composer.ts +116 -84
- package/src/runtime/assistant-event-hub.ts +25 -6
- package/src/runtime/assistant-event.ts +4 -4
- package/src/runtime/assistant-scope.ts +1 -1
- package/src/runtime/auth/__tests__/guard-tests.test.ts +36 -14
- package/src/runtime/auth/context.ts +8 -7
- package/src/runtime/auth/credential-service.ts +60 -38
- package/src/runtime/auth/external-assistant-id.ts +16 -8
- package/src/runtime/auth/index.ts +23 -16
- package/src/runtime/auth/route-policy.ts +170 -104
- package/src/runtime/auth/scopes.ts +22 -29
- package/src/runtime/auth/subject.ts +19 -13
- package/src/runtime/auth/token-service.ts +3 -3
- package/src/runtime/auth/types.ts +23 -23
- package/src/runtime/channel-approval-parser.ts +37 -14
- package/src/runtime/channel-approval-types.ts +12 -4
- package/src/runtime/channel-approvals.ts +41 -23
- package/src/runtime/channel-guardian-service.ts +144 -103
- package/src/runtime/channel-invite-transport.ts +4 -2
- package/src/runtime/channel-invite-transports/telegram.ts +16 -10
- package/src/runtime/channel-invite-transports/voice.ts +7 -7
- package/src/runtime/channel-readiness-service.ts +139 -90
- package/src/runtime/channel-readiness-types.ts +4 -2
- package/src/runtime/channel-reply-delivery.ts +21 -11
- package/src/runtime/channel-retry-sweep.ts +111 -62
- package/src/runtime/confirmation-request-guardian-bridge.ts +73 -54
- package/src/runtime/gateway-client.ts +86 -53
- package/src/runtime/guardian-action-conversation-turn.ts +34 -18
- package/src/runtime/guardian-action-followup-executor.ts +115 -45
- package/src/runtime/guardian-action-grant-minter.ts +40 -24
- package/src/runtime/guardian-action-message-composer.ts +105 -84
- package/src/runtime/guardian-decision-types.ts +28 -13
- package/src/runtime/guardian-outbound-actions.ts +9 -0
- package/src/runtime/guardian-reply-router.ts +274 -145
- package/src/runtime/guardian-vellum-migration.ts +38 -24
- package/src/runtime/guardian-verification-templates.ts +8 -11
- package/src/runtime/http-router.ts +175 -0
- package/src/runtime/http-server.ts +931 -669
- package/src/runtime/http-types.ts +2 -2
- package/src/runtime/ingress-service.ts +182 -89
- package/src/runtime/invite-redemption-service.ts +211 -134
- package/src/runtime/invite-redemption-templates.ts +18 -11
- package/src/runtime/local-actor-identity.ts +73 -55
- package/src/runtime/middleware/auth.ts +25 -14
- package/src/runtime/middleware/error-handler.ts +15 -11
- package/src/runtime/middleware/rate-limiter.ts +23 -17
- package/src/runtime/middleware/request-logger.ts +4 -4
- package/src/runtime/middleware/twilio-validation.ts +29 -20
- package/src/runtime/migrations/migration-transport.ts +575 -0
- package/src/runtime/migrations/migration-wizard.ts +715 -0
- package/src/runtime/migrations/rebind-secrets-screen.ts +351 -0
- package/src/runtime/migrations/transfer-progress-screen.ts +321 -0
- package/src/runtime/migrations/validation-results-screen.ts +467 -0
- package/src/runtime/migrations/vbundle-builder.ts +295 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +212 -0
- package/src/runtime/migrations/vbundle-importer.ts +339 -0
- package/src/runtime/migrations/vbundle-validator.ts +356 -0
- package/src/runtime/pending-interactions.ts +16 -7
- package/src/runtime/routes/access-request-decision.ts +73 -52
- package/src/runtime/routes/app-routes.ts +56 -38
- package/src/runtime/routes/approval-routes.ts +165 -74
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +930 -0
- package/src/runtime/routes/approval-strategies/guardian-legacy-fallback-strategy.ts +82 -0
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +151 -0
- package/src/runtime/routes/attachment-routes.ts +59 -48
- package/src/runtime/routes/brain-graph-routes.ts +85 -69
- package/src/runtime/routes/call-routes.ts +79 -38
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +10 -10
- package/src/runtime/routes/channel-delivery-routes.ts +19 -14
- package/src/runtime/routes/channel-guardian-routes.ts +3 -3
- package/src/runtime/routes/channel-inbound-routes.ts +2 -2
- package/src/runtime/routes/channel-readiness-routes.ts +12 -6
- package/src/runtime/routes/channel-route-shared.ts +33 -25
- package/src/runtime/routes/channel-routes.ts +4 -6
- package/src/runtime/routes/contact-routes.ts +205 -16
- package/src/runtime/routes/conversation-attention-routes.ts +57 -28
- package/src/runtime/routes/conversation-routes.ts +321 -174
- package/src/runtime/routes/debug-routes.ts +14 -10
- package/src/runtime/routes/events-routes.ts +90 -57
- package/src/runtime/routes/global-search-routes.ts +266 -0
- package/src/runtime/routes/guardian-action-routes.ts +147 -56
- package/src/runtime/routes/guardian-approval-interception.ts +255 -880
- package/src/runtime/routes/guardian-approval-prompt.ts +40 -24
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +135 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +55 -36
- package/src/runtime/routes/guardian-expiry-sweep.ts +63 -37
- package/src/runtime/routes/guardian-refresh-routes.ts +40 -19
- package/src/runtime/routes/identity-routes.ts +71 -42
- package/src/runtime/routes/inbound-conversation.ts +17 -11
- package/src/runtime/routes/inbound-message-handler.ts +278 -1460
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +658 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +492 -0
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +214 -0
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +116 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +167 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +185 -0
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +132 -0
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +340 -0
- package/src/runtime/routes/ingress-routes.ts +34 -23
- package/src/runtime/routes/integration-routes.ts +60 -21
- package/src/runtime/routes/migration-routes.ts +434 -0
- package/src/runtime/routes/pairing-routes.ts +157 -79
- package/src/runtime/routes/secret-routes.ts +6 -2
- package/src/runtime/routes/twilio-routes.ts +443 -249
- package/src/runtime/tool-grant-request-helper.ts +36 -27
- package/src/runtime/{guardian-context-resolver.ts → trust-context-resolver.ts} +29 -41
- package/src/schedule/integration-status.ts +44 -9
- package/src/schedule/recurrence-engine.ts +47 -24
- package/src/schedule/recurrence-types.ts +12 -7
- package/src/schedule/schedule-store.ts +166 -83
- package/src/schedule/scheduler.ts +26 -22
- package/src/security/encrypted-store.ts +68 -38
- package/src/security/keychain.ts +183 -120
- package/src/security/oauth-callback-registry.ts +3 -3
- package/src/security/oauth2.ts +226 -138
- package/src/security/redaction.ts +24 -24
- package/src/security/secret-allowlist.ts +46 -21
- package/src/security/secret-ingress.ts +15 -7
- package/src/security/secret-scanner.ts +193 -104
- package/src/security/secure-keys.ts +9 -3
- package/src/security/token-manager.ts +99 -40
- package/src/security/tool-approval-digest.ts +3 -3
- package/src/sequence/analytics.ts +52 -27
- package/src/sequence/engine.ts +135 -72
- package/src/sequence/guardrails.ts +32 -20
- package/src/sequence/importer.ts +75 -37
- package/src/sequence/reply-matcher.ts +36 -18
- package/src/sequence/store.ts +137 -75
- package/src/sequence/types.ts +30 -16
- package/src/services/published-app-updater.ts +26 -16
- package/src/services/vercel-deploy.ts +19 -15
- package/src/skills/active-skill-tools.ts +3 -3
- package/src/skills/clawhub.ts +178 -90
- package/src/skills/include-graph.ts +24 -17
- package/src/skills/managed-store.ts +89 -42
- package/src/skills/path-classifier.ts +10 -10
- package/src/skills/remote-skill-policy.ts +31 -22
- package/src/skills/slash-commands.ts +36 -30
- package/src/skills/tool-manifest.ts +60 -31
- package/src/skills/version-hash.ts +25 -15
- package/src/slack/slack-webhook.ts +19 -15
- package/src/subagent/index.ts +4 -8
- package/src/subagent/manager.ts +119 -69
- package/src/subagent/types.ts +9 -12
- package/src/swarm/backend-claude-code.ts +124 -45
- package/src/swarm/checkpoint.ts +36 -16
- package/src/swarm/graph-utils.ts +1 -3
- package/src/swarm/index.ts +38 -19
- package/src/swarm/limits.ts +13 -4
- package/src/swarm/orchestrator.ts +108 -57
- package/src/swarm/plan-validator.ts +23 -17
- package/src/swarm/router-planner.ts +51 -22
- package/src/swarm/router-prompts.ts +4 -1
- package/src/swarm/synthesizer.ts +26 -18
- package/src/swarm/types.ts +14 -4
- package/src/swarm/worker-backend.ts +36 -26
- package/src/swarm/worker-prompts.ts +13 -9
- package/src/swarm/worker-runner.ts +40 -34
- package/src/tasks/candidate-store.ts +14 -6
- package/src/tasks/ephemeral-permissions.ts +9 -5
- package/src/tasks/task-compiler.ts +41 -38
- package/src/tasks/task-runner.ts +54 -26
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/task-store.ts +20 -7
- package/src/tasks/tool-sanitizer.ts +3 -3
- package/src/tools/apps/definitions.ts +23 -15
- package/src/tools/apps/executors.ts +118 -37
- package/src/tools/apps/open-proxy.ts +5 -5
- package/src/tools/apps/registry.ts +2 -2
- package/src/tools/assets/materialize.ts +59 -41
- package/src/tools/assets/search.ts +86 -48
- package/src/tools/browser/api-map.ts +52 -36
- package/src/tools/browser/auth-cache.ts +21 -18
- package/src/tools/browser/auth-detector.ts +43 -28
- package/src/tools/browser/auto-navigate.ts +149 -68
- package/src/tools/browser/browser-execution.ts +9 -3
- package/src/tools/browser/headless-browser.ts +287 -150
- package/src/tools/browser/jit-auth.ts +37 -21
- package/src/tools/browser/network-recorder.ts +138 -56
- package/src/tools/browser/recording-store.ts +22 -15
- package/src/tools/browser/runtime-check.ts +8 -5
- package/src/tools/browser/x-auto-navigate.ts +88 -47
- package/src/tools/calls/call-end.ts +9 -6
- package/src/tools/calls/call-start.ts +30 -20
- package/src/tools/calls/call-status.ts +8 -5
- package/src/tools/claude-code/claude-code.ts +301 -165
- package/src/tools/computer-use/definitions.ts +159 -130
- package/src/tools/computer-use/registry.ts +2 -2
- package/src/tools/computer-use/request-computer-control.ts +21 -13
- package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
- package/src/tools/credentials/account-registry.ts +52 -35
- package/src/tools/credentials/broker-types.ts +1 -1
- package/src/tools/credentials/broker.ts +97 -55
- package/src/tools/credentials/domain-policy.ts +5 -2
- package/src/tools/credentials/host-pattern-match.ts +15 -8
- package/src/tools/credentials/metadata-store.ts +93 -43
- package/src/tools/credentials/policy-types.ts +5 -2
- package/src/tools/credentials/policy-validate.ts +21 -14
- package/src/tools/credentials/post-connect-hooks.ts +18 -7
- package/src/tools/credentials/resolve.ts +11 -10
- package/src/tools/credentials/selection.ts +30 -25
- package/src/tools/credentials/tool-policy.ts +5 -2
- package/src/tools/credentials/vault.ts +452 -183
- package/src/tools/document/document-tool.ts +23 -17
- package/src/tools/document/editor-template.ts +12 -7
- package/src/tools/execution-target.ts +13 -10
- package/src/tools/execution-timeout.ts +6 -5
- package/src/tools/executor.ts +141 -74
- package/src/tools/filesystem/edit.ts +82 -45
- package/src/tools/filesystem/fuzzy-match.ts +70 -32
- package/src/tools/filesystem/read.ts +46 -28
- package/src/tools/filesystem/view-image.ts +86 -42
- package/src/tools/filesystem/write.ts +53 -32
- package/src/tools/followups/followup_create.ts +43 -17
- package/src/tools/followups/followup_list.ts +28 -13
- package/src/tools/followups/followup_resolve.ts +9 -6
- package/src/tools/guardian-control-plane-policy.ts +15 -14
- package/src/tools/host-filesystem/edit.ts +77 -42
- package/src/tools/host-filesystem/read.ts +52 -33
- package/src/tools/host-filesystem/write.ts +50 -29
- package/src/tools/host-terminal/host-shell.ts +97 -61
- package/src/tools/mcp/mcp-tool-factory.ts +21 -14
- package/src/tools/memory/definitions.ts +60 -28
- package/src/tools/memory/handlers.ts +149 -77
- package/src/tools/memory/register.ts +39 -16
- package/src/tools/network/__tests__/web-search.test.ts +236 -177
- package/src/tools/network/domain-normalize.ts +13 -9
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +193 -123
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +225 -127
- package/src/tools/network/script-proxy/index.ts +1 -17
- package/src/tools/network/script-proxy/session-manager.ts +151 -84
- package/src/tools/network/url-safety.ts +56 -34
- package/src/tools/network/web-fetch.ts +273 -155
- package/src/tools/network/web-search.ts +166 -81
- package/src/tools/permission-checker.ts +6 -25
- package/src/tools/policy-context.ts +8 -5
- package/src/tools/registry.ts +73 -46
- package/src/tools/reminder/reminder-store.ts +65 -44
- package/src/tools/reminder/reminder.ts +76 -35
- package/src/tools/schedule/create.ts +44 -21
- package/src/tools/schedule/delete.ts +8 -5
- package/src/tools/schedule/list.ts +39 -19
- package/src/tools/schedule/update.ts +49 -26
- package/src/tools/secret-detection-handler.ts +130 -49
- package/src/tools/sensitive-output-placeholders.ts +15 -8
- package/src/tools/shared/filesystem/edit-engine.ts +45 -14
- package/src/tools/shared/filesystem/errors.ts +18 -18
- package/src/tools/shared/filesystem/file-ops-service.ts +59 -32
- package/src/tools/shared/filesystem/format-diff.ts +21 -11
- package/src/tools/shared/filesystem/path-policy.ts +17 -13
- package/src/tools/shared/filesystem/size-guard.ts +8 -4
- package/src/tools/shared/filesystem/types.ts +2 -2
- package/src/tools/shared/shell-output.ts +4 -3
- package/src/tools/side-effects.ts +36 -28
- package/src/tools/skills/delete-managed.ts +30 -17
- package/src/tools/skills/load.ts +88 -46
- package/src/tools/skills/sandbox-runner.ts +62 -46
- package/src/tools/skills/scaffold-managed.ts +98 -48
- package/src/tools/skills/script-contract.ts +5 -2
- package/src/tools/skills/skill-script-runner.ts +29 -13
- package/src/tools/skills/skill-tool-factory.ts +20 -10
- package/src/tools/subagent/abort.ts +10 -4
- package/src/tools/subagent/message.ts +14 -8
- package/src/tools/subagent/read.ts +20 -11
- package/src/tools/subagent/spawn.ts +14 -6
- package/src/tools/subagent/status.ts +7 -4
- package/src/tools/swarm/delegate.ts +75 -49
- package/src/tools/system/avatar-generator.ts +46 -33
- package/src/tools/system/navigate-settings.ts +29 -19
- package/src/tools/system/open-system-settings.ts +30 -20
- package/src/tools/system/request-permission.ts +59 -44
- package/src/tools/system/version.ts +27 -16
- package/src/tools/system/voice-config.ts +116 -53
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +61 -22
- package/src/tools/tasks/task-list.ts +23 -11
- package/src/tools/tasks/task-run.ts +41 -16
- package/src/tools/tasks/task-save.ts +27 -10
- package/src/tools/tasks/work-item-enqueue.ts +114 -48
- package/src/tools/tasks/work-item-list.ts +20 -10
- package/src/tools/tasks/work-item-remove.ts +49 -15
- package/src/tools/tasks/work-item-run.ts +34 -13
- package/src/tools/tasks/work-item-update.ts +84 -31
- package/src/tools/terminal/backends/native.ts +64 -35
- package/src/tools/terminal/backends/types.ts +6 -2
- package/src/tools/terminal/parser.ts +200 -125
- package/src/tools/terminal/safe-env.ts +27 -21
- package/src/tools/terminal/sandbox-diagnostics.ts +31 -13
- package/src/tools/terminal/sandbox.ts +10 -6
- package/src/tools/terminal/shell.ts +124 -68
- package/src/tools/tool-approval-handler.ts +193 -138
- package/src/tools/types.ts +43 -23
- package/src/tools/ui-surface/definitions.ts +124 -89
- package/src/tools/ui-surface/registry.ts +2 -2
- package/src/tools/watch/screen-watch.ts +50 -32
- package/src/tools/watch/watch-state.ts +41 -15
- package/src/tools/watcher/create.ts +37 -15
- package/src/tools/watcher/delete.ts +9 -6
- package/src/tools/watcher/digest.ts +10 -6
- package/src/tools/watcher/list.ts +37 -14
- package/src/tools/watcher/update.ts +33 -18
- package/src/tools/weather/service.ts +331 -174
- package/src/twitter/client.ts +261 -138
- package/src/twitter/oauth-client.ts +17 -13
- package/src/twitter/router.ts +51 -23
- package/src/twitter/session.ts +27 -18
- package/src/types/qrcode.d.ts +6 -3
- package/src/usage/actors.ts +16 -16
- package/src/usage/types.ts +3 -3
- package/src/util/bundled-asset.ts +10 -6
- package/src/util/canonicalize-identity.ts +11 -4
- package/src/util/clipboard.ts +7 -7
- package/src/util/content-id.ts +3 -3
- package/src/util/debounce.ts +3 -2
- package/src/util/diff.ts +55 -33
- package/src/util/errors.ts +26 -26
- package/src/util/fs.ts +8 -2
- package/src/util/log-redact.ts +12 -12
- package/src/util/logger.ts +112 -51
- package/src/util/network-info.ts +13 -5
- package/src/util/object.ts +4 -2
- package/src/util/phone.ts +4 -4
- package/src/util/platform.ts +80 -58
- package/src/util/pricing.ts +49 -31
- package/src/util/retry.ts +18 -7
- package/src/util/row-mapper.ts +7 -4
- package/src/util/silently.ts +7 -4
- package/src/util/spawn.ts +48 -0
- package/src/util/spinner.ts +9 -7
- package/src/util/time.ts +16 -3
- package/src/util/truncate.ts +1 -1
- package/src/util/voice-code.ts +6 -4
- package/src/util/xml.ts +5 -1
- package/src/version.ts +12 -8
- package/src/watcher/engine.ts +71 -44
- package/src/watcher/provider-registry.ts +1 -1
- package/src/watcher/providers/github.ts +40 -23
- package/src/watcher/providers/gmail.ts +59 -38
- package/src/watcher/providers/google-calendar.ts +62 -48
- package/src/watcher/providers/linear.ts +219 -150
- package/src/watcher/providers/slack.ts +93 -27
- package/src/watcher/watcher-store.ts +75 -55
- package/src/work-items/work-item-runner.ts +62 -29
- package/src/work-items/work-item-store.ts +137 -47
- package/src/workspace/commit-message-enrichment-service.ts +65 -25
- package/src/workspace/commit-message-provider.ts +14 -12
- package/src/workspace/git-service.ts +355 -239
- package/src/workspace/heartbeat-service.ts +74 -37
- package/src/workspace/provider-commit-message-generator.ts +95 -70
- package/src/workspace/top-level-renderer.ts +10 -8
- package/src/workspace/top-level-scanner.ts +9 -3
- package/src/workspace/turn-commit.ts +63 -36
- package/src/__tests__/ingress-member-store.test.ts +0 -294
- package/src/__tests__/script-proxy-router.test.ts +0 -215
- package/src/config/bundled-skills/trusted-contacts/SKILL.md +0 -372
- package/src/memory/guardian-bindings.ts +0 -158
- package/src/memory/ingress-member-store.ts +0 -352
- package/src/tools/network/script-proxy/__tests__/router.test.ts +0 -77
- package/src/tools/network/script-proxy/certs.ts +0 -7
- package/src/tools/network/script-proxy/connect-tunnel.ts +0 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +0 -2
- package/src/tools/network/script-proxy/logging.ts +0 -12
- package/src/tools/network/script-proxy/mitm-handler.ts +0 -2
- package/src/tools/network/script-proxy/policy.ts +0 -4
- package/src/tools/network/script-proxy/router.ts +0 -2
- package/src/tools/network/script-proxy/server.ts +0 -5
- package/src/tools/network/script-proxy/types.ts +0 -19
|
@@ -0,0 +1,1854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for macOS/iOS parity and persistence/resume behavior.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Cross-platform parity: shared modules produce identical results regardless
|
|
6
|
+
* of platform context. Serialized state is portable between macOS and iOS.
|
|
7
|
+
* - Persistence/resume at every wizard step: serialize, deserialize, resume,
|
|
8
|
+
* verify correct screen states.
|
|
9
|
+
* - Interrupted flows: crash during transfer, partial completion, loading states.
|
|
10
|
+
* - Full end-to-end flows: managed-to-self-hosted and self-hosted-to-managed
|
|
11
|
+
* through all screen view models.
|
|
12
|
+
* - Edge cases: timeouts during polling, stale/expired states, concurrent wizard
|
|
13
|
+
* instances, state corruption recovery, bundle data loss after deserialization.
|
|
14
|
+
* - Cross-screen consistency: screen transitions maintain consistent state across
|
|
15
|
+
* validation -> transfer -> rebind screens.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
ImportCommitResponse,
|
|
22
|
+
ImportPreflightResponse,
|
|
23
|
+
TransportConfig,
|
|
24
|
+
ValidateResponse,
|
|
25
|
+
} from "../runtime/migrations/migration-transport.js";
|
|
26
|
+
import type {
|
|
27
|
+
MigrationDirection,
|
|
28
|
+
MigrationWizardState,
|
|
29
|
+
StepExecutorOptions,
|
|
30
|
+
WizardStep,
|
|
31
|
+
} from "../runtime/migrations/migration-wizard.js";
|
|
32
|
+
import {
|
|
33
|
+
canRetryCurrentStep,
|
|
34
|
+
completeRebindSecrets,
|
|
35
|
+
createWizardState,
|
|
36
|
+
deserializeWizardState,
|
|
37
|
+
getCurrentStepIndex,
|
|
38
|
+
getStepOrder,
|
|
39
|
+
getTotalSteps,
|
|
40
|
+
goBackTo,
|
|
41
|
+
isResumable,
|
|
42
|
+
isStepAccessible,
|
|
43
|
+
isWizardComplete,
|
|
44
|
+
prepareForResume,
|
|
45
|
+
resetStepForRetry,
|
|
46
|
+
selectDirection,
|
|
47
|
+
serializeWizardState,
|
|
48
|
+
setBundleUploaded,
|
|
49
|
+
validateWizardTransition,
|
|
50
|
+
} from "../runtime/migrations/migration-wizard.js";
|
|
51
|
+
import type { RebindTaskCompletionState } from "../runtime/migrations/rebind-secrets-screen.js";
|
|
52
|
+
import {
|
|
53
|
+
areAllRequiredTasksComplete,
|
|
54
|
+
completeMigration,
|
|
55
|
+
createTaskCompletionState,
|
|
56
|
+
deriveRebindSecretsScreenState,
|
|
57
|
+
getTaskIds,
|
|
58
|
+
isRebindSecretsScreenAccessible,
|
|
59
|
+
markTaskComplete,
|
|
60
|
+
} from "../runtime/migrations/rebind-secrets-screen.js";
|
|
61
|
+
import {
|
|
62
|
+
deriveTransferScreenState,
|
|
63
|
+
executeTransferFlow,
|
|
64
|
+
isTransferScreenAccessible,
|
|
65
|
+
} from "../runtime/migrations/transfer-progress-screen.js";
|
|
66
|
+
import {
|
|
67
|
+
deriveValidationScreenState,
|
|
68
|
+
executeValidationFlow,
|
|
69
|
+
isValidationScreenAccessible,
|
|
70
|
+
} from "../runtime/migrations/validation-results-screen.js";
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Test helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function mockFetch(
|
|
77
|
+
status: number,
|
|
78
|
+
body: unknown,
|
|
79
|
+
headers?: Record<string, string>,
|
|
80
|
+
): typeof fetch {
|
|
81
|
+
return (async () => {
|
|
82
|
+
const responseHeaders = new Headers(headers);
|
|
83
|
+
if (
|
|
84
|
+
typeof body === "object" &&
|
|
85
|
+
body !== undefined &&
|
|
86
|
+
!(body instanceof ArrayBuffer)
|
|
87
|
+
) {
|
|
88
|
+
responseHeaders.set("Content-Type", "application/json");
|
|
89
|
+
return new Response(JSON.stringify(body), {
|
|
90
|
+
status,
|
|
91
|
+
headers: responseHeaders,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (body instanceof ArrayBuffer) {
|
|
95
|
+
return new Response(body, { status, headers: responseHeaders });
|
|
96
|
+
}
|
|
97
|
+
return new Response(String(body), { status, headers: responseHeaders });
|
|
98
|
+
}) as unknown as typeof fetch;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runtimeConfig(overrides?: Partial<TransportConfig>): TransportConfig {
|
|
102
|
+
return {
|
|
103
|
+
baseURL: "http://localhost:7821",
|
|
104
|
+
target: "runtime",
|
|
105
|
+
authHeader: "Bearer test-jwt",
|
|
106
|
+
fetchFn: mockFetch(200, {}),
|
|
107
|
+
...overrides,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function managedConfig(overrides?: Partial<TransportConfig>): TransportConfig {
|
|
112
|
+
return {
|
|
113
|
+
baseURL: "https://platform.vellum.ai",
|
|
114
|
+
target: "managed",
|
|
115
|
+
authHeader: "session-token-abc",
|
|
116
|
+
fetchFn: mockFetch(200, {}),
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function makeExecutorOptions(
|
|
122
|
+
overrides?: Partial<StepExecutorOptions>,
|
|
123
|
+
): StepExecutorOptions {
|
|
124
|
+
return {
|
|
125
|
+
sourceConfig: runtimeConfig(),
|
|
126
|
+
destConfig: runtimeConfig(),
|
|
127
|
+
bundleData: new ArrayBuffer(16),
|
|
128
|
+
...overrides,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function makeValidateSuccess(): ValidateResponse {
|
|
133
|
+
return {
|
|
134
|
+
is_valid: true,
|
|
135
|
+
errors: [],
|
|
136
|
+
manifest: {
|
|
137
|
+
schema_version: "1.0",
|
|
138
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
139
|
+
source: "test",
|
|
140
|
+
description: "Test bundle",
|
|
141
|
+
files: [
|
|
142
|
+
{ path: "config.json", sha256: "abc123", size: 1024 },
|
|
143
|
+
{ path: "skills/test.md", sha256: "def456", size: 2048 },
|
|
144
|
+
],
|
|
145
|
+
manifest_sha256: "manifest-hash",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function makePreflightSuccess(): ImportPreflightResponse {
|
|
151
|
+
return {
|
|
152
|
+
can_import: true,
|
|
153
|
+
summary: {
|
|
154
|
+
total_files: 3,
|
|
155
|
+
files_to_create: 1,
|
|
156
|
+
files_to_overwrite: 1,
|
|
157
|
+
files_unchanged: 1,
|
|
158
|
+
files_to_skip: 0,
|
|
159
|
+
},
|
|
160
|
+
files: [
|
|
161
|
+
{
|
|
162
|
+
path: "config.json",
|
|
163
|
+
action: "overwrite",
|
|
164
|
+
bundle_size: 1024,
|
|
165
|
+
current_size: 800,
|
|
166
|
+
bundle_sha256: "abc123",
|
|
167
|
+
current_sha256: "old123",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
path: "skills/new-skill.md",
|
|
171
|
+
action: "create",
|
|
172
|
+
bundle_size: 512,
|
|
173
|
+
current_size: null,
|
|
174
|
+
bundle_sha256: "ghi789",
|
|
175
|
+
current_sha256: null,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
path: "playbooks/default.md",
|
|
179
|
+
action: "unchanged",
|
|
180
|
+
bundle_size: 2048,
|
|
181
|
+
current_size: 2048,
|
|
182
|
+
bundle_sha256: "mno345",
|
|
183
|
+
current_sha256: "mno345",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
conflicts: [],
|
|
187
|
+
manifest: {
|
|
188
|
+
schema_version: "1.0",
|
|
189
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
190
|
+
files: [
|
|
191
|
+
{ path: "config.json", sha256: "abc123", size: 1024 },
|
|
192
|
+
{ path: "skills/new-skill.md", sha256: "ghi789", size: 512 },
|
|
193
|
+
],
|
|
194
|
+
manifest_sha256: "manifest-hash",
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function makeImportSuccess(): ImportCommitResponse {
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
summary: {
|
|
203
|
+
total_files: 3,
|
|
204
|
+
files_created: 1,
|
|
205
|
+
files_overwritten: 1,
|
|
206
|
+
files_skipped: 1,
|
|
207
|
+
backups_created: 1,
|
|
208
|
+
},
|
|
209
|
+
files: [
|
|
210
|
+
{
|
|
211
|
+
path: "config.json",
|
|
212
|
+
disk_path: "/data/config.json",
|
|
213
|
+
action: "overwritten",
|
|
214
|
+
size: 1024,
|
|
215
|
+
sha256: "abc123",
|
|
216
|
+
backup_path: "/data/config.json.bak",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
manifest: {
|
|
220
|
+
schema_version: "1.0",
|
|
221
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
222
|
+
files: [{ path: "config.json", sha256: "abc123", size: 1024 }],
|
|
223
|
+
manifest_sha256: "manifest-hash",
|
|
224
|
+
},
|
|
225
|
+
warnings: ["Backup created for config.json"],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Advance state to a specific step for testing. */
|
|
230
|
+
function advanceTo(
|
|
231
|
+
step: WizardStep,
|
|
232
|
+
direction: MigrationDirection = "managed-to-self-hosted",
|
|
233
|
+
): MigrationWizardState {
|
|
234
|
+
let state = createWizardState();
|
|
235
|
+
const stepOrder: WizardStep[] = [
|
|
236
|
+
"select-direction",
|
|
237
|
+
"upload-bundle",
|
|
238
|
+
"validate",
|
|
239
|
+
"preflight-review",
|
|
240
|
+
"transfer",
|
|
241
|
+
"rebind-secrets",
|
|
242
|
+
"complete",
|
|
243
|
+
];
|
|
244
|
+
const targetIdx = stepOrder.indexOf(step);
|
|
245
|
+
|
|
246
|
+
if (targetIdx >= 1) {
|
|
247
|
+
state = selectDirection(state, direction);
|
|
248
|
+
}
|
|
249
|
+
if (targetIdx >= 2) {
|
|
250
|
+
state = setBundleUploaded(state);
|
|
251
|
+
}
|
|
252
|
+
if (targetIdx >= 3) {
|
|
253
|
+
state = {
|
|
254
|
+
...state,
|
|
255
|
+
steps: { ...state.steps, validate: { status: "success" } },
|
|
256
|
+
validateResult: makeValidateSuccess(),
|
|
257
|
+
currentStep: "preflight-review",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (targetIdx >= 4) {
|
|
261
|
+
state = {
|
|
262
|
+
...state,
|
|
263
|
+
steps: { ...state.steps, "preflight-review": { status: "success" } },
|
|
264
|
+
preflightResult: makePreflightSuccess(),
|
|
265
|
+
currentStep: "transfer",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (targetIdx >= 5) {
|
|
269
|
+
state = {
|
|
270
|
+
...state,
|
|
271
|
+
steps: { ...state.steps, transfer: { status: "success" } },
|
|
272
|
+
importResult: makeImportSuccess(),
|
|
273
|
+
currentStep: "rebind-secrets",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (targetIdx >= 6) {
|
|
277
|
+
state = completeRebindSecrets(state);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return state;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Mark all required tasks as complete. */
|
|
284
|
+
function completeOnlyRequired(
|
|
285
|
+
state: RebindTaskCompletionState,
|
|
286
|
+
): RebindTaskCompletionState {
|
|
287
|
+
let current = state;
|
|
288
|
+
current = markTaskComplete(current, "re-enter-secrets");
|
|
289
|
+
current = markTaskComplete(current, "rebind-channels");
|
|
290
|
+
current = markTaskComplete(current, "reconfigure-auth");
|
|
291
|
+
return current;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Mark all tasks (including optional) as complete. */
|
|
295
|
+
function completeAllTasks(
|
|
296
|
+
state: RebindTaskCompletionState,
|
|
297
|
+
): RebindTaskCompletionState {
|
|
298
|
+
let current = state;
|
|
299
|
+
for (const id of getTaskIds()) {
|
|
300
|
+
current = markTaskComplete(current, id);
|
|
301
|
+
}
|
|
302
|
+
return current;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ===========================================================================
|
|
306
|
+
// 1. CROSS-PLATFORM PARITY
|
|
307
|
+
// ===========================================================================
|
|
308
|
+
|
|
309
|
+
describe("macOS + iOS parity — shared module determinism", () => {
|
|
310
|
+
test("wizard state machine produces identical results for both directions", () => {
|
|
311
|
+
// Both platforms use the same shared modules, so identical inputs must
|
|
312
|
+
// produce identical outputs regardless of which platform instantiates them.
|
|
313
|
+
const stateA = createWizardState();
|
|
314
|
+
const stateB = createWizardState();
|
|
315
|
+
|
|
316
|
+
// Compare structure (timestamps may differ slightly, so strip them)
|
|
317
|
+
const normalize = (s: MigrationWizardState) => ({
|
|
318
|
+
...s,
|
|
319
|
+
createdAt: "FIXED",
|
|
320
|
+
updatedAt: "FIXED",
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(normalize(stateA)).toEqual(normalize(stateB));
|
|
324
|
+
|
|
325
|
+
// Advance both through the same transitions
|
|
326
|
+
const advA = selectDirection(stateA, "managed-to-self-hosted");
|
|
327
|
+
const advB = selectDirection(stateB, "managed-to-self-hosted");
|
|
328
|
+
|
|
329
|
+
expect(normalize(advA).currentStep).toBe(normalize(advB).currentStep);
|
|
330
|
+
expect(normalize(advA).direction).toBe(normalize(advB).direction);
|
|
331
|
+
expect(normalize(advA).steps).toEqual(normalize(advB).steps);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("serialized state is portable — serialize on 'macOS', deserialize on 'iOS'", () => {
|
|
335
|
+
// Simulate creating state on one platform and deserializing on another.
|
|
336
|
+
// The key invariant: the deserialized state must be structurally identical
|
|
337
|
+
// (except hasBundleData which is always false after deserialization).
|
|
338
|
+
const original = advanceTo("rebind-secrets", "managed-to-self-hosted");
|
|
339
|
+
const json = serializeWizardState(original);
|
|
340
|
+
|
|
341
|
+
// "Transfer" the JSON string to another platform
|
|
342
|
+
const restored = deserializeWizardState(json);
|
|
343
|
+
expect(restored).toBeDefined();
|
|
344
|
+
|
|
345
|
+
// All fields except hasBundleData and timestamps should match
|
|
346
|
+
expect(restored!.currentStep).toBe(original.currentStep);
|
|
347
|
+
expect(restored!.direction).toBe(original.direction);
|
|
348
|
+
expect(restored!.hasBundleData).toBe(false); // Always false after deserialization
|
|
349
|
+
|
|
350
|
+
// Step states should be identical
|
|
351
|
+
for (const step of getStepOrder(original)) {
|
|
352
|
+
expect(restored!.steps[step].status).toBe(original.steps[step].status);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Results should be preserved
|
|
356
|
+
expect(restored!.validateResult).toEqual(original.validateResult);
|
|
357
|
+
expect(restored!.preflightResult).toEqual(original.preflightResult);
|
|
358
|
+
expect(restored!.importResult).toEqual(original.importResult);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("state machine transitions produce identical step orders for both directions", () => {
|
|
362
|
+
const m2s = getStepOrder(
|
|
363
|
+
advanceTo("upload-bundle", "managed-to-self-hosted"),
|
|
364
|
+
);
|
|
365
|
+
const s2m = getStepOrder(
|
|
366
|
+
advanceTo("upload-bundle", "self-hosted-to-managed"),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Both directions use the same step sequence
|
|
370
|
+
expect(m2s).toEqual(s2m);
|
|
371
|
+
expect(m2s).toEqual([
|
|
372
|
+
"select-direction",
|
|
373
|
+
"upload-bundle",
|
|
374
|
+
"validate",
|
|
375
|
+
"preflight-review",
|
|
376
|
+
"transfer",
|
|
377
|
+
"rebind-secrets",
|
|
378
|
+
"complete",
|
|
379
|
+
]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("validation screen view model produces identical output for same wizard state", () => {
|
|
383
|
+
const stateM2S = advanceTo("transfer", "managed-to-self-hosted");
|
|
384
|
+
const stateS2M = advanceTo("transfer", "self-hosted-to-managed");
|
|
385
|
+
|
|
386
|
+
// Both should produce success screen states with identical structure
|
|
387
|
+
// (only difference is the direction field on wizard state)
|
|
388
|
+
const screenM2S = deriveValidationScreenState(stateM2S);
|
|
389
|
+
const screenS2M = deriveValidationScreenState(stateS2M);
|
|
390
|
+
|
|
391
|
+
expect(screenM2S.phase).toBe("success");
|
|
392
|
+
expect(screenS2M.phase).toBe("success");
|
|
393
|
+
|
|
394
|
+
if (screenM2S.phase === "success" && screenS2M.phase === "success") {
|
|
395
|
+
expect(screenM2S.validation).toEqual(screenS2M.validation);
|
|
396
|
+
expect(screenM2S.preflight).toEqual(screenS2M.preflight);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("transfer screen view model produces identical output for same wizard state", () => {
|
|
401
|
+
const stateM2S = advanceTo("rebind-secrets", "managed-to-self-hosted");
|
|
402
|
+
const stateS2M = advanceTo("rebind-secrets", "self-hosted-to-managed");
|
|
403
|
+
|
|
404
|
+
const screenM2S = deriveTransferScreenState(stateM2S);
|
|
405
|
+
const screenS2M = deriveTransferScreenState(stateS2M);
|
|
406
|
+
|
|
407
|
+
expect(screenM2S.phase).toBe("success");
|
|
408
|
+
expect(screenS2M.phase).toBe("success");
|
|
409
|
+
|
|
410
|
+
if (screenM2S.phase === "success" && screenS2M.phase === "success") {
|
|
411
|
+
expect(screenM2S.importSummary).toEqual(screenS2M.importSummary);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("rebind secrets screen produces identical output for same wizard state", () => {
|
|
416
|
+
const stateM2S = advanceTo("rebind-secrets", "managed-to-self-hosted");
|
|
417
|
+
const stateS2M = advanceTo("rebind-secrets", "self-hosted-to-managed");
|
|
418
|
+
|
|
419
|
+
const completion = createTaskCompletionState();
|
|
420
|
+
const screenM2S = deriveRebindSecretsScreenState(stateM2S, completion);
|
|
421
|
+
const screenS2M = deriveRebindSecretsScreenState(stateS2M, completion);
|
|
422
|
+
|
|
423
|
+
expect(screenM2S.phase).toBe("active");
|
|
424
|
+
expect(screenS2M.phase).toBe("active");
|
|
425
|
+
|
|
426
|
+
if (screenM2S.phase === "active" && screenS2M.phase === "active") {
|
|
427
|
+
expect(screenM2S.tasks).toEqual(screenS2M.tasks);
|
|
428
|
+
expect(screenM2S.requiredCount).toBe(screenS2M.requiredCount);
|
|
429
|
+
expect(screenM2S.totalCount).toBe(screenS2M.totalCount);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("query helpers produce identical results for both directions", () => {
|
|
434
|
+
const stateM2S = advanceTo("transfer", "managed-to-self-hosted");
|
|
435
|
+
const stateS2M = advanceTo("transfer", "self-hosted-to-managed");
|
|
436
|
+
|
|
437
|
+
expect(getCurrentStepIndex(stateM2S)).toBe(getCurrentStepIndex(stateS2M));
|
|
438
|
+
expect(getTotalSteps()).toBe(7);
|
|
439
|
+
expect(isWizardComplete(stateM2S)).toBe(false);
|
|
440
|
+
expect(isWizardComplete(stateS2M)).toBe(false);
|
|
441
|
+
|
|
442
|
+
const completeM2S = advanceTo("complete", "managed-to-self-hosted");
|
|
443
|
+
const completeS2M = advanceTo("complete", "self-hosted-to-managed");
|
|
444
|
+
expect(isWizardComplete(completeM2S)).toBe(true);
|
|
445
|
+
expect(isWizardComplete(completeS2M)).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("transition validation is symmetric across platform contexts", () => {
|
|
449
|
+
const state = advanceTo("validate");
|
|
450
|
+
|
|
451
|
+
// Forward transition validation
|
|
452
|
+
const forwardResult = validateWizardTransition(state, "preflight-review");
|
|
453
|
+
expect(forwardResult.valid).toBe(false); // validate step is idle, not success
|
|
454
|
+
|
|
455
|
+
// Same call with identical state produces identical result
|
|
456
|
+
const forwardResult2 = validateWizardTransition(state, "preflight-review");
|
|
457
|
+
expect(forwardResult2).toEqual(forwardResult);
|
|
458
|
+
|
|
459
|
+
// Back transition is always valid
|
|
460
|
+
const backResult = validateWizardTransition(state, "upload-bundle");
|
|
461
|
+
expect(backResult.valid).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// ===========================================================================
|
|
466
|
+
// 2. PERSISTENCE / RESUME BEHAVIOR
|
|
467
|
+
// ===========================================================================
|
|
468
|
+
|
|
469
|
+
describe("persistence/resume — serialize at each step", () => {
|
|
470
|
+
test("persist and resume at select-direction step", () => {
|
|
471
|
+
const state = createWizardState();
|
|
472
|
+
const json = serializeWizardState(state);
|
|
473
|
+
const restored = deserializeWizardState(json);
|
|
474
|
+
expect(restored).toBeDefined();
|
|
475
|
+
expect(restored!.currentStep).toBe("select-direction");
|
|
476
|
+
expect(isResumable(restored!)).toBe(false); // First step, nothing to resume
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("persist and resume at upload-bundle step", () => {
|
|
480
|
+
const state = advanceTo("upload-bundle");
|
|
481
|
+
const json = serializeWizardState(state);
|
|
482
|
+
const restored = deserializeWizardState(json);
|
|
483
|
+
expect(restored).toBeDefined();
|
|
484
|
+
expect(restored!.currentStep).toBe("upload-bundle");
|
|
485
|
+
expect(restored!.direction).toBe("managed-to-self-hosted");
|
|
486
|
+
expect(isResumable(restored!)).toBe(true);
|
|
487
|
+
|
|
488
|
+
const resumed = prepareForResume(restored!);
|
|
489
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("persist and resume at validate step — redirects to upload-bundle (no bundle data)", () => {
|
|
493
|
+
const state = advanceTo("validate");
|
|
494
|
+
const json = serializeWizardState(state);
|
|
495
|
+
const restored = deserializeWizardState(json);
|
|
496
|
+
expect(restored).toBeDefined();
|
|
497
|
+
expect(restored!.hasBundleData).toBe(false);
|
|
498
|
+
|
|
499
|
+
const resumed = prepareForResume(restored!);
|
|
500
|
+
// Validate step needs bundle data, so redirect to upload-bundle
|
|
501
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("persist and resume at preflight-review step — redirects to upload-bundle (no bundle data)", () => {
|
|
505
|
+
const state = advanceTo("preflight-review");
|
|
506
|
+
const json = serializeWizardState(state);
|
|
507
|
+
const restored = deserializeWizardState(json);
|
|
508
|
+
expect(restored).toBeDefined();
|
|
509
|
+
|
|
510
|
+
const resumed = prepareForResume(restored!);
|
|
511
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("persist and resume at transfer step — redirects to upload-bundle (no bundle data)", () => {
|
|
515
|
+
const state = advanceTo("transfer");
|
|
516
|
+
const json = serializeWizardState(state);
|
|
517
|
+
const restored = deserializeWizardState(json);
|
|
518
|
+
expect(restored).toBeDefined();
|
|
519
|
+
|
|
520
|
+
const resumed = prepareForResume(restored!);
|
|
521
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("persist and resume at rebind-secrets step — stays at rebind-secrets (no bundle needed)", () => {
|
|
525
|
+
const state = advanceTo("rebind-secrets");
|
|
526
|
+
const json = serializeWizardState(state);
|
|
527
|
+
const restored = deserializeWizardState(json);
|
|
528
|
+
expect(restored).toBeDefined();
|
|
529
|
+
expect(isResumable(restored!)).toBe(true);
|
|
530
|
+
|
|
531
|
+
const resumed = prepareForResume(restored!);
|
|
532
|
+
expect(resumed.currentStep).toBe("rebind-secrets");
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("persist and resume at complete step — not resumable", () => {
|
|
536
|
+
const state = advanceTo("complete");
|
|
537
|
+
const json = serializeWizardState(state);
|
|
538
|
+
const restored = deserializeWizardState(json);
|
|
539
|
+
expect(restored).toBeDefined();
|
|
540
|
+
expect(isResumable(restored!)).toBe(false);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("persist and resume with loading status resets to idle", () => {
|
|
544
|
+
let state = advanceTo("validate");
|
|
545
|
+
state = {
|
|
546
|
+
...state,
|
|
547
|
+
steps: { ...state.steps, validate: { status: "loading" } },
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const json = serializeWizardState(state);
|
|
551
|
+
const restored = deserializeWizardState(json);
|
|
552
|
+
expect(restored).toBeDefined();
|
|
553
|
+
|
|
554
|
+
const resumed = prepareForResume(restored!);
|
|
555
|
+
// Loading was reset + redirected to upload-bundle because no bundle data
|
|
556
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("persist and resume with error status preserves error details", () => {
|
|
560
|
+
let state = advanceTo("validate");
|
|
561
|
+
state = {
|
|
562
|
+
...state,
|
|
563
|
+
steps: {
|
|
564
|
+
...state.steps,
|
|
565
|
+
validate: {
|
|
566
|
+
status: "error",
|
|
567
|
+
error: {
|
|
568
|
+
message: "validate: HTTP 500",
|
|
569
|
+
code: "HTTP_500",
|
|
570
|
+
retryable: true,
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const json = serializeWizardState(state);
|
|
577
|
+
const restored = deserializeWizardState(json);
|
|
578
|
+
expect(restored).toBeDefined();
|
|
579
|
+
expect(restored!.steps.validate.status).toBe("error");
|
|
580
|
+
expect(restored!.steps.validate.error?.message).toBe("validate: HTTP 500");
|
|
581
|
+
expect(restored!.steps.validate.error?.retryable).toBe(true);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("results survive round-trip serialization", () => {
|
|
585
|
+
let state = advanceTo("transfer");
|
|
586
|
+
state = {
|
|
587
|
+
...state,
|
|
588
|
+
validateResult: makeValidateSuccess(),
|
|
589
|
+
preflightResult: makePreflightSuccess(),
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const json = serializeWizardState(state);
|
|
593
|
+
const restored = deserializeWizardState(json);
|
|
594
|
+
expect(restored).toBeDefined();
|
|
595
|
+
|
|
596
|
+
// Validate result
|
|
597
|
+
expect(restored!.validateResult).toBeDefined();
|
|
598
|
+
expect(restored!.validateResult!.is_valid).toBe(true);
|
|
599
|
+
if (restored!.validateResult!.is_valid) {
|
|
600
|
+
expect(restored!.validateResult!.manifest.schema_version).toBe("1.0");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Preflight result
|
|
604
|
+
expect(restored!.preflightResult).toBeDefined();
|
|
605
|
+
expect(restored!.preflightResult!.can_import).toBe(true);
|
|
606
|
+
if (restored!.preflightResult!.can_import) {
|
|
607
|
+
expect(restored!.preflightResult!.summary.total_files).toBe(3);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("import result survives round-trip serialization", () => {
|
|
612
|
+
let state = advanceTo("rebind-secrets");
|
|
613
|
+
state = {
|
|
614
|
+
...state,
|
|
615
|
+
importResult: makeImportSuccess(),
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const json = serializeWizardState(state);
|
|
619
|
+
const restored = deserializeWizardState(json);
|
|
620
|
+
expect(restored).toBeDefined();
|
|
621
|
+
|
|
622
|
+
expect(restored!.importResult).toBeDefined();
|
|
623
|
+
expect(restored!.importResult!.success).toBe(true);
|
|
624
|
+
if (restored!.importResult!.success) {
|
|
625
|
+
expect(restored!.importResult!.summary.total_files).toBe(3);
|
|
626
|
+
expect(restored!.importResult!.warnings).toHaveLength(1);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// ===========================================================================
|
|
632
|
+
// 3. INTERRUPTED FLOWS
|
|
633
|
+
// ===========================================================================
|
|
634
|
+
|
|
635
|
+
describe("interrupted flow recovery", () => {
|
|
636
|
+
test("crash during transfer loading — resume resets to upload-bundle", () => {
|
|
637
|
+
let state = advanceTo("transfer");
|
|
638
|
+
state = {
|
|
639
|
+
...state,
|
|
640
|
+
steps: { ...state.steps, transfer: { status: "loading" } },
|
|
641
|
+
exportResult: {
|
|
642
|
+
ok: true,
|
|
643
|
+
filename: "export.vbundle",
|
|
644
|
+
schemaVersion: "1.0",
|
|
645
|
+
manifestSha256: "abc",
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const json = serializeWizardState(state);
|
|
650
|
+
const restored = deserializeWizardState(json);
|
|
651
|
+
expect(restored).toBeDefined();
|
|
652
|
+
|
|
653
|
+
const resumed = prepareForResume(restored!);
|
|
654
|
+
// Transfer needs bundle data; since hasBundleData=false after deserialization,
|
|
655
|
+
// redirect to upload-bundle
|
|
656
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
657
|
+
// Export result is preserved in the state even though we go back
|
|
658
|
+
// (the user will need to re-upload the bundle)
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("crash during validate loading — resume resets to upload-bundle", () => {
|
|
662
|
+
let state = advanceTo("validate");
|
|
663
|
+
state = {
|
|
664
|
+
...state,
|
|
665
|
+
steps: { ...state.steps, validate: { status: "loading" } },
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const json = serializeWizardState(state);
|
|
669
|
+
const restored = deserializeWizardState(json);
|
|
670
|
+
const resumed = prepareForResume(restored!);
|
|
671
|
+
|
|
672
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
673
|
+
expect(resumed.steps.validate.status).toBe("idle");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test("crash during preflight-review loading — resume resets to upload-bundle", () => {
|
|
677
|
+
let state = advanceTo("preflight-review");
|
|
678
|
+
state = {
|
|
679
|
+
...state,
|
|
680
|
+
steps: { ...state.steps, "preflight-review": { status: "loading" } },
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const json = serializeWizardState(state);
|
|
684
|
+
const restored = deserializeWizardState(json);
|
|
685
|
+
const resumed = prepareForResume(restored!);
|
|
686
|
+
|
|
687
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
688
|
+
expect(resumed.steps["preflight-review"].status).toBe("idle");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("partial completion — validate done but preflight crashed", () => {
|
|
692
|
+
let state = advanceTo("preflight-review");
|
|
693
|
+
// Validation succeeded, but preflight was in progress when crash occurred
|
|
694
|
+
state = {
|
|
695
|
+
...state,
|
|
696
|
+
validateResult: makeValidateSuccess(),
|
|
697
|
+
steps: {
|
|
698
|
+
...state.steps,
|
|
699
|
+
validate: { status: "success" },
|
|
700
|
+
"preflight-review": { status: "loading" },
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const json = serializeWizardState(state);
|
|
705
|
+
const restored = deserializeWizardState(json);
|
|
706
|
+
const resumed = prepareForResume(restored!);
|
|
707
|
+
|
|
708
|
+
// Goes to upload-bundle because no bundle data
|
|
709
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
710
|
+
// Results are cleared when rewinding to upload-bundle (no bundle data)
|
|
711
|
+
expect(resumed.validateResult).toBeUndefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("partial completion — export done but import crashed", () => {
|
|
715
|
+
let state = advanceTo("transfer");
|
|
716
|
+
state = {
|
|
717
|
+
...state,
|
|
718
|
+
steps: { ...state.steps, transfer: { status: "loading" } },
|
|
719
|
+
exportResult: {
|
|
720
|
+
ok: true,
|
|
721
|
+
filename: "export.vbundle",
|
|
722
|
+
schemaVersion: "1.0",
|
|
723
|
+
manifestSha256: "abc",
|
|
724
|
+
},
|
|
725
|
+
// No importResult — import had not started or crashed
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const json = serializeWizardState(state);
|
|
729
|
+
const restored = deserializeWizardState(json);
|
|
730
|
+
const resumed = prepareForResume(restored!);
|
|
731
|
+
|
|
732
|
+
// Goes back to upload-bundle (no bundle data)
|
|
733
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
734
|
+
// Results are cleared when rewinding to upload-bundle (no bundle data)
|
|
735
|
+
expect(resumed.exportResult).toBeUndefined();
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("error state at transfer — user can retry after resume at rebind step doesn't apply", () => {
|
|
739
|
+
let state = advanceTo("transfer");
|
|
740
|
+
state = {
|
|
741
|
+
...state,
|
|
742
|
+
steps: {
|
|
743
|
+
...state.steps,
|
|
744
|
+
transfer: {
|
|
745
|
+
status: "error",
|
|
746
|
+
error: {
|
|
747
|
+
message: "import: HTTP 500",
|
|
748
|
+
code: "HTTP_500",
|
|
749
|
+
retryable: true,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const json = serializeWizardState(state);
|
|
756
|
+
const restored = deserializeWizardState(json);
|
|
757
|
+
expect(restored).toBeDefined();
|
|
758
|
+
|
|
759
|
+
// Without preparing for resume (raw deserialized state), error is preserved
|
|
760
|
+
expect(restored!.steps.transfer.status).toBe("error");
|
|
761
|
+
expect(canRetryCurrentStep(restored!)).toBe(true);
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// ===========================================================================
|
|
766
|
+
// 4. FULL END-TO-END FLOWS
|
|
767
|
+
// ===========================================================================
|
|
768
|
+
|
|
769
|
+
describe("full end-to-end — managed-to-self-hosted migration", () => {
|
|
770
|
+
test("complete flow through all screen view models", async () => {
|
|
771
|
+
// Step 1: Create wizard and select direction
|
|
772
|
+
let state = createWizardState();
|
|
773
|
+
expect(deriveValidationScreenState(state).phase).toBe("disabled");
|
|
774
|
+
expect(deriveTransferScreenState(state).phase).toBe("disabled");
|
|
775
|
+
expect(
|
|
776
|
+
deriveRebindSecretsScreenState(state, createTaskCompletionState()).phase,
|
|
777
|
+
).toBe("disabled");
|
|
778
|
+
|
|
779
|
+
state = selectDirection(state, "managed-to-self-hosted");
|
|
780
|
+
expect(state.currentStep).toBe("upload-bundle");
|
|
781
|
+
expect(state.direction).toBe("managed-to-self-hosted");
|
|
782
|
+
|
|
783
|
+
// Step 2: Upload bundle
|
|
784
|
+
state = setBundleUploaded(state);
|
|
785
|
+
expect(state.currentStep).toBe("validate");
|
|
786
|
+
expect(state.hasBundleData).toBe(true);
|
|
787
|
+
|
|
788
|
+
// Step 3: Validate + preflight
|
|
789
|
+
const validateSuccess = makeValidateSuccess();
|
|
790
|
+
const preflightSuccess = makePreflightSuccess();
|
|
791
|
+
|
|
792
|
+
let callCount = 0;
|
|
793
|
+
const sequentialFetch = (async () => {
|
|
794
|
+
callCount++;
|
|
795
|
+
if (callCount === 1) {
|
|
796
|
+
return new Response(JSON.stringify(validateSuccess), {
|
|
797
|
+
status: 200,
|
|
798
|
+
headers: { "Content-Type": "application/json" },
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return new Response(JSON.stringify(preflightSuccess), {
|
|
802
|
+
status: 200,
|
|
803
|
+
headers: { "Content-Type": "application/json" },
|
|
804
|
+
});
|
|
805
|
+
}) as unknown as typeof fetch;
|
|
806
|
+
|
|
807
|
+
const validateOptions = makeExecutorOptions({
|
|
808
|
+
destConfig: runtimeConfig({ fetchFn: sequentialFetch }),
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
state = await executeValidationFlow(state, validateOptions);
|
|
812
|
+
expect(state.currentStep).toBe("transfer");
|
|
813
|
+
|
|
814
|
+
// Verify validation screen shows success
|
|
815
|
+
const valScreen = deriveValidationScreenState(state);
|
|
816
|
+
expect(valScreen.phase).toBe("success");
|
|
817
|
+
if (valScreen.phase === "success") {
|
|
818
|
+
expect(valScreen.validation.isValid).toBe(true);
|
|
819
|
+
expect(valScreen.preflight.summary.totalFiles).toBe(3);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Step 4: Transfer (managed export via async polling + runtime import)
|
|
823
|
+
const archiveBytes = new ArrayBuffer(32);
|
|
824
|
+
const importResponse = makeImportSuccess();
|
|
825
|
+
|
|
826
|
+
let exportCallCount = 0;
|
|
827
|
+
const sourceFetch = (async (url: string | URL | Request) => {
|
|
828
|
+
const urlStr = String(url);
|
|
829
|
+
if (urlStr.endsWith("/export/")) {
|
|
830
|
+
// Managed export: initiate async job
|
|
831
|
+
return new Response(
|
|
832
|
+
JSON.stringify({ job_id: "exp-m2sh", status: "pending" }),
|
|
833
|
+
{
|
|
834
|
+
status: 200,
|
|
835
|
+
headers: { "Content-Type": "application/json" },
|
|
836
|
+
},
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
if (urlStr.includes("/export/") && urlStr.includes("/status/")) {
|
|
840
|
+
exportCallCount++;
|
|
841
|
+
if (exportCallCount === 1) {
|
|
842
|
+
// First poll: still in progress
|
|
843
|
+
return new Response(
|
|
844
|
+
JSON.stringify({
|
|
845
|
+
job_id: "exp-m2sh",
|
|
846
|
+
status: "in_progress",
|
|
847
|
+
progress: 50,
|
|
848
|
+
}),
|
|
849
|
+
{
|
|
850
|
+
status: 200,
|
|
851
|
+
headers: { "Content-Type": "application/json" },
|
|
852
|
+
},
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
// Second poll: complete with download URL
|
|
856
|
+
return new Response(
|
|
857
|
+
JSON.stringify({
|
|
858
|
+
job_id: "exp-m2sh",
|
|
859
|
+
status: "complete",
|
|
860
|
+
download_url: "https://platform.vellum.ai/downloads/exp-m2sh",
|
|
861
|
+
}),
|
|
862
|
+
{
|
|
863
|
+
status: 200,
|
|
864
|
+
headers: { "Content-Type": "application/json" },
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (urlStr.includes("/downloads/")) {
|
|
869
|
+
// Download the exported archive
|
|
870
|
+
return new Response(archiveBytes, {
|
|
871
|
+
status: 200,
|
|
872
|
+
headers: {
|
|
873
|
+
"Content-Disposition": 'attachment; filename="export.vbundle"',
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
return new Response("Not found", { status: 404 });
|
|
878
|
+
}) as unknown as typeof fetch;
|
|
879
|
+
|
|
880
|
+
const destFetch = (async () => {
|
|
881
|
+
return new Response(JSON.stringify(importResponse), {
|
|
882
|
+
status: 200,
|
|
883
|
+
headers: { "Content-Type": "application/json" },
|
|
884
|
+
});
|
|
885
|
+
}) as unknown as typeof fetch;
|
|
886
|
+
|
|
887
|
+
const transferOptions = makeExecutorOptions({
|
|
888
|
+
sourceConfig: managedConfig({ fetchFn: sourceFetch }),
|
|
889
|
+
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
state = await executeTransferFlow(state, transferOptions);
|
|
893
|
+
expect(state.currentStep).toBe("rebind-secrets");
|
|
894
|
+
|
|
895
|
+
// Verify transfer screen shows success
|
|
896
|
+
const transferScreen = deriveTransferScreenState(state);
|
|
897
|
+
expect(transferScreen.phase).toBe("success");
|
|
898
|
+
if (transferScreen.phase === "success") {
|
|
899
|
+
expect(transferScreen.importSummary.totalFiles).toBe(3);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Step 5: Rebind secrets
|
|
903
|
+
let completion = createTaskCompletionState();
|
|
904
|
+
let rebindScreen = deriveRebindSecretsScreenState(state, completion);
|
|
905
|
+
expect(rebindScreen.phase).toBe("active");
|
|
906
|
+
if (rebindScreen.phase === "active") {
|
|
907
|
+
expect(rebindScreen.allRequiredComplete).toBe(false);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Complete required tasks one by one
|
|
911
|
+
completion = markTaskComplete(completion, "re-enter-secrets");
|
|
912
|
+
completion = markTaskComplete(completion, "rebind-channels");
|
|
913
|
+
completion = markTaskComplete(completion, "reconfigure-auth");
|
|
914
|
+
|
|
915
|
+
rebindScreen = deriveRebindSecretsScreenState(state, completion);
|
|
916
|
+
if (rebindScreen.phase === "active") {
|
|
917
|
+
expect(rebindScreen.allRequiredComplete).toBe(true);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Step 6: Complete migration
|
|
921
|
+
state = completeMigration(state, completion);
|
|
922
|
+
expect(state.currentStep).toBe("complete");
|
|
923
|
+
expect(isWizardComplete(state)).toBe(true);
|
|
924
|
+
|
|
925
|
+
// All screens should still show their success states
|
|
926
|
+
expect(deriveValidationScreenState(state).phase).toBe("success");
|
|
927
|
+
expect(deriveTransferScreenState(state).phase).toBe("success");
|
|
928
|
+
expect(deriveRebindSecretsScreenState(state, completion).phase).toBe(
|
|
929
|
+
"complete",
|
|
930
|
+
);
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
describe("full end-to-end — self-hosted-to-managed migration", () => {
|
|
935
|
+
test("complete flow with managed export (async polling)", async () => {
|
|
936
|
+
// Step 1: Create wizard and select direction
|
|
937
|
+
let state = createWizardState();
|
|
938
|
+
state = selectDirection(state, "self-hosted-to-managed");
|
|
939
|
+
expect(state.direction).toBe("self-hosted-to-managed");
|
|
940
|
+
|
|
941
|
+
// Step 2: Upload bundle
|
|
942
|
+
state = setBundleUploaded(state);
|
|
943
|
+
|
|
944
|
+
// Step 3: Validate + preflight
|
|
945
|
+
const validateSuccess = makeValidateSuccess();
|
|
946
|
+
const preflightSuccess = makePreflightSuccess();
|
|
947
|
+
|
|
948
|
+
let valCallCount = 0;
|
|
949
|
+
const sequentialFetch = (async () => {
|
|
950
|
+
valCallCount++;
|
|
951
|
+
if (valCallCount === 1) {
|
|
952
|
+
return new Response(JSON.stringify(validateSuccess), {
|
|
953
|
+
status: 200,
|
|
954
|
+
headers: { "Content-Type": "application/json" },
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
return new Response(JSON.stringify(preflightSuccess), {
|
|
958
|
+
status: 200,
|
|
959
|
+
headers: { "Content-Type": "application/json" },
|
|
960
|
+
});
|
|
961
|
+
}) as unknown as typeof fetch;
|
|
962
|
+
|
|
963
|
+
state = await executeValidationFlow(
|
|
964
|
+
state,
|
|
965
|
+
makeExecutorOptions({
|
|
966
|
+
destConfig: managedConfig({ fetchFn: sequentialFetch }),
|
|
967
|
+
}),
|
|
968
|
+
);
|
|
969
|
+
expect(state.currentStep).toBe("transfer");
|
|
970
|
+
|
|
971
|
+
// Step 4: Transfer with managed source (async polling)
|
|
972
|
+
const importResponse = makeImportSuccess();
|
|
973
|
+
|
|
974
|
+
const sourceFetch = (async (url: string | URL | Request) => {
|
|
975
|
+
const urlStr = String(url);
|
|
976
|
+
if (urlStr.endsWith("/export")) {
|
|
977
|
+
// Runtime export returns binary directly
|
|
978
|
+
return new Response(new ArrayBuffer(32), {
|
|
979
|
+
status: 200,
|
|
980
|
+
headers: {
|
|
981
|
+
"Content-Disposition": 'attachment; filename="export.vbundle"',
|
|
982
|
+
"X-Vbundle-Schema-Version": "1.0",
|
|
983
|
+
"X-Vbundle-Manifest-Sha256": "abc",
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
return new Response("Not found", { status: 404 });
|
|
988
|
+
}) as unknown as typeof fetch;
|
|
989
|
+
|
|
990
|
+
const destFetch = (async () => {
|
|
991
|
+
return new Response(JSON.stringify(importResponse), {
|
|
992
|
+
status: 200,
|
|
993
|
+
headers: { "Content-Type": "application/json" },
|
|
994
|
+
});
|
|
995
|
+
}) as unknown as typeof fetch;
|
|
996
|
+
|
|
997
|
+
state = await executeTransferFlow(
|
|
998
|
+
state,
|
|
999
|
+
makeExecutorOptions({
|
|
1000
|
+
sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
|
|
1001
|
+
destConfig: managedConfig({ fetchFn: destFetch }),
|
|
1002
|
+
}),
|
|
1003
|
+
);
|
|
1004
|
+
expect(state.currentStep).toBe("rebind-secrets");
|
|
1005
|
+
|
|
1006
|
+
// Step 5-6: Rebind and complete
|
|
1007
|
+
const completion = completeOnlyRequired(createTaskCompletionState());
|
|
1008
|
+
state = completeMigration(state, completion);
|
|
1009
|
+
expect(isWizardComplete(state)).toBe(true);
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// ===========================================================================
|
|
1014
|
+
// 5. EDGE CASES
|
|
1015
|
+
// ===========================================================================
|
|
1016
|
+
|
|
1017
|
+
describe("edge cases — import failures and transport errors", () => {
|
|
1018
|
+
test("transfer step handles import validation failure as retryable error", async () => {
|
|
1019
|
+
const destFetch = (async () => {
|
|
1020
|
+
return new Response(
|
|
1021
|
+
JSON.stringify({
|
|
1022
|
+
success: false,
|
|
1023
|
+
reason: "validation_failed",
|
|
1024
|
+
message: "Manifest SHA mismatch",
|
|
1025
|
+
}),
|
|
1026
|
+
{
|
|
1027
|
+
status: 200,
|
|
1028
|
+
headers: { "Content-Type": "application/json" },
|
|
1029
|
+
},
|
|
1030
|
+
);
|
|
1031
|
+
}) as unknown as typeof fetch;
|
|
1032
|
+
|
|
1033
|
+
const state = advanceTo("transfer");
|
|
1034
|
+
const options = makeExecutorOptions({
|
|
1035
|
+
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
const result = await executeTransferFlow(state, options);
|
|
1039
|
+
expect(result.steps.transfer.status).toBe("error");
|
|
1040
|
+
expect(result.steps.transfer.error?.message).toContain(
|
|
1041
|
+
"Manifest SHA mismatch",
|
|
1042
|
+
);
|
|
1043
|
+
expect(result.steps.transfer.error?.code).toBe("validation_failed");
|
|
1044
|
+
|
|
1045
|
+
const screen = deriveTransferScreenState(result);
|
|
1046
|
+
expect(screen.phase).toBe("error");
|
|
1047
|
+
if (screen.phase === "error") {
|
|
1048
|
+
expect(screen.failedPhase).toBe("import");
|
|
1049
|
+
expect(screen.canRetry).toBe(true);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test("transfer step handles import write failure", async () => {
|
|
1054
|
+
const destFetch = (async () => {
|
|
1055
|
+
return new Response(
|
|
1056
|
+
JSON.stringify({
|
|
1057
|
+
success: false,
|
|
1058
|
+
reason: "write_failed",
|
|
1059
|
+
message: "Disk full",
|
|
1060
|
+
}),
|
|
1061
|
+
{
|
|
1062
|
+
status: 200,
|
|
1063
|
+
headers: { "Content-Type": "application/json" },
|
|
1064
|
+
},
|
|
1065
|
+
);
|
|
1066
|
+
}) as unknown as typeof fetch;
|
|
1067
|
+
|
|
1068
|
+
const state = advanceTo("transfer");
|
|
1069
|
+
const options = makeExecutorOptions({
|
|
1070
|
+
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
const result = await executeTransferFlow(state, options);
|
|
1074
|
+
expect(result.steps.transfer.status).toBe("error");
|
|
1075
|
+
expect(result.steps.transfer.error?.code).toBe("write_failed");
|
|
1076
|
+
expect(result.steps.transfer.error?.retryable).toBe(true);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
test("transfer step handles import extraction failure", async () => {
|
|
1080
|
+
const destFetch = (async () => {
|
|
1081
|
+
return new Response(
|
|
1082
|
+
JSON.stringify({
|
|
1083
|
+
success: false,
|
|
1084
|
+
reason: "extraction_failed",
|
|
1085
|
+
message: "Corrupt archive",
|
|
1086
|
+
}),
|
|
1087
|
+
{
|
|
1088
|
+
status: 200,
|
|
1089
|
+
headers: { "Content-Type": "application/json" },
|
|
1090
|
+
},
|
|
1091
|
+
);
|
|
1092
|
+
}) as unknown as typeof fetch;
|
|
1093
|
+
|
|
1094
|
+
const state = advanceTo("transfer");
|
|
1095
|
+
const options = makeExecutorOptions({
|
|
1096
|
+
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const result = await executeTransferFlow(state, options);
|
|
1100
|
+
expect(result.steps.transfer.status).toBe("error");
|
|
1101
|
+
expect(result.steps.transfer.error?.message).toContain("Corrupt archive");
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
test("transfer step handles destination HTTP 503 as retryable transport error", async () => {
|
|
1105
|
+
const state = advanceTo("transfer");
|
|
1106
|
+
const options = makeExecutorOptions({
|
|
1107
|
+
destConfig: runtimeConfig({
|
|
1108
|
+
fetchFn: mockFetch(503, "Service Unavailable"),
|
|
1109
|
+
}),
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
const result = await executeTransferFlow(state, options);
|
|
1113
|
+
expect(result.steps.transfer.status).toBe("error");
|
|
1114
|
+
|
|
1115
|
+
const screen = deriveTransferScreenState(result);
|
|
1116
|
+
expect(screen.phase).toBe("error");
|
|
1117
|
+
if (screen.phase === "error") {
|
|
1118
|
+
expect(screen.canRetry).toBe(true);
|
|
1119
|
+
expect(screen.failedPhase).toBe("import");
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test("transfer step handles destination HTTP 429 rate limit as retryable", async () => {
|
|
1124
|
+
const archiveBytes = new ArrayBuffer(32);
|
|
1125
|
+
const sourceFetch = (async () => {
|
|
1126
|
+
return new Response(archiveBytes, {
|
|
1127
|
+
status: 200,
|
|
1128
|
+
headers: {
|
|
1129
|
+
"Content-Disposition": 'attachment; filename="export.vbundle"',
|
|
1130
|
+
"X-Vbundle-Schema-Version": "1.0",
|
|
1131
|
+
"X-Vbundle-Manifest-Sha256": "abc",
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
}) as unknown as typeof fetch;
|
|
1135
|
+
|
|
1136
|
+
const state = advanceTo("transfer");
|
|
1137
|
+
const options = makeExecutorOptions({
|
|
1138
|
+
sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
|
|
1139
|
+
destConfig: runtimeConfig({
|
|
1140
|
+
fetchFn: mockFetch(429, "Too Many Requests"),
|
|
1141
|
+
}),
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
const result = await executeTransferFlow(state, options);
|
|
1145
|
+
expect(result.steps.transfer.status).toBe("error");
|
|
1146
|
+
expect(result.steps.transfer.error?.retryable).toBe(true);
|
|
1147
|
+
expect(result.steps.transfer.error?.code).toBe("HTTP_429");
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
describe("edge cases — stale/expired states", () => {
|
|
1152
|
+
test("very old serialized state deserializes correctly", () => {
|
|
1153
|
+
// Simulate a state serialized months ago
|
|
1154
|
+
const oldState: MigrationWizardState = {
|
|
1155
|
+
currentStep: "rebind-secrets",
|
|
1156
|
+
direction: "managed-to-self-hosted",
|
|
1157
|
+
steps: {
|
|
1158
|
+
"select-direction": { status: "success" },
|
|
1159
|
+
"upload-bundle": { status: "success" },
|
|
1160
|
+
validate: { status: "success" },
|
|
1161
|
+
"preflight-review": { status: "success" },
|
|
1162
|
+
transfer: { status: "success" },
|
|
1163
|
+
"rebind-secrets": { status: "idle" },
|
|
1164
|
+
complete: { status: "idle" },
|
|
1165
|
+
},
|
|
1166
|
+
hasBundleData: true,
|
|
1167
|
+
createdAt: "2024-01-01T00:00:00Z", // Over a year old
|
|
1168
|
+
updatedAt: "2024-01-01T01:00:00Z",
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const json = JSON.stringify(oldState);
|
|
1172
|
+
const restored = deserializeWizardState(json);
|
|
1173
|
+
expect(restored).toBeDefined();
|
|
1174
|
+
expect(restored!.currentStep).toBe("rebind-secrets");
|
|
1175
|
+
expect(restored!.hasBundleData).toBe(false); // Always false after deserialization
|
|
1176
|
+
|
|
1177
|
+
// Resume should work even for stale state
|
|
1178
|
+
expect(isResumable(restored!)).toBe(true);
|
|
1179
|
+
const resumed = prepareForResume(restored!);
|
|
1180
|
+
expect(resumed.currentStep).toBe("rebind-secrets");
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("state with expired timestamps still functions correctly", () => {
|
|
1184
|
+
const state = advanceTo("transfer");
|
|
1185
|
+
// Manually set old timestamps
|
|
1186
|
+
const oldState = {
|
|
1187
|
+
...state,
|
|
1188
|
+
createdAt: "2023-06-15T00:00:00Z",
|
|
1189
|
+
updatedAt: "2023-06-15T01:00:00Z",
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const json = serializeWizardState(oldState);
|
|
1193
|
+
const restored = deserializeWizardState(json);
|
|
1194
|
+
expect(restored).toBeDefined();
|
|
1195
|
+
expect(restored!.createdAt).toBe("2023-06-15T00:00:00Z");
|
|
1196
|
+
|
|
1197
|
+
// The wizard should still work — transfer step is import-only
|
|
1198
|
+
const screen = deriveTransferScreenState(restored!);
|
|
1199
|
+
expect(screen.phase).toBe("importing");
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
describe("edge cases — concurrent wizard instances", () => {
|
|
1204
|
+
test("two independent wizard instances maintain separate state", () => {
|
|
1205
|
+
// Advance two wizards to different steps
|
|
1206
|
+
const w1Advanced = advanceTo("transfer", "managed-to-self-hosted");
|
|
1207
|
+
const w2Advanced = advanceTo("upload-bundle", "self-hosted-to-managed");
|
|
1208
|
+
|
|
1209
|
+
// States are completely independent
|
|
1210
|
+
expect(w1Advanced.currentStep).toBe("transfer");
|
|
1211
|
+
expect(w2Advanced.currentStep).toBe("upload-bundle");
|
|
1212
|
+
expect(w1Advanced.direction).toBe("managed-to-self-hosted");
|
|
1213
|
+
expect(w2Advanced.direction).toBe("self-hosted-to-managed");
|
|
1214
|
+
|
|
1215
|
+
// Serializing both produces independent JSON
|
|
1216
|
+
const json1 = serializeWizardState(w1Advanced);
|
|
1217
|
+
const json2 = serializeWizardState(w2Advanced);
|
|
1218
|
+
expect(json1).not.toBe(json2);
|
|
1219
|
+
|
|
1220
|
+
// Deserializing restores independent state
|
|
1221
|
+
const restored1 = deserializeWizardState(json1);
|
|
1222
|
+
const restored2 = deserializeWizardState(json2);
|
|
1223
|
+
expect(restored1!.currentStep).toBe("transfer");
|
|
1224
|
+
expect(restored2!.currentStep).toBe("upload-bundle");
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
test("modifying one wizard does not affect a serialized snapshot of another", () => {
|
|
1228
|
+
let wizard = advanceTo("rebind-secrets");
|
|
1229
|
+
const snapshot = serializeWizardState(wizard);
|
|
1230
|
+
|
|
1231
|
+
// Advance the wizard to complete
|
|
1232
|
+
wizard = completeRebindSecrets(wizard);
|
|
1233
|
+
expect(wizard.currentStep).toBe("complete");
|
|
1234
|
+
|
|
1235
|
+
// The snapshot should still restore to rebind-secrets
|
|
1236
|
+
const restored = deserializeWizardState(snapshot);
|
|
1237
|
+
expect(restored!.currentStep).toBe("rebind-secrets");
|
|
1238
|
+
expect(isWizardComplete(restored!)).toBe(false);
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
describe("edge cases — state corruption recovery", () => {
|
|
1243
|
+
test("empty string returns undefined", () => {
|
|
1244
|
+
expect(deserializeWizardState("")).toBeUndefined();
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
test("null returns undefined", () => {
|
|
1248
|
+
expect(deserializeWizardState("null")).toBeUndefined();
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
test("invalid JSON returns undefined", () => {
|
|
1252
|
+
expect(deserializeWizardState("{invalid json}")).toBeUndefined();
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test("valid JSON but missing required fields returns undefined", () => {
|
|
1256
|
+
expect(deserializeWizardState('{"foo": "bar"}')).toBeUndefined();
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
test("missing currentStep returns undefined", () => {
|
|
1260
|
+
const partial = {
|
|
1261
|
+
steps: {},
|
|
1262
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
1263
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
1264
|
+
};
|
|
1265
|
+
expect(deserializeWizardState(JSON.stringify(partial))).toBeUndefined();
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
test("unknown currentStep value returns undefined", () => {
|
|
1269
|
+
const invalid: Record<string, unknown> = {
|
|
1270
|
+
currentStep: "nonexistent-step",
|
|
1271
|
+
steps: {
|
|
1272
|
+
"select-direction": { status: "idle" },
|
|
1273
|
+
"upload-bundle": { status: "idle" },
|
|
1274
|
+
validate: { status: "idle" },
|
|
1275
|
+
"preflight-review": { status: "idle" },
|
|
1276
|
+
transfer: { status: "idle" },
|
|
1277
|
+
"rebind-secrets": { status: "idle" },
|
|
1278
|
+
complete: { status: "idle" },
|
|
1279
|
+
},
|
|
1280
|
+
hasBundleData: false,
|
|
1281
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
1282
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
1283
|
+
};
|
|
1284
|
+
expect(deserializeWizardState(JSON.stringify(invalid))).toBeUndefined();
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test("missing step entries returns undefined", () => {
|
|
1288
|
+
const incomplete = {
|
|
1289
|
+
currentStep: "validate",
|
|
1290
|
+
steps: {
|
|
1291
|
+
"select-direction": { status: "success" },
|
|
1292
|
+
// Missing other steps
|
|
1293
|
+
},
|
|
1294
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
1295
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
1296
|
+
};
|
|
1297
|
+
expect(deserializeWizardState(JSON.stringify(incomplete))).toBeUndefined();
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
test("step with missing status returns undefined", () => {
|
|
1301
|
+
const badStatus = {
|
|
1302
|
+
currentStep: "validate",
|
|
1303
|
+
steps: {
|
|
1304
|
+
"select-direction": { status: "success" },
|
|
1305
|
+
"upload-bundle": { status: "success" },
|
|
1306
|
+
validate: {}, // Missing status
|
|
1307
|
+
"preflight-review": { status: "idle" },
|
|
1308
|
+
transfer: { status: "idle" },
|
|
1309
|
+
"rebind-secrets": { status: "idle" },
|
|
1310
|
+
complete: { status: "idle" },
|
|
1311
|
+
},
|
|
1312
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
1313
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
1314
|
+
};
|
|
1315
|
+
expect(deserializeWizardState(JSON.stringify(badStatus))).toBeUndefined();
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test("corrupted JSON with partial data is safely rejected", () => {
|
|
1319
|
+
const truncated = '{"currentStep":"validate","steps":{"select-direction":{';
|
|
1320
|
+
expect(deserializeWizardState(truncated)).toBeUndefined();
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
test("array instead of object returns undefined", () => {
|
|
1324
|
+
expect(deserializeWizardState("[1, 2, 3]")).toBeUndefined();
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
test("number instead of object returns undefined", () => {
|
|
1328
|
+
expect(deserializeWizardState("42")).toBeUndefined();
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
describe("edge cases — bundle data loss after deserialization", () => {
|
|
1333
|
+
test("hasBundleData is always false after deserialization", () => {
|
|
1334
|
+
const state = advanceTo("validate");
|
|
1335
|
+
expect(state.hasBundleData).toBe(true);
|
|
1336
|
+
|
|
1337
|
+
const json = serializeWizardState(state);
|
|
1338
|
+
const restored = deserializeWizardState(json);
|
|
1339
|
+
expect(restored!.hasBundleData).toBe(false);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
test("all bundle-requiring steps redirect to upload-bundle on resume without bundle data", () => {
|
|
1343
|
+
const bundleRequiringSteps: WizardStep[] = [
|
|
1344
|
+
"validate",
|
|
1345
|
+
"preflight-review",
|
|
1346
|
+
"transfer",
|
|
1347
|
+
];
|
|
1348
|
+
|
|
1349
|
+
for (const step of bundleRequiringSteps) {
|
|
1350
|
+
const state = advanceTo(step);
|
|
1351
|
+
const json = serializeWizardState(state);
|
|
1352
|
+
const restored = deserializeWizardState(json);
|
|
1353
|
+
const resumed = prepareForResume(restored!);
|
|
1354
|
+
|
|
1355
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
test("non-bundle-requiring steps stay in place on resume", () => {
|
|
1360
|
+
// rebind-secrets does not need bundle data
|
|
1361
|
+
const state = advanceTo("rebind-secrets");
|
|
1362
|
+
const json = serializeWizardState(state);
|
|
1363
|
+
const restored = deserializeWizardState(json);
|
|
1364
|
+
const resumed = prepareForResume(restored!);
|
|
1365
|
+
|
|
1366
|
+
expect(resumed.currentStep).toBe("rebind-secrets");
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
test("direction survives but step results are cleared when bundle data is lost", () => {
|
|
1370
|
+
let state = advanceTo("transfer");
|
|
1371
|
+
state = {
|
|
1372
|
+
...state,
|
|
1373
|
+
validateResult: makeValidateSuccess(),
|
|
1374
|
+
preflightResult: makePreflightSuccess(),
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
const json = serializeWizardState(state);
|
|
1378
|
+
const restored = deserializeWizardState(json);
|
|
1379
|
+
const resumed = prepareForResume(restored!);
|
|
1380
|
+
|
|
1381
|
+
// Redirected to upload-bundle; direction is preserved but results are cleared
|
|
1382
|
+
expect(resumed.currentStep).toBe("upload-bundle");
|
|
1383
|
+
expect(resumed.direction).toBe("managed-to-self-hosted");
|
|
1384
|
+
expect(resumed.validateResult).toBeUndefined();
|
|
1385
|
+
expect(resumed.preflightResult).toBeUndefined();
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// ===========================================================================
|
|
1390
|
+
// 6. CROSS-SCREEN CONSISTENCY
|
|
1391
|
+
// ===========================================================================
|
|
1392
|
+
|
|
1393
|
+
describe("cross-screen consistency — validation -> transfer -> rebind", () => {
|
|
1394
|
+
test("all screens report disabled before direction is selected", () => {
|
|
1395
|
+
const state = createWizardState();
|
|
1396
|
+
const completion = createTaskCompletionState();
|
|
1397
|
+
|
|
1398
|
+
expect(deriveValidationScreenState(state).phase).toBe("disabled");
|
|
1399
|
+
expect(deriveTransferScreenState(state).phase).toBe("disabled");
|
|
1400
|
+
expect(deriveRebindSecretsScreenState(state, completion).phase).toBe(
|
|
1401
|
+
"disabled",
|
|
1402
|
+
);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
test("only validation screen is accessible at validate step", () => {
|
|
1406
|
+
const state = advanceTo("validate");
|
|
1407
|
+
|
|
1408
|
+
expect(isValidationScreenAccessible(state)).toBe(true);
|
|
1409
|
+
expect(isTransferScreenAccessible(state)).toBe(false);
|
|
1410
|
+
expect(isRebindSecretsScreenAccessible(state)).toBe(false);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
test("validation and transfer screens accessible at transfer step", () => {
|
|
1414
|
+
const state = advanceTo("transfer");
|
|
1415
|
+
|
|
1416
|
+
expect(isValidationScreenAccessible(state)).toBe(true);
|
|
1417
|
+
expect(isTransferScreenAccessible(state)).toBe(true);
|
|
1418
|
+
expect(isRebindSecretsScreenAccessible(state)).toBe(false);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
test("all screens accessible at rebind-secrets step", () => {
|
|
1422
|
+
const state = advanceTo("rebind-secrets");
|
|
1423
|
+
|
|
1424
|
+
// Validation screen shows success from earlier steps
|
|
1425
|
+
expect(deriveValidationScreenState(state).phase).toBe("success");
|
|
1426
|
+
// Transfer screen shows success
|
|
1427
|
+
expect(deriveTransferScreenState(state).phase).toBe("success");
|
|
1428
|
+
// Rebind screen is active
|
|
1429
|
+
expect(isRebindSecretsScreenAccessible(state)).toBe(true);
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
test("all screens show their success states at complete step", () => {
|
|
1433
|
+
const state = advanceTo("complete");
|
|
1434
|
+
const completion = completeAllTasks(createTaskCompletionState());
|
|
1435
|
+
|
|
1436
|
+
expect(deriveValidationScreenState(state).phase).toBe("success");
|
|
1437
|
+
expect(deriveTransferScreenState(state).phase).toBe("success");
|
|
1438
|
+
expect(deriveRebindSecretsScreenState(state, completion).phase).toBe(
|
|
1439
|
+
"complete",
|
|
1440
|
+
);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test("going back from transfer resets transfer but preserves validation", () => {
|
|
1444
|
+
let state = advanceTo("transfer");
|
|
1445
|
+
state = {
|
|
1446
|
+
...state,
|
|
1447
|
+
validateResult: makeValidateSuccess(),
|
|
1448
|
+
preflightResult: makePreflightSuccess(),
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// Go back to preflight-review
|
|
1452
|
+
const backState = goBackTo(state, "preflight-review");
|
|
1453
|
+
|
|
1454
|
+
// Validation screen should still show results
|
|
1455
|
+
const valScreen = deriveValidationScreenState(backState);
|
|
1456
|
+
expect(valScreen.phase).toBe("loading"); // preflight idle means "preparing"
|
|
1457
|
+
|
|
1458
|
+
// Transfer screen should be disabled
|
|
1459
|
+
expect(deriveTransferScreenState(backState).phase).toBe("disabled");
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
test("going back from rebind-secrets resets rebind and transfer", () => {
|
|
1463
|
+
let state = advanceTo("rebind-secrets");
|
|
1464
|
+
state = {
|
|
1465
|
+
...state,
|
|
1466
|
+
validateResult: makeValidateSuccess(),
|
|
1467
|
+
preflightResult: makePreflightSuccess(),
|
|
1468
|
+
importResult: makeImportSuccess(),
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
// Go back to transfer
|
|
1472
|
+
const backState = goBackTo(state, "transfer");
|
|
1473
|
+
|
|
1474
|
+
expect(backState.currentStep).toBe("transfer");
|
|
1475
|
+
expect(backState.steps["rebind-secrets"].status).toBe("idle");
|
|
1476
|
+
expect(backState.steps.transfer.status).toBe("idle");
|
|
1477
|
+
|
|
1478
|
+
// Validation results should be preserved
|
|
1479
|
+
expect(backState.validateResult).toBeDefined();
|
|
1480
|
+
expect(backState.preflightResult).toBeDefined();
|
|
1481
|
+
// Transfer results should be cleared
|
|
1482
|
+
expect(backState.exportResult).toBeUndefined();
|
|
1483
|
+
expect(backState.importResult).toBeUndefined();
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
test("screen states are consistent through full serialize/resume cycle at each step", () => {
|
|
1487
|
+
const steps: WizardStep[] = [
|
|
1488
|
+
"select-direction",
|
|
1489
|
+
"upload-bundle",
|
|
1490
|
+
"validate",
|
|
1491
|
+
"preflight-review",
|
|
1492
|
+
"transfer",
|
|
1493
|
+
"rebind-secrets",
|
|
1494
|
+
"complete",
|
|
1495
|
+
];
|
|
1496
|
+
|
|
1497
|
+
for (const step of steps) {
|
|
1498
|
+
const state = advanceTo(step);
|
|
1499
|
+
const json = serializeWizardState(state);
|
|
1500
|
+
const restored = deserializeWizardState(json);
|
|
1501
|
+
expect(restored).toBeDefined();
|
|
1502
|
+
|
|
1503
|
+
// Screen derivation should not throw for any step
|
|
1504
|
+
const valScreen = deriveValidationScreenState(restored!);
|
|
1505
|
+
const transferScreen = deriveTransferScreenState(restored!);
|
|
1506
|
+
const rebindScreen = deriveRebindSecretsScreenState(
|
|
1507
|
+
restored!,
|
|
1508
|
+
createTaskCompletionState(),
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
// All screens should return a valid phase
|
|
1512
|
+
expect(valScreen.phase).toBeDefined();
|
|
1513
|
+
expect(transferScreen.phase).toBeDefined();
|
|
1514
|
+
expect(rebindScreen.phase).toBeDefined();
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
test("validation screen success data is consistent with preflight results", () => {
|
|
1519
|
+
const state = advanceTo("transfer");
|
|
1520
|
+
const screen = deriveValidationScreenState(state);
|
|
1521
|
+
|
|
1522
|
+
if (screen.phase === "success") {
|
|
1523
|
+
const preflightResult = state.preflightResult;
|
|
1524
|
+
expect(preflightResult).toBeDefined();
|
|
1525
|
+
|
|
1526
|
+
if (preflightResult && preflightResult.can_import) {
|
|
1527
|
+
// Screen summary should match raw preflight data
|
|
1528
|
+
expect(screen.preflight.summary.totalFiles).toBe(
|
|
1529
|
+
preflightResult.summary.total_files,
|
|
1530
|
+
);
|
|
1531
|
+
expect(screen.preflight.summary.filesToCreate).toBe(
|
|
1532
|
+
preflightResult.summary.files_to_create,
|
|
1533
|
+
);
|
|
1534
|
+
expect(screen.preflight.summary.filesToOverwrite).toBe(
|
|
1535
|
+
preflightResult.summary.files_to_overwrite,
|
|
1536
|
+
);
|
|
1537
|
+
expect(screen.preflight.summary.filesUnchanged).toBe(
|
|
1538
|
+
preflightResult.summary.files_unchanged,
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
// File count should match
|
|
1542
|
+
expect(screen.preflight.files.length).toBe(
|
|
1543
|
+
preflightResult.files.length,
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
test("transfer screen import summary is consistent with import result", () => {
|
|
1550
|
+
const state = advanceTo("rebind-secrets");
|
|
1551
|
+
const screen = deriveTransferScreenState(state);
|
|
1552
|
+
|
|
1553
|
+
if (screen.phase === "success") {
|
|
1554
|
+
const importResult = state.importResult;
|
|
1555
|
+
expect(importResult).toBeDefined();
|
|
1556
|
+
|
|
1557
|
+
if (importResult && importResult.success) {
|
|
1558
|
+
expect(screen.importSummary.totalFiles).toBe(
|
|
1559
|
+
importResult.summary.total_files,
|
|
1560
|
+
);
|
|
1561
|
+
expect(screen.importSummary.filesCreated).toBe(
|
|
1562
|
+
importResult.summary.files_created,
|
|
1563
|
+
);
|
|
1564
|
+
expect(screen.importSummary.filesOverwritten).toBe(
|
|
1565
|
+
importResult.summary.files_overwritten,
|
|
1566
|
+
);
|
|
1567
|
+
expect(screen.importSummary.filesSkipped).toBe(
|
|
1568
|
+
importResult.summary.files_skipped,
|
|
1569
|
+
);
|
|
1570
|
+
expect(screen.importSummary.backupsCreated).toBe(
|
|
1571
|
+
importResult.summary.backups_created,
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
test("rebind task count stays consistent throughout the wizard lifecycle", () => {
|
|
1578
|
+
const taskIds = getTaskIds();
|
|
1579
|
+
const expectedTaskCount = 4;
|
|
1580
|
+
|
|
1581
|
+
expect(taskIds.length).toBe(expectedTaskCount);
|
|
1582
|
+
|
|
1583
|
+
// At every step, if the screen is active, it should show the same tasks
|
|
1584
|
+
const state = advanceTo("rebind-secrets");
|
|
1585
|
+
const completion = createTaskCompletionState();
|
|
1586
|
+
const screen = deriveRebindSecretsScreenState(state, completion);
|
|
1587
|
+
|
|
1588
|
+
if (screen.phase === "active") {
|
|
1589
|
+
expect(screen.totalCount).toBe(expectedTaskCount);
|
|
1590
|
+
expect(screen.tasks.length).toBe(expectedTaskCount);
|
|
1591
|
+
// Task IDs match
|
|
1592
|
+
const screenTaskIds = screen.tasks.map((t) => t.id);
|
|
1593
|
+
expect(screenTaskIds).toEqual([...taskIds]);
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
describe("cross-screen consistency — error propagation", () => {
|
|
1599
|
+
test("validation error does not leak into transfer or rebind screens", () => {
|
|
1600
|
+
let state = advanceTo("validate");
|
|
1601
|
+
state = {
|
|
1602
|
+
...state,
|
|
1603
|
+
steps: {
|
|
1604
|
+
...state.steps,
|
|
1605
|
+
validate: {
|
|
1606
|
+
status: "error",
|
|
1607
|
+
error: {
|
|
1608
|
+
message: "validate: HTTP 500",
|
|
1609
|
+
code: "HTTP_500",
|
|
1610
|
+
retryable: true,
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
// Validation screen shows the error
|
|
1617
|
+
const valScreen = deriveValidationScreenState(state);
|
|
1618
|
+
expect(valScreen.phase).toBe("transport-error");
|
|
1619
|
+
|
|
1620
|
+
// Transfer and rebind screens should be disabled, not errored
|
|
1621
|
+
expect(deriveTransferScreenState(state).phase).toBe("disabled");
|
|
1622
|
+
expect(
|
|
1623
|
+
deriveRebindSecretsScreenState(state, createTaskCompletionState()).phase,
|
|
1624
|
+
).toBe("disabled");
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
test("transfer error does not leak into validation or rebind screens", () => {
|
|
1628
|
+
let state = advanceTo("transfer");
|
|
1629
|
+
state = {
|
|
1630
|
+
...state,
|
|
1631
|
+
steps: {
|
|
1632
|
+
...state.steps,
|
|
1633
|
+
transfer: {
|
|
1634
|
+
status: "error",
|
|
1635
|
+
error: {
|
|
1636
|
+
message: "export: HTTP 500",
|
|
1637
|
+
code: "HTTP_500",
|
|
1638
|
+
retryable: true,
|
|
1639
|
+
},
|
|
1640
|
+
},
|
|
1641
|
+
},
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
// Transfer screen shows the error
|
|
1645
|
+
const transferScreen = deriveTransferScreenState(state);
|
|
1646
|
+
expect(transferScreen.phase).toBe("error");
|
|
1647
|
+
|
|
1648
|
+
// Validation screen should still show success (validate and preflight passed)
|
|
1649
|
+
const valScreen = deriveValidationScreenState(state);
|
|
1650
|
+
expect(valScreen.phase).toBe("success");
|
|
1651
|
+
|
|
1652
|
+
// Rebind screen should be disabled
|
|
1653
|
+
expect(
|
|
1654
|
+
deriveRebindSecretsScreenState(state, createTaskCompletionState()).phase,
|
|
1655
|
+
).toBe("disabled");
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
describe("cross-screen consistency — step accessibility invariants", () => {
|
|
1660
|
+
test("step accessibility is monotonically increasing through normal flow", () => {
|
|
1661
|
+
const steps: WizardStep[] = [
|
|
1662
|
+
"select-direction",
|
|
1663
|
+
"upload-bundle",
|
|
1664
|
+
"validate",
|
|
1665
|
+
"preflight-review",
|
|
1666
|
+
"transfer",
|
|
1667
|
+
"rebind-secrets",
|
|
1668
|
+
];
|
|
1669
|
+
|
|
1670
|
+
// At each step, all previous steps should be accessible (can go back)
|
|
1671
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1672
|
+
const state = advanceTo(steps[i]);
|
|
1673
|
+
for (let j = 0; j < i; j++) {
|
|
1674
|
+
expect(isStepAccessible(state, steps[j])).toBe(true);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
test("complete step is terminal — no transitions allowed", () => {
|
|
1680
|
+
const state = advanceTo("complete");
|
|
1681
|
+
const steps: WizardStep[] = [
|
|
1682
|
+
"select-direction",
|
|
1683
|
+
"upload-bundle",
|
|
1684
|
+
"validate",
|
|
1685
|
+
"preflight-review",
|
|
1686
|
+
"transfer",
|
|
1687
|
+
"rebind-secrets",
|
|
1688
|
+
];
|
|
1689
|
+
|
|
1690
|
+
for (const step of steps) {
|
|
1691
|
+
const result = validateWizardTransition(state, step);
|
|
1692
|
+
expect(result.valid).toBe(false);
|
|
1693
|
+
expect(result.reason).toContain("terminal");
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
// ===========================================================================
|
|
1699
|
+
// 7. ADDITIONAL PERSISTENCE EDGE CASES
|
|
1700
|
+
// ===========================================================================
|
|
1701
|
+
|
|
1702
|
+
describe("persistence — task completion state portability", () => {
|
|
1703
|
+
test("task completion state serializes and deserializes independently", () => {
|
|
1704
|
+
let completion = createTaskCompletionState();
|
|
1705
|
+
completion = markTaskComplete(completion, "re-enter-secrets");
|
|
1706
|
+
completion = markTaskComplete(completion, "rebind-channels");
|
|
1707
|
+
|
|
1708
|
+
const json = JSON.stringify(completion);
|
|
1709
|
+
const restored = JSON.parse(json) as RebindTaskCompletionState;
|
|
1710
|
+
|
|
1711
|
+
expect(restored["re-enter-secrets"]).toBe(true);
|
|
1712
|
+
expect(restored["rebind-channels"]).toBe(true);
|
|
1713
|
+
expect(restored["reconfigure-auth"]).toBe(false);
|
|
1714
|
+
expect(restored["verify-webhooks"]).toBe(false);
|
|
1715
|
+
|
|
1716
|
+
// areAllRequiredTasksComplete should work on deserialized state
|
|
1717
|
+
expect(areAllRequiredTasksComplete(restored)).toBe(false);
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
test("wizard state + task completion state can be stored and restored together", () => {
|
|
1721
|
+
const wizardState = advanceTo("rebind-secrets");
|
|
1722
|
+
let completion = createTaskCompletionState();
|
|
1723
|
+
completion = completeOnlyRequired(completion);
|
|
1724
|
+
|
|
1725
|
+
// Simulate persisting both as a combined payload
|
|
1726
|
+
const combined = JSON.stringify({
|
|
1727
|
+
wizard: JSON.parse(serializeWizardState(wizardState)),
|
|
1728
|
+
tasks: completion,
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
const parsed = JSON.parse(combined);
|
|
1732
|
+
const restoredWizard = deserializeWizardState(
|
|
1733
|
+
JSON.stringify(parsed.wizard),
|
|
1734
|
+
);
|
|
1735
|
+
const restoredTasks = parsed.tasks as RebindTaskCompletionState;
|
|
1736
|
+
|
|
1737
|
+
expect(restoredWizard).toBeDefined();
|
|
1738
|
+
expect(restoredWizard!.currentStep).toBe("rebind-secrets");
|
|
1739
|
+
expect(areAllRequiredTasksComplete(restoredTasks)).toBe(true);
|
|
1740
|
+
|
|
1741
|
+
// Derive screen from restored state
|
|
1742
|
+
const screen = deriveRebindSecretsScreenState(
|
|
1743
|
+
restoredWizard!,
|
|
1744
|
+
restoredTasks,
|
|
1745
|
+
);
|
|
1746
|
+
expect(screen.phase).toBe("active");
|
|
1747
|
+
if (screen.phase === "active") {
|
|
1748
|
+
expect(screen.allRequiredComplete).toBe(true);
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
describe("persistence — double serialization idempotency", () => {
|
|
1754
|
+
test("serializing twice produces identical output", () => {
|
|
1755
|
+
const state = advanceTo("transfer");
|
|
1756
|
+
const json1 = serializeWizardState(state);
|
|
1757
|
+
const restored = deserializeWizardState(json1);
|
|
1758
|
+
const json2 = serializeWizardState(restored!);
|
|
1759
|
+
|
|
1760
|
+
// hasBundleData changes from true to false on deserialization,
|
|
1761
|
+
// so the second serialization will differ from the first in that field.
|
|
1762
|
+
const parsed1 = JSON.parse(json1);
|
|
1763
|
+
const parsed2 = JSON.parse(json2);
|
|
1764
|
+
parsed1.hasBundleData = false; // Normalize for comparison
|
|
1765
|
+
expect(parsed2).toEqual(parsed1);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
test("prepareForResume is idempotent for non-loading, non-bundle steps", () => {
|
|
1769
|
+
const state = advanceTo("rebind-secrets");
|
|
1770
|
+
const json = serializeWizardState(state);
|
|
1771
|
+
const restored = deserializeWizardState(json)!;
|
|
1772
|
+
|
|
1773
|
+
const resumed1 = prepareForResume(restored);
|
|
1774
|
+
const resumed2 = prepareForResume(resumed1);
|
|
1775
|
+
|
|
1776
|
+
// Both should be at rebind-secrets with idle status
|
|
1777
|
+
expect(resumed1.currentStep).toBe(resumed2.currentStep);
|
|
1778
|
+
expect(resumed1.steps["rebind-secrets"].status).toBe(
|
|
1779
|
+
resumed2.steps["rebind-secrets"].status,
|
|
1780
|
+
);
|
|
1781
|
+
});
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
// ===========================================================================
|
|
1785
|
+
// 8. RETRY + BACK NAVIGATION AFTER PERSISTENCE
|
|
1786
|
+
// ===========================================================================
|
|
1787
|
+
|
|
1788
|
+
describe("retry and back navigation after persistence round-trip", () => {
|
|
1789
|
+
test("can retry after restoring error state from persistence", () => {
|
|
1790
|
+
let state = advanceTo("validate");
|
|
1791
|
+
state = {
|
|
1792
|
+
...state,
|
|
1793
|
+
steps: {
|
|
1794
|
+
...state.steps,
|
|
1795
|
+
validate: {
|
|
1796
|
+
status: "error",
|
|
1797
|
+
error: {
|
|
1798
|
+
message: "validate: HTTP 500",
|
|
1799
|
+
code: "HTTP_500",
|
|
1800
|
+
retryable: true,
|
|
1801
|
+
},
|
|
1802
|
+
},
|
|
1803
|
+
},
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
const json = serializeWizardState(state);
|
|
1807
|
+
const restored = deserializeWizardState(json)!;
|
|
1808
|
+
|
|
1809
|
+
// Should be able to retry from restored state
|
|
1810
|
+
expect(canRetryCurrentStep(restored)).toBe(true);
|
|
1811
|
+
|
|
1812
|
+
const reset = resetStepForRetry(restored);
|
|
1813
|
+
expect(reset.steps.validate.status).toBe("idle");
|
|
1814
|
+
expect(reset.currentStep).toBe("validate");
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
test("can navigate back after restoring state from persistence", () => {
|
|
1818
|
+
const state = advanceTo("transfer");
|
|
1819
|
+
const json = serializeWizardState(state);
|
|
1820
|
+
const restored = deserializeWizardState(json)!;
|
|
1821
|
+
|
|
1822
|
+
// Should be able to go back
|
|
1823
|
+
const backState = goBackTo(restored, "validate");
|
|
1824
|
+
expect(backState.currentStep).toBe("validate");
|
|
1825
|
+
expect(backState.steps.validate.status).toBe("idle");
|
|
1826
|
+
expect(backState.steps["preflight-review"].status).toBe("idle");
|
|
1827
|
+
expect(backState.steps.transfer.status).toBe("idle");
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
test("non-retryable error is preserved through persistence round-trip", () => {
|
|
1831
|
+
let state = advanceTo("validate");
|
|
1832
|
+
state = {
|
|
1833
|
+
...state,
|
|
1834
|
+
steps: {
|
|
1835
|
+
...state.steps,
|
|
1836
|
+
validate: {
|
|
1837
|
+
status: "error",
|
|
1838
|
+
error: {
|
|
1839
|
+
message: "Fatal: corrupted bundle",
|
|
1840
|
+
retryable: false,
|
|
1841
|
+
},
|
|
1842
|
+
},
|
|
1843
|
+
},
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
const json = serializeWizardState(state);
|
|
1847
|
+
const restored = deserializeWizardState(json)!;
|
|
1848
|
+
|
|
1849
|
+
expect(canRetryCurrentStep(restored)).toBe(false);
|
|
1850
|
+
expect(() => resetStepForRetry(restored)).toThrow(
|
|
1851
|
+
"not in a retryable error state",
|
|
1852
|
+
);
|
|
1853
|
+
});
|
|
1854
|
+
});
|