@vellumai/assistant 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +32 -49
- package/Dockerfile +1 -0
- package/README.md +1 -2
- package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
- package/bun.lock +26 -26
- package/docs/architecture/security.md +20 -0
- package/docs/plugins.md +7 -9
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
- package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
- package/node_modules/@vellumai/twilio-client/package.json +18 -0
- package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
- package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
- package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
- package/openapi.yaml +565 -12
- package/package.json +6 -3
- package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
- package/src/__tests__/app-bundler.test.ts +170 -1
- package/src/__tests__/app-control-flow.test.ts +374 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
- package/src/__tests__/app-executors.test.ts +30 -43
- package/src/__tests__/approval-routes-http.test.ts +23 -6
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
- package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
- package/src/__tests__/assistant-event-hub.test.ts +109 -2
- package/src/__tests__/assistant-event.test.ts +10 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
- package/src/__tests__/background-shell-host-bash.test.ts +14 -15
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
- package/src/__tests__/btw-routes.test.ts +13 -4
- package/src/__tests__/call-controller.test.ts +49 -1
- package/src/__tests__/call-domain.test.ts +0 -2
- package/src/__tests__/call-routes-http.test.ts +0 -2
- package/src/__tests__/channel-readiness-service.test.ts +59 -1
- package/src/__tests__/checker.test.ts +3 -4
- package/src/__tests__/config-loader-backfill.test.ts +90 -155
- package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-set-platform-guard.test.ts +48 -4
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-lifecycle.test.ts +36 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-slash-commands.test.ts +0 -4
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
- package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
- package/src/__tests__/credentials-cli.test.ts +5 -12
- package/src/__tests__/cu-unified-flow.test.ts +185 -23
- package/src/__tests__/daemon-credential-client.test.ts +101 -19
- package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +718 -1
- package/src/__tests__/helpers/call-route-handler.ts +7 -1
- package/src/__tests__/host-app-control-proxy.test.ts +602 -0
- package/src/__tests__/host-app-control-routes.test.ts +263 -0
- package/src/__tests__/host-bash-proxy.test.ts +246 -47
- package/src/__tests__/host-bash-routes.test.ts +294 -0
- package/src/__tests__/host-browser-proxy.test.ts +24 -22
- package/src/__tests__/host-browser-routes.test.ts +39 -13
- package/src/__tests__/host-cu-proxy.test.ts +41 -52
- package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
- package/src/__tests__/host-file-edit-tool.test.ts +47 -1
- package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
- package/src/__tests__/host-file-proxy.test.ts +37 -43
- package/src/__tests__/host-file-read-tool.test.ts +17 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
- package/src/__tests__/host-file-write-tool.test.ts +42 -1
- package/src/__tests__/host-proxy-base.test.ts +312 -0
- package/src/__tests__/host-shell-tool.test.ts +22 -4
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
- package/src/__tests__/host-transfer-proxy.test.ts +121 -22
- package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +29 -0
- package/src/__tests__/identity-routes.test.ts +103 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
- package/src/__tests__/inline-command-runner.test.ts +0 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
- package/src/__tests__/integration-status.test.ts +85 -5
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/mcp-auth-routes.test.ts +197 -0
- package/src/__tests__/mcp-cli.test.ts +338 -2
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
- package/src/__tests__/migration-import-commit-http.test.ts +108 -2
- package/src/__tests__/mock-gateway-ipc.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +0 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
- package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +97 -0
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/retry-backoff.test.ts +87 -0
- package/src/__tests__/runtime-events-sse.test.ts +10 -6
- package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
- package/src/__tests__/schedule-retry.test.ts +715 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +43 -41
- package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
- package/src/__tests__/skill-load-inline-command.test.ts +0 -51
- package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +9 -14
- package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-config.test.ts +0 -1
- package/src/__tests__/test-preload.ts +8 -0
- package/src/__tests__/tool-approval-handler.test.ts +3 -4
- package/src/__tests__/tool-audit-listener.test.ts +48 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +3 -5
- package/src/__tests__/twilio-validation.test.ts +93 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
- package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
- package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
- package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
- package/src/backup/__tests__/paths.test.ts +0 -22
- package/src/backup/__tests__/restore.test.ts +51 -151
- package/src/backup/paths.ts +2 -18
- package/src/backup/restore.ts +107 -231
- package/src/bundler/app-bundler.ts +51 -3
- package/src/calls/relay-server.ts +4 -44
- package/src/calls/twilio-config.ts +2 -17
- package/src/calls/twilio-rest.ts +33 -105
- package/src/calls/twilio-routes.ts +11 -12
- package/src/channels/types.ts +8 -7
- package/src/cli/commands/__tests__/backup.test.ts +6 -277
- package/src/cli/commands/__tests__/gateway.test.ts +288 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
- package/src/cli/commands/backup.ts +6 -331
- package/src/cli/commands/clients.ts +36 -37
- package/src/cli/commands/contacts.ts +73 -0
- package/src/cli/commands/conversations.ts +2 -5
- package/src/cli/commands/credentials.ts +15 -7
- package/src/cli/commands/domain.ts +66 -15
- package/src/cli/commands/gateway.ts +183 -0
- package/src/cli/commands/keys.ts +9 -6
- package/src/cli/commands/mcp.ts +116 -156
- package/src/cli/commands/memory-v2.ts +296 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
- package/src/cli/commands/platform/disconnect.ts +5 -4
- package/src/cli/commands/platform/index.ts +0 -18
- package/src/cli/lib/daemon-credential-client.ts +110 -28
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +67 -10
- package/src/config/bundled-skills/acp/SKILL.md +6 -0
- package/src/config/bundled-skills/acp/TOOLS.json +1 -22
- package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
- package/src/config/bundled-skills/app-control/SKILL.md +75 -0
- package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
- package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
- package/src/config/bundled-skills/document/TOOLS.json +0 -8
- package/src/config/bundled-skills/followups/TOOLS.json +0 -12
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
- package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
- package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
- package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
- package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
- package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
- package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
- package/src/config/bundled-skills/settings/SKILL.md +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +0 -12
- package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
- package/src/config/bundled-skills/subagent/SKILL.md +6 -2
- package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
- package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
- package/src/config/bundled-tool-registry.ts +21 -0
- package/src/config/env-registry.ts +0 -2
- package/src/config/env.ts +19 -12
- package/src/config/feature-flag-registry.json +21 -133
- package/src/config/loader.ts +73 -99
- package/src/config/sanitize-for-transfer.ts +2 -0
- package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
- package/src/config/schemas/calls.ts +0 -9
- package/src/config/schemas/heartbeat.ts +63 -0
- package/src/config/schemas/ingress.ts +10 -6
- package/src/config/schemas/llm.ts +5 -10
- package/src/config/schemas/memory-lifecycle.ts +77 -24
- package/src/config/schemas/memory-v2.ts +48 -4
- package/src/config/schemas/platform.ts +6 -0
- package/src/config/schemas/services.ts +1 -15
- package/src/config/schemas/skills.ts +0 -6
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/contacts/contact-store.ts +0 -30
- package/src/contacts/contacts-write.ts +0 -27
- package/src/context/window-manager.ts +1 -2
- package/src/credential-execution/feature-gates.ts +10 -10
- package/src/credential-execution/process-manager.ts +12 -41
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
- package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
- package/src/daemon/config-watcher.ts +4 -3
- package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
- package/src/daemon/conversation-agent-loop.ts +32 -28
- package/src/daemon/conversation-lifecycle.ts +8 -1
- package/src/daemon/conversation-process.ts +16 -11
- package/src/daemon/conversation-runtime-assembly.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +125 -4
- package/src/daemon/conversation-tool-setup.ts +16 -55
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/doordash-steps.ts +1 -1
- package/src/daemon/handlers/shared.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +293 -0
- package/src/daemon/host-bash-proxy.ts +84 -74
- package/src/daemon/host-browser-proxy.ts +67 -82
- package/src/daemon/host-cu-proxy.ts +81 -86
- package/src/daemon/host-file-proxy.ts +93 -69
- package/src/daemon/host-proxy-base.ts +294 -0
- package/src/daemon/host-proxy-preactivation.ts +82 -0
- package/src/daemon/host-transfer-proxy.ts +247 -129
- package/src/daemon/lifecycle.ts +115 -117
- package/src/daemon/message-protocol.ts +3 -8
- package/src/daemon/message-types/contacts.ts +23 -1
- package/src/daemon/message-types/conversations.ts +11 -8
- package/src/daemon/message-types/host-app-control.ts +150 -0
- package/src/daemon/message-types/host-bash.ts +4 -0
- package/src/daemon/message-types/host-cu.ts +2 -0
- package/src/daemon/message-types/host-file.ts +4 -0
- package/src/daemon/message-types/host-transfer.ts +3 -0
- package/src/daemon/message-types/schedules.ts +8 -3
- package/src/daemon/message-types/skills.ts +2 -2
- package/src/daemon/process-message.ts +18 -1
- package/src/daemon/shutdown-handlers.ts +0 -3
- package/src/daemon/tool-setup-types.ts +51 -0
- package/src/daemon/tool-side-effects.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -1
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
- package/src/heartbeat/heartbeat-run-store.ts +236 -0
- package/src/heartbeat/heartbeat-service.ts +280 -49
- package/src/home/__tests__/post-connect-feed.test.ts +99 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
- package/src/home/__tests__/suggested-prompts.test.ts +89 -0
- package/src/home/post-connect-feed.ts +68 -0
- package/src/home/relationship-state-writer.ts +17 -92
- package/src/home/suggested-prompts.ts +46 -10
- package/src/inbound/public-ingress-urls.ts +32 -34
- package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
- package/src/ipc/assistant-server.ts +14 -1
- package/src/ipc/cli-client.ts +32 -1
- package/src/live-voice/live-voice-metrics.ts +10 -10
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
- package/src/mcp/mcp-auth-orchestrator.ts +213 -0
- package/src/mcp/mcp-auth-state.ts +133 -0
- package/src/mcp/mcp-oauth-provider.ts +19 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
- package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
- package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
- package/src/memory/anisotropy.test.ts +247 -0
- package/src/memory/anisotropy.ts +443 -0
- package/src/memory/auto-analysis-constants.ts +17 -0
- package/src/memory/auto-analysis-guard.ts +5 -15
- package/src/memory/canonical-guardian-store.ts +7 -7
- package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
- package/src/memory/context-search/agent-protocol.ts +6 -6
- package/src/memory/context-search/agent-runner.ts +32 -7
- package/src/memory/context-search/sources/memory-v2.ts +17 -5
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-key-store.ts +2 -15
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-backend.ts +9 -21
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
- package/src/memory/graph/conversation-graph-memory.ts +1 -24
- package/src/memory/graph/graph-search.ts +8 -0
- package/src/memory/graph/retriever.ts +28 -0
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
- package/src/memory/jobs/embed-concept-page.ts +28 -2
- package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
- package/src/memory/jobs-store.ts +66 -22
- package/src/memory/jobs-worker.ts +112 -63
- package/src/memory/memory-v2-activation-log-store.ts +1 -1
- package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
- package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/pkb/pkb-search.ts +7 -0
- package/src/memory/qdrant-client.ts +50 -20
- package/src/memory/schema/infrastructure.ts +15 -0
- package/src/memory/search/semantic.ts +7 -0
- package/src/memory/sparse-tokenize.ts +49 -0
- package/src/memory/v2/__tests__/activation.test.ts +77 -95
- package/src/memory/v2/__tests__/injection.test.ts +43 -21
- package/src/memory/v2/__tests__/sim.test.ts +166 -6
- package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
- package/src/memory/v2/__tests__/static-context.test.ts +0 -1
- package/src/memory/v2/activation.ts +69 -88
- package/src/memory/v2/consolidation-job.ts +3 -5
- package/src/memory/v2/constants.ts +7 -0
- package/src/memory/v2/injection.ts +86 -53
- package/src/memory/v2/prompts/consolidation.ts +312 -91
- package/src/memory/v2/qdrant.ts +99 -1
- package/src/memory/v2/sim.ts +126 -16
- package/src/memory/v2/skill-qdrant.ts +12 -3
- package/src/memory/v2/skill-store.ts +16 -1
- package/src/memory/v2/sparse-bm25.ts +245 -0
- package/src/memory/v2/static-context.ts +6 -5
- package/src/messaging/providers/gmail/types.ts +0 -49
- package/src/messaging/providers/slack/adapter.ts +1 -31
- package/src/messaging/providers/slack/types.ts +0 -32
- package/src/notifications/README.md +10 -10
- package/src/notifications/broadcaster.ts +1 -1
- package/src/notifications/guardian-question-mode.ts +5 -5
- package/src/oauth/connect-orchestrator.ts +4 -0
- package/src/oauth/credential-token-resolver.ts +1 -3
- package/src/oauth/manual-token-connection.ts +0 -4
- package/src/outbound-proxy/index.ts +1 -37
- package/src/outbound-proxy/logging.ts +1 -1
- package/src/outbound-proxy/policy.ts +6 -5
- package/src/outbound-proxy/router.ts +2 -1
- package/src/permissions/approval-policy.test.ts +6 -275
- package/src/permissions/approval-policy.ts +0 -51
- package/src/permissions/checker.test.ts +0 -1
- package/src/permissions/checker.ts +3 -17
- package/src/permissions/gateway-threshold-reader.ts +2 -0
- package/src/permissions/prompter.ts +34 -1
- package/src/permissions/secret-prompter.ts +6 -2
- package/src/prompts/bootstrap-cleanup.ts +27 -0
- package/src/prompts/system-prompt.ts +3 -18
- package/src/prompts/templates/SOUL.md +13 -1
- package/src/providers/speech-to-text/provider-catalog.ts +7 -8
- package/src/runtime/assistant-event-hub.ts +118 -96
- package/src/runtime/assistant-event.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
- package/src/runtime/auth/middleware.ts +0 -96
- package/src/runtime/auth/route-policy.ts +19 -0
- package/src/runtime/btw-sidechain.ts +2 -3
- package/src/runtime/channel-invite-transport.ts +2 -48
- package/src/runtime/channel-invite-transports/email.ts +1 -1
- package/src/runtime/channel-invite-transports/slack.ts +1 -1
- package/src/runtime/channel-invite-transports/telegram.ts +1 -1
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
- package/src/runtime/channel-invite-types.ts +54 -0
- package/src/runtime/channel-readiness-service.ts +32 -13
- package/src/runtime/http-server.ts +3 -329
- package/src/runtime/http-types.ts +0 -5
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
- package/src/runtime/migrations/migration-transport.ts +7 -7
- package/src/runtime/migrations/vbundle-builder.ts +327 -60
- package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
- package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
- package/src/runtime/migrations/vbundle-importer.ts +245 -68
- package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
- package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
- package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
- package/src/runtime/migrations/vbundle-validator.ts +114 -0
- package/src/runtime/pending-interactions.ts +35 -9
- package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
- package/src/runtime/routes/approval-interception-types.ts +13 -0
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
- package/src/runtime/routes/backup-routes.ts +15 -38
- package/src/runtime/routes/btw-routes.ts +14 -37
- package/src/runtime/routes/client-routes.ts +1 -0
- package/src/runtime/routes/contact-prompt-routes.ts +183 -0
- package/src/runtime/routes/conversation-query-routes.ts +36 -1
- package/src/runtime/routes/conversation-routes.ts +30 -13
- package/src/runtime/routes/document-pdf-renderer.ts +165 -0
- package/src/runtime/routes/documents-routes.ts +30 -0
- package/src/runtime/routes/errors.ts +19 -4
- package/src/runtime/routes/events-routes.ts +12 -6
- package/src/runtime/routes/gateway-log-routes.ts +79 -0
- package/src/runtime/routes/guardian-approval-interception.ts +2 -8
- package/src/runtime/routes/heartbeat-routes.ts +103 -38
- package/src/runtime/routes/host-app-control-routes.ts +134 -0
- package/src/runtime/routes/host-bash-routes.ts +36 -6
- package/src/runtime/routes/host-browser-routes.ts +108 -13
- package/src/runtime/routes/host-cu-routes.ts +44 -14
- package/src/runtime/routes/host-file-routes.ts +33 -10
- package/src/runtime/routes/host-transfer-routes.ts +64 -24
- package/src/runtime/routes/http-adapter.ts +1 -0
- package/src/runtime/routes/identity-intro-cache.ts +30 -0
- package/src/runtime/routes/identity-routes.ts +15 -43
- package/src/runtime/routes/inbound-message-handler.ts +1 -9
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/mcp-auth-routes.ts +132 -0
- package/src/runtime/routes/memory-item-routes.ts +10 -12
- package/src/runtime/routes/memory-v2-routes.ts +441 -1
- package/src/runtime/routes/migration-routes.ts +96 -0
- package/src/runtime/routes/schedule-routes.ts +7 -0
- package/src/runtime/verification-templates.ts +4 -7
- package/src/schedule/integration-status.ts +66 -2
- package/src/schedule/recurrence-engine.ts +4 -1
- package/src/schedule/retry-backoff.ts +18 -0
- package/src/schedule/retry-policy.ts +82 -0
- package/src/schedule/schedule-recovery.ts +64 -0
- package/src/schedule/schedule-store.ts +106 -2
- package/src/schedule/scheduler-types.ts +25 -0
- package/src/schedule/scheduler.ts +63 -38
- package/src/security/oauth-callback-registry.ts +8 -0
- package/src/sequence/analytics.ts +5 -5
- package/src/sequence/engine.ts +1 -1
- package/src/skills/catalog-files.ts +2 -8
- package/src/skills/include-graph.ts +5 -5
- package/src/skills/remote-skill-policy.ts +5 -5
- package/src/skills/skill-file-provider.ts +1 -1
- package/src/skills/skill-file-types.ts +13 -0
- package/src/skills/skillssh-audit-types.ts +28 -0
- package/src/skills/skillssh-registry.ts +8 -21
- package/src/telemetry/types.ts +2 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
- package/src/tools/apps/executors.ts +56 -69
- package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
- package/src/tools/browser/cdp-client/factory.ts +23 -24
- package/src/tools/browser/cdp-client/index.ts +1 -14
- package/src/tools/computer-use/definitions.ts +42 -20
- package/src/tools/executor.ts +2 -0
- package/src/tools/host-filesystem/edit.ts +26 -0
- package/src/tools/host-filesystem/read.ts +26 -0
- package/src/tools/host-filesystem/transfer.ts +31 -1
- package/src/tools/host-filesystem/write.ts +26 -0
- package/src/tools/host-terminal/host-shell.ts +58 -0
- package/src/tools/schedule/create.ts +6 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/schedule/update.ts +10 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
- package/src/tools/shared/filesystem/path-policy.ts +25 -1
- package/src/tools/skills/load.ts +0 -32
- package/src/tools/tool-approval-handler.ts +1 -5
- package/src/tools/types.ts +4 -0
- package/src/usage/pricing.ts +1 -1
- package/src/workspace/hatched-date.ts +86 -0
- package/src/workspace/migrations/003-seed-device-id.ts +1 -1
- package/src/workspace/migrations/006-services-config.ts +8 -5
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
- package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
- package/src/workspace/migrations/utils.ts +21 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
- package/src/__tests__/twilio-rest.test.ts +0 -34
- package/src/backup/__tests__/backup-key.test.ts +0 -152
- package/src/backup/__tests__/backup-worker.test.ts +0 -782
- package/src/backup/__tests__/offsite-writer.test.ts +0 -641
- package/src/backup/__tests__/stream-crypt.test.ts +0 -228
- package/src/backup/backup-key.ts +0 -137
- package/src/backup/backup-worker.ts +0 -472
- package/src/backup/offsite-writer.ts +0 -222
- package/src/backup/stream-crypt.ts +0 -263
- package/src/daemon/message-types/pairing.ts +0 -58
- package/src/outbound-proxy/config.ts +0 -20
- package/src/outbound-proxy/health.ts +0 -18
- package/src/outbound-proxy/types.ts +0 -150
- package/src/runtime/capability-tokens.ts +0 -190
- package/src/signals/mcp-reload.ts +0 -18
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../util/logger.js", () => ({
|
|
4
|
+
getLogger: () =>
|
|
5
|
+
new Proxy({} as Record<string, unknown>, {
|
|
6
|
+
get: () => () => {},
|
|
7
|
+
}),
|
|
8
|
+
truncateForLog: (value: string) => value,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const mockEmitFeedEvent = mock(() => Promise.resolve());
|
|
12
|
+
mock.module("../home/emit-feed-event.js", () => ({
|
|
13
|
+
emitFeedEvent: mockEmitFeedEvent,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { getDb } from "../memory/db-connection.js";
|
|
17
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
18
|
+
import { applyRetryDecision, decideRetry } from "../schedule/retry-policy.js";
|
|
19
|
+
import { recoverStaleSchedules } from "../schedule/schedule-recovery.js";
|
|
20
|
+
import {
|
|
21
|
+
completeScheduleRun,
|
|
22
|
+
createSchedule,
|
|
23
|
+
createScheduleRun,
|
|
24
|
+
findStaleInFlightJobs,
|
|
25
|
+
getSchedule,
|
|
26
|
+
getScheduleRuns,
|
|
27
|
+
resetRetryCount,
|
|
28
|
+
scheduleRetry,
|
|
29
|
+
} from "../schedule/schedule-store.js";
|
|
30
|
+
import type { SchedulerHandle } from "../schedule/scheduler.js";
|
|
31
|
+
import { startScheduler } from "../schedule/scheduler.js";
|
|
32
|
+
|
|
33
|
+
initializeDb();
|
|
34
|
+
|
|
35
|
+
/** Access the underlying bun:sqlite Database for raw parameterized queries. */
|
|
36
|
+
function getRawDb(): import("bun:sqlite").Database {
|
|
37
|
+
return (getDb() as unknown as { $client: import("bun:sqlite").Database })
|
|
38
|
+
.$client;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Force a schedule to be due by setting next_run_at in the past. */
|
|
42
|
+
function forceScheduleDue(scheduleId: string): void {
|
|
43
|
+
getRawDb().run("UPDATE cron_jobs SET next_run_at = ? WHERE id = ?", [
|
|
44
|
+
Date.now() - 1000,
|
|
45
|
+
scheduleId,
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Schedule retry store ──────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe("schedule retry store", () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
const db = getDb();
|
|
54
|
+
db.run("DELETE FROM cron_runs");
|
|
55
|
+
db.run("DELETE FROM cron_jobs");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("scheduleRetry reverts one-shot from firing to active with future nextRunAt", () => {
|
|
59
|
+
const schedule = createSchedule({
|
|
60
|
+
name: "One-shot retry",
|
|
61
|
+
expression: null,
|
|
62
|
+
nextRunAt: Date.now() - 1000,
|
|
63
|
+
message: "test",
|
|
64
|
+
syntax: "cron",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Simulate the claim step: transition to "firing"
|
|
68
|
+
getRawDb().run("UPDATE cron_jobs SET status = 'firing' WHERE id = ?", [
|
|
69
|
+
schedule.id,
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const futureTime = Date.now() + 120_000;
|
|
73
|
+
scheduleRetry(schedule.id, futureTime);
|
|
74
|
+
|
|
75
|
+
const updated = getSchedule(schedule.id)!;
|
|
76
|
+
expect(updated.status).toBe("active");
|
|
77
|
+
expect(updated.nextRunAt).toBe(futureTime);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("scheduleRetry sets nextRunAt for recurring schedule without changing status", () => {
|
|
81
|
+
const schedule = createSchedule({
|
|
82
|
+
name: "Recurring retry",
|
|
83
|
+
cronExpression: "0 * * * *",
|
|
84
|
+
message: "test",
|
|
85
|
+
syntax: "cron",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const originalStatus = getSchedule(schedule.id)!.status;
|
|
89
|
+
const futureTime = Date.now() + 120_000;
|
|
90
|
+
scheduleRetry(schedule.id, futureTime);
|
|
91
|
+
|
|
92
|
+
const updated = getSchedule(schedule.id)!;
|
|
93
|
+
expect(updated.nextRunAt).toBe(futureTime);
|
|
94
|
+
// Status unchanged for recurring (it's "active", not "firing")
|
|
95
|
+
expect(updated.status).toBe(originalStatus);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("resetRetryCount resets retryCount to 0 after error", () => {
|
|
99
|
+
const schedule = createSchedule({
|
|
100
|
+
name: "Reset test",
|
|
101
|
+
cronExpression: "0 * * * *",
|
|
102
|
+
message: "test",
|
|
103
|
+
syntax: "cron",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Simulate an error run which increments retryCount
|
|
107
|
+
const runId = createScheduleRun(schedule.id, "test-conv");
|
|
108
|
+
completeScheduleRun(runId, { status: "error", error: "boom" });
|
|
109
|
+
|
|
110
|
+
const afterError = getSchedule(schedule.id)!;
|
|
111
|
+
expect(afterError.retryCount).toBe(1);
|
|
112
|
+
|
|
113
|
+
resetRetryCount(schedule.id);
|
|
114
|
+
|
|
115
|
+
const afterReset = getSchedule(schedule.id)!;
|
|
116
|
+
expect(afterReset.retryCount).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("createSchedule with custom maxRetries and retryBackoffMs", () => {
|
|
120
|
+
const schedule = createSchedule({
|
|
121
|
+
name: "Custom retry config",
|
|
122
|
+
cronExpression: "0 * * * *",
|
|
123
|
+
message: "test",
|
|
124
|
+
syntax: "cron",
|
|
125
|
+
maxRetries: 5,
|
|
126
|
+
retryBackoffMs: 30000,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const retrieved = getSchedule(schedule.id)!;
|
|
130
|
+
expect(retrieved.maxRetries).toBe(5);
|
|
131
|
+
expect(retrieved.retryBackoffMs).toBe(30000);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("createSchedule with defaults has maxRetries=3 and retryBackoffMs=60000", () => {
|
|
135
|
+
const schedule = createSchedule({
|
|
136
|
+
name: "Default retry config",
|
|
137
|
+
cronExpression: "0 * * * *",
|
|
138
|
+
message: "test",
|
|
139
|
+
syntax: "cron",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const retrieved = getSchedule(schedule.id)!;
|
|
143
|
+
expect(retrieved.maxRetries).toBe(3);
|
|
144
|
+
expect(retrieved.retryBackoffMs).toBe(60000);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── Scheduler retry integration ───────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe("scheduler retry integration", () => {
|
|
151
|
+
let scheduler: SchedulerHandle;
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
const db = getDb();
|
|
155
|
+
db.run("DELETE FROM cron_runs");
|
|
156
|
+
db.run("DELETE FROM cron_jobs");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
scheduler?.stop();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("execute mode failure retries with backoff", async () => {
|
|
164
|
+
const schedule = createSchedule({
|
|
165
|
+
name: "Failing hourly",
|
|
166
|
+
cronExpression: "0 * * * *",
|
|
167
|
+
message: "do work",
|
|
168
|
+
syntax: "cron",
|
|
169
|
+
maxRetries: 2,
|
|
170
|
+
retryBackoffMs: 60000,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
forceScheduleDue(schedule.id);
|
|
174
|
+
|
|
175
|
+
const processMessage = async () => {
|
|
176
|
+
throw new Error("execute failed");
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
180
|
+
// Wait for the initial tick to complete
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
182
|
+
scheduler.stop();
|
|
183
|
+
|
|
184
|
+
const updated = getSchedule(schedule.id)!;
|
|
185
|
+
// completeScheduleRun incremented retryCount from 0 to 1
|
|
186
|
+
expect(updated.retryCount).toBe(1);
|
|
187
|
+
// nextRunAt should be backoff time (much sooner than next hour)
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
// Backoff should be roughly 60s from now, certainly not an hour
|
|
190
|
+
expect(updated.nextRunAt).toBeGreaterThan(now - 5000);
|
|
191
|
+
expect(updated.nextRunAt).toBeLessThan(now + 5 * 60 * 1000);
|
|
192
|
+
|
|
193
|
+
// A schedule run should be recorded with error status
|
|
194
|
+
const runs = getScheduleRuns(schedule.id);
|
|
195
|
+
expect(runs.length).toBeGreaterThanOrEqual(1);
|
|
196
|
+
expect(runs[0].status).toBe("error");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("execute mode retry success resets retryCount", async () => {
|
|
200
|
+
const schedule = createSchedule({
|
|
201
|
+
name: "Flaky hourly",
|
|
202
|
+
cronExpression: "0 * * * *",
|
|
203
|
+
message: "do work",
|
|
204
|
+
syntax: "cron",
|
|
205
|
+
maxRetries: 2,
|
|
206
|
+
retryBackoffMs: 1000,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// First run: failure
|
|
210
|
+
forceScheduleDue(schedule.id);
|
|
211
|
+
let callCount = 0;
|
|
212
|
+
const processMessage = async () => {
|
|
213
|
+
callCount++;
|
|
214
|
+
if (callCount === 1) throw new Error("transient failure");
|
|
215
|
+
// Second call succeeds
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
219
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
220
|
+
|
|
221
|
+
const afterFailure = getSchedule(schedule.id)!;
|
|
222
|
+
expect(afterFailure.retryCount).toBe(1);
|
|
223
|
+
|
|
224
|
+
// Second run: success
|
|
225
|
+
forceScheduleDue(schedule.id);
|
|
226
|
+
await scheduler.runOnce();
|
|
227
|
+
|
|
228
|
+
const afterSuccess = getSchedule(schedule.id)!;
|
|
229
|
+
expect(afterSuccess.retryCount).toBe(0);
|
|
230
|
+
expect(afterSuccess.lastStatus).toBe("ok");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("execute mode exhaustion resets retryCount and resumes cadence", async () => {
|
|
234
|
+
const schedule = createSchedule({
|
|
235
|
+
name: "Exhaust hourly",
|
|
236
|
+
cronExpression: "0 * * * *",
|
|
237
|
+
message: "do work",
|
|
238
|
+
syntax: "cron",
|
|
239
|
+
maxRetries: 1,
|
|
240
|
+
retryBackoffMs: 1000,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const processMessage = async () => {
|
|
244
|
+
throw new Error("always fails");
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// First failure: retryCount goes from 0 to 1, schedule retry with backoff
|
|
248
|
+
forceScheduleDue(schedule.id);
|
|
249
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
251
|
+
|
|
252
|
+
const afterFirst = getSchedule(schedule.id)!;
|
|
253
|
+
expect(afterFirst.retryCount).toBe(1);
|
|
254
|
+
// nextRunAt should be backoff time (much sooner than next hour)
|
|
255
|
+
const now1 = Date.now();
|
|
256
|
+
expect(afterFirst.nextRunAt).toBeLessThan(now1 + 5 * 60 * 1000);
|
|
257
|
+
|
|
258
|
+
// Second failure: retryCount goes from 1 to 2, exceeds maxRetries (1)
|
|
259
|
+
// so exhaust path resets retryCount to 0 and nextRunAt goes to normal cron cadence
|
|
260
|
+
forceScheduleDue(schedule.id);
|
|
261
|
+
await scheduler.runOnce();
|
|
262
|
+
|
|
263
|
+
const afterSecond = getSchedule(schedule.id)!;
|
|
264
|
+
// Exhaust path for recurring: resetRetryCount -> 0
|
|
265
|
+
expect(afterSecond.retryCount).toBe(0);
|
|
266
|
+
// Schedule should still be enabled (not cancelled for recurring)
|
|
267
|
+
expect(afterSecond.enabled).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("one-shot failure retries with backoff", async () => {
|
|
271
|
+
const schedule = createSchedule({
|
|
272
|
+
name: "One-shot failing",
|
|
273
|
+
expression: null,
|
|
274
|
+
nextRunAt: Date.now() - 1000,
|
|
275
|
+
message: "do work",
|
|
276
|
+
syntax: "cron",
|
|
277
|
+
maxRetries: 2,
|
|
278
|
+
retryBackoffMs: 60000,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const processMessage = async () => {
|
|
282
|
+
throw new Error("one-shot failed");
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
286
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
287
|
+
scheduler.stop();
|
|
288
|
+
|
|
289
|
+
const updated = getSchedule(schedule.id)!;
|
|
290
|
+
// One-shot should revert from "firing" back to "active" for retry
|
|
291
|
+
expect(updated.status).toBe("active");
|
|
292
|
+
// nextRunAt should be in the future (backoff delay)
|
|
293
|
+
expect(updated.nextRunAt).toBeGreaterThan(Date.now() - 5000);
|
|
294
|
+
expect(updated.retryCount).toBe(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("one-shot exhaustion permanently cancels", async () => {
|
|
298
|
+
const schedule = createSchedule({
|
|
299
|
+
name: "One-shot exhaust",
|
|
300
|
+
expression: null,
|
|
301
|
+
nextRunAt: Date.now() - 1000,
|
|
302
|
+
message: "do work",
|
|
303
|
+
syntax: "cron",
|
|
304
|
+
maxRetries: 1,
|
|
305
|
+
retryBackoffMs: 1000,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const processMessage = async () => {
|
|
309
|
+
throw new Error("always fails");
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// First failure: retries
|
|
313
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
315
|
+
|
|
316
|
+
const afterFirst = getSchedule(schedule.id)!;
|
|
317
|
+
expect(afterFirst.retryCount).toBe(1);
|
|
318
|
+
expect(afterFirst.status).toBe("active"); // reverted for retry
|
|
319
|
+
|
|
320
|
+
// Second failure: exhaust -> permanently cancel
|
|
321
|
+
forceScheduleDue(schedule.id);
|
|
322
|
+
await scheduler.runOnce();
|
|
323
|
+
|
|
324
|
+
const afterSecond = getSchedule(schedule.id)!;
|
|
325
|
+
expect(afterSecond.status).toBe("cancelled");
|
|
326
|
+
expect(afterSecond.enabled).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("notify one-shot failure creates schedule run", async () => {
|
|
330
|
+
const schedule = createSchedule({
|
|
331
|
+
name: "Notify one-shot",
|
|
332
|
+
expression: null,
|
|
333
|
+
nextRunAt: Date.now() - 1000,
|
|
334
|
+
message: "reminder",
|
|
335
|
+
syntax: "cron",
|
|
336
|
+
mode: "notify",
|
|
337
|
+
maxRetries: 2,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const notifyScheduleOneShot = async () => {
|
|
341
|
+
throw new Error("notify failed");
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
scheduler = startScheduler(async () => {}, notifyScheduleOneShot);
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
346
|
+
scheduler.stop();
|
|
347
|
+
|
|
348
|
+
const runs = getScheduleRuns(schedule.id);
|
|
349
|
+
expect(runs.length).toBeGreaterThanOrEqual(1);
|
|
350
|
+
expect(runs[0].status).toBe("error");
|
|
351
|
+
|
|
352
|
+
const updated = getSchedule(schedule.id)!;
|
|
353
|
+
expect(updated.retryCount).toBe(1);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("recurring schedule retry blocks normal cadence", async () => {
|
|
357
|
+
const schedule = createSchedule({
|
|
358
|
+
name: "Cadence blocking",
|
|
359
|
+
cronExpression: "0 * * * *",
|
|
360
|
+
message: "do work",
|
|
361
|
+
syntax: "cron",
|
|
362
|
+
maxRetries: 2,
|
|
363
|
+
retryBackoffMs: 60000,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const processMessage = async () => {
|
|
367
|
+
throw new Error("failed");
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
forceScheduleDue(schedule.id);
|
|
371
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
372
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
373
|
+
scheduler.stop();
|
|
374
|
+
|
|
375
|
+
const updated = getSchedule(schedule.id)!;
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
// nextRunAt should be backoff delay (~60s), NOT the next hourly occurrence (~60 min)
|
|
378
|
+
// Backoff at attempt 0 with base 60000 is ~60s (with jitter)
|
|
379
|
+
expect(updated.nextRunAt).toBeLessThan(now + 5 * 60 * 1000);
|
|
380
|
+
// Should be well before the next hour
|
|
381
|
+
const oneHourFromNow = now + 60 * 60 * 1000;
|
|
382
|
+
expect(updated.nextRunAt).toBeLessThan(oneHourFromNow - 30 * 60 * 1000);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ── Crash recovery ────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe("crash recovery", () => {
|
|
389
|
+
beforeEach(() => {
|
|
390
|
+
const db = getDb();
|
|
391
|
+
db.run("DELETE FROM cron_runs");
|
|
392
|
+
db.run("DELETE FROM cron_jobs");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("recovers stale firing one-shot", () => {
|
|
396
|
+
const schedule = createSchedule({
|
|
397
|
+
name: "Stale one-shot",
|
|
398
|
+
expression: null,
|
|
399
|
+
nextRunAt: Date.now() - 60_000,
|
|
400
|
+
message: "test",
|
|
401
|
+
syntax: "cron",
|
|
402
|
+
maxRetries: 3,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Simulate crash: force status to "firing" and set lastRunAt in the past
|
|
406
|
+
getRawDb().run(
|
|
407
|
+
"UPDATE cron_jobs SET status = 'firing', last_run_at = ? WHERE id = ?",
|
|
408
|
+
[Date.now() - 60_000, schedule.id],
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const recovered = recoverStaleSchedules();
|
|
412
|
+
expect(recovered).toBe(1);
|
|
413
|
+
|
|
414
|
+
// A schedule run should be created with error
|
|
415
|
+
const runs = getScheduleRuns(schedule.id);
|
|
416
|
+
expect(runs.length).toBeGreaterThanOrEqual(1);
|
|
417
|
+
expect(runs[0].status).toBe("error");
|
|
418
|
+
expect(runs[0].error).toContain("recovered on restart");
|
|
419
|
+
|
|
420
|
+
const updated = getSchedule(schedule.id)!;
|
|
421
|
+
// retryCount incremented by completeScheduleRun + retry applied
|
|
422
|
+
expect(updated.retryCount).toBe(1);
|
|
423
|
+
expect(updated.status).toBe("active"); // reverted for retry
|
|
424
|
+
expect(updated.nextRunAt).toBeGreaterThan(Date.now() - 5000);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("recovers stale running cron_run", () => {
|
|
428
|
+
const schedule = createSchedule({
|
|
429
|
+
name: "Stale run",
|
|
430
|
+
cronExpression: "0 * * * *",
|
|
431
|
+
message: "test",
|
|
432
|
+
syntax: "cron",
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Create a "running" schedule run to simulate crash
|
|
436
|
+
const runId = createScheduleRun(schedule.id, "stale-conv");
|
|
437
|
+
// The run is now in "running" status by default
|
|
438
|
+
|
|
439
|
+
const recovered = recoverStaleSchedules();
|
|
440
|
+
expect(recovered).toBe(1);
|
|
441
|
+
|
|
442
|
+
const runs = getScheduleRuns(schedule.id);
|
|
443
|
+
const recoveredRun = runs.find((r) => r.id === runId);
|
|
444
|
+
expect(recoveredRun).toBeDefined();
|
|
445
|
+
expect(recoveredRun!.status).toBe("error");
|
|
446
|
+
expect(recoveredRun!.error).toContain("recovered on restart");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("respects maxRetries on recovery - cancels exhausted one-shot", () => {
|
|
450
|
+
const schedule = createSchedule({
|
|
451
|
+
name: "Exhausted one-shot",
|
|
452
|
+
expression: null,
|
|
453
|
+
nextRunAt: Date.now() - 60_000,
|
|
454
|
+
message: "test",
|
|
455
|
+
syntax: "cron",
|
|
456
|
+
maxRetries: 3,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Set retryCount = maxRetries (3) and status = "firing"
|
|
460
|
+
getRawDb().run(
|
|
461
|
+
"UPDATE cron_jobs SET status = 'firing', retry_count = ?, last_run_at = ? WHERE id = ?",
|
|
462
|
+
[3, Date.now() - 60_000, schedule.id],
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
recoverStaleSchedules();
|
|
466
|
+
|
|
467
|
+
const updated = getSchedule(schedule.id)!;
|
|
468
|
+
expect(updated.status).toBe("cancelled");
|
|
469
|
+
expect(updated.enabled).toBe(false);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("idempotent - second call returns 0", () => {
|
|
473
|
+
const schedule = createSchedule({
|
|
474
|
+
name: "Idempotent test",
|
|
475
|
+
expression: null,
|
|
476
|
+
nextRunAt: Date.now() - 60_000,
|
|
477
|
+
message: "test",
|
|
478
|
+
syntax: "cron",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
getRawDb().run(
|
|
482
|
+
"UPDATE cron_jobs SET status = 'firing', last_run_at = ? WHERE id = ?",
|
|
483
|
+
[Date.now() - 60_000, schedule.id],
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const first = recoverStaleSchedules();
|
|
487
|
+
expect(first).toBe(1);
|
|
488
|
+
|
|
489
|
+
const second = recoverStaleSchedules();
|
|
490
|
+
expect(second).toBe(0);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("age threshold filters recent in-flight jobs", () => {
|
|
494
|
+
const schedule = createSchedule({
|
|
495
|
+
name: "Recent firing",
|
|
496
|
+
expression: null,
|
|
497
|
+
nextRunAt: Date.now() - 1000,
|
|
498
|
+
message: "test",
|
|
499
|
+
syntax: "cron",
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Force status to "firing" with a recent lastRunAt
|
|
503
|
+
getRawDb().run(
|
|
504
|
+
"UPDATE cron_jobs SET status = 'firing', last_run_at = ? WHERE id = ?",
|
|
505
|
+
[Date.now(), schedule.id],
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// With a 10-minute threshold, the job is too recent to be considered stale
|
|
509
|
+
const recentResults = findStaleInFlightJobs(10 * 60 * 1000);
|
|
510
|
+
expect(recentResults.length).toBe(0);
|
|
511
|
+
|
|
512
|
+
// With no threshold (0), the job is returned
|
|
513
|
+
const allResults = findStaleInFlightJobs(0);
|
|
514
|
+
expect(allResults.length).toBe(1);
|
|
515
|
+
expect(allResults[0].jobId).toBe(schedule.id);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// ── Scheduler-recovery equivalence ────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
describe("scheduler-recovery equivalence", () => {
|
|
522
|
+
let scheduler: SchedulerHandle;
|
|
523
|
+
|
|
524
|
+
beforeEach(() => {
|
|
525
|
+
const db = getDb();
|
|
526
|
+
db.run("DELETE FROM cron_runs");
|
|
527
|
+
db.run("DELETE FROM cron_jobs");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
afterEach(() => {
|
|
531
|
+
scheduler?.stop();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("same retry state for identical recurring job inputs", async () => {
|
|
535
|
+
// Create two identical recurring schedules
|
|
536
|
+
const scheduleA = createSchedule({
|
|
537
|
+
name: "Equiv A",
|
|
538
|
+
cronExpression: "0 * * * *",
|
|
539
|
+
message: "test",
|
|
540
|
+
syntax: "cron",
|
|
541
|
+
maxRetries: 2,
|
|
542
|
+
retryBackoffMs: 60000,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const scheduleB = createSchedule({
|
|
546
|
+
name: "Equiv B",
|
|
547
|
+
cronExpression: "0 * * * *",
|
|
548
|
+
message: "test",
|
|
549
|
+
syntax: "cron",
|
|
550
|
+
maxRetries: 2,
|
|
551
|
+
retryBackoffMs: 60000,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Schedule A: simulate scheduler failure path
|
|
555
|
+
forceScheduleDue(scheduleA.id);
|
|
556
|
+
const processMessage = async () => {
|
|
557
|
+
throw new Error("fail");
|
|
558
|
+
};
|
|
559
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
560
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
561
|
+
scheduler.stop();
|
|
562
|
+
|
|
563
|
+
// Schedule B: simulate crash recovery path
|
|
564
|
+
// First simulate the schedule being claimed (sets lastRunAt, advances nextRunAt)
|
|
565
|
+
forceScheduleDue(scheduleB.id);
|
|
566
|
+
getRawDb().run("UPDATE cron_jobs SET last_run_at = ? WHERE id = ?", [
|
|
567
|
+
Date.now(),
|
|
568
|
+
scheduleB.id,
|
|
569
|
+
]);
|
|
570
|
+
// Create a run and complete it with error (same as what scheduler does)
|
|
571
|
+
const runId = createScheduleRun(scheduleB.id, "recovery-conv");
|
|
572
|
+
completeScheduleRun(runId, {
|
|
573
|
+
status: "error",
|
|
574
|
+
error: "Process terminated during execution (recovered on restart)",
|
|
575
|
+
});
|
|
576
|
+
// Now run recovery's retry logic on the post-completeScheduleRun state
|
|
577
|
+
const jobB = getSchedule(scheduleB.id)!;
|
|
578
|
+
const decision = decideRetry(jobB);
|
|
579
|
+
const noopLogger = new Proxy({} as Record<string, unknown>, {
|
|
580
|
+
get: () => () => {},
|
|
581
|
+
});
|
|
582
|
+
applyRetryDecision({
|
|
583
|
+
job: jobB,
|
|
584
|
+
isOneShot: false,
|
|
585
|
+
errorMsg: "fail",
|
|
586
|
+
decision,
|
|
587
|
+
scheduleRetry,
|
|
588
|
+
failOneShotPermanently: () => {},
|
|
589
|
+
resetRetryCount,
|
|
590
|
+
emitAlert: () => {},
|
|
591
|
+
log: noopLogger as any,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const stateA = getSchedule(scheduleA.id)!;
|
|
595
|
+
const stateB = getSchedule(scheduleB.id)!;
|
|
596
|
+
|
|
597
|
+
// Both should have the same retryCount
|
|
598
|
+
expect(stateA.retryCount).toBe(stateB.retryCount);
|
|
599
|
+
expect(stateA.retryCount).toBe(1);
|
|
600
|
+
// Both should have nextRunAt in the future (retry with backoff)
|
|
601
|
+
expect(stateA.nextRunAt).toBeGreaterThan(Date.now() - 5000);
|
|
602
|
+
expect(stateB.nextRunAt).toBeGreaterThan(Date.now() - 5000);
|
|
603
|
+
// Both should still be active
|
|
604
|
+
expect(stateA.enabled).toBe(true);
|
|
605
|
+
expect(stateB.enabled).toBe(true);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("same exhaust state for identical one-shot inputs", async () => {
|
|
609
|
+
// Create two identical one-shot schedules at max retries
|
|
610
|
+
const scheduleA = createSchedule({
|
|
611
|
+
name: "Exhaust A",
|
|
612
|
+
expression: null,
|
|
613
|
+
nextRunAt: Date.now() - 1000,
|
|
614
|
+
message: "test",
|
|
615
|
+
syntax: "cron",
|
|
616
|
+
maxRetries: 1,
|
|
617
|
+
retryBackoffMs: 1000,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const scheduleB = createSchedule({
|
|
621
|
+
name: "Exhaust B",
|
|
622
|
+
expression: null,
|
|
623
|
+
nextRunAt: Date.now() - 1000,
|
|
624
|
+
message: "test",
|
|
625
|
+
syntax: "cron",
|
|
626
|
+
maxRetries: 1,
|
|
627
|
+
retryBackoffMs: 1000,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Schedule A: through scheduler — first failure retries, second exhausts
|
|
631
|
+
const processMessage = async () => {
|
|
632
|
+
throw new Error("always fails");
|
|
633
|
+
};
|
|
634
|
+
scheduler = startScheduler(processMessage, () => {});
|
|
635
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
636
|
+
|
|
637
|
+
// After first failure, A should be retrying
|
|
638
|
+
const afterFirstA = getSchedule(scheduleA.id)!;
|
|
639
|
+
expect(afterFirstA.retryCount).toBe(1);
|
|
640
|
+
expect(afterFirstA.status).toBe("active");
|
|
641
|
+
|
|
642
|
+
// Second failure exhausts A
|
|
643
|
+
forceScheduleDue(scheduleA.id);
|
|
644
|
+
await scheduler.runOnce();
|
|
645
|
+
scheduler.stop();
|
|
646
|
+
|
|
647
|
+
// Schedule B: through crash recovery path
|
|
648
|
+
// Simulate: claim -> fire -> error -> exhaust
|
|
649
|
+
// First failure
|
|
650
|
+
getRawDb().run(
|
|
651
|
+
"UPDATE cron_jobs SET status = 'firing', last_run_at = ? WHERE id = ?",
|
|
652
|
+
[Date.now() - 60_000, scheduleB.id],
|
|
653
|
+
);
|
|
654
|
+
const runB1 = createScheduleRun(scheduleB.id, "recovery-conv-1");
|
|
655
|
+
completeScheduleRun(runB1, { status: "error", error: "fail" });
|
|
656
|
+
// decideRetry + apply for first failure
|
|
657
|
+
const jobB1 = getSchedule(scheduleB.id)!;
|
|
658
|
+
const decision1 = decideRetry(jobB1);
|
|
659
|
+
const noopLogger = new Proxy({} as Record<string, unknown>, {
|
|
660
|
+
get: () => () => {},
|
|
661
|
+
});
|
|
662
|
+
applyRetryDecision({
|
|
663
|
+
job: jobB1,
|
|
664
|
+
isOneShot: true,
|
|
665
|
+
errorMsg: "fail",
|
|
666
|
+
decision: decision1,
|
|
667
|
+
scheduleRetry,
|
|
668
|
+
failOneShotPermanently: (id: string) => {
|
|
669
|
+
getRawDb().run(
|
|
670
|
+
"UPDATE cron_jobs SET status = 'cancelled', enabled = 0, last_status = 'error', updated_at = ? WHERE id = ? AND status = 'firing'",
|
|
671
|
+
[Date.now(), id],
|
|
672
|
+
);
|
|
673
|
+
},
|
|
674
|
+
resetRetryCount,
|
|
675
|
+
emitAlert: () => {},
|
|
676
|
+
log: noopLogger as any,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Second failure (simulate claim + fire again)
|
|
680
|
+
forceScheduleDue(scheduleB.id);
|
|
681
|
+
getRawDb().run(
|
|
682
|
+
"UPDATE cron_jobs SET status = 'firing', last_run_at = ? WHERE id = ?",
|
|
683
|
+
[Date.now() - 60_000, scheduleB.id],
|
|
684
|
+
);
|
|
685
|
+
const runB2 = createScheduleRun(scheduleB.id, "recovery-conv-2");
|
|
686
|
+
completeScheduleRun(runB2, { status: "error", error: "fail" });
|
|
687
|
+
const jobB2 = getSchedule(scheduleB.id)!;
|
|
688
|
+
const decision2 = decideRetry(jobB2);
|
|
689
|
+
applyRetryDecision({
|
|
690
|
+
job: jobB2,
|
|
691
|
+
isOneShot: true,
|
|
692
|
+
errorMsg: "fail",
|
|
693
|
+
decision: decision2,
|
|
694
|
+
scheduleRetry,
|
|
695
|
+
failOneShotPermanently: (id: string) => {
|
|
696
|
+
getRawDb().run(
|
|
697
|
+
"UPDATE cron_jobs SET status = 'cancelled', enabled = 0, last_status = 'error', updated_at = ? WHERE id = ? AND status = 'firing'",
|
|
698
|
+
[Date.now(), id],
|
|
699
|
+
);
|
|
700
|
+
},
|
|
701
|
+
resetRetryCount,
|
|
702
|
+
emitAlert: () => {},
|
|
703
|
+
log: noopLogger as any,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const stateA = getSchedule(scheduleA.id)!;
|
|
707
|
+
const stateB = getSchedule(scheduleB.id)!;
|
|
708
|
+
|
|
709
|
+
// Both should be cancelled and disabled
|
|
710
|
+
expect(stateA.status).toBe("cancelled");
|
|
711
|
+
expect(stateA.enabled).toBe(false);
|
|
712
|
+
expect(stateB.status).toBe("cancelled");
|
|
713
|
+
expect(stateB.enabled).toBe(false);
|
|
714
|
+
});
|
|
715
|
+
});
|