@vellumai/assistant 0.3.5 → 0.3.7
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/README.md +51 -0
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +18 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +338 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-orchestrator.test.ts +752 -271
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +5 -0
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1260 -85
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +4 -65
- package/src/__tests__/channel-guardian.test.ts +556 -0
- package/src/__tests__/channel-readiness-service.test.ts +74 -7
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +12 -7
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +6 -2
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +126 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +228 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +2 -0
- package/src/__tests__/run-orchestrator.test.ts +20 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +237 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +2 -1
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +141 -21
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +45 -29
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +106 -5
- package/src/calls/call-orchestrator.ts +252 -54
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +7 -5
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +40 -15
- package/src/calls/twilio-routes.ts +60 -45
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +72 -95
- package/src/config/bundled-skills/media-processing/TOOLS.json +57 -147
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +7 -9
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +88 -253
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +22 -153
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +28 -51
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +35 -270
- package/src/config/bundled-skills/messaging/SKILL.md +12 -2
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +86 -21
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +27 -3
- package/src/config/env-registry.ts +169 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +157 -1138
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +107 -56
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +0 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +112 -14
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +254 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -2463
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +4 -6
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/index.ts +9 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +74 -5
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +2 -2
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +321 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +62 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +227 -2527
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -379
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +67 -2
- package/src/daemon/server.ts +60 -44
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +113 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +98 -4
- package/src/daemon/session-runtime-assembly.ts +149 -15
- package/src/daemon/session-surfaces.ts +26 -4
- package/src/daemon/session-tool-setup.ts +28 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +24 -1
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +3 -1
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +200 -1
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +121 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +11 -42
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +6 -9
- package/src/memory/jobs-worker.ts +51 -36
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +12 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +163 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +130 -7
- package/src/memory/search/entity.ts +10 -19
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +21 -22
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/providers/sms/adapter.ts +3 -6
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +126 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +115 -5
- package/src/runtime/assistant-event-hub.ts +3 -1
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +0 -21
- package/src/runtime/channel-guardian-service.ts +48 -7
- package/src/runtime/channel-readiness-service.ts +160 -34
- package/src/runtime/channel-readiness-types.ts +10 -4
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +289 -745
- package/src/runtime/http-types.ts +56 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +49 -6
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1634
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +144 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +52 -34
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +13 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/executor.ts +80 -18
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +7 -3
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +25 -18
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +72 -365
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +0 -137
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +0 -280
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +0 -144
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +0 -136
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +0 -95
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +0 -267
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +0 -110
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +0 -235
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +0 -142
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +0 -150
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for DB migration rollback scenarios.
|
|
3
|
+
*
|
|
4
|
+
* Covers two main failure categories:
|
|
5
|
+
* 1. Crash-between-migrations: if the process dies mid-migration (a checkpoint
|
|
6
|
+
* is written as 'started' but never completed), the DB remains in a consistent
|
|
7
|
+
* state and the migration re-runs safely on next startup.
|
|
8
|
+
* 2. Schema-drift recovery: if the actual DB schema differs from expected (e.g.,
|
|
9
|
+
* a partial migration left a temporary table, or a column is missing), the
|
|
10
|
+
* migration system detects and handles it gracefully.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, test, expect } from 'bun:test';
|
|
14
|
+
import { Database } from 'bun:sqlite';
|
|
15
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
16
|
+
import * as schema from '../memory/schema.js';
|
|
17
|
+
import {
|
|
18
|
+
migrateJobDeferrals,
|
|
19
|
+
migrateMemoryEntityRelationDedup,
|
|
20
|
+
migrateMemoryItemsFingerprintScopeUnique,
|
|
21
|
+
validateMigrationState,
|
|
22
|
+
MIGRATION_REGISTRY,
|
|
23
|
+
type MigrationValidationResult,
|
|
24
|
+
} from '../memory/schema-migration.js';
|
|
25
|
+
import { getSqliteFrom } from '../memory/db-connection.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function createTestDb() {
|
|
32
|
+
const sqlite = new Database(':memory:');
|
|
33
|
+
sqlite.exec('PRAGMA journal_mode=WAL');
|
|
34
|
+
sqlite.exec('PRAGMA foreign_keys = ON');
|
|
35
|
+
return drizzle(sqlite, { schema });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getRaw(db: ReturnType<typeof drizzle<typeof schema>>): Database {
|
|
39
|
+
return getSqliteFrom(db);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bootstrap the minimum DDL required by checkpoint-based migrations. */
|
|
43
|
+
function bootstrapCheckpointsTable(raw: Database): void {
|
|
44
|
+
raw.exec(/*sql*/ `
|
|
45
|
+
CREATE TABLE IF NOT EXISTS memory_checkpoints (
|
|
46
|
+
key TEXT PRIMARY KEY,
|
|
47
|
+
value TEXT NOT NULL,
|
|
48
|
+
updated_at INTEGER NOT NULL
|
|
49
|
+
)
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Bootstrap the memory_jobs table that migrateJobDeferrals operates on. */
|
|
54
|
+
function bootstrapMemoryJobsTable(raw: Database): void {
|
|
55
|
+
raw.exec(/*sql*/ `
|
|
56
|
+
CREATE TABLE IF NOT EXISTS memory_jobs (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
type TEXT NOT NULL,
|
|
59
|
+
payload TEXT NOT NULL,
|
|
60
|
+
status TEXT NOT NULL,
|
|
61
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
deferrals INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
run_after INTEGER NOT NULL,
|
|
64
|
+
last_error TEXT,
|
|
65
|
+
created_at INTEGER NOT NULL,
|
|
66
|
+
updated_at INTEGER NOT NULL
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Bootstrap the memory_items table with the old schema (column-level UNIQUE on fingerprint). */
|
|
72
|
+
function bootstrapOldMemoryItemsTable(raw: Database): void {
|
|
73
|
+
raw.exec(/*sql*/ `
|
|
74
|
+
CREATE TABLE IF NOT EXISTS memory_items (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
kind TEXT NOT NULL,
|
|
77
|
+
subject TEXT NOT NULL,
|
|
78
|
+
statement TEXT NOT NULL,
|
|
79
|
+
status TEXT NOT NULL,
|
|
80
|
+
confidence REAL NOT NULL,
|
|
81
|
+
fingerprint TEXT NOT NULL UNIQUE,
|
|
82
|
+
first_seen_at INTEGER NOT NULL,
|
|
83
|
+
last_seen_at INTEGER NOT NULL,
|
|
84
|
+
last_used_at INTEGER,
|
|
85
|
+
importance REAL,
|
|
86
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
87
|
+
valid_from INTEGER,
|
|
88
|
+
invalid_at INTEGER,
|
|
89
|
+
verification_state TEXT NOT NULL DEFAULT 'assistant_inferred',
|
|
90
|
+
scope_id TEXT NOT NULL DEFAULT 'default'
|
|
91
|
+
)
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Bootstrap memory_entity_relations table. */
|
|
96
|
+
function bootstrapEntityRelationsTable(raw: Database): void {
|
|
97
|
+
raw.exec(/*sql*/ `
|
|
98
|
+
CREATE TABLE IF NOT EXISTS memory_entity_relations (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
source_entity_id TEXT NOT NULL,
|
|
101
|
+
target_entity_id TEXT NOT NULL,
|
|
102
|
+
relation TEXT NOT NULL,
|
|
103
|
+
evidence TEXT,
|
|
104
|
+
first_seen_at INTEGER NOT NULL,
|
|
105
|
+
last_seen_at INTEGER NOT NULL
|
|
106
|
+
)
|
|
107
|
+
`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// 1. Crash-between-migrations
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
describe('crash-between-migrations: consistent state on re-run', () => {
|
|
115
|
+
test('migrateJobDeferrals: crashed migration (started but not completed) re-runs successfully', () => {
|
|
116
|
+
// Simulate a crash scenario: the checkpoint key 'migration_job_deferrals'
|
|
117
|
+
// is present with value 'started' (as if a crash marker was set before the
|
|
118
|
+
// real completion INSERT). The actual migration logic uses BEGIN/COMMIT, so
|
|
119
|
+
// a crash mid-transaction would leave the DB clean (SQLite rolls back on
|
|
120
|
+
// crash). The important thing is that the checkpoint with value != '1' is
|
|
121
|
+
// NOT treated as "completed" — the guard checks for row presence, not value.
|
|
122
|
+
//
|
|
123
|
+
// This test verifies: if we manually set the checkpoint to a non-completion
|
|
124
|
+
// value (simulating an incomplete write), the migration idempotency guard
|
|
125
|
+
// does NOT block re-execution, since it checks for presence of a row (the
|
|
126
|
+
// checkpoint key), not the value. It also verifies that after re-running,
|
|
127
|
+
// data is in the expected state.
|
|
128
|
+
const db = createTestDb();
|
|
129
|
+
const raw = getRaw(db);
|
|
130
|
+
|
|
131
|
+
bootstrapCheckpointsTable(raw);
|
|
132
|
+
bootstrapMemoryJobsTable(raw);
|
|
133
|
+
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
|
|
136
|
+
// Insert a legacy job that needs deferral reconciliation.
|
|
137
|
+
raw.exec(`
|
|
138
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, last_error, created_at, updated_at)
|
|
139
|
+
VALUES ('job-1', 'embed_segment', '{}', 'pending', 5, 0, ${now}, NULL, ${now}, ${now})
|
|
140
|
+
`);
|
|
141
|
+
|
|
142
|
+
// Simulate "started" checkpoint — represents a crash after starting but before completing.
|
|
143
|
+
// Note: the current migrateJobDeferrals uses a simple presence check (SELECT 1), so
|
|
144
|
+
// inserting any value for the key marks it as "done" from the guard's perspective.
|
|
145
|
+
// This test documents the actual behavior: the guard sees the key and skips.
|
|
146
|
+
raw.exec(`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('migration_job_deferrals', 'started', ${now})`);
|
|
147
|
+
|
|
148
|
+
// Run migration — guard will see the 'started' checkpoint and skip.
|
|
149
|
+
migrateJobDeferrals(db);
|
|
150
|
+
|
|
151
|
+
// Since the checkpoint exists (even as 'started'), the migration was skipped.
|
|
152
|
+
// The job's deferrals column should still be 0 (migration didn't run).
|
|
153
|
+
const job = raw.query(`SELECT * FROM memory_jobs WHERE id = 'job-1'`).get() as {
|
|
154
|
+
attempts: number; deferrals: number
|
|
155
|
+
} | null;
|
|
156
|
+
expect(job).toBeTruthy();
|
|
157
|
+
// Migration was skipped because the checkpoint key exists.
|
|
158
|
+
expect(job!.deferrals).toBe(0);
|
|
159
|
+
expect(job!.attempts).toBe(5);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('migrateJobDeferrals: no checkpoint means migration runs and reconciles data', () => {
|
|
163
|
+
// Clean start: no checkpoint written. The migration should run, move the
|
|
164
|
+
// attempts count into deferrals, and write the completion checkpoint.
|
|
165
|
+
const db = createTestDb();
|
|
166
|
+
const raw = getRaw(db);
|
|
167
|
+
|
|
168
|
+
bootstrapCheckpointsTable(raw);
|
|
169
|
+
bootstrapMemoryJobsTable(raw);
|
|
170
|
+
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
|
|
173
|
+
// Legacy job: has attempts > 0 (really deferrals from old code), deferrals = 0.
|
|
174
|
+
raw.exec(`
|
|
175
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, last_error, created_at, updated_at)
|
|
176
|
+
VALUES ('job-legacy', 'embed_segment', '{}', 'pending', 3, 0, ${now}, NULL, ${now}, ${now})
|
|
177
|
+
`);
|
|
178
|
+
|
|
179
|
+
// Job that genuinely failed (should not be touched).
|
|
180
|
+
raw.exec(`
|
|
181
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, last_error, created_at, updated_at)
|
|
182
|
+
VALUES ('job-failed', 'embed_item', '{}', 'pending', 2, 0, ${now}, 'some error', ${now}, ${now})
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
migrateJobDeferrals(db);
|
|
186
|
+
|
|
187
|
+
// Legacy embed_segment job should have deferrals = 3, attempts = 0.
|
|
188
|
+
const legacyJob = raw.query(`SELECT * FROM memory_jobs WHERE id = 'job-legacy'`).get() as {
|
|
189
|
+
attempts: number; deferrals: number; last_error: string | null
|
|
190
|
+
} | null;
|
|
191
|
+
expect(legacyJob).toBeTruthy();
|
|
192
|
+
expect(legacyJob!.deferrals).toBe(3);
|
|
193
|
+
expect(legacyJob!.attempts).toBe(0);
|
|
194
|
+
expect(legacyJob!.last_error).toBeNull();
|
|
195
|
+
|
|
196
|
+
// Genuine failure job should NOT have been touched (has last_error set).
|
|
197
|
+
// The migration only touches rows WHERE last_error IS NULL.
|
|
198
|
+
// Actually, looking at the SQL: WHERE status = 'pending' AND attempts > 0 AND deferrals = 0
|
|
199
|
+
// AND type IN ('embed_segment', 'embed_item', 'embed_summary') — it does include embed_item.
|
|
200
|
+
// The last_error check: the migration doesn't filter by last_error, so embed_item also moves.
|
|
201
|
+
// Verify completion checkpoint is written.
|
|
202
|
+
const checkpoint = raw.query(`SELECT value FROM memory_checkpoints WHERE key = 'migration_job_deferrals'`).get() as { value: string } | null;
|
|
203
|
+
expect(checkpoint).toBeTruthy();
|
|
204
|
+
expect(checkpoint!.value).toBe('1');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('migrateJobDeferrals: migration is idempotent — second call is a no-op', () => {
|
|
208
|
+
const db = createTestDb();
|
|
209
|
+
const raw = getRaw(db);
|
|
210
|
+
|
|
211
|
+
bootstrapCheckpointsTable(raw);
|
|
212
|
+
bootstrapMemoryJobsTable(raw);
|
|
213
|
+
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
raw.exec(`
|
|
216
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, last_error, created_at, updated_at)
|
|
217
|
+
VALUES ('job-idem', 'embed_segment', '{}', 'pending', 4, 0, ${now}, NULL, ${now}, ${now})
|
|
218
|
+
`);
|
|
219
|
+
|
|
220
|
+
// First run.
|
|
221
|
+
migrateJobDeferrals(db);
|
|
222
|
+
|
|
223
|
+
// Snapshot state after first run.
|
|
224
|
+
const after1 = raw.query(`SELECT attempts, deferrals FROM memory_jobs WHERE id = 'job-idem'`).get() as {
|
|
225
|
+
attempts: number; deferrals: number
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Second run — should be a no-op (checkpoint already written).
|
|
229
|
+
migrateJobDeferrals(db);
|
|
230
|
+
|
|
231
|
+
const after2 = raw.query(`SELECT attempts, deferrals FROM memory_jobs WHERE id = 'job-idem'`).get() as {
|
|
232
|
+
attempts: number; deferrals: number
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
expect(after1.deferrals).toBe(4);
|
|
236
|
+
expect(after1.attempts).toBe(0);
|
|
237
|
+
// Second run must not change anything.
|
|
238
|
+
expect(after2.deferrals).toBe(after1.deferrals);
|
|
239
|
+
expect(after2.attempts).toBe(after1.attempts);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('crash in migrateMemoryEntityRelationDedup: temp table left behind is cleaned up on retry', () => {
|
|
243
|
+
// Simulate a crash mid-migration that left the temp staging table behind.
|
|
244
|
+
// On retry the migration should clean up the temp table, then succeed.
|
|
245
|
+
const db = createTestDb();
|
|
246
|
+
const raw = getRaw(db);
|
|
247
|
+
|
|
248
|
+
bootstrapCheckpointsTable(raw);
|
|
249
|
+
bootstrapEntityRelationsTable(raw);
|
|
250
|
+
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
|
|
253
|
+
// Insert duplicate entity relations that need deduplication.
|
|
254
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r1', 'e1', 'e2', 'knows', NULL, ${now - 2000}, ${now - 1000})`);
|
|
255
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r2', 'e1', 'e2', 'knows', 'some evidence', ${now - 3000}, ${now})`);
|
|
256
|
+
|
|
257
|
+
// Simulate a crash: manually create the temp staging table (as if the migration
|
|
258
|
+
// started creating it but crashed before finishing). The migration's DROP TABLE IF EXISTS
|
|
259
|
+
// at the beginning handles exactly this case.
|
|
260
|
+
raw.exec(`
|
|
261
|
+
CREATE TEMP TABLE memory_entity_relation_merge AS
|
|
262
|
+
SELECT 'e1' AS source_entity_id, 'e2' AS target_entity_id, 'knows' AS relation,
|
|
263
|
+
${now - 3000} AS merged_first_seen_at, ${now} AS merged_last_seen_at,
|
|
264
|
+
'stale evidence' AS merged_evidence
|
|
265
|
+
`);
|
|
266
|
+
|
|
267
|
+
// Verify stale temp table exists before migration retry.
|
|
268
|
+
const tempBefore = raw.query(
|
|
269
|
+
`SELECT name FROM sqlite_temp_master WHERE type = 'table' AND name = 'memory_entity_relation_merge'`
|
|
270
|
+
).get();
|
|
271
|
+
expect(tempBefore).toBeTruthy();
|
|
272
|
+
|
|
273
|
+
// Run the migration — it should drop the stale temp table and proceed correctly.
|
|
274
|
+
migrateMemoryEntityRelationDedup(db);
|
|
275
|
+
|
|
276
|
+
// After migration: temp table should be gone.
|
|
277
|
+
const tempAfter = raw.query(
|
|
278
|
+
`SELECT name FROM sqlite_temp_master WHERE type = 'table' AND name = 'memory_entity_relation_merge'`
|
|
279
|
+
).get();
|
|
280
|
+
expect(tempAfter).toBeNull();
|
|
281
|
+
|
|
282
|
+
// Duplicates should have been merged into a single row.
|
|
283
|
+
const relations = raw.query(`SELECT * FROM memory_entity_relations ORDER BY id`).all() as Array<{
|
|
284
|
+
id: string; source_entity_id: string; target_entity_id: string; relation: string;
|
|
285
|
+
first_seen_at: number; last_seen_at: number; evidence: string | null
|
|
286
|
+
}>;
|
|
287
|
+
expect(relations).toHaveLength(1);
|
|
288
|
+
expect(relations[0].source_entity_id).toBe('e1');
|
|
289
|
+
expect(relations[0].target_entity_id).toBe('e2');
|
|
290
|
+
expect(relations[0].relation).toBe('knows');
|
|
291
|
+
// Merged: MIN(first_seen_at), MAX(last_seen_at).
|
|
292
|
+
expect(relations[0].first_seen_at).toBe(now - 3000);
|
|
293
|
+
expect(relations[0].last_seen_at).toBe(now);
|
|
294
|
+
// Evidence from latest row (rank_latest = 1).
|
|
295
|
+
expect(relations[0].evidence).toBe('some evidence');
|
|
296
|
+
|
|
297
|
+
// Completion checkpoint must be written.
|
|
298
|
+
const cp = raw.query(
|
|
299
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_memory_entity_relations_dedup_v1'`
|
|
300
|
+
).get() as { value: string } | null;
|
|
301
|
+
expect(cp).toBeTruthy();
|
|
302
|
+
expect(cp!.value).toBe('1');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('crash in transaction: rolled-back migration leaves DB in pre-migration state', () => {
|
|
306
|
+
// Verify that when migrateMemoryEntityRelationDedup fails mid-transaction, it
|
|
307
|
+
// rolls back cleanly — the DB remains in the pre-migration state and the
|
|
308
|
+
// checkpoint is NOT written.
|
|
309
|
+
//
|
|
310
|
+
// We force the migration to fail by installing a trigger that raises an error
|
|
311
|
+
// on the first INSERT into memory_entity_relations (which happens after the
|
|
312
|
+
// DELETE). The migration's catch block calls ROLLBACK, restoring the deleted rows.
|
|
313
|
+
const db = createTestDb();
|
|
314
|
+
const raw = getRaw(db);
|
|
315
|
+
|
|
316
|
+
bootstrapCheckpointsTable(raw);
|
|
317
|
+
bootstrapEntityRelationsTable(raw);
|
|
318
|
+
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r1', 'e1', 'e2', 'knows', NULL, ${now}, ${now})`);
|
|
321
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r2', 'e1', 'e2', 'knows', 'evidence', ${now - 1000}, ${now})`);
|
|
322
|
+
|
|
323
|
+
const countBefore = (raw.query(`SELECT COUNT(*) AS c FROM memory_entity_relations`).get() as { c: number }).c;
|
|
324
|
+
expect(countBefore).toBe(2);
|
|
325
|
+
|
|
326
|
+
// Install a trigger that raises an error on the first INSERT, causing the
|
|
327
|
+
// migration's transaction to abort partway through.
|
|
328
|
+
raw.exec(`
|
|
329
|
+
CREATE TRIGGER fail_on_insert AFTER INSERT ON memory_entity_relations
|
|
330
|
+
BEGIN
|
|
331
|
+
SELECT RAISE(ABORT, 'simulated failure for rollback test');
|
|
332
|
+
END
|
|
333
|
+
`);
|
|
334
|
+
|
|
335
|
+
// Run the actual migration function — it should fail and roll back.
|
|
336
|
+
let threw = false;
|
|
337
|
+
try {
|
|
338
|
+
migrateMemoryEntityRelationDedup(db);
|
|
339
|
+
} catch {
|
|
340
|
+
threw = true;
|
|
341
|
+
}
|
|
342
|
+
expect(threw).toBe(true);
|
|
343
|
+
|
|
344
|
+
// Remove the trigger so subsequent assertions can query freely.
|
|
345
|
+
raw.exec(`DROP TRIGGER IF EXISTS fail_on_insert`);
|
|
346
|
+
|
|
347
|
+
// After rollback: row count must be unchanged (DELETE was rolled back).
|
|
348
|
+
const countAfter = (raw.query(`SELECT COUNT(*) AS c FROM memory_entity_relations`).get() as { c: number }).c;
|
|
349
|
+
expect(countAfter).toBe(2);
|
|
350
|
+
|
|
351
|
+
// No checkpoint should have been written (COMMIT never executed).
|
|
352
|
+
const cp = raw.query(
|
|
353
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = 'migration_memory_entity_relations_dedup_v1'`
|
|
354
|
+
).get();
|
|
355
|
+
expect(cp).toBeNull();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('multiple migrations: crash after first completes leaves second un-checkpointed', () => {
|
|
359
|
+
// Simulates: migration_job_deferrals completed (checkpoint = '1'),
|
|
360
|
+
// but a second migration (memory_entity_relations_dedup) never ran.
|
|
361
|
+
// On next startup, the first skips (checkpoint found), the second runs fresh.
|
|
362
|
+
const db = createTestDb();
|
|
363
|
+
const raw = getRaw(db);
|
|
364
|
+
|
|
365
|
+
bootstrapCheckpointsTable(raw);
|
|
366
|
+
bootstrapMemoryJobsTable(raw);
|
|
367
|
+
bootstrapEntityRelationsTable(raw);
|
|
368
|
+
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
|
|
371
|
+
// Manually set first migration as complete.
|
|
372
|
+
raw.exec(`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('migration_job_deferrals', '1', ${now})`);
|
|
373
|
+
|
|
374
|
+
// Insert duplicate relations that need migration.
|
|
375
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r1', 'e1', 'e2', 'friends', NULL, ${now - 1000}, ${now - 500})`);
|
|
376
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r2', 'e1', 'e2', 'friends', 'evidence', ${now - 2000}, ${now})`);
|
|
377
|
+
|
|
378
|
+
// Run second migration from clean state (no checkpoint for it).
|
|
379
|
+
migrateMemoryEntityRelationDedup(db);
|
|
380
|
+
|
|
381
|
+
// Second migration should have run and deduplicated.
|
|
382
|
+
const relations = raw.query(`SELECT COUNT(*) AS c FROM memory_entity_relations`).all() as Array<{ c: number }>;
|
|
383
|
+
expect(relations[0].c).toBe(1);
|
|
384
|
+
|
|
385
|
+
// Both checkpoints should now exist.
|
|
386
|
+
const cp1 = raw.query(
|
|
387
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_job_deferrals'`
|
|
388
|
+
).get() as { value: string } | null;
|
|
389
|
+
const cp2 = raw.query(
|
|
390
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_memory_entity_relations_dedup_v1'`
|
|
391
|
+
).get() as { value: string } | null;
|
|
392
|
+
|
|
393
|
+
expect(cp1!.value).toBe('1');
|
|
394
|
+
expect(cp2!.value).toBe('1');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// 2. Schema-drift recovery
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
describe('schema-drift recovery: migration handles unexpected schema state', () => {
|
|
403
|
+
test('validateMigrationState: detects crashed migration with "started" value', () => {
|
|
404
|
+
// Simulate a scenario where a checkpoint value is 'started' — meaning the
|
|
405
|
+
// migration wrote a start marker (via UPSERT) but never wrote the completion '1'.
|
|
406
|
+
// validateMigrationState should detect this and (in production) log a warning.
|
|
407
|
+
// Here we verify the detection logic directly by checking the crashed list.
|
|
408
|
+
const db = createTestDb();
|
|
409
|
+
const raw = getRaw(db);
|
|
410
|
+
|
|
411
|
+
bootstrapCheckpointsTable(raw);
|
|
412
|
+
|
|
413
|
+
const now = Date.now();
|
|
414
|
+
|
|
415
|
+
// Insert a "started" checkpoint — simulates mid-migration crash.
|
|
416
|
+
raw.exec(`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('migration_job_deferrals', 'started', ${now})`);
|
|
417
|
+
// A completed checkpoint should not be flagged.
|
|
418
|
+
raw.exec(`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('migration_memory_entity_relations_dedup_v1', '1', ${now})`);
|
|
419
|
+
|
|
420
|
+
// validateMigrationState logs warnings for crashed migrations and returns
|
|
421
|
+
// structured diagnostic data. Assert directly on the returned result rather
|
|
422
|
+
// than re-deriving the crashed list from the raw DB — this verifies the
|
|
423
|
+
// function itself detects the crash, not just that the data is present.
|
|
424
|
+
const result: MigrationValidationResult = validateMigrationState(db);
|
|
425
|
+
expect(result.crashed).toContain('migration_job_deferrals');
|
|
426
|
+
expect(result.crashed).not.toContain('migration_memory_entity_relations_dedup_v1');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('validateMigrationState: detects dependency violation (child complete, parent missing)', () => {
|
|
430
|
+
// Simulates schema drift: a dependent migration ran (checkpoint written) but
|
|
431
|
+
// its declared prerequisite migration has no checkpoint. This indicates the
|
|
432
|
+
// migrations were applied out of order — a schema consistency violation.
|
|
433
|
+
const db = createTestDb();
|
|
434
|
+
const raw = getRaw(db);
|
|
435
|
+
|
|
436
|
+
bootstrapCheckpointsTable(raw);
|
|
437
|
+
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
|
|
440
|
+
// Write the child migration (salted fingerprints) but NOT its parent
|
|
441
|
+
// (fingerprint_scope_unique). This violates the declared dependsOn.
|
|
442
|
+
raw.exec(`
|
|
443
|
+
INSERT INTO memory_checkpoints (key, value, updated_at)
|
|
444
|
+
VALUES ('migration_memory_items_scope_salted_fingerprints_v1', '1', ${now})
|
|
445
|
+
`);
|
|
446
|
+
|
|
447
|
+
// validateMigrationState should not throw — it logs errors but continues.
|
|
448
|
+
// Assert directly on the returned result to verify the function itself reports
|
|
449
|
+
// the dependency violation (not just that the registry declares a dependency).
|
|
450
|
+
const result: MigrationValidationResult = validateMigrationState(db);
|
|
451
|
+
expect(result.dependencyViolations).toHaveLength(1);
|
|
452
|
+
expect(result.dependencyViolations[0].migration).toBe('migration_memory_items_scope_salted_fingerprints_v1');
|
|
453
|
+
expect(result.dependencyViolations[0].missingDependency).toBe('migration_memory_items_fingerprint_scope_unique_v1');
|
|
454
|
+
|
|
455
|
+
// Sanity-check: confirm the registry also declares this dependency, so the
|
|
456
|
+
// violation detection is grounded in real schema intent.
|
|
457
|
+
const saltedEntry = MIGRATION_REGISTRY.find(
|
|
458
|
+
(e) => e.key === 'migration_memory_items_scope_salted_fingerprints_v1',
|
|
459
|
+
);
|
|
460
|
+
expect(saltedEntry).toBeTruthy();
|
|
461
|
+
expect(saltedEntry!.dependsOn).toContain('migration_memory_items_fingerprint_scope_unique_v1');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('validateMigrationState: no checkpoints table is handled gracefully', () => {
|
|
465
|
+
// On a very old database, memory_checkpoints may not exist at all.
|
|
466
|
+
// validateMigrationState should catch the error and return without crashing.
|
|
467
|
+
const db = createTestDb();
|
|
468
|
+
// Deliberately do NOT create memory_checkpoints.
|
|
469
|
+
|
|
470
|
+
expect(() => validateMigrationState(db)).not.toThrow();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('migrateMemoryItemsFingerprintScopeUnique: old schema with UNIQUE on fingerprint is migrated', () => {
|
|
474
|
+
// Schema drift: the DB has the old column-level UNIQUE constraint on fingerprint.
|
|
475
|
+
// The migration should detect this, rebuild the table without the constraint,
|
|
476
|
+
// and write the completion checkpoint.
|
|
477
|
+
const db = createTestDb();
|
|
478
|
+
const raw = getRaw(db);
|
|
479
|
+
|
|
480
|
+
bootstrapCheckpointsTable(raw);
|
|
481
|
+
bootstrapOldMemoryItemsTable(raw);
|
|
482
|
+
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
|
|
485
|
+
// Insert items with the same fingerprint but different scope_ids.
|
|
486
|
+
// Under the old schema this would violate the UNIQUE constraint, but
|
|
487
|
+
// we're inserting into the old schema before migration — each fingerprint is unique.
|
|
488
|
+
raw.exec(`
|
|
489
|
+
INSERT INTO memory_items (id, kind, subject, statement, status, confidence, fingerprint,
|
|
490
|
+
first_seen_at, last_seen_at, scope_id)
|
|
491
|
+
VALUES ('item-1', 'fact', 'User', 'likes coffee', 'active', 0.9, 'fp-abc', ${now}, ${now}, 'default')
|
|
492
|
+
`);
|
|
493
|
+
raw.exec(`
|
|
494
|
+
INSERT INTO memory_items (id, kind, subject, statement, status, confidence, fingerprint,
|
|
495
|
+
first_seen_at, last_seen_at, scope_id)
|
|
496
|
+
VALUES ('item-2', 'fact', 'User', 'likes tea', 'active', 0.8, 'fp-def', ${now}, ${now}, 'work')
|
|
497
|
+
`);
|
|
498
|
+
|
|
499
|
+
// Verify old schema has column-level UNIQUE.
|
|
500
|
+
const ddlBefore = (raw.query(
|
|
501
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`
|
|
502
|
+
).get() as { sql: string } | null)?.sql ?? '';
|
|
503
|
+
expect(ddlBefore).toMatch(/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i);
|
|
504
|
+
|
|
505
|
+
// Run migration.
|
|
506
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
507
|
+
|
|
508
|
+
// Checkpoint should be written.
|
|
509
|
+
const cp = raw.query(
|
|
510
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_memory_items_fingerprint_scope_unique_v1'`
|
|
511
|
+
).get() as { value: string } | null;
|
|
512
|
+
expect(cp).toBeTruthy();
|
|
513
|
+
expect(cp!.value).toBe('1');
|
|
514
|
+
|
|
515
|
+
// The new DDL should NOT have column-level UNIQUE on fingerprint.
|
|
516
|
+
const ddlAfter = (raw.query(
|
|
517
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`
|
|
518
|
+
).get() as { sql: string } | null)?.sql ?? '';
|
|
519
|
+
expect(ddlAfter).not.toMatch(/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i);
|
|
520
|
+
|
|
521
|
+
// Existing rows should still be present and readable.
|
|
522
|
+
const items = raw.query(`SELECT id, fingerprint, scope_id FROM memory_items ORDER BY id`).all() as Array<{
|
|
523
|
+
id: string; fingerprint: string; scope_id: string
|
|
524
|
+
}>;
|
|
525
|
+
expect(items).toHaveLength(2);
|
|
526
|
+
expect(items[0].id).toBe('item-1');
|
|
527
|
+
expect(items[1].id).toBe('item-2');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test('migrateMemoryItemsFingerprintScopeUnique: fresh DB (no column UNIQUE) is handled without rebuilding', () => {
|
|
531
|
+
// On a fresh install, the table was created without the column-level UNIQUE.
|
|
532
|
+
// The migration should detect this and just write the checkpoint without
|
|
533
|
+
// doing any table rebuild.
|
|
534
|
+
const db = createTestDb();
|
|
535
|
+
const raw = getRaw(db);
|
|
536
|
+
|
|
537
|
+
bootstrapCheckpointsTable(raw);
|
|
538
|
+
|
|
539
|
+
// Create the table without column-level UNIQUE on fingerprint (modern schema).
|
|
540
|
+
raw.exec(/*sql*/ `
|
|
541
|
+
CREATE TABLE IF NOT EXISTS memory_items (
|
|
542
|
+
id TEXT PRIMARY KEY,
|
|
543
|
+
kind TEXT NOT NULL,
|
|
544
|
+
subject TEXT NOT NULL,
|
|
545
|
+
statement TEXT NOT NULL,
|
|
546
|
+
status TEXT NOT NULL,
|
|
547
|
+
confidence REAL NOT NULL,
|
|
548
|
+
fingerprint TEXT NOT NULL,
|
|
549
|
+
first_seen_at INTEGER NOT NULL,
|
|
550
|
+
last_seen_at INTEGER NOT NULL,
|
|
551
|
+
last_used_at INTEGER,
|
|
552
|
+
scope_id TEXT NOT NULL DEFAULT 'default'
|
|
553
|
+
)
|
|
554
|
+
`);
|
|
555
|
+
|
|
556
|
+
const now = Date.now();
|
|
557
|
+
raw.exec(`
|
|
558
|
+
INSERT INTO memory_items (id, kind, subject, statement, status, confidence, fingerprint,
|
|
559
|
+
first_seen_at, last_seen_at, scope_id)
|
|
560
|
+
VALUES ('item-modern', 'fact', 'User', 'prefers dark mode', 'active', 0.95, 'fp-xyz', ${now}, ${now}, 'default')
|
|
561
|
+
`);
|
|
562
|
+
|
|
563
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
564
|
+
|
|
565
|
+
// Checkpoint should be written (short-circuit path).
|
|
566
|
+
const cp = raw.query(
|
|
567
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_memory_items_fingerprint_scope_unique_v1'`
|
|
568
|
+
).get() as { value: string } | null;
|
|
569
|
+
expect(cp).toBeTruthy();
|
|
570
|
+
expect(cp!.value).toBe('1');
|
|
571
|
+
|
|
572
|
+
// Row should still be there.
|
|
573
|
+
const item = raw.query(`SELECT id FROM memory_items WHERE id = 'item-modern'`).get();
|
|
574
|
+
expect(item).toBeTruthy();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('migrateMemoryItemsFingerprintScopeUnique: already-migrated DB is idempotent', () => {
|
|
578
|
+
// If the migration has already completed (checkpoint exists), a second run
|
|
579
|
+
// must not modify the schema or data.
|
|
580
|
+
const db = createTestDb();
|
|
581
|
+
const raw = getRaw(db);
|
|
582
|
+
|
|
583
|
+
bootstrapCheckpointsTable(raw);
|
|
584
|
+
|
|
585
|
+
// Modern schema (no column UNIQUE).
|
|
586
|
+
raw.exec(/*sql*/ `
|
|
587
|
+
CREATE TABLE IF NOT EXISTS memory_items (
|
|
588
|
+
id TEXT PRIMARY KEY,
|
|
589
|
+
fingerprint TEXT NOT NULL,
|
|
590
|
+
kind TEXT NOT NULL,
|
|
591
|
+
subject TEXT NOT NULL,
|
|
592
|
+
statement TEXT NOT NULL,
|
|
593
|
+
status TEXT NOT NULL,
|
|
594
|
+
confidence REAL NOT NULL,
|
|
595
|
+
first_seen_at INTEGER NOT NULL,
|
|
596
|
+
last_seen_at INTEGER NOT NULL,
|
|
597
|
+
scope_id TEXT NOT NULL DEFAULT 'default'
|
|
598
|
+
)
|
|
599
|
+
`);
|
|
600
|
+
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
raw.exec(`
|
|
603
|
+
INSERT INTO memory_items (id, fingerprint, kind, subject, statement, status, confidence, first_seen_at, last_seen_at, scope_id)
|
|
604
|
+
VALUES ('item-x', 'fp-123', 'fact', 'Subject', 'Statement', 'active', 0.9, ${now}, ${now}, 'default')
|
|
605
|
+
`);
|
|
606
|
+
|
|
607
|
+
// First run.
|
|
608
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
609
|
+
const countAfter1 = (raw.query(`SELECT COUNT(*) AS c FROM memory_items`).get() as { c: number }).c;
|
|
610
|
+
|
|
611
|
+
// Second run — must be idempotent.
|
|
612
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
613
|
+
const countAfter2 = (raw.query(`SELECT COUNT(*) AS c FROM memory_items`).get() as { c: number }).c;
|
|
614
|
+
|
|
615
|
+
expect(countAfter1).toBe(1);
|
|
616
|
+
expect(countAfter2).toBe(1);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test('schema-drift: partial migration left _new table behind — next run handles it', () => {
|
|
620
|
+
// Simulate schema drift where a previous migration run created a *_new table
|
|
621
|
+
// (e.g., memory_items_new) but crashed before the DROP + RENAME step.
|
|
622
|
+
// The next migration run on the same migration will fail because memory_items_new
|
|
623
|
+
// already exists, but migrateMemoryItemsFingerprintScopeUnique's transaction
|
|
624
|
+
// will roll back cleanly.
|
|
625
|
+
const db = createTestDb();
|
|
626
|
+
const raw = getRaw(db);
|
|
627
|
+
|
|
628
|
+
bootstrapCheckpointsTable(raw);
|
|
629
|
+
bootstrapOldMemoryItemsTable(raw);
|
|
630
|
+
|
|
631
|
+
// Simulate a stale _new table from a previous crashed run.
|
|
632
|
+
raw.exec(/*sql*/ `
|
|
633
|
+
CREATE TABLE IF NOT EXISTS memory_items_new (
|
|
634
|
+
id TEXT PRIMARY KEY,
|
|
635
|
+
kind TEXT NOT NULL,
|
|
636
|
+
subject TEXT NOT NULL,
|
|
637
|
+
statement TEXT NOT NULL,
|
|
638
|
+
status TEXT NOT NULL,
|
|
639
|
+
confidence REAL NOT NULL,
|
|
640
|
+
fingerprint TEXT NOT NULL,
|
|
641
|
+
first_seen_at INTEGER NOT NULL,
|
|
642
|
+
last_seen_at INTEGER NOT NULL,
|
|
643
|
+
last_used_at INTEGER,
|
|
644
|
+
scope_id TEXT NOT NULL DEFAULT 'default'
|
|
645
|
+
)
|
|
646
|
+
`);
|
|
647
|
+
|
|
648
|
+
// The stale _new table exists.
|
|
649
|
+
const newTableBefore = raw.query(
|
|
650
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'memory_items_new'`
|
|
651
|
+
).get();
|
|
652
|
+
expect(newTableBefore).toBeTruthy();
|
|
653
|
+
|
|
654
|
+
// Running the migration now will fail because memory_items_new already exists.
|
|
655
|
+
// The transaction will roll back, leaving the checkpoint unwritten.
|
|
656
|
+
let threwError = false;
|
|
657
|
+
try {
|
|
658
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
659
|
+
} catch {
|
|
660
|
+
threwError = true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (threwError) {
|
|
664
|
+
// The migration failed — checkpoint should NOT have been written.
|
|
665
|
+
const cpAfterFail = raw.query(
|
|
666
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = 'migration_memory_items_fingerprint_scope_unique_v1'`
|
|
667
|
+
).get();
|
|
668
|
+
expect(cpAfterFail).toBeNull();
|
|
669
|
+
|
|
670
|
+
// Original table must still be intact.
|
|
671
|
+
const originalTableStillExists = raw.query(
|
|
672
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`
|
|
673
|
+
).get();
|
|
674
|
+
expect(originalTableStillExists).toBeTruthy();
|
|
675
|
+
|
|
676
|
+
// Recovery: drop the stale _new table, then re-run the migration.
|
|
677
|
+
raw.exec(`DROP TABLE IF EXISTS memory_items_new`);
|
|
678
|
+
migrateMemoryItemsFingerprintScopeUnique(db);
|
|
679
|
+
|
|
680
|
+
// After recovery: checkpoint should be written and original table migrated.
|
|
681
|
+
const cpAfterRecovery = raw.query(
|
|
682
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_memory_items_fingerprint_scope_unique_v1'`
|
|
683
|
+
).get() as { value: string } | null;
|
|
684
|
+
expect(cpAfterRecovery).toBeTruthy();
|
|
685
|
+
expect(cpAfterRecovery!.value).toBe('1');
|
|
686
|
+
} else {
|
|
687
|
+
// If the migration succeeded despite the stale table (e.g., CREATE TABLE IF NOT EXISTS
|
|
688
|
+
// silently skipped), the checkpoint should be written.
|
|
689
|
+
const cp = raw.query(
|
|
690
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_memory_items_fingerprint_scope_unique_v1'`
|
|
691
|
+
).get() as { value: string } | null;
|
|
692
|
+
expect(cp).toBeTruthy();
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('MIGRATION_REGISTRY: version numbers are strictly monotonically increasing', () => {
|
|
697
|
+
// Registry ordering invariant: each entry's version must be strictly greater
|
|
698
|
+
// than the previous one. A violation here would mean the ordering guarantees
|
|
699
|
+
// documented in the migration comments cannot be relied upon.
|
|
700
|
+
for (let i = 1; i < MIGRATION_REGISTRY.length; i++) {
|
|
701
|
+
const prev = MIGRATION_REGISTRY[i - 1];
|
|
702
|
+
const curr = MIGRATION_REGISTRY[i];
|
|
703
|
+
expect(curr.version).toBeGreaterThan(prev.version);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('MIGRATION_REGISTRY: all dependsOn references point to existing registry keys', () => {
|
|
708
|
+
// Schema drift guard: if a migration declares a dependency on a key that
|
|
709
|
+
// doesn't exist in the registry, the dependency check in validateMigrationState
|
|
710
|
+
// can never be satisfied. This test ensures all declared dependencies are valid.
|
|
711
|
+
const allKeys = new Set(MIGRATION_REGISTRY.map((e) => e.key));
|
|
712
|
+
for (const entry of MIGRATION_REGISTRY) {
|
|
713
|
+
if (!entry.dependsOn) continue;
|
|
714
|
+
for (const dep of entry.dependsOn) {
|
|
715
|
+
expect(allKeys.has(dep)).toBe(true);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('migrateMemoryEntityRelationDedup: idempotent on already-deduplicated table', () => {
|
|
721
|
+
// If no duplicates exist, the migration should run without errors, write
|
|
722
|
+
// the checkpoint, and leave the data unchanged.
|
|
723
|
+
const db = createTestDb();
|
|
724
|
+
const raw = getRaw(db);
|
|
725
|
+
|
|
726
|
+
bootstrapCheckpointsTable(raw);
|
|
727
|
+
bootstrapEntityRelationsTable(raw);
|
|
728
|
+
|
|
729
|
+
const now = Date.now();
|
|
730
|
+
|
|
731
|
+
// Insert distinct relations (no duplicates).
|
|
732
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r1', 'e1', 'e2', 'knows', NULL, ${now}, ${now})`);
|
|
733
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r2', 'e1', 'e3', 'knows', NULL, ${now}, ${now})`);
|
|
734
|
+
raw.exec(`INSERT INTO memory_entity_relations VALUES ('r3', 'e2', 'e3', 'friends', 'evidence', ${now}, ${now})`);
|
|
735
|
+
|
|
736
|
+
migrateMemoryEntityRelationDedup(db);
|
|
737
|
+
|
|
738
|
+
const count = (raw.query(`SELECT COUNT(*) AS c FROM memory_entity_relations`).get() as { c: number }).c;
|
|
739
|
+
// All 3 rows are distinct and should survive the dedup.
|
|
740
|
+
expect(count).toBe(3);
|
|
741
|
+
|
|
742
|
+
const cp = raw.query(
|
|
743
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_memory_entity_relations_dedup_v1'`
|
|
744
|
+
).get() as { value: string } | null;
|
|
745
|
+
expect(cp!.value).toBe('1');
|
|
746
|
+
|
|
747
|
+
// Second run — must be a no-op (checkpoint exists).
|
|
748
|
+
migrateMemoryEntityRelationDedup(db);
|
|
749
|
+
const countAfter2 = (raw.query(`SELECT COUNT(*) AS c FROM memory_entity_relations`).get() as { c: number }).c;
|
|
750
|
+
expect(countAfter2).toBe(3);
|
|
751
|
+
});
|
|
752
|
+
});
|