@vellumai/assistant 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +32 -49
- package/Dockerfile +1 -0
- package/README.md +1 -2
- package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
- package/bun.lock +26 -26
- package/docs/architecture/security.md +20 -0
- package/docs/plugins.md +7 -9
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
- package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
- package/node_modules/@vellumai/twilio-client/package.json +18 -0
- package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
- package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
- package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
- package/openapi.yaml +565 -12
- package/package.json +6 -3
- package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
- package/src/__tests__/app-bundler.test.ts +170 -1
- package/src/__tests__/app-control-flow.test.ts +374 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
- package/src/__tests__/app-executors.test.ts +30 -43
- package/src/__tests__/approval-routes-http.test.ts +23 -6
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
- package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
- package/src/__tests__/assistant-event-hub.test.ts +109 -2
- package/src/__tests__/assistant-event.test.ts +10 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
- package/src/__tests__/background-shell-host-bash.test.ts +14 -15
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
- package/src/__tests__/btw-routes.test.ts +13 -4
- package/src/__tests__/call-controller.test.ts +49 -1
- package/src/__tests__/call-domain.test.ts +0 -2
- package/src/__tests__/call-routes-http.test.ts +0 -2
- package/src/__tests__/channel-readiness-service.test.ts +59 -1
- package/src/__tests__/checker.test.ts +3 -4
- package/src/__tests__/config-loader-backfill.test.ts +90 -155
- package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-set-platform-guard.test.ts +48 -4
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-lifecycle.test.ts +36 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-slash-commands.test.ts +0 -4
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
- package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
- package/src/__tests__/credentials-cli.test.ts +5 -12
- package/src/__tests__/cu-unified-flow.test.ts +185 -23
- package/src/__tests__/daemon-credential-client.test.ts +101 -19
- package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +718 -1
- package/src/__tests__/helpers/call-route-handler.ts +7 -1
- package/src/__tests__/host-app-control-proxy.test.ts +602 -0
- package/src/__tests__/host-app-control-routes.test.ts +263 -0
- package/src/__tests__/host-bash-proxy.test.ts +246 -47
- package/src/__tests__/host-bash-routes.test.ts +294 -0
- package/src/__tests__/host-browser-proxy.test.ts +24 -22
- package/src/__tests__/host-browser-routes.test.ts +39 -13
- package/src/__tests__/host-cu-proxy.test.ts +41 -52
- package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
- package/src/__tests__/host-file-edit-tool.test.ts +47 -1
- package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
- package/src/__tests__/host-file-proxy.test.ts +37 -43
- package/src/__tests__/host-file-read-tool.test.ts +17 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
- package/src/__tests__/host-file-write-tool.test.ts +42 -1
- package/src/__tests__/host-proxy-base.test.ts +312 -0
- package/src/__tests__/host-shell-tool.test.ts +22 -4
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
- package/src/__tests__/host-transfer-proxy.test.ts +121 -22
- package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +29 -0
- package/src/__tests__/identity-routes.test.ts +103 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
- package/src/__tests__/inline-command-runner.test.ts +0 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
- package/src/__tests__/integration-status.test.ts +85 -5
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/mcp-auth-routes.test.ts +197 -0
- package/src/__tests__/mcp-cli.test.ts +338 -2
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
- package/src/__tests__/migration-import-commit-http.test.ts +108 -2
- package/src/__tests__/mock-gateway-ipc.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +0 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
- package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +97 -0
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/retry-backoff.test.ts +87 -0
- package/src/__tests__/runtime-events-sse.test.ts +10 -6
- package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
- package/src/__tests__/schedule-retry.test.ts +715 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +43 -41
- package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
- package/src/__tests__/skill-load-inline-command.test.ts +0 -51
- package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +9 -14
- package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-config.test.ts +0 -1
- package/src/__tests__/test-preload.ts +8 -0
- package/src/__tests__/tool-approval-handler.test.ts +3 -4
- package/src/__tests__/tool-audit-listener.test.ts +48 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +3 -5
- package/src/__tests__/twilio-validation.test.ts +93 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
- package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
- package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
- package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
- package/src/backup/__tests__/paths.test.ts +0 -22
- package/src/backup/__tests__/restore.test.ts +51 -151
- package/src/backup/paths.ts +2 -18
- package/src/backup/restore.ts +107 -231
- package/src/bundler/app-bundler.ts +51 -3
- package/src/calls/relay-server.ts +4 -44
- package/src/calls/twilio-config.ts +2 -17
- package/src/calls/twilio-rest.ts +33 -105
- package/src/calls/twilio-routes.ts +11 -12
- package/src/channels/types.ts +8 -7
- package/src/cli/commands/__tests__/backup.test.ts +6 -277
- package/src/cli/commands/__tests__/gateway.test.ts +288 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
- package/src/cli/commands/backup.ts +6 -331
- package/src/cli/commands/clients.ts +36 -37
- package/src/cli/commands/contacts.ts +73 -0
- package/src/cli/commands/conversations.ts +2 -5
- package/src/cli/commands/credentials.ts +15 -7
- package/src/cli/commands/domain.ts +66 -15
- package/src/cli/commands/gateway.ts +183 -0
- package/src/cli/commands/keys.ts +9 -6
- package/src/cli/commands/mcp.ts +116 -156
- package/src/cli/commands/memory-v2.ts +296 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
- package/src/cli/commands/platform/disconnect.ts +5 -4
- package/src/cli/commands/platform/index.ts +0 -18
- package/src/cli/lib/daemon-credential-client.ts +110 -28
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +67 -10
- package/src/config/bundled-skills/acp/SKILL.md +6 -0
- package/src/config/bundled-skills/acp/TOOLS.json +1 -22
- package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
- package/src/config/bundled-skills/app-control/SKILL.md +75 -0
- package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
- package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
- package/src/config/bundled-skills/document/TOOLS.json +0 -8
- package/src/config/bundled-skills/followups/TOOLS.json +0 -12
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
- package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
- package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
- package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
- package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
- package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
- package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
- package/src/config/bundled-skills/settings/SKILL.md +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +0 -12
- package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
- package/src/config/bundled-skills/subagent/SKILL.md +6 -2
- package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
- package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
- package/src/config/bundled-tool-registry.ts +21 -0
- package/src/config/env-registry.ts +0 -2
- package/src/config/env.ts +19 -12
- package/src/config/feature-flag-registry.json +21 -133
- package/src/config/loader.ts +73 -99
- package/src/config/sanitize-for-transfer.ts +2 -0
- package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
- package/src/config/schemas/calls.ts +0 -9
- package/src/config/schemas/heartbeat.ts +63 -0
- package/src/config/schemas/ingress.ts +10 -6
- package/src/config/schemas/llm.ts +5 -10
- package/src/config/schemas/memory-lifecycle.ts +77 -24
- package/src/config/schemas/memory-v2.ts +48 -4
- package/src/config/schemas/platform.ts +6 -0
- package/src/config/schemas/services.ts +1 -15
- package/src/config/schemas/skills.ts +0 -6
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/contacts/contact-store.ts +0 -30
- package/src/contacts/contacts-write.ts +0 -27
- package/src/context/window-manager.ts +1 -2
- package/src/credential-execution/feature-gates.ts +10 -10
- package/src/credential-execution/process-manager.ts +12 -41
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
- package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
- package/src/daemon/config-watcher.ts +4 -3
- package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
- package/src/daemon/conversation-agent-loop.ts +32 -28
- package/src/daemon/conversation-lifecycle.ts +8 -1
- package/src/daemon/conversation-process.ts +16 -11
- package/src/daemon/conversation-runtime-assembly.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +125 -4
- package/src/daemon/conversation-tool-setup.ts +16 -55
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/doordash-steps.ts +1 -1
- package/src/daemon/handlers/shared.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +293 -0
- package/src/daemon/host-bash-proxy.ts +84 -74
- package/src/daemon/host-browser-proxy.ts +67 -82
- package/src/daemon/host-cu-proxy.ts +81 -86
- package/src/daemon/host-file-proxy.ts +93 -69
- package/src/daemon/host-proxy-base.ts +294 -0
- package/src/daemon/host-proxy-preactivation.ts +82 -0
- package/src/daemon/host-transfer-proxy.ts +247 -129
- package/src/daemon/lifecycle.ts +115 -117
- package/src/daemon/message-protocol.ts +3 -8
- package/src/daemon/message-types/contacts.ts +23 -1
- package/src/daemon/message-types/conversations.ts +11 -8
- package/src/daemon/message-types/host-app-control.ts +150 -0
- package/src/daemon/message-types/host-bash.ts +4 -0
- package/src/daemon/message-types/host-cu.ts +2 -0
- package/src/daemon/message-types/host-file.ts +4 -0
- package/src/daemon/message-types/host-transfer.ts +3 -0
- package/src/daemon/message-types/schedules.ts +8 -3
- package/src/daemon/message-types/skills.ts +2 -2
- package/src/daemon/process-message.ts +18 -1
- package/src/daemon/shutdown-handlers.ts +0 -3
- package/src/daemon/tool-setup-types.ts +51 -0
- package/src/daemon/tool-side-effects.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -1
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
- package/src/heartbeat/heartbeat-run-store.ts +236 -0
- package/src/heartbeat/heartbeat-service.ts +280 -49
- package/src/home/__tests__/post-connect-feed.test.ts +99 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
- package/src/home/__tests__/suggested-prompts.test.ts +89 -0
- package/src/home/post-connect-feed.ts +68 -0
- package/src/home/relationship-state-writer.ts +17 -92
- package/src/home/suggested-prompts.ts +46 -10
- package/src/inbound/public-ingress-urls.ts +32 -34
- package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
- package/src/ipc/assistant-server.ts +14 -1
- package/src/ipc/cli-client.ts +32 -1
- package/src/live-voice/live-voice-metrics.ts +10 -10
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
- package/src/mcp/mcp-auth-orchestrator.ts +213 -0
- package/src/mcp/mcp-auth-state.ts +133 -0
- package/src/mcp/mcp-oauth-provider.ts +19 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
- package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
- package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
- package/src/memory/anisotropy.test.ts +247 -0
- package/src/memory/anisotropy.ts +443 -0
- package/src/memory/auto-analysis-constants.ts +17 -0
- package/src/memory/auto-analysis-guard.ts +5 -15
- package/src/memory/canonical-guardian-store.ts +7 -7
- package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
- package/src/memory/context-search/agent-protocol.ts +6 -6
- package/src/memory/context-search/agent-runner.ts +32 -7
- package/src/memory/context-search/sources/memory-v2.ts +17 -5
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-key-store.ts +2 -15
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-backend.ts +9 -21
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
- package/src/memory/graph/conversation-graph-memory.ts +1 -24
- package/src/memory/graph/graph-search.ts +8 -0
- package/src/memory/graph/retriever.ts +28 -0
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
- package/src/memory/jobs/embed-concept-page.ts +28 -2
- package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
- package/src/memory/jobs-store.ts +66 -22
- package/src/memory/jobs-worker.ts +112 -63
- package/src/memory/memory-v2-activation-log-store.ts +1 -1
- package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
- package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/pkb/pkb-search.ts +7 -0
- package/src/memory/qdrant-client.ts +50 -20
- package/src/memory/schema/infrastructure.ts +15 -0
- package/src/memory/search/semantic.ts +7 -0
- package/src/memory/sparse-tokenize.ts +49 -0
- package/src/memory/v2/__tests__/activation.test.ts +77 -95
- package/src/memory/v2/__tests__/injection.test.ts +43 -21
- package/src/memory/v2/__tests__/sim.test.ts +166 -6
- package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
- package/src/memory/v2/__tests__/static-context.test.ts +0 -1
- package/src/memory/v2/activation.ts +69 -88
- package/src/memory/v2/consolidation-job.ts +3 -5
- package/src/memory/v2/constants.ts +7 -0
- package/src/memory/v2/injection.ts +86 -53
- package/src/memory/v2/prompts/consolidation.ts +312 -91
- package/src/memory/v2/qdrant.ts +99 -1
- package/src/memory/v2/sim.ts +126 -16
- package/src/memory/v2/skill-qdrant.ts +12 -3
- package/src/memory/v2/skill-store.ts +16 -1
- package/src/memory/v2/sparse-bm25.ts +245 -0
- package/src/memory/v2/static-context.ts +6 -5
- package/src/messaging/providers/gmail/types.ts +0 -49
- package/src/messaging/providers/slack/adapter.ts +1 -31
- package/src/messaging/providers/slack/types.ts +0 -32
- package/src/notifications/README.md +10 -10
- package/src/notifications/broadcaster.ts +1 -1
- package/src/notifications/guardian-question-mode.ts +5 -5
- package/src/oauth/connect-orchestrator.ts +4 -0
- package/src/oauth/credential-token-resolver.ts +1 -3
- package/src/oauth/manual-token-connection.ts +0 -4
- package/src/outbound-proxy/index.ts +1 -37
- package/src/outbound-proxy/logging.ts +1 -1
- package/src/outbound-proxy/policy.ts +6 -5
- package/src/outbound-proxy/router.ts +2 -1
- package/src/permissions/approval-policy.test.ts +6 -275
- package/src/permissions/approval-policy.ts +0 -51
- package/src/permissions/checker.test.ts +0 -1
- package/src/permissions/checker.ts +3 -17
- package/src/permissions/gateway-threshold-reader.ts +2 -0
- package/src/permissions/prompter.ts +34 -1
- package/src/permissions/secret-prompter.ts +6 -2
- package/src/prompts/bootstrap-cleanup.ts +27 -0
- package/src/prompts/system-prompt.ts +3 -18
- package/src/prompts/templates/SOUL.md +13 -1
- package/src/providers/speech-to-text/provider-catalog.ts +7 -8
- package/src/runtime/assistant-event-hub.ts +118 -96
- package/src/runtime/assistant-event.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
- package/src/runtime/auth/middleware.ts +0 -96
- package/src/runtime/auth/route-policy.ts +19 -0
- package/src/runtime/btw-sidechain.ts +2 -3
- package/src/runtime/channel-invite-transport.ts +2 -48
- package/src/runtime/channel-invite-transports/email.ts +1 -1
- package/src/runtime/channel-invite-transports/slack.ts +1 -1
- package/src/runtime/channel-invite-transports/telegram.ts +1 -1
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
- package/src/runtime/channel-invite-types.ts +54 -0
- package/src/runtime/channel-readiness-service.ts +32 -13
- package/src/runtime/http-server.ts +3 -329
- package/src/runtime/http-types.ts +0 -5
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
- package/src/runtime/migrations/migration-transport.ts +7 -7
- package/src/runtime/migrations/vbundle-builder.ts +327 -60
- package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
- package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
- package/src/runtime/migrations/vbundle-importer.ts +245 -68
- package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
- package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
- package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
- package/src/runtime/migrations/vbundle-validator.ts +114 -0
- package/src/runtime/pending-interactions.ts +35 -9
- package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
- package/src/runtime/routes/approval-interception-types.ts +13 -0
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
- package/src/runtime/routes/backup-routes.ts +15 -38
- package/src/runtime/routes/btw-routes.ts +14 -37
- package/src/runtime/routes/client-routes.ts +1 -0
- package/src/runtime/routes/contact-prompt-routes.ts +183 -0
- package/src/runtime/routes/conversation-query-routes.ts +36 -1
- package/src/runtime/routes/conversation-routes.ts +30 -13
- package/src/runtime/routes/document-pdf-renderer.ts +165 -0
- package/src/runtime/routes/documents-routes.ts +30 -0
- package/src/runtime/routes/errors.ts +19 -4
- package/src/runtime/routes/events-routes.ts +12 -6
- package/src/runtime/routes/gateway-log-routes.ts +79 -0
- package/src/runtime/routes/guardian-approval-interception.ts +2 -8
- package/src/runtime/routes/heartbeat-routes.ts +103 -38
- package/src/runtime/routes/host-app-control-routes.ts +134 -0
- package/src/runtime/routes/host-bash-routes.ts +36 -6
- package/src/runtime/routes/host-browser-routes.ts +108 -13
- package/src/runtime/routes/host-cu-routes.ts +44 -14
- package/src/runtime/routes/host-file-routes.ts +33 -10
- package/src/runtime/routes/host-transfer-routes.ts +64 -24
- package/src/runtime/routes/http-adapter.ts +1 -0
- package/src/runtime/routes/identity-intro-cache.ts +30 -0
- package/src/runtime/routes/identity-routes.ts +15 -43
- package/src/runtime/routes/inbound-message-handler.ts +1 -9
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/mcp-auth-routes.ts +132 -0
- package/src/runtime/routes/memory-item-routes.ts +10 -12
- package/src/runtime/routes/memory-v2-routes.ts +441 -1
- package/src/runtime/routes/migration-routes.ts +96 -0
- package/src/runtime/routes/schedule-routes.ts +7 -0
- package/src/runtime/verification-templates.ts +4 -7
- package/src/schedule/integration-status.ts +66 -2
- package/src/schedule/recurrence-engine.ts +4 -1
- package/src/schedule/retry-backoff.ts +18 -0
- package/src/schedule/retry-policy.ts +82 -0
- package/src/schedule/schedule-recovery.ts +64 -0
- package/src/schedule/schedule-store.ts +106 -2
- package/src/schedule/scheduler-types.ts +25 -0
- package/src/schedule/scheduler.ts +63 -38
- package/src/security/oauth-callback-registry.ts +8 -0
- package/src/sequence/analytics.ts +5 -5
- package/src/sequence/engine.ts +1 -1
- package/src/skills/catalog-files.ts +2 -8
- package/src/skills/include-graph.ts +5 -5
- package/src/skills/remote-skill-policy.ts +5 -5
- package/src/skills/skill-file-provider.ts +1 -1
- package/src/skills/skill-file-types.ts +13 -0
- package/src/skills/skillssh-audit-types.ts +28 -0
- package/src/skills/skillssh-registry.ts +8 -21
- package/src/telemetry/types.ts +2 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
- package/src/tools/apps/executors.ts +56 -69
- package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
- package/src/tools/browser/cdp-client/factory.ts +23 -24
- package/src/tools/browser/cdp-client/index.ts +1 -14
- package/src/tools/computer-use/definitions.ts +42 -20
- package/src/tools/executor.ts +2 -0
- package/src/tools/host-filesystem/edit.ts +26 -0
- package/src/tools/host-filesystem/read.ts +26 -0
- package/src/tools/host-filesystem/transfer.ts +31 -1
- package/src/tools/host-filesystem/write.ts +26 -0
- package/src/tools/host-terminal/host-shell.ts +58 -0
- package/src/tools/schedule/create.ts +6 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/schedule/update.ts +10 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
- package/src/tools/shared/filesystem/path-policy.ts +25 -1
- package/src/tools/skills/load.ts +0 -32
- package/src/tools/tool-approval-handler.ts +1 -5
- package/src/tools/types.ts +4 -0
- package/src/usage/pricing.ts +1 -1
- package/src/workspace/hatched-date.ts +86 -0
- package/src/workspace/migrations/003-seed-device-id.ts +1 -1
- package/src/workspace/migrations/006-services-config.ts +8 -5
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
- package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
- package/src/workspace/migrations/utils.ts +21 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
- package/src/__tests__/twilio-rest.test.ts +0 -34
- package/src/backup/__tests__/backup-key.test.ts +0 -152
- package/src/backup/__tests__/backup-worker.test.ts +0 -782
- package/src/backup/__tests__/offsite-writer.test.ts +0 -641
- package/src/backup/__tests__/stream-crypt.test.ts +0 -228
- package/src/backup/backup-key.ts +0 -137
- package/src/backup/backup-worker.ts +0 -472
- package/src/backup/offsite-writer.ts +0 -222
- package/src/backup/stream-crypt.ts +0 -263
- package/src/daemon/message-types/pairing.ts +0 -58
- package/src/outbound-proxy/config.ts +0 -20
- package/src/outbound-proxy/health.ts +0 -18
- package/src/outbound-proxy/types.ts +0 -150
- package/src/runtime/capability-tokens.ts +0 -190
- package/src/signals/mcp-reload.ts +0 -18
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
afterEach,
|
|
5
|
+
beforeEach,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
jest,
|
|
9
|
+
mock,
|
|
10
|
+
test,
|
|
11
|
+
} from "bun:test";
|
|
4
12
|
|
|
5
13
|
const testWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
6
14
|
|
|
15
|
+
// ── Heartbeat run store mock ───────────────────────────────────────
|
|
16
|
+
const mockInsertPendingHeartbeatRun = mock(() => "mock-run-id");
|
|
17
|
+
const mockStartHeartbeatRun = mock(() => true);
|
|
18
|
+
const mockCompleteHeartbeatRun = mock(() => true);
|
|
19
|
+
const mockSkipHeartbeatRun = mock(() => true);
|
|
20
|
+
const mockSupersedePendingRun = mock(() => true);
|
|
21
|
+
const mockMarkStaleRunsAsMissed = mock(() => 0);
|
|
22
|
+
const mockMarkStaleRunningAsError = mock(() => 0);
|
|
23
|
+
mock.module("../heartbeat/heartbeat-run-store.js", () => ({
|
|
24
|
+
insertPendingHeartbeatRun: mockInsertPendingHeartbeatRun,
|
|
25
|
+
startHeartbeatRun: mockStartHeartbeatRun,
|
|
26
|
+
completeHeartbeatRun: mockCompleteHeartbeatRun,
|
|
27
|
+
skipHeartbeatRun: mockSkipHeartbeatRun,
|
|
28
|
+
supersedePendingRun: mockSupersedePendingRun,
|
|
29
|
+
markStaleRunsAsMissed: mockMarkStaleRunsAsMissed,
|
|
30
|
+
markStaleRunningAsError: mockMarkStaleRunningAsError,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// ── Feed event mock ───────────────────────────────────────────────
|
|
34
|
+
const mockEmitFeedEvent = mock(() => Promise.resolve());
|
|
35
|
+
mock.module("../home/emit-feed-event.js", () => ({
|
|
36
|
+
emitFeedEvent: mockEmitFeedEvent,
|
|
37
|
+
}));
|
|
38
|
+
|
|
7
39
|
// Mock config loader
|
|
8
40
|
let mockConfig = {
|
|
9
41
|
heartbeat: {
|
|
10
42
|
enabled: true,
|
|
11
43
|
intervalMs: 60_000,
|
|
44
|
+
cronExpression: null as string | null,
|
|
45
|
+
timezone: null as string | null,
|
|
12
46
|
activeHoursStart: undefined as number | undefined,
|
|
13
47
|
activeHoursEnd: undefined as number | undefined,
|
|
14
48
|
},
|
|
@@ -22,6 +56,32 @@ mock.module("../config/loader.js", () => ({
|
|
|
22
56
|
invalidateConfigCache: () => {},
|
|
23
57
|
}));
|
|
24
58
|
|
|
59
|
+
// ── Recurrence engine mock ──────────────────────────────────────────
|
|
60
|
+
//
|
|
61
|
+
// HeartbeatService imports computeNextRunAt for cron scheduling.
|
|
62
|
+
// Tests mutate `mockComputeNextRunAt` to control the next cron occurrence.
|
|
63
|
+
let mockComputeNextRunAtResult: number | null = null;
|
|
64
|
+
let mockComputeNextRunAtError: Error | null = null;
|
|
65
|
+
let computeNextRunAtCallCount = 0;
|
|
66
|
+
|
|
67
|
+
mock.module("../schedule/recurrence-engine.js", () => ({
|
|
68
|
+
computeNextRunAt: (_spec: {
|
|
69
|
+
syntax: string;
|
|
70
|
+
expression: string;
|
|
71
|
+
timezone?: string | null;
|
|
72
|
+
}) => {
|
|
73
|
+
computeNextRunAtCallCount++;
|
|
74
|
+
if (mockComputeNextRunAtError) {
|
|
75
|
+
throw mockComputeNextRunAtError;
|
|
76
|
+
}
|
|
77
|
+
if (mockComputeNextRunAtResult != null) {
|
|
78
|
+
return mockComputeNextRunAtResult;
|
|
79
|
+
}
|
|
80
|
+
// Default: 1 hour from now
|
|
81
|
+
return Date.now() + 3_600_000;
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
|
|
25
85
|
// ── Guardian persona mock ─────────────────────────────────────────
|
|
26
86
|
//
|
|
27
87
|
// `heartbeat-service.isShallowProfile` reads the guardian persona via
|
|
@@ -263,6 +323,9 @@ describe("HeartbeatService", () => {
|
|
|
263
323
|
mockCheckAllCredentialsFail = false;
|
|
264
324
|
emittedNotificationSignals.length = 0;
|
|
265
325
|
loggerWarnCalls.length = 0;
|
|
326
|
+
mockComputeNextRunAtResult = null;
|
|
327
|
+
mockComputeNextRunAtError = null;
|
|
328
|
+
computeNextRunAtCallCount = 0;
|
|
266
329
|
|
|
267
330
|
// Default processMessage mock: capture calls for assertions.
|
|
268
331
|
setTestProcessMessage(async (...args: unknown[]) => {
|
|
@@ -274,10 +337,29 @@ describe("HeartbeatService", () => {
|
|
|
274
337
|
return { messageId: "msg-1" };
|
|
275
338
|
});
|
|
276
339
|
|
|
340
|
+
mockInsertPendingHeartbeatRun.mockClear();
|
|
341
|
+
mockInsertPendingHeartbeatRun.mockImplementation(() => "mock-run-id");
|
|
342
|
+
mockStartHeartbeatRun.mockClear();
|
|
343
|
+
mockStartHeartbeatRun.mockImplementation(() => true);
|
|
344
|
+
mockCompleteHeartbeatRun.mockClear();
|
|
345
|
+
mockCompleteHeartbeatRun.mockImplementation(() => true);
|
|
346
|
+
mockSkipHeartbeatRun.mockClear();
|
|
347
|
+
mockSkipHeartbeatRun.mockImplementation(() => true);
|
|
348
|
+
mockSupersedePendingRun.mockClear();
|
|
349
|
+
mockSupersedePendingRun.mockImplementation(() => true);
|
|
350
|
+
mockMarkStaleRunsAsMissed.mockClear();
|
|
351
|
+
mockMarkStaleRunsAsMissed.mockImplementation(() => 0);
|
|
352
|
+
mockMarkStaleRunningAsError.mockClear();
|
|
353
|
+
mockMarkStaleRunningAsError.mockImplementation(() => 0);
|
|
354
|
+
mockEmitFeedEvent.mockClear();
|
|
355
|
+
mockEmitFeedEvent.mockImplementation(() => Promise.resolve());
|
|
356
|
+
|
|
277
357
|
mockConfig = {
|
|
278
358
|
heartbeat: {
|
|
279
359
|
enabled: true,
|
|
280
360
|
intervalMs: 60_000,
|
|
361
|
+
cronExpression: null,
|
|
362
|
+
timezone: null,
|
|
281
363
|
activeHoursStart: undefined,
|
|
282
364
|
activeHoursEnd: undefined,
|
|
283
365
|
},
|
|
@@ -1053,4 +1135,639 @@ describe("HeartbeatService", () => {
|
|
|
1053
1135
|
expect(unreachableWarns[0].unreachableCount).toBe(2);
|
|
1054
1136
|
});
|
|
1055
1137
|
});
|
|
1138
|
+
|
|
1139
|
+
describe("cron scheduling mode", () => {
|
|
1140
|
+
test("start() with cronExpression sets nextRunAt to cron occurrence, not now+intervalMs", () => {
|
|
1141
|
+
const cronNextRunAt = Date.now() + 7_200_000; // 2 hours from now
|
|
1142
|
+
mockComputeNextRunAtResult = cronNextRunAt;
|
|
1143
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1144
|
+
mockConfig.heartbeat.timezone = "America/New_York";
|
|
1145
|
+
|
|
1146
|
+
const service = createService();
|
|
1147
|
+
service.start();
|
|
1148
|
+
|
|
1149
|
+
expect(service.nextRunAt).toBe(cronNextRunAt);
|
|
1150
|
+
// Should NOT be now + intervalMs
|
|
1151
|
+
expect(service.nextRunAt).not.toBeCloseTo(
|
|
1152
|
+
Date.now() + mockConfig.heartbeat.intervalMs,
|
|
1153
|
+
-3,
|
|
1154
|
+
);
|
|
1155
|
+
service.stop();
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test("runOnce() does not call scheduleNextRun(intervalMs) in cron mode — nextRunAt is not clobbered", async () => {
|
|
1159
|
+
const cronNextRunAt = Date.now() + 7_200_000;
|
|
1160
|
+
mockComputeNextRunAtResult = cronNextRunAt;
|
|
1161
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1162
|
+
|
|
1163
|
+
const service = createService();
|
|
1164
|
+
service.start();
|
|
1165
|
+
|
|
1166
|
+
// nextRunAt should be the cron time before runOnce
|
|
1167
|
+
expect(service.nextRunAt).toBe(cronNextRunAt);
|
|
1168
|
+
|
|
1169
|
+
await service.runOnce();
|
|
1170
|
+
|
|
1171
|
+
// After runOnce(), nextRunAt should still reflect a cron time, not now + intervalMs.
|
|
1172
|
+
// The finally chain in scheduleNextCronRun recalculates it, but the runOnce()
|
|
1173
|
+
// finally block should NOT have called scheduleNextRun(intervalMs).
|
|
1174
|
+
// Since our mock always returns cronNextRunAt, nextRunAt should remain that value.
|
|
1175
|
+
expect(service.nextRunAt).toBe(cronNextRunAt);
|
|
1176
|
+
service.stop();
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
test("after runOnce() rejects in cron mode, the next cron run is still scheduled via finally", async () => {
|
|
1180
|
+
const cronNextRunAt = Date.now() + 7_200_000;
|
|
1181
|
+
mockComputeNextRunAtResult = cronNextRunAt;
|
|
1182
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1183
|
+
|
|
1184
|
+
const service = createService({
|
|
1185
|
+
processMessage: async () => {
|
|
1186
|
+
throw new Error("LLM down");
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
service.start();
|
|
1190
|
+
|
|
1191
|
+
await service.runOnce();
|
|
1192
|
+
|
|
1193
|
+
// Even though executeRun failed, the service should still have a nextRunAt
|
|
1194
|
+
// set to the cron occurrence (the finally chain reschedules)
|
|
1195
|
+
expect(service.nextRunAt).toBe(cronNextRunAt);
|
|
1196
|
+
service.stop();
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test("resetTimer() in cron mode recomputes from the current time", () => {
|
|
1200
|
+
const firstCronTime = Date.now() + 3_600_000;
|
|
1201
|
+
mockComputeNextRunAtResult = firstCronTime;
|
|
1202
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1203
|
+
|
|
1204
|
+
const service = createService();
|
|
1205
|
+
service.start();
|
|
1206
|
+
expect(service.nextRunAt).toBe(firstCronTime);
|
|
1207
|
+
|
|
1208
|
+
// Simulate time passing and a new cron occurrence
|
|
1209
|
+
const secondCronTime = Date.now() + 5_400_000;
|
|
1210
|
+
mockComputeNextRunAtResult = secondCronTime;
|
|
1211
|
+
|
|
1212
|
+
service.resetTimer();
|
|
1213
|
+
expect(service.nextRunAt).toBe(secondCronTime);
|
|
1214
|
+
service.stop();
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
test("reconfigure() switches from interval to cron mode", () => {
|
|
1218
|
+
const service = createService();
|
|
1219
|
+
// Start in interval mode
|
|
1220
|
+
service.start();
|
|
1221
|
+
const intervalNextRunAt = service.nextRunAt;
|
|
1222
|
+
expect(intervalNextRunAt).not.toBeNull();
|
|
1223
|
+
|
|
1224
|
+
// Reconfigure to cron mode
|
|
1225
|
+
const cronNextRunAt = Date.now() + 7_200_000;
|
|
1226
|
+
mockComputeNextRunAtResult = cronNextRunAt;
|
|
1227
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1228
|
+
service.reconfigure();
|
|
1229
|
+
|
|
1230
|
+
expect(service.nextRunAt).toBe(cronNextRunAt);
|
|
1231
|
+
service.stop();
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
test("reconfigure() switches from cron to interval mode", () => {
|
|
1235
|
+
const cronNextRunAt = Date.now() + 7_200_000;
|
|
1236
|
+
mockComputeNextRunAtResult = cronNextRunAt;
|
|
1237
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1238
|
+
|
|
1239
|
+
const service = createService();
|
|
1240
|
+
service.start();
|
|
1241
|
+
expect(service.nextRunAt).toBe(cronNextRunAt);
|
|
1242
|
+
|
|
1243
|
+
// Reconfigure to interval mode
|
|
1244
|
+
mockConfig.heartbeat.cronExpression = null;
|
|
1245
|
+
const before = Date.now();
|
|
1246
|
+
service.reconfigure();
|
|
1247
|
+
|
|
1248
|
+
expect(service.nextRunAt).not.toBeNull();
|
|
1249
|
+
expect(service.nextRunAt!).toBeGreaterThanOrEqual(
|
|
1250
|
+
before + mockConfig.heartbeat.intervalMs,
|
|
1251
|
+
);
|
|
1252
|
+
service.stop();
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test("active hours guard uses cron timezone when configured", async () => {
|
|
1256
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1257
|
+
mockConfig.heartbeat.timezone = "UTC";
|
|
1258
|
+
mockConfig.heartbeat.activeHoursStart = 9;
|
|
1259
|
+
mockConfig.heartbeat.activeHoursEnd = 17;
|
|
1260
|
+
mockComputeNextRunAtResult = Date.now() + 3_600_000;
|
|
1261
|
+
|
|
1262
|
+
const service = createService();
|
|
1263
|
+
service.start();
|
|
1264
|
+
|
|
1265
|
+
// In cron mode with timezone, the hour is computed via Intl.DateTimeFormat
|
|
1266
|
+
// rather than getCurrentHour(). The test verifies the code path runs without
|
|
1267
|
+
// error — the actual hour depends on the system clock and UTC conversion.
|
|
1268
|
+
// We just verify it doesn't throw and returns a boolean result.
|
|
1269
|
+
const result = await service.runOnce();
|
|
1270
|
+
// Result depends on current UTC hour vs active window — either outcome is valid
|
|
1271
|
+
expect(typeof result).toBe("boolean");
|
|
1272
|
+
service.stop();
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
test("active hours guard falls back to getCurrentHour when cron mode has no timezone", async () => {
|
|
1276
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1277
|
+
mockConfig.heartbeat.timezone = null;
|
|
1278
|
+
mockConfig.heartbeat.activeHoursStart = 9;
|
|
1279
|
+
mockConfig.heartbeat.activeHoursEnd = 17;
|
|
1280
|
+
mockComputeNextRunAtResult = Date.now() + 3_600_000;
|
|
1281
|
+
|
|
1282
|
+
// getCurrentHour returns 3 (outside 9-17 window), so runOnce should skip
|
|
1283
|
+
const service = createService({ getCurrentHour: () => 3 });
|
|
1284
|
+
service.start();
|
|
1285
|
+
const result = await service.runOnce();
|
|
1286
|
+
expect(result).toBe(false);
|
|
1287
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
1288
|
+
service.stop();
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test("runtime fallback: computeNextRunAt throws, service falls back to interval mode", () => {
|
|
1292
|
+
mockComputeNextRunAtError = new Error("No upcoming runs");
|
|
1293
|
+
mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
|
|
1294
|
+
|
|
1295
|
+
const service = createService();
|
|
1296
|
+
service.start();
|
|
1297
|
+
|
|
1298
|
+
// Should have fallen back to interval mode — nextRunAt should be ~now + intervalMs
|
|
1299
|
+
expect(service.nextRunAt).not.toBeNull();
|
|
1300
|
+
const expectedMin = Date.now() + mockConfig.heartbeat.intervalMs - 100;
|
|
1301
|
+
expect(service.nextRunAt!).toBeGreaterThanOrEqual(expectedMin);
|
|
1302
|
+
|
|
1303
|
+
// Should have logged a warning about the fallback
|
|
1304
|
+
const fallbackWarns = loggerWarnCalls.filter((call) => "err" in call);
|
|
1305
|
+
expect(fallbackWarns.length).toBeGreaterThanOrEqual(1);
|
|
1306
|
+
service.stop();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
test("null cronExpression behaves identically to current fixed-interval mode", () => {
|
|
1310
|
+
mockConfig.heartbeat.cronExpression = null;
|
|
1311
|
+
|
|
1312
|
+
const service = createService();
|
|
1313
|
+
const before = Date.now();
|
|
1314
|
+
service.start();
|
|
1315
|
+
|
|
1316
|
+
expect(service.nextRunAt).not.toBeNull();
|
|
1317
|
+
expect(service.nextRunAt!).toBeGreaterThanOrEqual(
|
|
1318
|
+
before + mockConfig.heartbeat.intervalMs,
|
|
1319
|
+
);
|
|
1320
|
+
// computeNextRunAt should not have been called
|
|
1321
|
+
expect(computeNextRunAtCallCount).toBe(0);
|
|
1322
|
+
service.stop();
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
describe("heartbeat run store instrumentation", () => {
|
|
1327
|
+
test("successful run: pending → running → ok with conversationId", async () => {
|
|
1328
|
+
const service = createService();
|
|
1329
|
+
await service.runOnce();
|
|
1330
|
+
|
|
1331
|
+
expect(mockStartHeartbeatRun).toHaveBeenCalledTimes(1);
|
|
1332
|
+
expect(mockCompleteHeartbeatRun).toHaveBeenCalledTimes(1);
|
|
1333
|
+
expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
|
|
1334
|
+
status: "ok",
|
|
1335
|
+
conversationId: "conv-1",
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
test("failed run: pending → running → error preserving conversationId", async () => {
|
|
1340
|
+
const service = createService({
|
|
1341
|
+
processMessage: async () => {
|
|
1342
|
+
throw new Error("LLM timeout");
|
|
1343
|
+
},
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
await service.runOnce();
|
|
1347
|
+
|
|
1348
|
+
expect(mockStartHeartbeatRun).toHaveBeenCalledTimes(1);
|
|
1349
|
+
expect(mockCompleteHeartbeatRun).toHaveBeenCalledTimes(1);
|
|
1350
|
+
expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
|
|
1351
|
+
status: "error",
|
|
1352
|
+
conversationId: "conv-1",
|
|
1353
|
+
error: "LLM timeout",
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
test("CAS false suppresses success feed event", async () => {
|
|
1358
|
+
mockCompleteHeartbeatRun.mockImplementation(() => false);
|
|
1359
|
+
|
|
1360
|
+
const service = createService();
|
|
1361
|
+
await service.runOnce();
|
|
1362
|
+
|
|
1363
|
+
// completeHeartbeatRun returned false, so no feed event should be emitted for success
|
|
1364
|
+
const successCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1365
|
+
(call: unknown[]) => {
|
|
1366
|
+
const opts = call[0] as { dedupKey?: string };
|
|
1367
|
+
return opts.dedupKey?.startsWith("heartbeat:ok:");
|
|
1368
|
+
},
|
|
1369
|
+
);
|
|
1370
|
+
expect(successCalls).toHaveLength(0);
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
test("CAS false suppresses failure alerter and feed event", async () => {
|
|
1374
|
+
mockCompleteHeartbeatRun.mockImplementation(() => false);
|
|
1375
|
+
|
|
1376
|
+
const service = createService({
|
|
1377
|
+
processMessage: async () => {
|
|
1378
|
+
throw new Error("LLM timeout");
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
await service.runOnce();
|
|
1383
|
+
|
|
1384
|
+
// completeHeartbeatRun returned false, so alerter should NOT be called
|
|
1385
|
+
expect(alerterCalls).toHaveLength(0);
|
|
1386
|
+
|
|
1387
|
+
// No failure feed event either
|
|
1388
|
+
const failCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1389
|
+
(call: unknown[]) => {
|
|
1390
|
+
const opts = call[0] as { dedupKey?: string };
|
|
1391
|
+
return opts.dedupKey?.startsWith("heartbeat:fail:");
|
|
1392
|
+
},
|
|
1393
|
+
);
|
|
1394
|
+
expect(failCalls).toHaveLength(0);
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
test("active-hours skip calls skipHeartbeatRun", async () => {
|
|
1398
|
+
mockConfig.heartbeat.activeHoursStart = 9;
|
|
1399
|
+
mockConfig.heartbeat.activeHoursEnd = 17;
|
|
1400
|
+
|
|
1401
|
+
const service = createService({ getCurrentHour: () => 3 });
|
|
1402
|
+
service.start();
|
|
1403
|
+
await service.runOnce();
|
|
1404
|
+
|
|
1405
|
+
expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
|
|
1406
|
+
"mock-run-id",
|
|
1407
|
+
"outside_active_hours",
|
|
1408
|
+
);
|
|
1409
|
+
service.stop();
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
test("overlap skip calls skipHeartbeatRun", async () => {
|
|
1413
|
+
let resolveFirst: () => void;
|
|
1414
|
+
const firstPromise = new Promise<void>((r) => {
|
|
1415
|
+
resolveFirst = r;
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
const service = createService({
|
|
1419
|
+
processMessage: async () => {
|
|
1420
|
+
await firstPromise;
|
|
1421
|
+
return { messageId: "msg-1" };
|
|
1422
|
+
},
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
// Start first run (will block)
|
|
1426
|
+
const run1 = service.runOnce();
|
|
1427
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1428
|
+
|
|
1429
|
+
// Start service so the second runOnce has a pending row
|
|
1430
|
+
service.start();
|
|
1431
|
+
mockSkipHeartbeatRun.mockClear();
|
|
1432
|
+
|
|
1433
|
+
// Second run should be skipped due to overlap
|
|
1434
|
+
await service.runOnce();
|
|
1435
|
+
|
|
1436
|
+
expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
|
|
1437
|
+
"mock-run-id",
|
|
1438
|
+
"overlap",
|
|
1439
|
+
);
|
|
1440
|
+
|
|
1441
|
+
resolveFirst!();
|
|
1442
|
+
await run1;
|
|
1443
|
+
service.stop();
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
test("start() calls markStaleRunsAsMissed and markStaleRunningAsError", () => {
|
|
1447
|
+
const service = createService();
|
|
1448
|
+
service.start();
|
|
1449
|
+
|
|
1450
|
+
expect(mockMarkStaleRunsAsMissed).toHaveBeenCalledTimes(1);
|
|
1451
|
+
expect(mockMarkStaleRunningAsError).toHaveBeenCalledTimes(1);
|
|
1452
|
+
service.stop();
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
test("scheduleNextRun supersedes old pending row before creating new one", () => {
|
|
1456
|
+
const service = createService();
|
|
1457
|
+
service.start();
|
|
1458
|
+
|
|
1459
|
+
// start() called scheduleNextRun which set _pendingRunId.
|
|
1460
|
+
// Calling resetTimer triggers another scheduleNextRun which
|
|
1461
|
+
// should supersede the existing pending row before inserting
|
|
1462
|
+
// a new one.
|
|
1463
|
+
const callOrder: string[] = [];
|
|
1464
|
+
mockSupersedePendingRun.mockImplementation(() => {
|
|
1465
|
+
callOrder.push("supersede");
|
|
1466
|
+
return true;
|
|
1467
|
+
});
|
|
1468
|
+
mockInsertPendingHeartbeatRun.mockImplementation(() => {
|
|
1469
|
+
callOrder.push("insert");
|
|
1470
|
+
return "mock-run-id";
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
service.resetTimer();
|
|
1474
|
+
|
|
1475
|
+
// resetTimer's scheduleNextRun should supersede then insert
|
|
1476
|
+
expect(callOrder.filter((c) => c === "supersede").length).toBeGreaterThan(
|
|
1477
|
+
0,
|
|
1478
|
+
);
|
|
1479
|
+
const firstSupersede = callOrder.indexOf("supersede");
|
|
1480
|
+
const firstInsert = callOrder.indexOf("insert");
|
|
1481
|
+
expect(firstSupersede).toBeLessThan(firstInsert);
|
|
1482
|
+
|
|
1483
|
+
service.stop();
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
test("resetTimer() supersedes pending row", () => {
|
|
1487
|
+
const service = createService();
|
|
1488
|
+
service.start();
|
|
1489
|
+
|
|
1490
|
+
mockSupersedePendingRun.mockClear();
|
|
1491
|
+
service.resetTimer();
|
|
1492
|
+
|
|
1493
|
+
// resetTimer calls scheduleNextRun which supersedes existing pending
|
|
1494
|
+
expect(mockSupersedePendingRun).toHaveBeenCalled();
|
|
1495
|
+
service.stop();
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
test("force run creates its own pending row, does not consume scheduled one", async () => {
|
|
1499
|
+
const service = createService();
|
|
1500
|
+
service.start();
|
|
1501
|
+
|
|
1502
|
+
// Clear to track only the force run's calls
|
|
1503
|
+
mockInsertPendingHeartbeatRun.mockClear();
|
|
1504
|
+
|
|
1505
|
+
await service.runOnce({ force: true });
|
|
1506
|
+
|
|
1507
|
+
// Force run should have called insertPendingHeartbeatRun for itself
|
|
1508
|
+
// (at least once for its own row, plus the scheduleNextRun in finally)
|
|
1509
|
+
expect(mockInsertPendingHeartbeatRun).toHaveBeenCalled();
|
|
1510
|
+
|
|
1511
|
+
// The scheduled pending row (from start()) should NOT have been consumed
|
|
1512
|
+
// by the force run — force creates its own
|
|
1513
|
+
service.stop();
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
test("disabled config with stale pending row skips it as disabled", async () => {
|
|
1517
|
+
const service = createService();
|
|
1518
|
+
service.start();
|
|
1519
|
+
|
|
1520
|
+
// Now disable config and call runOnce — should skip the pending row
|
|
1521
|
+
mockConfig.heartbeat.enabled = false;
|
|
1522
|
+
mockSkipHeartbeatRun.mockClear();
|
|
1523
|
+
|
|
1524
|
+
await service.runOnce();
|
|
1525
|
+
|
|
1526
|
+
expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
|
|
1527
|
+
"mock-run-id",
|
|
1528
|
+
"disabled",
|
|
1529
|
+
);
|
|
1530
|
+
service.stop();
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
test("stop() supersedes outstanding pending row", async () => {
|
|
1534
|
+
const service = createService();
|
|
1535
|
+
service.start();
|
|
1536
|
+
|
|
1537
|
+
mockSupersedePendingRun.mockClear();
|
|
1538
|
+
await service.stop();
|
|
1539
|
+
|
|
1540
|
+
expect(mockSupersedePendingRun).toHaveBeenCalledWith("mock-run-id");
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
test("timeout calls completeHeartbeatRun with status timeout", async () => {
|
|
1544
|
+
jest.useFakeTimers();
|
|
1545
|
+
try {
|
|
1546
|
+
let resolveRun: () => void;
|
|
1547
|
+
const runPromise = new Promise<void>((r) => {
|
|
1548
|
+
resolveRun = r;
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
const service = createService({
|
|
1552
|
+
processMessage: async () => {
|
|
1553
|
+
await runPromise;
|
|
1554
|
+
return { messageId: "msg-1" };
|
|
1555
|
+
},
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
const runOncePromise = service.runOnce();
|
|
1559
|
+
// Advance past the 30-minute timeout
|
|
1560
|
+
jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
|
|
1561
|
+
await runOncePromise;
|
|
1562
|
+
|
|
1563
|
+
expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
|
|
1564
|
+
status: "timeout",
|
|
1565
|
+
error: "Heartbeat execution exceeded the 30-minute timeout",
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
// Clean up — resolve the hanging promise so it doesn't leak
|
|
1569
|
+
resolveRun!();
|
|
1570
|
+
} finally {
|
|
1571
|
+
jest.useRealTimers();
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
test("failure feed event has urgency high and includes error message", async () => {
|
|
1576
|
+
const service = createService({
|
|
1577
|
+
processMessage: async () => {
|
|
1578
|
+
throw new Error("web_search outage");
|
|
1579
|
+
},
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
await service.runOnce();
|
|
1583
|
+
|
|
1584
|
+
const failCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1585
|
+
(call: unknown[]) => {
|
|
1586
|
+
const opts = call[0] as { title?: string };
|
|
1587
|
+
return opts.title === "Heartbeat Failed";
|
|
1588
|
+
},
|
|
1589
|
+
);
|
|
1590
|
+
expect(failCalls).toHaveLength(1);
|
|
1591
|
+
const opts = (failCalls as any[][])[0][0] as {
|
|
1592
|
+
urgency?: string;
|
|
1593
|
+
summary?: string;
|
|
1594
|
+
};
|
|
1595
|
+
expect(opts.urgency).toBe("high");
|
|
1596
|
+
expect(opts.summary).toContain("web_search outage");
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
test("CAS false on complete suppresses failure feed event", async () => {
|
|
1600
|
+
mockCompleteHeartbeatRun.mockImplementation(() => false);
|
|
1601
|
+
|
|
1602
|
+
const service = createService({
|
|
1603
|
+
processMessage: async () => {
|
|
1604
|
+
throw new Error("some error");
|
|
1605
|
+
},
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
await service.runOnce();
|
|
1609
|
+
|
|
1610
|
+
const failCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1611
|
+
(call: unknown[]) => {
|
|
1612
|
+
const opts = call[0] as { title?: string };
|
|
1613
|
+
return opts.title === "Heartbeat Failed";
|
|
1614
|
+
},
|
|
1615
|
+
);
|
|
1616
|
+
expect(failCalls).toHaveLength(0);
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
test("timeout emits feed event with urgency high", async () => {
|
|
1620
|
+
jest.useFakeTimers();
|
|
1621
|
+
try {
|
|
1622
|
+
let resolveRun: () => void;
|
|
1623
|
+
const runPromise = new Promise<void>((r) => {
|
|
1624
|
+
resolveRun = r;
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
const service = createService({
|
|
1628
|
+
processMessage: async () => {
|
|
1629
|
+
await runPromise;
|
|
1630
|
+
return { messageId: "msg-1" };
|
|
1631
|
+
},
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
const runOncePromise = service.runOnce();
|
|
1635
|
+
jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
|
|
1636
|
+
await runOncePromise;
|
|
1637
|
+
|
|
1638
|
+
const timeoutCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1639
|
+
(call: unknown[]) => {
|
|
1640
|
+
const opts = call[0] as { title?: string };
|
|
1641
|
+
return opts.title === "Heartbeat Timed Out";
|
|
1642
|
+
},
|
|
1643
|
+
);
|
|
1644
|
+
expect(timeoutCalls).toHaveLength(1);
|
|
1645
|
+
const opts = (timeoutCalls as any[][])[0][0] as {
|
|
1646
|
+
urgency?: string;
|
|
1647
|
+
};
|
|
1648
|
+
expect(opts.urgency).toBe("high");
|
|
1649
|
+
|
|
1650
|
+
resolveRun!();
|
|
1651
|
+
} finally {
|
|
1652
|
+
jest.useRealTimers();
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
test("CAS false on timeout suppresses timeout feed event", async () => {
|
|
1657
|
+
jest.useFakeTimers();
|
|
1658
|
+
try {
|
|
1659
|
+
mockCompleteHeartbeatRun.mockImplementation(() => false);
|
|
1660
|
+
|
|
1661
|
+
let resolveRun: () => void;
|
|
1662
|
+
const runPromise = new Promise<void>((r) => {
|
|
1663
|
+
resolveRun = r;
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
const service = createService({
|
|
1667
|
+
processMessage: async () => {
|
|
1668
|
+
await runPromise;
|
|
1669
|
+
return { messageId: "msg-1" };
|
|
1670
|
+
},
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
const runOncePromise = service.runOnce();
|
|
1674
|
+
jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
|
|
1675
|
+
await runOncePromise;
|
|
1676
|
+
|
|
1677
|
+
// completeHeartbeatRun returned false, so no timeout feed event
|
|
1678
|
+
const timeoutCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1679
|
+
(call: unknown[]) => {
|
|
1680
|
+
const opts = call[0] as { title?: string };
|
|
1681
|
+
return opts.title === "Heartbeat Timed Out";
|
|
1682
|
+
},
|
|
1683
|
+
);
|
|
1684
|
+
expect(timeoutCalls).toHaveLength(0);
|
|
1685
|
+
|
|
1686
|
+
resolveRun!();
|
|
1687
|
+
} finally {
|
|
1688
|
+
jest.useRealTimers();
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
test("late run emits late feed event", async () => {
|
|
1693
|
+
const service = createService();
|
|
1694
|
+
service.start();
|
|
1695
|
+
|
|
1696
|
+
// Set the pending run to be 10 minutes in the past
|
|
1697
|
+
(service as any)._nextRunAt = Date.now() - 10 * 60 * 1000;
|
|
1698
|
+
(service as any)._pendingRunId = "late-run-id";
|
|
1699
|
+
|
|
1700
|
+
await service.runOnce();
|
|
1701
|
+
|
|
1702
|
+
const lateCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1703
|
+
(call: unknown[]) => {
|
|
1704
|
+
const opts = call[0] as { title?: string };
|
|
1705
|
+
return opts.title === "Heartbeat Ran Late";
|
|
1706
|
+
},
|
|
1707
|
+
);
|
|
1708
|
+
expect(lateCalls).toHaveLength(1);
|
|
1709
|
+
const opts = (lateCalls as any[][])[0][0] as {
|
|
1710
|
+
urgency?: string;
|
|
1711
|
+
summary?: string;
|
|
1712
|
+
};
|
|
1713
|
+
expect(opts.urgency).toBe("medium");
|
|
1714
|
+
expect(opts.summary).toContain("10 minutes late");
|
|
1715
|
+
|
|
1716
|
+
await service.stop();
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
test("on-time run does not emit late feed event", async () => {
|
|
1720
|
+
const service = createService();
|
|
1721
|
+
await service.runOnce();
|
|
1722
|
+
|
|
1723
|
+
const lateCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1724
|
+
(call: unknown[]) => {
|
|
1725
|
+
const opts = call[0] as { title?: string };
|
|
1726
|
+
return opts.title === "Heartbeat Ran Late";
|
|
1727
|
+
},
|
|
1728
|
+
);
|
|
1729
|
+
expect(lateCalls).toHaveLength(0);
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
test("start() emits missed-run feed event when stale rows exist", () => {
|
|
1733
|
+
mockMarkStaleRunsAsMissed.mockImplementation(() => 2);
|
|
1734
|
+
mockMarkStaleRunningAsError.mockImplementation(() => 1);
|
|
1735
|
+
|
|
1736
|
+
const service = createService();
|
|
1737
|
+
service.start();
|
|
1738
|
+
|
|
1739
|
+
const missedCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1740
|
+
(call: unknown[]) => {
|
|
1741
|
+
const opts = call[0] as { title?: string };
|
|
1742
|
+
return opts.title === "Heartbeat Runs Missed";
|
|
1743
|
+
},
|
|
1744
|
+
);
|
|
1745
|
+
expect(missedCalls).toHaveLength(1);
|
|
1746
|
+
const opts = (missedCalls as any[][])[0][0] as {
|
|
1747
|
+
urgency?: string;
|
|
1748
|
+
summary?: string;
|
|
1749
|
+
};
|
|
1750
|
+
expect(opts.urgency).toBe("high");
|
|
1751
|
+
expect(opts.summary).toContain("3");
|
|
1752
|
+
|
|
1753
|
+
service.stop();
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
test("start() does not emit missed-run feed event when counts are 0", () => {
|
|
1757
|
+
mockMarkStaleRunsAsMissed.mockImplementation(() => 0);
|
|
1758
|
+
mockMarkStaleRunningAsError.mockImplementation(() => 0);
|
|
1759
|
+
|
|
1760
|
+
const service = createService();
|
|
1761
|
+
service.start();
|
|
1762
|
+
|
|
1763
|
+
const missedCalls = mockEmitFeedEvent.mock.calls.filter(
|
|
1764
|
+
(call: unknown[]) => {
|
|
1765
|
+
const opts = call[0] as { title?: string };
|
|
1766
|
+
return opts.title === "Heartbeat Runs Missed";
|
|
1767
|
+
},
|
|
1768
|
+
);
|
|
1769
|
+
expect(missedCalls).toHaveLength(0);
|
|
1770
|
+
service.stop();
|
|
1771
|
+
});
|
|
1772
|
+
});
|
|
1056
1773
|
});
|