@vellumai/assistant 0.5.6 → 0.5.8
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 +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +3 -2
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/memory.md +13 -11
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -2
- package/src/__tests__/config-schema.test.ts +3 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
- package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
- package/src/__tests__/credential-security-e2e.test.ts +1 -67
- package/src/__tests__/credential-security-invariants.test.ts +6 -50
- package/src/__tests__/credentials-cli.test.ts +82 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +2 -7
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +95 -272
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +90 -8
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +129 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +34 -5
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +38 -5
- package/src/calls/voice-session-bridge.ts +7 -12
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +128 -82
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +525 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +47 -2
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +5 -11
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +5 -5
- package/src/config/env.ts +21 -14
- package/src/config/feature-flag-registry.json +49 -9
- package/src/config/loader.ts +106 -42
- package/src/config/schema.ts +9 -29
- package/src/config/schemas/calls.ts +30 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +1 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +21 -1
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +234 -51
- package/src/daemon/message-types/conversations.ts +4 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +32 -95
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/app-store.ts +31 -0
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +16 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +328 -321
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +63 -6
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +98 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/decision-engine.ts +4 -1
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +194 -0
- package/src/prompts/system-prompt.ts +44 -4
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +29 -179
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +17 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +31 -3
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +19 -30
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +46 -14
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +96 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +75 -29
- package/src/security/ces-rpc-credential-backend.ts +86 -0
- package/src/security/credential-backend.ts +22 -92
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +113 -115
- package/src/skills/catalog-install.ts +6 -32
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/executor.ts +0 -4
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/types.ts +0 -8
- package/src/util/browser.ts +15 -0
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +4 -51
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
- package/src/workspace/migrations/registry.ts +12 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -10,18 +10,60 @@
|
|
|
10
10
|
* migration system detects and handles it gracefully.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
13
14
|
import { Database } from "bun:sqlite";
|
|
14
|
-
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
15
16
|
|
|
16
17
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
17
18
|
|
|
18
19
|
import { getSqliteFrom } from "../memory/db-connection.js";
|
|
20
|
+
import { downJobDeferrals } from "../memory/migrations/001-job-deferrals.js";
|
|
21
|
+
import { downMemoryEntityRelationDedup } from "../memory/migrations/004-entity-relation-dedup.js";
|
|
22
|
+
import { downMemoryItemsFingerprintScopeUnique } from "../memory/migrations/005-fingerprint-scope-unique.js";
|
|
23
|
+
import { downMemoryItemsScopeSaltedFingerprints } from "../memory/migrations/006-scope-salted-fingerprints.js";
|
|
24
|
+
import { downAssistantIdToSelf } from "../memory/migrations/007-assistant-id-to-self.js";
|
|
25
|
+
import { downRemoveAssistantIdColumns } from "../memory/migrations/008-remove-assistant-id-columns.js";
|
|
26
|
+
import { downLlmUsageEventsDropAssistantId } from "../memory/migrations/009-llm-usage-events-drop-assistant-id.js";
|
|
27
|
+
import { downBackfillInboxThreadState } from "../memory/migrations/014-backfill-inbox-thread-state.js";
|
|
28
|
+
import { downDropActiveSearchIndex } from "../memory/migrations/015-drop-active-search-index.js";
|
|
29
|
+
import { downNotificationTablesSchema } from "../memory/migrations/019-notification-tables-schema-migration.js";
|
|
30
|
+
import { downRenameChannelToVellum } from "../memory/migrations/020-rename-macos-ios-channel-to-vellum.js";
|
|
31
|
+
import { downEmbeddingVectorBlob } from "../memory/migrations/024-embedding-vector-blob.js";
|
|
32
|
+
import { downEmbeddingsNullableVectorJson } from "../memory/migrations/026a-embeddings-nullable-vector-json.js";
|
|
33
|
+
import { downNormalizePhoneIdentities } from "../memory/migrations/036-normalize-phone-identities.js";
|
|
34
|
+
import { downBackfillGuardianPrincipalId } from "../memory/migrations/126-backfill-guardian-principal-id.js";
|
|
35
|
+
import { downGuardianPrincipalIdNotNull } from "../memory/migrations/127-guardian-principal-id-not-null.js";
|
|
36
|
+
import { downContactsNotesColumn } from "../memory/migrations/134-contacts-notes-column.js";
|
|
37
|
+
import { downBackfillContactInteractionStats } from "../memory/migrations/135-backfill-contact-interaction-stats.js";
|
|
38
|
+
import { downDropAssistantIdColumns } from "../memory/migrations/136-drop-assistant-id-columns.js";
|
|
39
|
+
import { downBackfillUsageCacheAccounting } from "../memory/migrations/140-backfill-usage-cache-accounting.js";
|
|
40
|
+
import { downRenameVerificationTable } from "../memory/migrations/141-rename-verification-table.js";
|
|
41
|
+
import { downRenameVerificationSessionIdColumn } from "../memory/migrations/142-rename-verification-session-id-column.js";
|
|
42
|
+
import { downRenameGuardianVerificationValues } from "../memory/migrations/143-rename-guardian-verification-values.js";
|
|
43
|
+
import { downRenameVoiceToPhone } from "../memory/migrations/144-rename-voice-to-phone.js";
|
|
44
|
+
import { migrateDropAccountsTableDown } from "../memory/migrations/145-drop-accounts-table.js";
|
|
45
|
+
import { migrateRemindersToSchedulesDown } from "../memory/migrations/147-migrate-reminders-to-schedules.js";
|
|
46
|
+
import { migrateDropRemindersTableDown } from "../memory/migrations/148-drop-reminders-table.js";
|
|
47
|
+
import { migrateOAuthAppsClientSecretPathDown } from "../memory/migrations/150-oauth-apps-client-secret-path.js";
|
|
48
|
+
import {
|
|
49
|
+
migrateGuardianTimestampsEpochMsDown,
|
|
50
|
+
migrateGuardianTimestampsRebuildDown,
|
|
51
|
+
} from "../memory/migrations/162-guardian-timestamps-epoch-ms.js";
|
|
52
|
+
import { migrateRenameGmailProviderKeyToGoogleDown } from "../memory/migrations/169-rename-gmail-provider-key-to-google.js";
|
|
53
|
+
import { migrateRenameThreadStartersTableDown } from "../memory/migrations/174-rename-thread-starters-table.js";
|
|
54
|
+
import { migrateDropCapabilityCardStateDown } from "../memory/migrations/176-drop-capability-card-state.js";
|
|
55
|
+
import { migrateBackfillInlineAttachmentsToDiskDown } from "../memory/migrations/180-backfill-inline-attachments-to-disk.js";
|
|
56
|
+
import { migrateRenameThreadStartersCheckpointsDown } from "../memory/migrations/181-rename-thread-starters-checkpoints.js";
|
|
57
|
+
import { migrateBackfillAudioAttachmentMimeTypesDown } from "../memory/migrations/191-backfill-audio-attachment-mime-types.js";
|
|
19
58
|
import {
|
|
20
59
|
migrateJobDeferrals,
|
|
21
60
|
migrateMemoryEntityRelationDedup,
|
|
22
61
|
migrateMemoryItemsFingerprintScopeUnique,
|
|
62
|
+
migrateMemoryItemsScopeSaltedFingerprints,
|
|
23
63
|
MIGRATION_REGISTRY,
|
|
64
|
+
type MigrationRegistryEntry,
|
|
24
65
|
type MigrationValidationResult,
|
|
66
|
+
rollbackMemoryMigration,
|
|
25
67
|
validateMigrationState,
|
|
26
68
|
} from "../memory/migrations/index.js";
|
|
27
69
|
import * as schema from "../memory/schema.js";
|
|
@@ -877,3 +919,1975 @@ describe("schema-drift recovery: migration handles unexpected schema state", ()
|
|
|
877
919
|
expect(countAfter2).toBe(3);
|
|
878
920
|
});
|
|
879
921
|
});
|
|
922
|
+
|
|
923
|
+
// ---------------------------------------------------------------------------
|
|
924
|
+
// 3. rollbackMemoryMigration
|
|
925
|
+
// ---------------------------------------------------------------------------
|
|
926
|
+
|
|
927
|
+
describe("rollbackMemoryMigration", () => {
|
|
928
|
+
// Track test entries pushed onto MIGRATION_REGISTRY so we can restore after
|
|
929
|
+
// each test. This avoids polluting the real registry across test runs.
|
|
930
|
+
let registrySnapshot: MigrationRegistryEntry[];
|
|
931
|
+
|
|
932
|
+
function saveRegistry() {
|
|
933
|
+
registrySnapshot = [...MIGRATION_REGISTRY];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function restoreRegistry() {
|
|
937
|
+
MIGRATION_REGISTRY.length = 0;
|
|
938
|
+
MIGRATION_REGISTRY.push(...registrySnapshot);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
afterEach(() => {
|
|
942
|
+
restoreRegistry();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test("rolls back checkpoint-tracked migrations in reverse version order", () => {
|
|
946
|
+
saveRegistry();
|
|
947
|
+
|
|
948
|
+
const db = createTestDb();
|
|
949
|
+
const raw = getRaw(db);
|
|
950
|
+
bootstrapCheckpointsTable(raw);
|
|
951
|
+
|
|
952
|
+
// Track execution order of down() calls.
|
|
953
|
+
const downCalls: string[] = [];
|
|
954
|
+
|
|
955
|
+
const now = Date.now();
|
|
956
|
+
|
|
957
|
+
// Use very high version numbers to avoid colliding with real registry entries.
|
|
958
|
+
const testEntries: MigrationRegistryEntry[] = [
|
|
959
|
+
{
|
|
960
|
+
key: "test_rollback_v1000",
|
|
961
|
+
version: 1000,
|
|
962
|
+
description: "test migration v1000",
|
|
963
|
+
down: () => {
|
|
964
|
+
downCalls.push("test_rollback_v1000");
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
key: "test_rollback_v1001",
|
|
969
|
+
version: 1001,
|
|
970
|
+
description: "test migration v1001",
|
|
971
|
+
down: () => {
|
|
972
|
+
downCalls.push("test_rollback_v1001");
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
key: "test_rollback_v1002",
|
|
977
|
+
version: 1002,
|
|
978
|
+
description: "test migration v1002",
|
|
979
|
+
down: () => {
|
|
980
|
+
downCalls.push("test_rollback_v1002");
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
];
|
|
984
|
+
|
|
985
|
+
MIGRATION_REGISTRY.push(...testEntries);
|
|
986
|
+
|
|
987
|
+
// Simulate all three migrations as completed.
|
|
988
|
+
for (const entry of testEntries) {
|
|
989
|
+
raw.exec(
|
|
990
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('${entry.key}', '1', ${now})`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Roll back to version 1000 — should roll back v1002 and v1001 (version > 1000).
|
|
995
|
+
const rolledBack = rollbackMemoryMigration(db, 1000);
|
|
996
|
+
|
|
997
|
+
// Verify returned keys.
|
|
998
|
+
expect(rolledBack).toEqual(["test_rollback_v1002", "test_rollback_v1001"]);
|
|
999
|
+
|
|
1000
|
+
// Verify down() was called in reverse version order.
|
|
1001
|
+
expect(downCalls).toEqual(["test_rollback_v1002", "test_rollback_v1001"]);
|
|
1002
|
+
|
|
1003
|
+
// Checkpoints for rolled-back migrations should be deleted.
|
|
1004
|
+
const cp1001 = raw
|
|
1005
|
+
.query(
|
|
1006
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = 'test_rollback_v1001'`,
|
|
1007
|
+
)
|
|
1008
|
+
.get();
|
|
1009
|
+
expect(cp1001).toBeNull();
|
|
1010
|
+
|
|
1011
|
+
const cp1002 = raw
|
|
1012
|
+
.query(
|
|
1013
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = 'test_rollback_v1002'`,
|
|
1014
|
+
)
|
|
1015
|
+
.get();
|
|
1016
|
+
expect(cp1002).toBeNull();
|
|
1017
|
+
|
|
1018
|
+
// Checkpoint for the migration at target version should still exist.
|
|
1019
|
+
const cp1000 = raw
|
|
1020
|
+
.query(
|
|
1021
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'test_rollback_v1000'`,
|
|
1022
|
+
)
|
|
1023
|
+
.get() as { value: string } | null;
|
|
1024
|
+
expect(cp1000).toBeTruthy();
|
|
1025
|
+
expect(cp1000!.value).toBe("1");
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("handles transaction failure in down() — rolls back and preserves checkpoint", () => {
|
|
1029
|
+
saveRegistry();
|
|
1030
|
+
|
|
1031
|
+
const db = createTestDb();
|
|
1032
|
+
const raw = getRaw(db);
|
|
1033
|
+
bootstrapCheckpointsTable(raw);
|
|
1034
|
+
|
|
1035
|
+
const now = Date.now();
|
|
1036
|
+
|
|
1037
|
+
// Create a table that the down() function will try to modify.
|
|
1038
|
+
raw.exec(/*sql*/ `
|
|
1039
|
+
CREATE TABLE IF NOT EXISTS test_rollback_data (
|
|
1040
|
+
id TEXT PRIMARY KEY,
|
|
1041
|
+
value TEXT NOT NULL
|
|
1042
|
+
)
|
|
1043
|
+
`);
|
|
1044
|
+
raw.exec(
|
|
1045
|
+
`INSERT INTO test_rollback_data (id, value) VALUES ('row-1', 'original')`,
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
// Register a migration whose down() modifies test_rollback_data,
|
|
1049
|
+
// but a trigger will force the modification to fail.
|
|
1050
|
+
MIGRATION_REGISTRY.push({
|
|
1051
|
+
key: "test_fail_down_v3000",
|
|
1052
|
+
version: 3000,
|
|
1053
|
+
description: "test migration with failing down()",
|
|
1054
|
+
down: (database) => {
|
|
1055
|
+
const sqlite = getSqliteFrom(database);
|
|
1056
|
+
// This UPDATE will trigger our failure trigger.
|
|
1057
|
+
sqlite.exec(
|
|
1058
|
+
`UPDATE test_rollback_data SET value = 'rolled-back' WHERE id = 'row-1'`,
|
|
1059
|
+
);
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// Mark as completed.
|
|
1064
|
+
raw.exec(
|
|
1065
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('test_fail_down_v3000', '1', ${now})`,
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
// Install a trigger to force the down() function to fail.
|
|
1069
|
+
raw.exec(/*sql*/ `
|
|
1070
|
+
CREATE TRIGGER fail_on_update_test_rollback AFTER UPDATE ON test_rollback_data
|
|
1071
|
+
BEGIN
|
|
1072
|
+
SELECT RAISE(ABORT, 'simulated down() failure');
|
|
1073
|
+
END
|
|
1074
|
+
`);
|
|
1075
|
+
|
|
1076
|
+
// Rollback should throw because down() fails.
|
|
1077
|
+
let threw = false;
|
|
1078
|
+
try {
|
|
1079
|
+
rollbackMemoryMigration(db, 2999);
|
|
1080
|
+
} catch {
|
|
1081
|
+
threw = true;
|
|
1082
|
+
}
|
|
1083
|
+
expect(threw).toBe(true);
|
|
1084
|
+
|
|
1085
|
+
// Remove the trigger for inspection.
|
|
1086
|
+
raw.exec(`DROP TRIGGER IF EXISTS fail_on_update_test_rollback`);
|
|
1087
|
+
|
|
1088
|
+
// The checkpoint should still exist — down() threw before execution reached
|
|
1089
|
+
// the DELETE FROM memory_checkpoints line. The 'rolling_back' marker was
|
|
1090
|
+
// written before down() was called and is preserved.
|
|
1091
|
+
const cp = raw
|
|
1092
|
+
.query(
|
|
1093
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'test_fail_down_v3000'`,
|
|
1094
|
+
)
|
|
1095
|
+
.get() as { value: string } | null;
|
|
1096
|
+
expect(cp).toBeTruthy();
|
|
1097
|
+
expect(cp!.value).toBe("rolling_back");
|
|
1098
|
+
|
|
1099
|
+
// The data should be unchanged — the RAISE(ABORT) trigger aborted the statement.
|
|
1100
|
+
const row = raw
|
|
1101
|
+
.query(`SELECT value FROM test_rollback_data WHERE id = 'row-1'`)
|
|
1102
|
+
.get() as { value: string } | null;
|
|
1103
|
+
expect(row).toBeTruthy();
|
|
1104
|
+
expect(row!.value).toBe("original");
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
test("down() with its own BEGIN/COMMIT succeeds without nested-transaction errors", () => {
|
|
1108
|
+
saveRegistry();
|
|
1109
|
+
|
|
1110
|
+
const db = createTestDb();
|
|
1111
|
+
const raw = getRaw(db);
|
|
1112
|
+
bootstrapCheckpointsTable(raw);
|
|
1113
|
+
|
|
1114
|
+
const now = Date.now();
|
|
1115
|
+
|
|
1116
|
+
// Create a table for the down() function to operate on.
|
|
1117
|
+
raw.exec(/*sql*/ `
|
|
1118
|
+
CREATE TABLE IF NOT EXISTS test_self_txn_data (
|
|
1119
|
+
id TEXT PRIMARY KEY,
|
|
1120
|
+
value TEXT NOT NULL
|
|
1121
|
+
)
|
|
1122
|
+
`);
|
|
1123
|
+
raw.exec(
|
|
1124
|
+
`INSERT INTO test_self_txn_data (id, value) VALUES ('row-1', 'migrated')`,
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
// Register a migration whose down() manages its own transaction —
|
|
1128
|
+
// this previously caused nested-transaction errors when rollbackMemoryMigration
|
|
1129
|
+
// wrapped every down() call in BEGIN/COMMIT.
|
|
1130
|
+
MIGRATION_REGISTRY.push({
|
|
1131
|
+
key: "test_self_txn_down_v3500",
|
|
1132
|
+
version: 3500,
|
|
1133
|
+
description: "test migration with self-transactional down()",
|
|
1134
|
+
down: (database) => {
|
|
1135
|
+
const sqlite = getSqliteFrom(database);
|
|
1136
|
+
sqlite.exec("BEGIN");
|
|
1137
|
+
sqlite.exec(
|
|
1138
|
+
`UPDATE test_self_txn_data SET value = 'original' WHERE id = 'row-1'`,
|
|
1139
|
+
);
|
|
1140
|
+
sqlite.exec("COMMIT");
|
|
1141
|
+
},
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// Mark as completed.
|
|
1145
|
+
raw.exec(
|
|
1146
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('test_self_txn_down_v3500', '1', ${now})`,
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
// This should succeed — no nested transaction error.
|
|
1150
|
+
const rolledBack = rollbackMemoryMigration(db, 3499);
|
|
1151
|
+
|
|
1152
|
+
expect(rolledBack).toEqual(["test_self_txn_down_v3500"]);
|
|
1153
|
+
|
|
1154
|
+
// Verify the down() function's changes were applied.
|
|
1155
|
+
const row = raw
|
|
1156
|
+
.query(`SELECT value FROM test_self_txn_data WHERE id = 'row-1'`)
|
|
1157
|
+
.get() as { value: string } | null;
|
|
1158
|
+
expect(row).toBeTruthy();
|
|
1159
|
+
expect(row!.value).toBe("original");
|
|
1160
|
+
|
|
1161
|
+
// Checkpoint should be deleted.
|
|
1162
|
+
const cp = raw
|
|
1163
|
+
.query(
|
|
1164
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = 'test_self_txn_down_v3500'`,
|
|
1165
|
+
)
|
|
1166
|
+
.get();
|
|
1167
|
+
expect(cp).toBeNull();
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
test("no-op when already at target version", () => {
|
|
1171
|
+
saveRegistry();
|
|
1172
|
+
|
|
1173
|
+
const db = createTestDb();
|
|
1174
|
+
const raw = getRaw(db);
|
|
1175
|
+
bootstrapCheckpointsTable(raw);
|
|
1176
|
+
|
|
1177
|
+
const now = Date.now();
|
|
1178
|
+
|
|
1179
|
+
// Register entries with down functions — they should NOT be called.
|
|
1180
|
+
const downCalls: string[] = [];
|
|
1181
|
+
|
|
1182
|
+
MIGRATION_REGISTRY.push(
|
|
1183
|
+
{
|
|
1184
|
+
key: "test_noop_v4000",
|
|
1185
|
+
version: 4000,
|
|
1186
|
+
description: "test noop v4000",
|
|
1187
|
+
down: () => {
|
|
1188
|
+
downCalls.push("test_noop_v4000");
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
key: "test_noop_v4001",
|
|
1193
|
+
version: 4001,
|
|
1194
|
+
description: "test noop v4001",
|
|
1195
|
+
down: () => {
|
|
1196
|
+
downCalls.push("test_noop_v4001");
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
// Mark both as completed.
|
|
1202
|
+
raw.exec(
|
|
1203
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('test_noop_v4000', '1', ${now})`,
|
|
1204
|
+
);
|
|
1205
|
+
raw.exec(
|
|
1206
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('test_noop_v4001', '1', ${now})`,
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
// Roll back to version >= latest applied migration — should be a no-op.
|
|
1210
|
+
const rolledBack = rollbackMemoryMigration(db, 4001);
|
|
1211
|
+
|
|
1212
|
+
expect(rolledBack).toEqual([]);
|
|
1213
|
+
expect(downCalls).toEqual([]);
|
|
1214
|
+
|
|
1215
|
+
// Both checkpoints should remain.
|
|
1216
|
+
const cp4000 = raw
|
|
1217
|
+
.query(
|
|
1218
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'test_noop_v4000'`,
|
|
1219
|
+
)
|
|
1220
|
+
.get() as { value: string } | null;
|
|
1221
|
+
const cp4001 = raw
|
|
1222
|
+
.query(
|
|
1223
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'test_noop_v4001'`,
|
|
1224
|
+
)
|
|
1225
|
+
.get() as { value: string } | null;
|
|
1226
|
+
expect(cp4000!.value).toBe("1");
|
|
1227
|
+
expect(cp4001!.value).toBe("1");
|
|
1228
|
+
|
|
1229
|
+
// Also verify with a target version greater than the latest.
|
|
1230
|
+
const rolledBack2 = rollbackMemoryMigration(db, 9999);
|
|
1231
|
+
expect(rolledBack2).toEqual([]);
|
|
1232
|
+
expect(downCalls).toEqual([]);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
test("respects dependency ordering on rollback (children rolled back before parents)", () => {
|
|
1236
|
+
saveRegistry();
|
|
1237
|
+
|
|
1238
|
+
const db = createTestDb();
|
|
1239
|
+
const raw = getRaw(db);
|
|
1240
|
+
bootstrapCheckpointsTable(raw);
|
|
1241
|
+
|
|
1242
|
+
const now = Date.now();
|
|
1243
|
+
const downCalls: string[] = [];
|
|
1244
|
+
|
|
1245
|
+
// Parent migration at version 5000 — has a down().
|
|
1246
|
+
// Child migration at version 5001 — depends on parent, has a down().
|
|
1247
|
+
// Since the child has a higher version number, rolling back in reverse
|
|
1248
|
+
// version order means the child (v5001) is rolled back BEFORE the parent
|
|
1249
|
+
// (v5000), which is the correct dependency-safe ordering.
|
|
1250
|
+
MIGRATION_REGISTRY.push(
|
|
1251
|
+
{
|
|
1252
|
+
key: "test_parent_v5000",
|
|
1253
|
+
version: 5000,
|
|
1254
|
+
description: "test parent migration",
|
|
1255
|
+
down: () => {
|
|
1256
|
+
downCalls.push("test_parent_v5000");
|
|
1257
|
+
},
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
key: "test_child_v5001",
|
|
1261
|
+
version: 5001,
|
|
1262
|
+
dependsOn: ["test_parent_v5000"],
|
|
1263
|
+
description: "test child migration depending on parent",
|
|
1264
|
+
down: () => {
|
|
1265
|
+
downCalls.push("test_child_v5001");
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
// Both are completed.
|
|
1271
|
+
raw.exec(
|
|
1272
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('test_parent_v5000', '1', ${now})`,
|
|
1273
|
+
);
|
|
1274
|
+
raw.exec(
|
|
1275
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('test_child_v5001', '1', ${now})`,
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
// Roll back to version 4999 — both should be rolled back, child first.
|
|
1279
|
+
const rolledBack = rollbackMemoryMigration(db, 4999);
|
|
1280
|
+
|
|
1281
|
+
expect(rolledBack).toEqual(["test_child_v5001", "test_parent_v5000"]);
|
|
1282
|
+
|
|
1283
|
+
// Verify down() execution order: child before parent.
|
|
1284
|
+
expect(downCalls).toEqual(["test_child_v5001", "test_parent_v5000"]);
|
|
1285
|
+
|
|
1286
|
+
// Both checkpoints should be deleted.
|
|
1287
|
+
const cpParent = raw
|
|
1288
|
+
.query(`SELECT 1 FROM memory_checkpoints WHERE key = 'test_parent_v5000'`)
|
|
1289
|
+
.get();
|
|
1290
|
+
const cpChild = raw
|
|
1291
|
+
.query(`SELECT 1 FROM memory_checkpoints WHERE key = 'test_child_v5001'`)
|
|
1292
|
+
.get();
|
|
1293
|
+
expect(cpParent).toBeNull();
|
|
1294
|
+
expect(cpChild).toBeNull();
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// ---------------------------------------------------------------------------
|
|
1299
|
+
// 4. Memory migration down() functions
|
|
1300
|
+
// ---------------------------------------------------------------------------
|
|
1301
|
+
|
|
1302
|
+
describe("memory migration down() functions", () => {
|
|
1303
|
+
// ── v1: downJobDeferrals ─────────────────────────────────────────────
|
|
1304
|
+
|
|
1305
|
+
describe("v1: downJobDeferrals", () => {
|
|
1306
|
+
test("round-trip: forward + down restores original state", () => {
|
|
1307
|
+
const db = createTestDb();
|
|
1308
|
+
const raw = getRaw(db);
|
|
1309
|
+
bootstrapCheckpointsTable(raw);
|
|
1310
|
+
bootstrapMemoryJobsTable(raw);
|
|
1311
|
+
|
|
1312
|
+
const now = Date.now();
|
|
1313
|
+
raw.exec(`
|
|
1314
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, last_error, created_at, updated_at)
|
|
1315
|
+
VALUES ('job-rt', 'embed_segment', '{}', 'pending', 3, 0, ${now}, NULL, ${now}, ${now})
|
|
1316
|
+
`);
|
|
1317
|
+
|
|
1318
|
+
// Snapshot pre-migration state.
|
|
1319
|
+
const before = raw
|
|
1320
|
+
.query(
|
|
1321
|
+
`SELECT attempts, deferrals FROM memory_jobs WHERE id = 'job-rt'`,
|
|
1322
|
+
)
|
|
1323
|
+
.get() as { attempts: number; deferrals: number };
|
|
1324
|
+
expect(before.attempts).toBe(3);
|
|
1325
|
+
expect(before.deferrals).toBe(0);
|
|
1326
|
+
|
|
1327
|
+
// Forward migration: moves attempts -> deferrals.
|
|
1328
|
+
migrateJobDeferrals(db);
|
|
1329
|
+
|
|
1330
|
+
const afterForward = raw
|
|
1331
|
+
.query(
|
|
1332
|
+
`SELECT attempts, deferrals FROM memory_jobs WHERE id = 'job-rt'`,
|
|
1333
|
+
)
|
|
1334
|
+
.get() as { attempts: number; deferrals: number };
|
|
1335
|
+
expect(afterForward.attempts).toBe(0);
|
|
1336
|
+
expect(afterForward.deferrals).toBe(3);
|
|
1337
|
+
|
|
1338
|
+
// Down: moves deferrals -> attempts.
|
|
1339
|
+
downJobDeferrals(db);
|
|
1340
|
+
|
|
1341
|
+
const afterDown = raw
|
|
1342
|
+
.query(
|
|
1343
|
+
`SELECT attempts, deferrals FROM memory_jobs WHERE id = 'job-rt'`,
|
|
1344
|
+
)
|
|
1345
|
+
.get() as { attempts: number; deferrals: number };
|
|
1346
|
+
expect(afterDown.attempts).toBe(3);
|
|
1347
|
+
expect(afterDown.deferrals).toBe(0);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1351
|
+
const db = createTestDb();
|
|
1352
|
+
const raw = getRaw(db);
|
|
1353
|
+
bootstrapCheckpointsTable(raw);
|
|
1354
|
+
bootstrapMemoryJobsTable(raw);
|
|
1355
|
+
|
|
1356
|
+
const now = Date.now();
|
|
1357
|
+
raw.exec(`
|
|
1358
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, last_error, created_at, updated_at)
|
|
1359
|
+
VALUES ('job-idem2', 'embed_item', '{}', 'pending', 0, 5, ${now}, NULL, ${now}, ${now})
|
|
1360
|
+
`);
|
|
1361
|
+
|
|
1362
|
+
downJobDeferrals(db);
|
|
1363
|
+
const after1 = raw
|
|
1364
|
+
.query(
|
|
1365
|
+
`SELECT attempts, deferrals FROM memory_jobs WHERE id = 'job-idem2'`,
|
|
1366
|
+
)
|
|
1367
|
+
.get() as { attempts: number; deferrals: number };
|
|
1368
|
+
|
|
1369
|
+
// Second call — should be a no-op (deferrals already 0).
|
|
1370
|
+
downJobDeferrals(db);
|
|
1371
|
+
const after2 = raw
|
|
1372
|
+
.query(
|
|
1373
|
+
`SELECT attempts, deferrals FROM memory_jobs WHERE id = 'job-idem2'`,
|
|
1374
|
+
)
|
|
1375
|
+
.get() as { attempts: number; deferrals: number };
|
|
1376
|
+
|
|
1377
|
+
expect(after1.attempts).toBe(5);
|
|
1378
|
+
expect(after1.deferrals).toBe(0);
|
|
1379
|
+
expect(after2.attempts).toBe(after1.attempts);
|
|
1380
|
+
expect(after2.deferrals).toBe(after1.deferrals);
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// ── v2: downMemoryEntityRelationDedup (no-op) ────────────────────────
|
|
1385
|
+
|
|
1386
|
+
describe("v2: downMemoryEntityRelationDedup (no-op)", () => {
|
|
1387
|
+
test("does not throw and does not modify data", () => {
|
|
1388
|
+
const db = createTestDb();
|
|
1389
|
+
const raw = getRaw(db);
|
|
1390
|
+
bootstrapEntityRelationsTable(raw);
|
|
1391
|
+
|
|
1392
|
+
const now = Date.now();
|
|
1393
|
+
raw.exec(
|
|
1394
|
+
`INSERT INTO memory_entity_relations VALUES ('r1', 'e1', 'e2', 'knows', 'ev', ${now}, ${now})`,
|
|
1395
|
+
);
|
|
1396
|
+
|
|
1397
|
+
const countBefore = (
|
|
1398
|
+
raw
|
|
1399
|
+
.query(`SELECT COUNT(*) AS c FROM memory_entity_relations`)
|
|
1400
|
+
.get() as { c: number }
|
|
1401
|
+
).c;
|
|
1402
|
+
|
|
1403
|
+
downMemoryEntityRelationDedup(db);
|
|
1404
|
+
|
|
1405
|
+
const countAfter = (
|
|
1406
|
+
raw
|
|
1407
|
+
.query(`SELECT COUNT(*) AS c FROM memory_entity_relations`)
|
|
1408
|
+
.get() as { c: number }
|
|
1409
|
+
).c;
|
|
1410
|
+
expect(countAfter).toBe(countBefore);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
test("idempotency: calling twice does not throw", () => {
|
|
1414
|
+
const db = createTestDb();
|
|
1415
|
+
downMemoryEntityRelationDedup(db);
|
|
1416
|
+
downMemoryEntityRelationDedup(db);
|
|
1417
|
+
});
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// ── v3: downMemoryItemsFingerprintScopeUnique ────────────────────────
|
|
1421
|
+
|
|
1422
|
+
describe("v3: downMemoryItemsFingerprintScopeUnique", () => {
|
|
1423
|
+
test("round-trip: forward + down restores column-level UNIQUE", () => {
|
|
1424
|
+
const db = createTestDb();
|
|
1425
|
+
const raw = getRaw(db);
|
|
1426
|
+
bootstrapCheckpointsTable(raw);
|
|
1427
|
+
bootstrapOldMemoryItemsTable(raw);
|
|
1428
|
+
|
|
1429
|
+
const now = Date.now();
|
|
1430
|
+
raw.exec(`
|
|
1431
|
+
INSERT INTO memory_items (id, kind, subject, statement, status, confidence, fingerprint,
|
|
1432
|
+
first_seen_at, last_seen_at, scope_id)
|
|
1433
|
+
VALUES ('item-rt', 'fact', 'User', 'likes coffee', 'active', 0.9, 'fp-rt1', ${now}, ${now}, 'default')
|
|
1434
|
+
`);
|
|
1435
|
+
|
|
1436
|
+
// Old schema has UNIQUE on fingerprint.
|
|
1437
|
+
const ddlBefore =
|
|
1438
|
+
(
|
|
1439
|
+
raw
|
|
1440
|
+
.query(
|
|
1441
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`,
|
|
1442
|
+
)
|
|
1443
|
+
.get() as { sql: string }
|
|
1444
|
+
)?.sql ?? "";
|
|
1445
|
+
expect(ddlBefore).toMatch(/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i);
|
|
1446
|
+
|
|
1447
|
+
// Forward migration: remove column-level UNIQUE.
|
|
1448
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
1449
|
+
|
|
1450
|
+
const ddlAfterForward =
|
|
1451
|
+
(
|
|
1452
|
+
raw
|
|
1453
|
+
.query(
|
|
1454
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`,
|
|
1455
|
+
)
|
|
1456
|
+
.get() as { sql: string }
|
|
1457
|
+
)?.sql ?? "";
|
|
1458
|
+
expect(ddlAfterForward).not.toMatch(
|
|
1459
|
+
/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i,
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
// Down: restore column-level UNIQUE.
|
|
1463
|
+
downMemoryItemsFingerprintScopeUnique(db);
|
|
1464
|
+
|
|
1465
|
+
const ddlAfterDown =
|
|
1466
|
+
(
|
|
1467
|
+
raw
|
|
1468
|
+
.query(
|
|
1469
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`,
|
|
1470
|
+
)
|
|
1471
|
+
.get() as { sql: string }
|
|
1472
|
+
)?.sql ?? "";
|
|
1473
|
+
expect(ddlAfterDown).toMatch(/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i);
|
|
1474
|
+
|
|
1475
|
+
// Data preserved.
|
|
1476
|
+
const item = raw
|
|
1477
|
+
.query(`SELECT id FROM memory_items WHERE id = 'item-rt'`)
|
|
1478
|
+
.get();
|
|
1479
|
+
expect(item).toBeTruthy();
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1483
|
+
const db = createTestDb();
|
|
1484
|
+
const raw = getRaw(db);
|
|
1485
|
+
bootstrapCheckpointsTable(raw);
|
|
1486
|
+
bootstrapOldMemoryItemsTable(raw);
|
|
1487
|
+
|
|
1488
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
1489
|
+
downMemoryItemsFingerprintScopeUnique(db);
|
|
1490
|
+
// Second call — column-level UNIQUE already restored.
|
|
1491
|
+
downMemoryItemsFingerprintScopeUnique(db);
|
|
1492
|
+
|
|
1493
|
+
const ddl =
|
|
1494
|
+
(
|
|
1495
|
+
raw
|
|
1496
|
+
.query(
|
|
1497
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`,
|
|
1498
|
+
)
|
|
1499
|
+
.get() as { sql: string }
|
|
1500
|
+
)?.sql ?? "";
|
|
1501
|
+
expect(ddl).toMatch(/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i);
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// ── v4: downMemoryItemsScopeSaltedFingerprints ───────────────────────
|
|
1506
|
+
|
|
1507
|
+
describe("v4: downMemoryItemsScopeSaltedFingerprints", () => {
|
|
1508
|
+
test("round-trip: forward + down restores unsalted fingerprints", () => {
|
|
1509
|
+
const db = createTestDb();
|
|
1510
|
+
const raw = getRaw(db);
|
|
1511
|
+
bootstrapCheckpointsTable(raw);
|
|
1512
|
+
|
|
1513
|
+
// Use modern schema (no column-level UNIQUE).
|
|
1514
|
+
raw.exec(/*sql*/ `
|
|
1515
|
+
CREATE TABLE IF NOT EXISTS memory_items (
|
|
1516
|
+
id TEXT PRIMARY KEY,
|
|
1517
|
+
kind TEXT NOT NULL,
|
|
1518
|
+
subject TEXT NOT NULL,
|
|
1519
|
+
statement TEXT NOT NULL,
|
|
1520
|
+
status TEXT NOT NULL,
|
|
1521
|
+
confidence REAL NOT NULL,
|
|
1522
|
+
fingerprint TEXT NOT NULL,
|
|
1523
|
+
first_seen_at INTEGER NOT NULL,
|
|
1524
|
+
last_seen_at INTEGER NOT NULL,
|
|
1525
|
+
last_used_at INTEGER,
|
|
1526
|
+
scope_id TEXT NOT NULL DEFAULT 'default'
|
|
1527
|
+
)
|
|
1528
|
+
`);
|
|
1529
|
+
|
|
1530
|
+
// Compute the old (unsalted) fingerprint.
|
|
1531
|
+
const kind = "fact";
|
|
1532
|
+
const subject = "User";
|
|
1533
|
+
const statement = "likes coffee";
|
|
1534
|
+
const oldNormalized = `${kind}|${subject.toLowerCase()}|${statement.toLowerCase()}`;
|
|
1535
|
+
const oldFingerprint = createHash("sha256")
|
|
1536
|
+
.update(oldNormalized)
|
|
1537
|
+
.digest("hex");
|
|
1538
|
+
|
|
1539
|
+
const now = Date.now();
|
|
1540
|
+
raw.exec(`
|
|
1541
|
+
INSERT INTO memory_items (id, kind, subject, statement, status, confidence, fingerprint,
|
|
1542
|
+
first_seen_at, last_seen_at, scope_id)
|
|
1543
|
+
VALUES ('item-salt', '${kind}', '${subject}', '${statement}', 'active', 0.9, '${oldFingerprint}', ${now}, ${now}, 'default')
|
|
1544
|
+
`);
|
|
1545
|
+
|
|
1546
|
+
// Write fingerprint_scope_unique checkpoint so forward migration runs.
|
|
1547
|
+
raw.exec(
|
|
1548
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('migration_memory_items_fingerprint_scope_unique_v1', '1', ${now})`,
|
|
1549
|
+
);
|
|
1550
|
+
|
|
1551
|
+
// Forward migration: recompute with scope_id prefix.
|
|
1552
|
+
migrateMemoryItemsScopeSaltedFingerprints(db);
|
|
1553
|
+
|
|
1554
|
+
const afterForward = raw
|
|
1555
|
+
.query(`SELECT fingerprint FROM memory_items WHERE id = 'item-salt'`)
|
|
1556
|
+
.get() as { fingerprint: string };
|
|
1557
|
+
expect(afterForward.fingerprint).not.toBe(oldFingerprint);
|
|
1558
|
+
|
|
1559
|
+
// Down: recompute WITHOUT scope_id prefix (old format).
|
|
1560
|
+
downMemoryItemsScopeSaltedFingerprints(db);
|
|
1561
|
+
|
|
1562
|
+
const afterDown = raw
|
|
1563
|
+
.query(`SELECT fingerprint FROM memory_items WHERE id = 'item-salt'`)
|
|
1564
|
+
.get() as { fingerprint: string };
|
|
1565
|
+
expect(afterDown.fingerprint).toBe(oldFingerprint);
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1569
|
+
const db = createTestDb();
|
|
1570
|
+
const raw = getRaw(db);
|
|
1571
|
+
|
|
1572
|
+
raw.exec(/*sql*/ `
|
|
1573
|
+
CREATE TABLE IF NOT EXISTS memory_items (
|
|
1574
|
+
id TEXT PRIMARY KEY,
|
|
1575
|
+
kind TEXT NOT NULL,
|
|
1576
|
+
subject TEXT NOT NULL,
|
|
1577
|
+
statement TEXT NOT NULL,
|
|
1578
|
+
status TEXT NOT NULL,
|
|
1579
|
+
confidence REAL NOT NULL,
|
|
1580
|
+
fingerprint TEXT NOT NULL,
|
|
1581
|
+
first_seen_at INTEGER NOT NULL,
|
|
1582
|
+
last_seen_at INTEGER NOT NULL,
|
|
1583
|
+
scope_id TEXT NOT NULL DEFAULT 'default'
|
|
1584
|
+
)
|
|
1585
|
+
`);
|
|
1586
|
+
|
|
1587
|
+
const now = Date.now();
|
|
1588
|
+
raw.exec(`
|
|
1589
|
+
INSERT INTO memory_items (id, kind, subject, statement, status, confidence, fingerprint,
|
|
1590
|
+
first_seen_at, last_seen_at, scope_id)
|
|
1591
|
+
VALUES ('item-idem', 'fact', 'User', 'likes tea', 'active', 0.8, 'some-fp', ${now}, ${now}, 'default')
|
|
1592
|
+
`);
|
|
1593
|
+
|
|
1594
|
+
downMemoryItemsScopeSaltedFingerprints(db);
|
|
1595
|
+
const fp1 = (
|
|
1596
|
+
raw
|
|
1597
|
+
.query(`SELECT fingerprint FROM memory_items WHERE id = 'item-idem'`)
|
|
1598
|
+
.get() as { fingerprint: string }
|
|
1599
|
+
).fingerprint;
|
|
1600
|
+
|
|
1601
|
+
downMemoryItemsScopeSaltedFingerprints(db);
|
|
1602
|
+
const fp2 = (
|
|
1603
|
+
raw
|
|
1604
|
+
.query(`SELECT fingerprint FROM memory_items WHERE id = 'item-idem'`)
|
|
1605
|
+
.get() as { fingerprint: string }
|
|
1606
|
+
).fingerprint;
|
|
1607
|
+
|
|
1608
|
+
expect(fp1).toBe(fp2);
|
|
1609
|
+
});
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// ── No-op down functions (v5, v7/assistant-id-to-self, v8, v10, v14, v17, v18/contacts-notes, v20, v26, v33, v34, v36) ──
|
|
1613
|
+
|
|
1614
|
+
describe("no-op down() functions", () => {
|
|
1615
|
+
const noOpFunctions = [
|
|
1616
|
+
{ name: "v5: downAssistantIdToSelf", fn: downAssistantIdToSelf },
|
|
1617
|
+
{
|
|
1618
|
+
name: "v8: downBackfillInboxThreadState",
|
|
1619
|
+
fn: downBackfillInboxThreadState,
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
name: "v10: downNotificationTablesSchema",
|
|
1623
|
+
fn: downNotificationTablesSchema,
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "v14: downNormalizePhoneIdentities",
|
|
1627
|
+
fn: downNormalizePhoneIdentities,
|
|
1628
|
+
},
|
|
1629
|
+
{ name: "v17: downContactsNotesColumn", fn: downContactsNotesColumn },
|
|
1630
|
+
{
|
|
1631
|
+
name: "v20: downBackfillUsageCacheAccounting",
|
|
1632
|
+
fn: downBackfillUsageCacheAccounting,
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
name: "v26: migrateRemindersToSchedulesDown",
|
|
1636
|
+
fn: migrateRemindersToSchedulesDown,
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
name: "v33: migrateDropCapabilityCardStateDown",
|
|
1640
|
+
fn: migrateDropCapabilityCardStateDown,
|
|
1641
|
+
},
|
|
1642
|
+
{
|
|
1643
|
+
name: "v34: migrateBackfillInlineAttachmentsToDiskDown",
|
|
1644
|
+
fn: migrateBackfillInlineAttachmentsToDiskDown,
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
name: "v36: migrateBackfillAudioAttachmentMimeTypesDown",
|
|
1648
|
+
fn: migrateBackfillAudioAttachmentMimeTypesDown,
|
|
1649
|
+
},
|
|
1650
|
+
];
|
|
1651
|
+
|
|
1652
|
+
for (const { name, fn } of noOpFunctions) {
|
|
1653
|
+
test(`${name}: does not throw`, () => {
|
|
1654
|
+
const db = createTestDb();
|
|
1655
|
+
expect(() => fn(db)).not.toThrow();
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
test(`${name}: idempotency — calling twice does not throw`, () => {
|
|
1659
|
+
const db = createTestDb();
|
|
1660
|
+
fn(db);
|
|
1661
|
+
fn(db);
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
// ── v6: downRemoveAssistantIdColumns (re-add via ALTER TABLE) ────────
|
|
1667
|
+
|
|
1668
|
+
describe("v6: downRemoveAssistantIdColumns", () => {
|
|
1669
|
+
test("adds assistant_id column back to tables that lack it", () => {
|
|
1670
|
+
const db = createTestDb();
|
|
1671
|
+
const raw = getRaw(db);
|
|
1672
|
+
|
|
1673
|
+
// Create tables WITHOUT assistant_id (post-forward-migration state).
|
|
1674
|
+
raw.exec(/*sql*/ `
|
|
1675
|
+
CREATE TABLE conversations (id TEXT PRIMARY KEY, created_at INTEGER NOT NULL);
|
|
1676
|
+
CREATE TABLE conversation_keys (id TEXT PRIMARY KEY, conversation_key TEXT NOT NULL UNIQUE, conversation_id TEXT NOT NULL, created_at INTEGER NOT NULL);
|
|
1677
|
+
CREATE TABLE attachments (id TEXT PRIMARY KEY, original_filename TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, kind TEXT NOT NULL, data_base64 TEXT NOT NULL, content_hash TEXT, thumbnail_base64 TEXT, created_at INTEGER NOT NULL);
|
|
1678
|
+
CREATE TABLE channel_inbound_events (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL, external_chat_id TEXT NOT NULL, external_message_id TEXT NOT NULL, conversation_id TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
1679
|
+
CREATE TABLE messages (id TEXT PRIMARY KEY, created_at INTEGER NOT NULL);
|
|
1680
|
+
CREATE TABLE message_runs (id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
1681
|
+
`);
|
|
1682
|
+
|
|
1683
|
+
downRemoveAssistantIdColumns(db);
|
|
1684
|
+
|
|
1685
|
+
// Verify assistant_id column was added to the 4 affected tables.
|
|
1686
|
+
for (const table of [
|
|
1687
|
+
"conversation_keys",
|
|
1688
|
+
"attachments",
|
|
1689
|
+
"channel_inbound_events",
|
|
1690
|
+
"message_runs",
|
|
1691
|
+
]) {
|
|
1692
|
+
const col = raw
|
|
1693
|
+
.query(
|
|
1694
|
+
`SELECT 1 FROM pragma_table_info('${table}') WHERE name = 'assistant_id'`,
|
|
1695
|
+
)
|
|
1696
|
+
.get();
|
|
1697
|
+
expect(col).toBeTruthy();
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1702
|
+
const db = createTestDb();
|
|
1703
|
+
const raw = getRaw(db);
|
|
1704
|
+
|
|
1705
|
+
raw.exec(/*sql*/ `
|
|
1706
|
+
CREATE TABLE conversations (id TEXT PRIMARY KEY, created_at INTEGER NOT NULL);
|
|
1707
|
+
CREATE TABLE conversation_keys (id TEXT PRIMARY KEY, conversation_key TEXT NOT NULL, conversation_id TEXT NOT NULL, created_at INTEGER NOT NULL);
|
|
1708
|
+
CREATE TABLE attachments (id TEXT PRIMARY KEY, original_filename TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, kind TEXT NOT NULL, data_base64 TEXT NOT NULL, content_hash TEXT, created_at INTEGER NOT NULL);
|
|
1709
|
+
CREATE TABLE channel_inbound_events (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL, external_chat_id TEXT NOT NULL, external_message_id TEXT NOT NULL, conversation_id TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
1710
|
+
CREATE TABLE message_runs (id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL, status TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
1711
|
+
`);
|
|
1712
|
+
|
|
1713
|
+
downRemoveAssistantIdColumns(db);
|
|
1714
|
+
downRemoveAssistantIdColumns(db);
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
// ── v7: downLlmUsageEventsDropAssistantId (re-add via ALTER TABLE) ──
|
|
1719
|
+
|
|
1720
|
+
describe("v7: downLlmUsageEventsDropAssistantId", () => {
|
|
1721
|
+
test("adds assistant_id column back to llm_usage_events", () => {
|
|
1722
|
+
const db = createTestDb();
|
|
1723
|
+
const raw = getRaw(db);
|
|
1724
|
+
|
|
1725
|
+
raw.exec(/*sql*/ `
|
|
1726
|
+
CREATE TABLE llm_usage_events (
|
|
1727
|
+
id TEXT PRIMARY KEY,
|
|
1728
|
+
created_at INTEGER NOT NULL,
|
|
1729
|
+
actor TEXT NOT NULL,
|
|
1730
|
+
provider TEXT NOT NULL,
|
|
1731
|
+
model TEXT NOT NULL,
|
|
1732
|
+
input_tokens INTEGER NOT NULL,
|
|
1733
|
+
output_tokens INTEGER NOT NULL,
|
|
1734
|
+
pricing_status TEXT NOT NULL
|
|
1735
|
+
)
|
|
1736
|
+
`);
|
|
1737
|
+
|
|
1738
|
+
downLlmUsageEventsDropAssistantId(db);
|
|
1739
|
+
|
|
1740
|
+
const col = raw
|
|
1741
|
+
.query(
|
|
1742
|
+
`SELECT 1 FROM pragma_table_info('llm_usage_events') WHERE name = 'assistant_id'`,
|
|
1743
|
+
)
|
|
1744
|
+
.get();
|
|
1745
|
+
expect(col).toBeTruthy();
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1749
|
+
const db = createTestDb();
|
|
1750
|
+
const raw = getRaw(db);
|
|
1751
|
+
|
|
1752
|
+
raw.exec(/*sql*/ `
|
|
1753
|
+
CREATE TABLE llm_usage_events (
|
|
1754
|
+
id TEXT PRIMARY KEY,
|
|
1755
|
+
created_at INTEGER NOT NULL,
|
|
1756
|
+
actor TEXT NOT NULL,
|
|
1757
|
+
provider TEXT NOT NULL,
|
|
1758
|
+
model TEXT NOT NULL,
|
|
1759
|
+
input_tokens INTEGER NOT NULL,
|
|
1760
|
+
output_tokens INTEGER NOT NULL,
|
|
1761
|
+
pricing_status TEXT NOT NULL
|
|
1762
|
+
)
|
|
1763
|
+
`);
|
|
1764
|
+
|
|
1765
|
+
downLlmUsageEventsDropAssistantId(db);
|
|
1766
|
+
downLlmUsageEventsDropAssistantId(db);
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
// ── v9: downDropActiveSearchIndex ────────────────────────────────────
|
|
1771
|
+
|
|
1772
|
+
describe("v9: downDropActiveSearchIndex", () => {
|
|
1773
|
+
test("recreates the old index", () => {
|
|
1774
|
+
const db = createTestDb();
|
|
1775
|
+
const raw = getRaw(db);
|
|
1776
|
+
bootstrapOldMemoryItemsTable(raw);
|
|
1777
|
+
|
|
1778
|
+
downDropActiveSearchIndex(db);
|
|
1779
|
+
|
|
1780
|
+
const idx = raw
|
|
1781
|
+
.query(
|
|
1782
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_memory_items_active_search'`,
|
|
1783
|
+
)
|
|
1784
|
+
.get();
|
|
1785
|
+
expect(idx).toBeTruthy();
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1789
|
+
const db = createTestDb();
|
|
1790
|
+
const raw = getRaw(db);
|
|
1791
|
+
bootstrapOldMemoryItemsTable(raw);
|
|
1792
|
+
|
|
1793
|
+
downDropActiveSearchIndex(db);
|
|
1794
|
+
downDropActiveSearchIndex(db);
|
|
1795
|
+
});
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
// ── v11: downRenameChannelToVellum (value rename) ───────────────────
|
|
1799
|
+
|
|
1800
|
+
describe("v11: downRenameChannelToVellum", () => {
|
|
1801
|
+
test("renames 'vellum' values back to 'macos'", () => {
|
|
1802
|
+
const db = createTestDb();
|
|
1803
|
+
const raw = getRaw(db);
|
|
1804
|
+
|
|
1805
|
+
raw.exec(/*sql*/ `
|
|
1806
|
+
CREATE TABLE guardian_action_deliveries (id TEXT PRIMARY KEY, destination_channel TEXT NOT NULL);
|
|
1807
|
+
INSERT INTO guardian_action_deliveries VALUES ('d1', 'vellum');
|
|
1808
|
+
INSERT INTO guardian_action_deliveries VALUES ('d2', 'sms');
|
|
1809
|
+
`);
|
|
1810
|
+
|
|
1811
|
+
downRenameChannelToVellum(db);
|
|
1812
|
+
|
|
1813
|
+
const row = raw
|
|
1814
|
+
.query(
|
|
1815
|
+
`SELECT destination_channel FROM guardian_action_deliveries WHERE id = 'd1'`,
|
|
1816
|
+
)
|
|
1817
|
+
.get() as { destination_channel: string };
|
|
1818
|
+
expect(row.destination_channel).toBe("macos");
|
|
1819
|
+
|
|
1820
|
+
// Non-vellum values are unchanged.
|
|
1821
|
+
const row2 = raw
|
|
1822
|
+
.query(
|
|
1823
|
+
`SELECT destination_channel FROM guardian_action_deliveries WHERE id = 'd2'`,
|
|
1824
|
+
)
|
|
1825
|
+
.get() as { destination_channel: string };
|
|
1826
|
+
expect(row2.destination_channel).toBe("sms");
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1830
|
+
const db = createTestDb();
|
|
1831
|
+
const raw = getRaw(db);
|
|
1832
|
+
raw.exec(
|
|
1833
|
+
`CREATE TABLE guardian_action_deliveries (id TEXT PRIMARY KEY, destination_channel TEXT NOT NULL)`,
|
|
1834
|
+
);
|
|
1835
|
+
raw.exec(
|
|
1836
|
+
`INSERT INTO guardian_action_deliveries VALUES ('d1', 'vellum')`,
|
|
1837
|
+
);
|
|
1838
|
+
|
|
1839
|
+
downRenameChannelToVellum(db);
|
|
1840
|
+
downRenameChannelToVellum(db);
|
|
1841
|
+
|
|
1842
|
+
const row = raw
|
|
1843
|
+
.query(
|
|
1844
|
+
`SELECT destination_channel FROM guardian_action_deliveries WHERE id = 'd1'`,
|
|
1845
|
+
)
|
|
1846
|
+
.get() as { destination_channel: string };
|
|
1847
|
+
expect(row.destination_channel).toBe("macos");
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
// ── v19: downDropAssistantIdColumns (16-table column re-add) ────────
|
|
1852
|
+
|
|
1853
|
+
describe("v19: downDropAssistantIdColumns", () => {
|
|
1854
|
+
test("adds assistant_id column to tables that lack it", () => {
|
|
1855
|
+
const db = createTestDb();
|
|
1856
|
+
const raw = getRaw(db);
|
|
1857
|
+
|
|
1858
|
+
// Create a subset of the 16 tables without assistant_id.
|
|
1859
|
+
raw.exec(
|
|
1860
|
+
`CREATE TABLE contacts (id TEXT PRIMARY KEY, name TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
1861
|
+
);
|
|
1862
|
+
raw.exec(
|
|
1863
|
+
`CREATE TABLE notification_events (id TEXT PRIMARY KEY, created_at INTEGER NOT NULL)`,
|
|
1864
|
+
);
|
|
1865
|
+
|
|
1866
|
+
downDropAssistantIdColumns(db);
|
|
1867
|
+
|
|
1868
|
+
for (const table of ["contacts", "notification_events"]) {
|
|
1869
|
+
const col = raw
|
|
1870
|
+
.query(
|
|
1871
|
+
`SELECT 1 FROM pragma_table_info('${table}') WHERE name = 'assistant_id'`,
|
|
1872
|
+
)
|
|
1873
|
+
.get();
|
|
1874
|
+
expect(col).toBeTruthy();
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1879
|
+
const db = createTestDb();
|
|
1880
|
+
const raw = getRaw(db);
|
|
1881
|
+
raw.exec(
|
|
1882
|
+
`CREATE TABLE contacts (id TEXT PRIMARY KEY, name TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
1883
|
+
);
|
|
1884
|
+
|
|
1885
|
+
downDropAssistantIdColumns(db);
|
|
1886
|
+
downDropAssistantIdColumns(db);
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
// ── v21: downRenameVerificationTable (table rename) ─────────────────
|
|
1891
|
+
|
|
1892
|
+
describe("v21: downRenameVerificationTable", () => {
|
|
1893
|
+
test("renames channel_verification_sessions back to channel_guardian_verification_challenges", () => {
|
|
1894
|
+
const db = createTestDb();
|
|
1895
|
+
const raw = getRaw(db);
|
|
1896
|
+
|
|
1897
|
+
// Setup: new table name (post-forward-migration).
|
|
1898
|
+
raw.exec(/*sql*/ `
|
|
1899
|
+
CREATE TABLE channel_verification_sessions (
|
|
1900
|
+
id TEXT PRIMARY KEY,
|
|
1901
|
+
channel TEXT NOT NULL,
|
|
1902
|
+
challenge_hash TEXT,
|
|
1903
|
+
status TEXT NOT NULL,
|
|
1904
|
+
expected_external_user_id TEXT,
|
|
1905
|
+
expected_chat_id TEXT,
|
|
1906
|
+
destination_address TEXT,
|
|
1907
|
+
bootstrap_token_hash TEXT,
|
|
1908
|
+
created_at INTEGER NOT NULL
|
|
1909
|
+
)
|
|
1910
|
+
`);
|
|
1911
|
+
raw.exec(
|
|
1912
|
+
/*sql*/ `CREATE INDEX idx_verification_sessions_lookup ON channel_verification_sessions(channel, challenge_hash, status)`,
|
|
1913
|
+
);
|
|
1914
|
+
|
|
1915
|
+
downRenameVerificationTable(db);
|
|
1916
|
+
|
|
1917
|
+
// Old table name should exist.
|
|
1918
|
+
const oldTable = raw
|
|
1919
|
+
.query(
|
|
1920
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'channel_guardian_verification_challenges'`,
|
|
1921
|
+
)
|
|
1922
|
+
.get();
|
|
1923
|
+
expect(oldTable).toBeTruthy();
|
|
1924
|
+
|
|
1925
|
+
// New table name should no longer exist.
|
|
1926
|
+
const newTable = raw
|
|
1927
|
+
.query(
|
|
1928
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'channel_verification_sessions'`,
|
|
1929
|
+
)
|
|
1930
|
+
.get();
|
|
1931
|
+
expect(newTable).toBeNull();
|
|
1932
|
+
|
|
1933
|
+
// Old-style indexes should exist.
|
|
1934
|
+
const oldIdx = raw
|
|
1935
|
+
.query(
|
|
1936
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_channel_guardian_challenges_lookup'`,
|
|
1937
|
+
)
|
|
1938
|
+
.get();
|
|
1939
|
+
expect(oldIdx).toBeTruthy();
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1943
|
+
const db = createTestDb();
|
|
1944
|
+
const raw = getRaw(db);
|
|
1945
|
+
raw.exec(
|
|
1946
|
+
`CREATE TABLE channel_verification_sessions (id TEXT PRIMARY KEY, channel TEXT NOT NULL, challenge_hash TEXT, status TEXT NOT NULL, expected_external_user_id TEXT, expected_chat_id TEXT, destination_address TEXT, bootstrap_token_hash TEXT, created_at INTEGER NOT NULL)`,
|
|
1947
|
+
);
|
|
1948
|
+
|
|
1949
|
+
downRenameVerificationTable(db);
|
|
1950
|
+
downRenameVerificationTable(db);
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
// ── v22: downRenameVerificationSessionIdColumn ──────────────────────
|
|
1955
|
+
|
|
1956
|
+
describe("v22: downRenameVerificationSessionIdColumn", () => {
|
|
1957
|
+
test("renames verification_session_id back to guardian_verification_session_id", () => {
|
|
1958
|
+
const db = createTestDb();
|
|
1959
|
+
const raw = getRaw(db);
|
|
1960
|
+
|
|
1961
|
+
raw.exec(/*sql*/ `
|
|
1962
|
+
CREATE TABLE call_sessions (
|
|
1963
|
+
id TEXT PRIMARY KEY,
|
|
1964
|
+
verification_session_id TEXT,
|
|
1965
|
+
created_at INTEGER NOT NULL
|
|
1966
|
+
)
|
|
1967
|
+
`);
|
|
1968
|
+
|
|
1969
|
+
downRenameVerificationSessionIdColumn(db);
|
|
1970
|
+
|
|
1971
|
+
const columns = raw
|
|
1972
|
+
.query(`PRAGMA table_info(call_sessions)`)
|
|
1973
|
+
.all() as Array<{ name: string }>;
|
|
1974
|
+
const hasOld = columns.some(
|
|
1975
|
+
(c) => c.name === "guardian_verification_session_id",
|
|
1976
|
+
);
|
|
1977
|
+
const hasNew = columns.some((c) => c.name === "verification_session_id");
|
|
1978
|
+
expect(hasOld).toBe(true);
|
|
1979
|
+
expect(hasNew).toBe(false);
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
1983
|
+
const db = createTestDb();
|
|
1984
|
+
const raw = getRaw(db);
|
|
1985
|
+
raw.exec(
|
|
1986
|
+
`CREATE TABLE call_sessions (id TEXT PRIMARY KEY, verification_session_id TEXT, created_at INTEGER NOT NULL)`,
|
|
1987
|
+
);
|
|
1988
|
+
|
|
1989
|
+
downRenameVerificationSessionIdColumn(db);
|
|
1990
|
+
downRenameVerificationSessionIdColumn(db);
|
|
1991
|
+
});
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
// ── v23: downRenameGuardianVerificationValues ───────────────────────
|
|
1995
|
+
|
|
1996
|
+
describe("v23: downRenameGuardianVerificationValues", () => {
|
|
1997
|
+
test("restores guardian_ prefix on call_mode and event_type values", () => {
|
|
1998
|
+
const db = createTestDb();
|
|
1999
|
+
const raw = getRaw(db);
|
|
2000
|
+
|
|
2001
|
+
raw.exec(/*sql*/ `
|
|
2002
|
+
CREATE TABLE call_sessions (id TEXT PRIMARY KEY, call_mode TEXT NOT NULL);
|
|
2003
|
+
CREATE TABLE call_events (id TEXT PRIMARY KEY, event_type TEXT NOT NULL);
|
|
2004
|
+
INSERT INTO call_sessions VALUES ('s1', 'verification');
|
|
2005
|
+
INSERT INTO call_events VALUES ('e1', 'voice_verification_started');
|
|
2006
|
+
INSERT INTO call_events VALUES ('e2', 'outbound_voice_verification_succeeded');
|
|
2007
|
+
`);
|
|
2008
|
+
|
|
2009
|
+
downRenameGuardianVerificationValues(db);
|
|
2010
|
+
|
|
2011
|
+
const session = raw
|
|
2012
|
+
.query(`SELECT call_mode FROM call_sessions WHERE id = 's1'`)
|
|
2013
|
+
.get() as { call_mode: string };
|
|
2014
|
+
expect(session.call_mode).toBe("guardian_verification");
|
|
2015
|
+
|
|
2016
|
+
const event1 = raw
|
|
2017
|
+
.query(`SELECT event_type FROM call_events WHERE id = 'e1'`)
|
|
2018
|
+
.get() as { event_type: string };
|
|
2019
|
+
expect(event1.event_type).toBe("guardian_voice_verification_started");
|
|
2020
|
+
|
|
2021
|
+
const event2 = raw
|
|
2022
|
+
.query(`SELECT event_type FROM call_events WHERE id = 'e2'`)
|
|
2023
|
+
.get() as { event_type: string };
|
|
2024
|
+
expect(event2.event_type).toBe(
|
|
2025
|
+
"outbound_guardian_voice_verification_succeeded",
|
|
2026
|
+
);
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2030
|
+
const db = createTestDb();
|
|
2031
|
+
const raw = getRaw(db);
|
|
2032
|
+
raw.exec(
|
|
2033
|
+
`CREATE TABLE call_sessions (id TEXT PRIMARY KEY, call_mode TEXT NOT NULL)`,
|
|
2034
|
+
);
|
|
2035
|
+
raw.exec(
|
|
2036
|
+
`CREATE TABLE call_events (id TEXT PRIMARY KEY, event_type TEXT NOT NULL)`,
|
|
2037
|
+
);
|
|
2038
|
+
raw.exec(`INSERT INTO call_sessions VALUES ('s1', 'verification')`);
|
|
2039
|
+
|
|
2040
|
+
downRenameGuardianVerificationValues(db);
|
|
2041
|
+
downRenameGuardianVerificationValues(db);
|
|
2042
|
+
});
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
// ── v24: downRenameVoiceToPhone (value rename) ──────────────────────
|
|
2046
|
+
|
|
2047
|
+
describe("v24: downRenameVoiceToPhone", () => {
|
|
2048
|
+
test("renames 'phone' values back to 'voice'", () => {
|
|
2049
|
+
const db = createTestDb();
|
|
2050
|
+
const raw = getRaw(db);
|
|
2051
|
+
|
|
2052
|
+
raw.exec(/*sql*/ `
|
|
2053
|
+
CREATE TABLE contact_channels (id TEXT PRIMARY KEY, type TEXT NOT NULL);
|
|
2054
|
+
CREATE TABLE conversations (id TEXT PRIMARY KEY, origin_channel TEXT, origin_interface TEXT);
|
|
2055
|
+
CREATE TABLE messages (id TEXT PRIMARY KEY, metadata TEXT);
|
|
2056
|
+
CREATE TABLE assistant_ingress_invites (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL);
|
|
2057
|
+
CREATE TABLE assistant_inbox_thread_state (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL);
|
|
2058
|
+
CREATE TABLE guardian_action_requests (id TEXT PRIMARY KEY, source_channel TEXT, answered_by_channel TEXT);
|
|
2059
|
+
CREATE TABLE channel_verification_sessions (id TEXT PRIMARY KEY, channel TEXT NOT NULL);
|
|
2060
|
+
CREATE TABLE channel_guardian_approval_requests (id TEXT PRIMARY KEY, channel TEXT NOT NULL);
|
|
2061
|
+
CREATE TABLE channel_guardian_rate_limits (id TEXT PRIMARY KEY, channel TEXT NOT NULL, actor_external_user_id TEXT, actor_chat_id TEXT);
|
|
2062
|
+
CREATE TABLE notification_events (id TEXT PRIMARY KEY, source_channel TEXT);
|
|
2063
|
+
CREATE TABLE notification_deliveries (id TEXT PRIMARY KEY, channel TEXT);
|
|
2064
|
+
CREATE TABLE external_conversation_bindings (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL);
|
|
2065
|
+
CREATE TABLE channel_inbound_events (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL);
|
|
2066
|
+
CREATE TABLE conversation_attention_events (id TEXT PRIMARY KEY, source_channel TEXT);
|
|
2067
|
+
CREATE TABLE conversation_assistant_attention_state (id TEXT PRIMARY KEY, last_seen_source_channel TEXT);
|
|
2068
|
+
CREATE TABLE canonical_guardian_requests (id TEXT PRIMARY KEY, source_channel TEXT);
|
|
2069
|
+
CREATE TABLE canonical_guardian_deliveries (id TEXT PRIMARY KEY, destination_channel TEXT NOT NULL);
|
|
2070
|
+
CREATE TABLE guardian_action_deliveries (id TEXT PRIMARY KEY, destination_channel TEXT NOT NULL);
|
|
2071
|
+
CREATE TABLE scoped_approval_grants (id TEXT PRIMARY KEY, request_channel TEXT NOT NULL, decision_channel TEXT NOT NULL, execution_channel TEXT);
|
|
2072
|
+
CREATE TABLE sequences (id TEXT PRIMARY KEY, channel TEXT);
|
|
2073
|
+
CREATE TABLE followups (id TEXT PRIMARY KEY, channel TEXT);
|
|
2074
|
+
`);
|
|
2075
|
+
|
|
2076
|
+
raw.exec(`INSERT INTO contact_channels VALUES ('cc1', 'phone')`);
|
|
2077
|
+
raw.exec(`INSERT INTO conversations VALUES ('c1', 'phone', 'phone')`);
|
|
2078
|
+
|
|
2079
|
+
downRenameVoiceToPhone(db);
|
|
2080
|
+
|
|
2081
|
+
const cc = raw
|
|
2082
|
+
.query(`SELECT type FROM contact_channels WHERE id = 'cc1'`)
|
|
2083
|
+
.get() as { type: string };
|
|
2084
|
+
expect(cc.type).toBe("voice");
|
|
2085
|
+
|
|
2086
|
+
const conv = raw
|
|
2087
|
+
.query(
|
|
2088
|
+
`SELECT origin_channel, origin_interface FROM conversations WHERE id = 'c1'`,
|
|
2089
|
+
)
|
|
2090
|
+
.get() as { origin_channel: string; origin_interface: string };
|
|
2091
|
+
expect(conv.origin_channel).toBe("voice");
|
|
2092
|
+
expect(conv.origin_interface).toBe("voice");
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2096
|
+
const db = createTestDb();
|
|
2097
|
+
const raw = getRaw(db);
|
|
2098
|
+
raw.exec(
|
|
2099
|
+
`CREATE TABLE contact_channels (id TEXT PRIMARY KEY, type TEXT NOT NULL)`,
|
|
2100
|
+
);
|
|
2101
|
+
raw.exec(
|
|
2102
|
+
`CREATE TABLE conversations (id TEXT PRIMARY KEY, origin_channel TEXT, origin_interface TEXT)`,
|
|
2103
|
+
);
|
|
2104
|
+
raw.exec(`CREATE TABLE messages (id TEXT PRIMARY KEY, metadata TEXT)`);
|
|
2105
|
+
raw.exec(
|
|
2106
|
+
`CREATE TABLE assistant_ingress_invites (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL)`,
|
|
2107
|
+
);
|
|
2108
|
+
raw.exec(
|
|
2109
|
+
`CREATE TABLE assistant_inbox_thread_state (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL)`,
|
|
2110
|
+
);
|
|
2111
|
+
raw.exec(
|
|
2112
|
+
`CREATE TABLE guardian_action_requests (id TEXT PRIMARY KEY, source_channel TEXT, answered_by_channel TEXT)`,
|
|
2113
|
+
);
|
|
2114
|
+
raw.exec(
|
|
2115
|
+
`CREATE TABLE channel_verification_sessions (id TEXT PRIMARY KEY, channel TEXT NOT NULL)`,
|
|
2116
|
+
);
|
|
2117
|
+
raw.exec(
|
|
2118
|
+
`CREATE TABLE channel_guardian_approval_requests (id TEXT PRIMARY KEY, channel TEXT NOT NULL)`,
|
|
2119
|
+
);
|
|
2120
|
+
raw.exec(
|
|
2121
|
+
`CREATE TABLE channel_guardian_rate_limits (id TEXT PRIMARY KEY, channel TEXT NOT NULL, actor_external_user_id TEXT, actor_chat_id TEXT)`,
|
|
2122
|
+
);
|
|
2123
|
+
raw.exec(
|
|
2124
|
+
`CREATE TABLE notification_events (id TEXT PRIMARY KEY, source_channel TEXT)`,
|
|
2125
|
+
);
|
|
2126
|
+
raw.exec(
|
|
2127
|
+
`CREATE TABLE notification_deliveries (id TEXT PRIMARY KEY, channel TEXT)`,
|
|
2128
|
+
);
|
|
2129
|
+
raw.exec(
|
|
2130
|
+
`CREATE TABLE external_conversation_bindings (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL)`,
|
|
2131
|
+
);
|
|
2132
|
+
raw.exec(
|
|
2133
|
+
`CREATE TABLE channel_inbound_events (id TEXT PRIMARY KEY, source_channel TEXT NOT NULL)`,
|
|
2134
|
+
);
|
|
2135
|
+
raw.exec(
|
|
2136
|
+
`CREATE TABLE conversation_attention_events (id TEXT PRIMARY KEY, source_channel TEXT)`,
|
|
2137
|
+
);
|
|
2138
|
+
raw.exec(
|
|
2139
|
+
`CREATE TABLE conversation_assistant_attention_state (id TEXT PRIMARY KEY, last_seen_source_channel TEXT)`,
|
|
2140
|
+
);
|
|
2141
|
+
raw.exec(
|
|
2142
|
+
`CREATE TABLE canonical_guardian_requests (id TEXT PRIMARY KEY, source_channel TEXT)`,
|
|
2143
|
+
);
|
|
2144
|
+
raw.exec(
|
|
2145
|
+
`CREATE TABLE canonical_guardian_deliveries (id TEXT PRIMARY KEY, destination_channel TEXT NOT NULL)`,
|
|
2146
|
+
);
|
|
2147
|
+
raw.exec(
|
|
2148
|
+
`CREATE TABLE guardian_action_deliveries (id TEXT PRIMARY KEY, destination_channel TEXT NOT NULL)`,
|
|
2149
|
+
);
|
|
2150
|
+
raw.exec(
|
|
2151
|
+
`CREATE TABLE scoped_approval_grants (id TEXT PRIMARY KEY, request_channel TEXT NOT NULL, decision_channel TEXT NOT NULL, execution_channel TEXT)`,
|
|
2152
|
+
);
|
|
2153
|
+
raw.exec(`CREATE TABLE sequences (id TEXT PRIMARY KEY, channel TEXT)`);
|
|
2154
|
+
raw.exec(`CREATE TABLE followups (id TEXT PRIMARY KEY, channel TEXT)`);
|
|
2155
|
+
|
|
2156
|
+
downRenameVoiceToPhone(db);
|
|
2157
|
+
downRenameVoiceToPhone(db);
|
|
2158
|
+
});
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
// ── v25: migrateDropAccountsTableDown (table recreation) ────────────
|
|
2162
|
+
|
|
2163
|
+
describe("v25: migrateDropAccountsTableDown", () => {
|
|
2164
|
+
test("recreates the accounts table with correct schema", () => {
|
|
2165
|
+
const db = createTestDb();
|
|
2166
|
+
const raw = getRaw(db);
|
|
2167
|
+
|
|
2168
|
+
migrateDropAccountsTableDown(db);
|
|
2169
|
+
|
|
2170
|
+
const table = raw
|
|
2171
|
+
.query(
|
|
2172
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'accounts'`,
|
|
2173
|
+
)
|
|
2174
|
+
.get();
|
|
2175
|
+
expect(table).toBeTruthy();
|
|
2176
|
+
|
|
2177
|
+
// Check indexes.
|
|
2178
|
+
const idxService = raw
|
|
2179
|
+
.query(
|
|
2180
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_accounts_service'`,
|
|
2181
|
+
)
|
|
2182
|
+
.get();
|
|
2183
|
+
expect(idxService).toBeTruthy();
|
|
2184
|
+
|
|
2185
|
+
const idxStatus = raw
|
|
2186
|
+
.query(
|
|
2187
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_accounts_status'`,
|
|
2188
|
+
)
|
|
2189
|
+
.get();
|
|
2190
|
+
expect(idxStatus).toBeTruthy();
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2194
|
+
const db = createTestDb();
|
|
2195
|
+
migrateDropAccountsTableDown(db);
|
|
2196
|
+
migrateDropAccountsTableDown(db);
|
|
2197
|
+
});
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
// ── v27: migrateDropRemindersTableDown (table recreation) ───────────
|
|
2201
|
+
|
|
2202
|
+
describe("v27: migrateDropRemindersTableDown", () => {
|
|
2203
|
+
test("recreates the reminders table with correct schema", () => {
|
|
2204
|
+
const db = createTestDb();
|
|
2205
|
+
const raw = getRaw(db);
|
|
2206
|
+
|
|
2207
|
+
migrateDropRemindersTableDown(db);
|
|
2208
|
+
|
|
2209
|
+
const table = raw
|
|
2210
|
+
.query(
|
|
2211
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'reminders'`,
|
|
2212
|
+
)
|
|
2213
|
+
.get();
|
|
2214
|
+
expect(table).toBeTruthy();
|
|
2215
|
+
|
|
2216
|
+
// Verify index.
|
|
2217
|
+
const idx = raw
|
|
2218
|
+
.query(
|
|
2219
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_reminders_status_fire_at'`,
|
|
2220
|
+
)
|
|
2221
|
+
.get();
|
|
2222
|
+
expect(idx).toBeTruthy();
|
|
2223
|
+
|
|
2224
|
+
// Verify columns include routing_intent and routing_hints_json.
|
|
2225
|
+
const cols = raw.query(`PRAGMA table_info(reminders)`).all() as Array<{
|
|
2226
|
+
name: string;
|
|
2227
|
+
}>;
|
|
2228
|
+
const colNames = cols.map((c) => c.name);
|
|
2229
|
+
expect(colNames).toContain("routing_intent");
|
|
2230
|
+
expect(colNames).toContain("routing_hints_json");
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2234
|
+
const db = createTestDb();
|
|
2235
|
+
migrateDropRemindersTableDown(db);
|
|
2236
|
+
migrateDropRemindersTableDown(db);
|
|
2237
|
+
});
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// ── v28: migrateOAuthAppsClientSecretPathDown (column drop) ─────────
|
|
2241
|
+
|
|
2242
|
+
describe("v28: migrateOAuthAppsClientSecretPathDown", () => {
|
|
2243
|
+
test("drops client_secret_credential_path column from oauth_apps", () => {
|
|
2244
|
+
const db = createTestDb();
|
|
2245
|
+
const raw = getRaw(db);
|
|
2246
|
+
|
|
2247
|
+
raw.exec(/*sql*/ `
|
|
2248
|
+
CREATE TABLE oauth_providers (provider_key TEXT PRIMARY KEY, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
2249
|
+
CREATE TABLE oauth_apps (
|
|
2250
|
+
id TEXT PRIMARY KEY,
|
|
2251
|
+
provider_key TEXT NOT NULL REFERENCES oauth_providers(provider_key),
|
|
2252
|
+
client_id TEXT NOT NULL,
|
|
2253
|
+
client_secret_credential_path TEXT,
|
|
2254
|
+
created_at INTEGER NOT NULL,
|
|
2255
|
+
updated_at INTEGER NOT NULL
|
|
2256
|
+
);
|
|
2257
|
+
`);
|
|
2258
|
+
|
|
2259
|
+
migrateOAuthAppsClientSecretPathDown(db);
|
|
2260
|
+
|
|
2261
|
+
const col = raw
|
|
2262
|
+
.query(
|
|
2263
|
+
`SELECT 1 FROM pragma_table_info('oauth_apps') WHERE name = 'client_secret_credential_path'`,
|
|
2264
|
+
)
|
|
2265
|
+
.get();
|
|
2266
|
+
expect(col).toBeNull();
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2270
|
+
const db = createTestDb();
|
|
2271
|
+
const raw = getRaw(db);
|
|
2272
|
+
raw.exec(
|
|
2273
|
+
`CREATE TABLE oauth_providers (provider_key TEXT PRIMARY KEY, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
2274
|
+
);
|
|
2275
|
+
raw.exec(
|
|
2276
|
+
`CREATE TABLE oauth_apps (id TEXT PRIMARY KEY, provider_key TEXT NOT NULL, client_id TEXT NOT NULL, client_secret_credential_path TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
2277
|
+
);
|
|
2278
|
+
|
|
2279
|
+
migrateOAuthAppsClientSecretPathDown(db);
|
|
2280
|
+
migrateOAuthAppsClientSecretPathDown(db);
|
|
2281
|
+
});
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
// ── v31: migrateRenameGmailProviderKeyToGoogleDown ──────────────────
|
|
2285
|
+
|
|
2286
|
+
describe("v31: migrateRenameGmailProviderKeyToGoogleDown", () => {
|
|
2287
|
+
test("renames integration:google back to integration:gmail", () => {
|
|
2288
|
+
const db = createTestDb();
|
|
2289
|
+
const raw = getRaw(db);
|
|
2290
|
+
|
|
2291
|
+
raw.exec(/*sql*/ `
|
|
2292
|
+
CREATE TABLE oauth_providers (provider_key TEXT PRIMARY KEY);
|
|
2293
|
+
CREATE TABLE oauth_apps (id TEXT PRIMARY KEY, provider_key TEXT NOT NULL);
|
|
2294
|
+
CREATE TABLE oauth_connections (id TEXT PRIMARY KEY, provider_key TEXT NOT NULL);
|
|
2295
|
+
INSERT INTO oauth_providers VALUES ('integration:google');
|
|
2296
|
+
INSERT INTO oauth_apps VALUES ('app1', 'integration:google');
|
|
2297
|
+
INSERT INTO oauth_connections VALUES ('conn1', 'integration:google');
|
|
2298
|
+
`);
|
|
2299
|
+
|
|
2300
|
+
migrateRenameGmailProviderKeyToGoogleDown(db);
|
|
2301
|
+
|
|
2302
|
+
const provider = raw
|
|
2303
|
+
.query(
|
|
2304
|
+
`SELECT provider_key FROM oauth_providers WHERE provider_key = 'integration:gmail'`,
|
|
2305
|
+
)
|
|
2306
|
+
.get();
|
|
2307
|
+
expect(provider).toBeTruthy();
|
|
2308
|
+
|
|
2309
|
+
const app = raw
|
|
2310
|
+
.query(`SELECT provider_key FROM oauth_apps WHERE id = 'app1'`)
|
|
2311
|
+
.get() as { provider_key: string };
|
|
2312
|
+
expect(app.provider_key).toBe("integration:gmail");
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2316
|
+
const db = createTestDb();
|
|
2317
|
+
const raw = getRaw(db);
|
|
2318
|
+
raw.exec(`CREATE TABLE oauth_providers (provider_key TEXT PRIMARY KEY)`);
|
|
2319
|
+
raw.exec(
|
|
2320
|
+
`CREATE TABLE oauth_apps (id TEXT PRIMARY KEY, provider_key TEXT NOT NULL)`,
|
|
2321
|
+
);
|
|
2322
|
+
raw.exec(
|
|
2323
|
+
`CREATE TABLE oauth_connections (id TEXT PRIMARY KEY, provider_key TEXT NOT NULL)`,
|
|
2324
|
+
);
|
|
2325
|
+
raw.exec(`INSERT INTO oauth_providers VALUES ('integration:google')`);
|
|
2326
|
+
|
|
2327
|
+
migrateRenameGmailProviderKeyToGoogleDown(db);
|
|
2328
|
+
migrateRenameGmailProviderKeyToGoogleDown(db);
|
|
2329
|
+
});
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
// ── v32: migrateRenameThreadStartersTableDown ───────────────────────
|
|
2333
|
+
|
|
2334
|
+
describe("v32: migrateRenameThreadStartersTableDown", () => {
|
|
2335
|
+
test("renames conversation_starters back to thread_starters", () => {
|
|
2336
|
+
const db = createTestDb();
|
|
2337
|
+
const raw = getRaw(db);
|
|
2338
|
+
|
|
2339
|
+
raw.exec(/*sql*/ `
|
|
2340
|
+
CREATE TABLE conversation_starters (
|
|
2341
|
+
id TEXT PRIMARY KEY,
|
|
2342
|
+
generation_batch TEXT,
|
|
2343
|
+
card_type TEXT,
|
|
2344
|
+
scope_id TEXT,
|
|
2345
|
+
created_at INTEGER NOT NULL
|
|
2346
|
+
)
|
|
2347
|
+
`);
|
|
2348
|
+
raw.exec(
|
|
2349
|
+
`CREATE INDEX idx_conversation_starters_batch ON conversation_starters(generation_batch, created_at)`,
|
|
2350
|
+
);
|
|
2351
|
+
raw.exec(
|
|
2352
|
+
`CREATE INDEX idx_conversation_starters_card_type ON conversation_starters(card_type, scope_id)`,
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2355
|
+
migrateRenameThreadStartersTableDown(db);
|
|
2356
|
+
|
|
2357
|
+
const oldTable = raw
|
|
2358
|
+
.query(
|
|
2359
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'thread_starters'`,
|
|
2360
|
+
)
|
|
2361
|
+
.get();
|
|
2362
|
+
expect(oldTable).toBeTruthy();
|
|
2363
|
+
|
|
2364
|
+
const newTable = raw
|
|
2365
|
+
.query(
|
|
2366
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'conversation_starters'`,
|
|
2367
|
+
)
|
|
2368
|
+
.get();
|
|
2369
|
+
expect(newTable).toBeNull();
|
|
2370
|
+
|
|
2371
|
+
// Old-style indexes should exist.
|
|
2372
|
+
const batchIdx = raw
|
|
2373
|
+
.query(
|
|
2374
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_thread_starters_batch'`,
|
|
2375
|
+
)
|
|
2376
|
+
.get();
|
|
2377
|
+
expect(batchIdx).toBeTruthy();
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2381
|
+
const db = createTestDb();
|
|
2382
|
+
const raw = getRaw(db);
|
|
2383
|
+
raw.exec(
|
|
2384
|
+
`CREATE TABLE conversation_starters (id TEXT PRIMARY KEY, generation_batch TEXT, card_type TEXT, scope_id TEXT, created_at INTEGER NOT NULL)`,
|
|
2385
|
+
);
|
|
2386
|
+
|
|
2387
|
+
migrateRenameThreadStartersTableDown(db);
|
|
2388
|
+
migrateRenameThreadStartersTableDown(db);
|
|
2389
|
+
});
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
// ── v35: migrateRenameThreadStartersCheckpointsDown ─────────────────
|
|
2393
|
+
|
|
2394
|
+
describe("v35: migrateRenameThreadStartersCheckpointsDown", () => {
|
|
2395
|
+
test("renames conversation_starters: checkpoint keys back to thread_starters:", () => {
|
|
2396
|
+
const db = createTestDb();
|
|
2397
|
+
const raw = getRaw(db);
|
|
2398
|
+
bootstrapCheckpointsTable(raw);
|
|
2399
|
+
|
|
2400
|
+
const now = Date.now();
|
|
2401
|
+
raw.exec(
|
|
2402
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('conversation_starters:gen_batch_1', '1', ${now})`,
|
|
2403
|
+
);
|
|
2404
|
+
raw.exec(
|
|
2405
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('conversation_starters:gen_batch_2', '1', ${now})`,
|
|
2406
|
+
);
|
|
2407
|
+
|
|
2408
|
+
migrateRenameThreadStartersCheckpointsDown(db);
|
|
2409
|
+
|
|
2410
|
+
const newPrefixCount = (
|
|
2411
|
+
raw
|
|
2412
|
+
.query(
|
|
2413
|
+
`SELECT COUNT(*) AS c FROM memory_checkpoints WHERE key LIKE 'conversation_starters:%'`,
|
|
2414
|
+
)
|
|
2415
|
+
.get() as { c: number }
|
|
2416
|
+
).c;
|
|
2417
|
+
expect(newPrefixCount).toBe(0);
|
|
2418
|
+
|
|
2419
|
+
const oldPrefixCount = (
|
|
2420
|
+
raw
|
|
2421
|
+
.query(
|
|
2422
|
+
`SELECT COUNT(*) AS c FROM memory_checkpoints WHERE key LIKE 'thread_starters:%'`,
|
|
2423
|
+
)
|
|
2424
|
+
.get() as { c: number }
|
|
2425
|
+
).c;
|
|
2426
|
+
expect(oldPrefixCount).toBe(2);
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2430
|
+
const db = createTestDb();
|
|
2431
|
+
const raw = getRaw(db);
|
|
2432
|
+
bootstrapCheckpointsTable(raw);
|
|
2433
|
+
|
|
2434
|
+
const now = Date.now();
|
|
2435
|
+
raw.exec(
|
|
2436
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('conversation_starters:gen_batch_1', '1', ${now})`,
|
|
2437
|
+
);
|
|
2438
|
+
|
|
2439
|
+
migrateRenameThreadStartersCheckpointsDown(db);
|
|
2440
|
+
migrateRenameThreadStartersCheckpointsDown(db);
|
|
2441
|
+
});
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
// ── v18: downBackfillContactInteractionStats ────────────────────────
|
|
2445
|
+
|
|
2446
|
+
describe("v18: downBackfillContactInteractionStats", () => {
|
|
2447
|
+
test("clears last_interaction column", () => {
|
|
2448
|
+
const db = createTestDb();
|
|
2449
|
+
const raw = getRaw(db);
|
|
2450
|
+
|
|
2451
|
+
raw.exec(/*sql*/ `
|
|
2452
|
+
CREATE TABLE contacts (
|
|
2453
|
+
id TEXT PRIMARY KEY,
|
|
2454
|
+
last_interaction INTEGER,
|
|
2455
|
+
created_at INTEGER NOT NULL,
|
|
2456
|
+
updated_at INTEGER NOT NULL
|
|
2457
|
+
)
|
|
2458
|
+
`);
|
|
2459
|
+
|
|
2460
|
+
const now = Date.now();
|
|
2461
|
+
raw.exec(`INSERT INTO contacts VALUES ('c1', ${now}, ${now}, ${now})`);
|
|
2462
|
+
|
|
2463
|
+
downBackfillContactInteractionStats(db);
|
|
2464
|
+
|
|
2465
|
+
const contact = raw
|
|
2466
|
+
.query(`SELECT last_interaction FROM contacts WHERE id = 'c1'`)
|
|
2467
|
+
.get() as { last_interaction: number | null };
|
|
2468
|
+
expect(contact.last_interaction).toBeNull();
|
|
2469
|
+
});
|
|
2470
|
+
|
|
2471
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2472
|
+
const db = createTestDb();
|
|
2473
|
+
const raw = getRaw(db);
|
|
2474
|
+
raw.exec(
|
|
2475
|
+
`CREATE TABLE contacts (id TEXT PRIMARY KEY, last_interaction INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
2476
|
+
);
|
|
2477
|
+
raw.exec(
|
|
2478
|
+
`INSERT INTO contacts VALUES ('c1', ${Date.now()}, ${Date.now()}, ${Date.now()})`,
|
|
2479
|
+
);
|
|
2480
|
+
|
|
2481
|
+
downBackfillContactInteractionStats(db);
|
|
2482
|
+
downBackfillContactInteractionStats(db);
|
|
2483
|
+
});
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
// ── v12: downEmbeddingVectorBlob (column drop) ──────────────────────
|
|
2487
|
+
|
|
2488
|
+
describe("v12: downEmbeddingVectorBlob", () => {
|
|
2489
|
+
test("drops vector_blob column from memory_embeddings", () => {
|
|
2490
|
+
const db = createTestDb();
|
|
2491
|
+
const raw = getRaw(db);
|
|
2492
|
+
|
|
2493
|
+
raw.exec(/*sql*/ `
|
|
2494
|
+
CREATE TABLE memory_embeddings (
|
|
2495
|
+
id TEXT PRIMARY KEY,
|
|
2496
|
+
target_type TEXT NOT NULL,
|
|
2497
|
+
target_id TEXT NOT NULL,
|
|
2498
|
+
provider TEXT NOT NULL,
|
|
2499
|
+
model TEXT NOT NULL,
|
|
2500
|
+
dimensions INTEGER NOT NULL,
|
|
2501
|
+
vector_json TEXT,
|
|
2502
|
+
vector_blob BLOB,
|
|
2503
|
+
content_hash TEXT,
|
|
2504
|
+
created_at INTEGER NOT NULL,
|
|
2505
|
+
updated_at INTEGER NOT NULL,
|
|
2506
|
+
UNIQUE (target_type, target_id, provider, model)
|
|
2507
|
+
)
|
|
2508
|
+
`);
|
|
2509
|
+
|
|
2510
|
+
downEmbeddingVectorBlob(db);
|
|
2511
|
+
|
|
2512
|
+
const col = raw
|
|
2513
|
+
.query(
|
|
2514
|
+
`SELECT 1 FROM pragma_table_info('memory_embeddings') WHERE name = 'vector_blob'`,
|
|
2515
|
+
)
|
|
2516
|
+
.get();
|
|
2517
|
+
expect(col).toBeNull();
|
|
2518
|
+
|
|
2519
|
+
// Other columns should still exist.
|
|
2520
|
+
const vectorJson = raw
|
|
2521
|
+
.query(
|
|
2522
|
+
`SELECT 1 FROM pragma_table_info('memory_embeddings') WHERE name = 'vector_json'`,
|
|
2523
|
+
)
|
|
2524
|
+
.get();
|
|
2525
|
+
expect(vectorJson).toBeTruthy();
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2529
|
+
const db = createTestDb();
|
|
2530
|
+
const raw = getRaw(db);
|
|
2531
|
+
raw.exec(/*sql*/ `
|
|
2532
|
+
CREATE TABLE memory_embeddings (
|
|
2533
|
+
id TEXT PRIMARY KEY,
|
|
2534
|
+
target_type TEXT NOT NULL,
|
|
2535
|
+
target_id TEXT NOT NULL,
|
|
2536
|
+
provider TEXT NOT NULL,
|
|
2537
|
+
model TEXT NOT NULL,
|
|
2538
|
+
dimensions INTEGER NOT NULL,
|
|
2539
|
+
vector_json TEXT,
|
|
2540
|
+
vector_blob BLOB,
|
|
2541
|
+
content_hash TEXT,
|
|
2542
|
+
created_at INTEGER NOT NULL,
|
|
2543
|
+
updated_at INTEGER NOT NULL
|
|
2544
|
+
)
|
|
2545
|
+
`);
|
|
2546
|
+
|
|
2547
|
+
downEmbeddingVectorBlob(db);
|
|
2548
|
+
downEmbeddingVectorBlob(db);
|
|
2549
|
+
});
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
// ── v13: downEmbeddingsNullableVectorJson ───────────────────────────
|
|
2553
|
+
|
|
2554
|
+
describe("v13: downEmbeddingsNullableVectorJson", () => {
|
|
2555
|
+
test("restores NOT NULL on vector_json column", () => {
|
|
2556
|
+
const db = createTestDb();
|
|
2557
|
+
const raw = getRaw(db);
|
|
2558
|
+
|
|
2559
|
+
// Post-forward-migration schema: vector_json is nullable.
|
|
2560
|
+
raw.exec(/*sql*/ `
|
|
2561
|
+
CREATE TABLE memory_embeddings (
|
|
2562
|
+
id TEXT PRIMARY KEY,
|
|
2563
|
+
target_type TEXT NOT NULL,
|
|
2564
|
+
target_id TEXT NOT NULL,
|
|
2565
|
+
provider TEXT NOT NULL,
|
|
2566
|
+
model TEXT NOT NULL,
|
|
2567
|
+
dimensions INTEGER NOT NULL,
|
|
2568
|
+
vector_json TEXT,
|
|
2569
|
+
vector_blob BLOB,
|
|
2570
|
+
content_hash TEXT,
|
|
2571
|
+
created_at INTEGER NOT NULL,
|
|
2572
|
+
updated_at INTEGER NOT NULL,
|
|
2573
|
+
UNIQUE (target_type, target_id, provider, model)
|
|
2574
|
+
)
|
|
2575
|
+
`);
|
|
2576
|
+
|
|
2577
|
+
const now = Date.now();
|
|
2578
|
+
raw.exec(
|
|
2579
|
+
`INSERT INTO memory_embeddings VALUES ('e1', 'item', 'item-1', 'openai', 'text-embedding-3-small', 1536, '[0.1,0.2]', NULL, 'hash1', ${now}, ${now})`,
|
|
2580
|
+
);
|
|
2581
|
+
|
|
2582
|
+
downEmbeddingsNullableVectorJson(db);
|
|
2583
|
+
|
|
2584
|
+
// Check that vector_json is now NOT NULL.
|
|
2585
|
+
const ddl =
|
|
2586
|
+
(
|
|
2587
|
+
raw
|
|
2588
|
+
.query(
|
|
2589
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_embeddings'`,
|
|
2590
|
+
)
|
|
2591
|
+
.get() as { sql: string }
|
|
2592
|
+
)?.sql ?? "";
|
|
2593
|
+
expect(ddl).toMatch(/vector_json\s+TEXT\s+NOT\s+NULL/i);
|
|
2594
|
+
|
|
2595
|
+
// Data with non-null vector_json should be preserved.
|
|
2596
|
+
const row = raw
|
|
2597
|
+
.query(`SELECT id FROM memory_embeddings WHERE id = 'e1'`)
|
|
2598
|
+
.get();
|
|
2599
|
+
expect(row).toBeTruthy();
|
|
2600
|
+
});
|
|
2601
|
+
|
|
2602
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2603
|
+
const db = createTestDb();
|
|
2604
|
+
const raw = getRaw(db);
|
|
2605
|
+
raw.exec(/*sql*/ `
|
|
2606
|
+
CREATE TABLE memory_embeddings (
|
|
2607
|
+
id TEXT PRIMARY KEY, target_type TEXT NOT NULL, target_id TEXT NOT NULL,
|
|
2608
|
+
provider TEXT NOT NULL, model TEXT NOT NULL, dimensions INTEGER NOT NULL,
|
|
2609
|
+
vector_json TEXT, vector_blob BLOB, content_hash TEXT,
|
|
2610
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
|
|
2611
|
+
UNIQUE (target_type, target_id, provider, model)
|
|
2612
|
+
)
|
|
2613
|
+
`);
|
|
2614
|
+
|
|
2615
|
+
downEmbeddingsNullableVectorJson(db);
|
|
2616
|
+
downEmbeddingsNullableVectorJson(db);
|
|
2617
|
+
});
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
// ── v15: downBackfillGuardianPrincipalId ─────────────────────────────
|
|
2621
|
+
|
|
2622
|
+
describe("v15: downBackfillGuardianPrincipalId", () => {
|
|
2623
|
+
test("nulls out guardian_principal_id on channel_guardian_bindings", () => {
|
|
2624
|
+
const db = createTestDb();
|
|
2625
|
+
const raw = getRaw(db);
|
|
2626
|
+
|
|
2627
|
+
raw.exec(/*sql*/ `
|
|
2628
|
+
CREATE TABLE channel_guardian_bindings (
|
|
2629
|
+
id TEXT PRIMARY KEY,
|
|
2630
|
+
assistant_id TEXT NOT NULL,
|
|
2631
|
+
channel TEXT NOT NULL,
|
|
2632
|
+
guardian_external_user_id TEXT NOT NULL,
|
|
2633
|
+
guardian_delivery_chat_id TEXT NOT NULL,
|
|
2634
|
+
guardian_principal_id TEXT,
|
|
2635
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
2636
|
+
verified_at INTEGER NOT NULL,
|
|
2637
|
+
verified_via TEXT NOT NULL DEFAULT 'challenge',
|
|
2638
|
+
metadata_json TEXT,
|
|
2639
|
+
created_at INTEGER NOT NULL,
|
|
2640
|
+
updated_at INTEGER NOT NULL
|
|
2641
|
+
)
|
|
2642
|
+
`);
|
|
2643
|
+
|
|
2644
|
+
const now = Date.now();
|
|
2645
|
+
raw.exec(
|
|
2646
|
+
`INSERT INTO channel_guardian_bindings VALUES ('b1', 'self', 'vellum', 'user1', 'chat1', 'principal1', 'active', ${now}, 'challenge', NULL, ${now}, ${now})`,
|
|
2647
|
+
);
|
|
2648
|
+
|
|
2649
|
+
downBackfillGuardianPrincipalId(db);
|
|
2650
|
+
|
|
2651
|
+
const row = raw
|
|
2652
|
+
.query(
|
|
2653
|
+
`SELECT guardian_principal_id FROM channel_guardian_bindings WHERE id = 'b1'`,
|
|
2654
|
+
)
|
|
2655
|
+
.get() as { guardian_principal_id: string | null };
|
|
2656
|
+
expect(row.guardian_principal_id).toBeNull();
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2659
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2660
|
+
const db = createTestDb();
|
|
2661
|
+
const raw = getRaw(db);
|
|
2662
|
+
raw.exec(
|
|
2663
|
+
`CREATE TABLE channel_guardian_bindings (id TEXT PRIMARY KEY, guardian_principal_id TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
2664
|
+
);
|
|
2665
|
+
|
|
2666
|
+
downBackfillGuardianPrincipalId(db);
|
|
2667
|
+
downBackfillGuardianPrincipalId(db);
|
|
2668
|
+
});
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2671
|
+
// ── v16: downGuardianPrincipalIdNotNull ──────────────────────────────
|
|
2672
|
+
|
|
2673
|
+
describe("v16: downGuardianPrincipalIdNotNull", () => {
|
|
2674
|
+
test("makes guardian_principal_id nullable again", () => {
|
|
2675
|
+
const db = createTestDb();
|
|
2676
|
+
const raw = getRaw(db);
|
|
2677
|
+
|
|
2678
|
+
raw.exec(/*sql*/ `
|
|
2679
|
+
CREATE TABLE channel_guardian_bindings (
|
|
2680
|
+
id TEXT PRIMARY KEY,
|
|
2681
|
+
assistant_id TEXT NOT NULL,
|
|
2682
|
+
channel TEXT NOT NULL,
|
|
2683
|
+
guardian_external_user_id TEXT NOT NULL,
|
|
2684
|
+
guardian_delivery_chat_id TEXT NOT NULL,
|
|
2685
|
+
guardian_principal_id TEXT NOT NULL,
|
|
2686
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
2687
|
+
verified_at INTEGER NOT NULL,
|
|
2688
|
+
verified_via TEXT NOT NULL DEFAULT 'challenge',
|
|
2689
|
+
metadata_json TEXT,
|
|
2690
|
+
created_at INTEGER NOT NULL,
|
|
2691
|
+
updated_at INTEGER NOT NULL
|
|
2692
|
+
)
|
|
2693
|
+
`);
|
|
2694
|
+
|
|
2695
|
+
// Confirm NOT NULL before down.
|
|
2696
|
+
const colBefore = raw
|
|
2697
|
+
.query(
|
|
2698
|
+
`SELECT "notnull" FROM pragma_table_info('channel_guardian_bindings') WHERE name = 'guardian_principal_id'`,
|
|
2699
|
+
)
|
|
2700
|
+
.get() as { notnull: number };
|
|
2701
|
+
expect(colBefore.notnull).toBe(1);
|
|
2702
|
+
|
|
2703
|
+
downGuardianPrincipalIdNotNull(db);
|
|
2704
|
+
|
|
2705
|
+
// After down, should be nullable.
|
|
2706
|
+
const colAfter = raw
|
|
2707
|
+
.query(
|
|
2708
|
+
`SELECT "notnull" FROM pragma_table_info('channel_guardian_bindings') WHERE name = 'guardian_principal_id'`,
|
|
2709
|
+
)
|
|
2710
|
+
.get() as { notnull: number };
|
|
2711
|
+
expect(colAfter.notnull).toBe(0);
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2714
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2715
|
+
const db = createTestDb();
|
|
2716
|
+
const raw = getRaw(db);
|
|
2717
|
+
raw.exec(/*sql*/ `
|
|
2718
|
+
CREATE TABLE channel_guardian_bindings (
|
|
2719
|
+
id TEXT PRIMARY KEY, assistant_id TEXT NOT NULL, channel TEXT NOT NULL,
|
|
2720
|
+
guardian_external_user_id TEXT NOT NULL, guardian_delivery_chat_id TEXT NOT NULL,
|
|
2721
|
+
guardian_principal_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active',
|
|
2722
|
+
verified_at INTEGER NOT NULL, verified_via TEXT NOT NULL DEFAULT 'challenge',
|
|
2723
|
+
metadata_json TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2724
|
+
)
|
|
2725
|
+
`);
|
|
2726
|
+
|
|
2727
|
+
downGuardianPrincipalIdNotNull(db);
|
|
2728
|
+
downGuardianPrincipalIdNotNull(db);
|
|
2729
|
+
});
|
|
2730
|
+
});
|
|
2731
|
+
|
|
2732
|
+
// ── v29: migrateGuardianTimestampsEpochMsDown ───────────────────────
|
|
2733
|
+
|
|
2734
|
+
describe("v29: migrateGuardianTimestampsEpochMsDown", () => {
|
|
2735
|
+
test("converts epoch ms integers back to ISO 8601 strings", () => {
|
|
2736
|
+
const db = createTestDb();
|
|
2737
|
+
const raw = getRaw(db);
|
|
2738
|
+
|
|
2739
|
+
raw.exec(/*sql*/ `
|
|
2740
|
+
CREATE TABLE canonical_guardian_requests (
|
|
2741
|
+
id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_type TEXT NOT NULL,
|
|
2742
|
+
source_channel TEXT, status TEXT NOT NULL DEFAULT 'pending',
|
|
2743
|
+
expires_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2744
|
+
);
|
|
2745
|
+
CREATE TABLE canonical_guardian_deliveries (
|
|
2746
|
+
id TEXT PRIMARY KEY, request_id TEXT NOT NULL, destination_channel TEXT NOT NULL,
|
|
2747
|
+
status TEXT NOT NULL DEFAULT 'pending', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2748
|
+
);
|
|
2749
|
+
CREATE TABLE scoped_approval_grants (
|
|
2750
|
+
id TEXT PRIMARY KEY, scope_mode TEXT NOT NULL, request_channel TEXT NOT NULL,
|
|
2751
|
+
decision_channel TEXT NOT NULL, status TEXT NOT NULL,
|
|
2752
|
+
expires_at INTEGER NOT NULL, consumed_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2753
|
+
);
|
|
2754
|
+
`);
|
|
2755
|
+
|
|
2756
|
+
// Insert with epoch ms values (post-forward-migration state).
|
|
2757
|
+
const epochMs = 1700000000000; // 2023-11-14T22:13:20.000Z
|
|
2758
|
+
raw.exec(
|
|
2759
|
+
`INSERT INTO canonical_guardian_requests VALUES ('r1', 'approval', 'desktop', 'vellum', 'pending', NULL, ${epochMs}, ${epochMs})`,
|
|
2760
|
+
);
|
|
2761
|
+
raw.exec(
|
|
2762
|
+
`INSERT INTO canonical_guardian_deliveries VALUES ('d1', 'r1', 'vellum', 'pending', ${epochMs}, ${epochMs})`,
|
|
2763
|
+
);
|
|
2764
|
+
raw.exec(
|
|
2765
|
+
`INSERT INTO scoped_approval_grants VALUES ('g1', 'once', 'vellum', 'vellum', 'active', ${epochMs}, NULL, ${epochMs}, ${epochMs})`,
|
|
2766
|
+
);
|
|
2767
|
+
|
|
2768
|
+
migrateGuardianTimestampsEpochMsDown(db);
|
|
2769
|
+
|
|
2770
|
+
// Verify created_at is now a text ISO 8601 string.
|
|
2771
|
+
const req = raw
|
|
2772
|
+
.query(
|
|
2773
|
+
`SELECT created_at, typeof(created_at) AS t FROM canonical_guardian_requests WHERE id = 'r1'`,
|
|
2774
|
+
)
|
|
2775
|
+
.get() as { created_at: string; t: string };
|
|
2776
|
+
expect(req.t).toBe("text");
|
|
2777
|
+
expect(req.created_at).toContain("2023-11-14");
|
|
2778
|
+
});
|
|
2779
|
+
|
|
2780
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2781
|
+
const db = createTestDb();
|
|
2782
|
+
const raw = getRaw(db);
|
|
2783
|
+
raw.exec(
|
|
2784
|
+
`CREATE TABLE canonical_guardian_requests (id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_type TEXT NOT NULL, source_channel TEXT, status TEXT NOT NULL, expires_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
2785
|
+
);
|
|
2786
|
+
raw.exec(
|
|
2787
|
+
`CREATE TABLE canonical_guardian_deliveries (id TEXT PRIMARY KEY, request_id TEXT NOT NULL, destination_channel TEXT NOT NULL, status TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
2788
|
+
);
|
|
2789
|
+
raw.exec(
|
|
2790
|
+
`CREATE TABLE scoped_approval_grants (id TEXT PRIMARY KEY, scope_mode TEXT NOT NULL, request_channel TEXT NOT NULL, decision_channel TEXT NOT NULL, status TEXT NOT NULL, expires_at INTEGER NOT NULL, consumed_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
2791
|
+
);
|
|
2792
|
+
|
|
2793
|
+
const epochMs = 1700000000000;
|
|
2794
|
+
raw.exec(
|
|
2795
|
+
`INSERT INTO canonical_guardian_requests VALUES ('r1', 'approval', 'desktop', 'vellum', 'pending', NULL, ${epochMs}, ${epochMs})`,
|
|
2796
|
+
);
|
|
2797
|
+
|
|
2798
|
+
migrateGuardianTimestampsEpochMsDown(db);
|
|
2799
|
+
// Second call — values are already text, typeof check skips them.
|
|
2800
|
+
migrateGuardianTimestampsEpochMsDown(db);
|
|
2801
|
+
});
|
|
2802
|
+
});
|
|
2803
|
+
|
|
2804
|
+
// ── v30: migrateGuardianTimestampsRebuildDown ───────────────────────
|
|
2805
|
+
|
|
2806
|
+
describe("v30: migrateGuardianTimestampsRebuildDown", () => {
|
|
2807
|
+
test("rebuilds tables with TEXT affinity on timestamp columns", () => {
|
|
2808
|
+
const db = createTestDb();
|
|
2809
|
+
const raw = getRaw(db);
|
|
2810
|
+
|
|
2811
|
+
// Post-forward-migration state: INTEGER affinity on timestamp columns.
|
|
2812
|
+
raw.exec(/*sql*/ `
|
|
2813
|
+
CREATE TABLE canonical_guardian_requests (
|
|
2814
|
+
id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_type TEXT NOT NULL,
|
|
2815
|
+
source_channel TEXT, conversation_id TEXT, requester_external_user_id TEXT,
|
|
2816
|
+
requester_chat_id TEXT, guardian_external_user_id TEXT, guardian_principal_id TEXT,
|
|
2817
|
+
call_session_id TEXT, pending_question_id TEXT, question_text TEXT,
|
|
2818
|
+
request_code TEXT, tool_name TEXT, input_digest TEXT,
|
|
2819
|
+
status TEXT NOT NULL DEFAULT 'pending', answer_text TEXT,
|
|
2820
|
+
decided_by_external_user_id TEXT, decided_by_principal_id TEXT,
|
|
2821
|
+
followup_state TEXT, expires_at INTEGER,
|
|
2822
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2823
|
+
);
|
|
2824
|
+
CREATE TABLE canonical_guardian_deliveries (
|
|
2825
|
+
id TEXT PRIMARY KEY, request_id TEXT NOT NULL REFERENCES canonical_guardian_requests(id) ON DELETE CASCADE,
|
|
2826
|
+
destination_channel TEXT NOT NULL, destination_conversation_id TEXT,
|
|
2827
|
+
destination_chat_id TEXT, destination_message_id TEXT,
|
|
2828
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
2829
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2830
|
+
);
|
|
2831
|
+
CREATE TABLE scoped_approval_grants (
|
|
2832
|
+
id TEXT PRIMARY KEY, scope_mode TEXT NOT NULL, request_id TEXT,
|
|
2833
|
+
tool_name TEXT, input_digest TEXT, request_channel TEXT NOT NULL,
|
|
2834
|
+
decision_channel TEXT NOT NULL, execution_channel TEXT,
|
|
2835
|
+
conversation_id TEXT, call_session_id TEXT,
|
|
2836
|
+
requester_external_user_id TEXT, guardian_external_user_id TEXT,
|
|
2837
|
+
status TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
2838
|
+
consumed_at INTEGER, consumed_by_request_id TEXT,
|
|
2839
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2840
|
+
);
|
|
2841
|
+
`);
|
|
2842
|
+
|
|
2843
|
+
migrateGuardianTimestampsRebuildDown(db);
|
|
2844
|
+
|
|
2845
|
+
// Verify TEXT affinity on created_at.
|
|
2846
|
+
const colType = raw
|
|
2847
|
+
.query(
|
|
2848
|
+
`SELECT type FROM pragma_table_info('canonical_guardian_requests') WHERE name = 'created_at'`,
|
|
2849
|
+
)
|
|
2850
|
+
.get() as { type: string };
|
|
2851
|
+
expect(colType.type.toUpperCase()).toBe("TEXT");
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
test("idempotency: calling down twice does not throw", () => {
|
|
2855
|
+
const db = createTestDb();
|
|
2856
|
+
const raw = getRaw(db);
|
|
2857
|
+
|
|
2858
|
+
raw.exec(/*sql*/ `
|
|
2859
|
+
CREATE TABLE canonical_guardian_requests (
|
|
2860
|
+
id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_type TEXT NOT NULL,
|
|
2861
|
+
source_channel TEXT, conversation_id TEXT, requester_external_user_id TEXT,
|
|
2862
|
+
requester_chat_id TEXT, guardian_external_user_id TEXT, guardian_principal_id TEXT,
|
|
2863
|
+
call_session_id TEXT, pending_question_id TEXT, question_text TEXT,
|
|
2864
|
+
request_code TEXT, tool_name TEXT, input_digest TEXT,
|
|
2865
|
+
status TEXT NOT NULL DEFAULT 'pending', answer_text TEXT,
|
|
2866
|
+
decided_by_external_user_id TEXT, decided_by_principal_id TEXT,
|
|
2867
|
+
followup_state TEXT, expires_at INTEGER,
|
|
2868
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2869
|
+
);
|
|
2870
|
+
CREATE TABLE canonical_guardian_deliveries (
|
|
2871
|
+
id TEXT PRIMARY KEY, request_id TEXT NOT NULL REFERENCES canonical_guardian_requests(id) ON DELETE CASCADE,
|
|
2872
|
+
destination_channel TEXT NOT NULL, destination_conversation_id TEXT,
|
|
2873
|
+
destination_chat_id TEXT, destination_message_id TEXT,
|
|
2874
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
2875
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2876
|
+
);
|
|
2877
|
+
CREATE TABLE scoped_approval_grants (
|
|
2878
|
+
id TEXT PRIMARY KEY, scope_mode TEXT NOT NULL, request_id TEXT,
|
|
2879
|
+
tool_name TEXT, input_digest TEXT, request_channel TEXT NOT NULL,
|
|
2880
|
+
decision_channel TEXT NOT NULL, execution_channel TEXT,
|
|
2881
|
+
conversation_id TEXT, call_session_id TEXT,
|
|
2882
|
+
requester_external_user_id TEXT, guardian_external_user_id TEXT,
|
|
2883
|
+
status TEXT NOT NULL, expires_at INTEGER NOT NULL,
|
|
2884
|
+
consumed_at INTEGER, consumed_by_request_id TEXT,
|
|
2885
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2886
|
+
);
|
|
2887
|
+
`);
|
|
2888
|
+
|
|
2889
|
+
migrateGuardianTimestampsRebuildDown(db);
|
|
2890
|
+
migrateGuardianTimestampsRebuildDown(db);
|
|
2891
|
+
});
|
|
2892
|
+
});
|
|
2893
|
+
});
|