@vellumai/assistant 0.7.2 → 0.7.3
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 +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ── Mock state ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
// Provider mock
|
|
6
|
+
let decisionProviderAvailable = true;
|
|
7
|
+
let buildProviderAvailable = true;
|
|
8
|
+
let decisionResponse = "";
|
|
9
|
+
let buildResponse = "";
|
|
10
|
+
let copyResponse = "";
|
|
11
|
+
let providerSendCalls: Array<{ callSite: string; messages: unknown[] }> = [];
|
|
12
|
+
|
|
13
|
+
const mockProvider = (callSite: string) => ({
|
|
14
|
+
name: "mock-provider",
|
|
15
|
+
sendMessage: async (messages: unknown[]) => {
|
|
16
|
+
providerSendCalls.push({ callSite, messages });
|
|
17
|
+
if (callSite === "proactiveArtifactDecision") {
|
|
18
|
+
return { content: [{ type: "text", text: decisionResponse }] };
|
|
19
|
+
}
|
|
20
|
+
if (callSite === "proactiveArtifactBuild") {
|
|
21
|
+
// copyResponse is used when it's for message copy (second call)
|
|
22
|
+
const isCopyCall = providerSendCalls.filter(
|
|
23
|
+
(c) => c.callSite === "proactiveArtifactBuild",
|
|
24
|
+
).length;
|
|
25
|
+
if (isCopyCall > 1) {
|
|
26
|
+
return { content: [{ type: "text", text: copyResponse }] };
|
|
27
|
+
}
|
|
28
|
+
return { content: [{ type: "text", text: buildResponse }] };
|
|
29
|
+
}
|
|
30
|
+
return { content: [{ type: "text", text: "" }] };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
mock.module("../providers/provider-send-message.js", () => ({
|
|
35
|
+
getConfiguredProvider: async (callSite: string) => {
|
|
36
|
+
if (
|
|
37
|
+
callSite === "proactiveArtifactDecision" &&
|
|
38
|
+
!decisionProviderAvailable
|
|
39
|
+
) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (callSite === "proactiveArtifactBuild" && !buildProviderAvailable) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return mockProvider(callSite);
|
|
46
|
+
},
|
|
47
|
+
extractText: (response: { content: Array<{ type: string; text: string }> }) =>
|
|
48
|
+
response.content.find((b: { type: string }) => b.type === "text")?.text ??
|
|
49
|
+
"",
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// rawAll mock
|
|
53
|
+
let rawAllRows: Array<{ role: string; content: string }> = [];
|
|
54
|
+
|
|
55
|
+
mock.module("../memory/raw-query.js", () => ({
|
|
56
|
+
rawAll: () => rawAllRows,
|
|
57
|
+
rawRun: () => 0,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// bootstrapConversation mock
|
|
61
|
+
let bootstrapCalls: Array<Record<string, unknown>> = [];
|
|
62
|
+
|
|
63
|
+
mock.module("../memory/conversation-bootstrap.js", () => ({
|
|
64
|
+
bootstrapConversation: (opts: Record<string, unknown>) => {
|
|
65
|
+
bootstrapCalls.push(opts);
|
|
66
|
+
return { id: `bg-conv-${bootstrapCalls.length}` };
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// processMessage mock
|
|
71
|
+
let processMessageCalls: Array<{
|
|
72
|
+
conversationId: string;
|
|
73
|
+
prompt: string;
|
|
74
|
+
options: unknown;
|
|
75
|
+
}> = [];
|
|
76
|
+
let processMessageShouldThrow = false;
|
|
77
|
+
|
|
78
|
+
mock.module("../daemon/process-message.js", () => ({
|
|
79
|
+
processMessage: async (
|
|
80
|
+
conversationId: string,
|
|
81
|
+
prompt: string,
|
|
82
|
+
_attachmentIds: unknown,
|
|
83
|
+
options: unknown,
|
|
84
|
+
) => {
|
|
85
|
+
processMessageCalls.push({ conversationId, prompt, options });
|
|
86
|
+
if (processMessageShouldThrow) {
|
|
87
|
+
throw new Error("processMessage failed");
|
|
88
|
+
}
|
|
89
|
+
return { messageId: "pm-msg-1" };
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// App store mock
|
|
94
|
+
let mockApps: Array<{
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
createdAt: number;
|
|
98
|
+
updatedAt?: number;
|
|
99
|
+
conversationIds?: string[];
|
|
100
|
+
}> = [];
|
|
101
|
+
let addAppConvCalls: Array<{ appId: string; conversationId: string }> = [];
|
|
102
|
+
|
|
103
|
+
mock.module("../memory/app-store.js", () => ({
|
|
104
|
+
listApps: () => mockApps,
|
|
105
|
+
listAppsByConversation: (conversationId: string) =>
|
|
106
|
+
mockApps.filter((app) => app.conversationIds?.includes(conversationId)),
|
|
107
|
+
addAppConversationId: (appId: string, conversationId: string) => {
|
|
108
|
+
addAppConvCalls.push({ appId, conversationId });
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
// Document store mock
|
|
114
|
+
let saveDocumentCalls: Array<Record<string, unknown>> = [];
|
|
115
|
+
let saveDocumentResult: {
|
|
116
|
+
success: boolean;
|
|
117
|
+
surfaceId?: string;
|
|
118
|
+
error?: string;
|
|
119
|
+
} = {
|
|
120
|
+
success: true,
|
|
121
|
+
surfaceId: "doc-123",
|
|
122
|
+
};
|
|
123
|
+
let addDocConvCalls: Array<{ surfaceId: string; conversationId: string }> = [];
|
|
124
|
+
|
|
125
|
+
mock.module("../documents/document-store.js", () => ({
|
|
126
|
+
saveDocument: (params: Record<string, unknown>) => {
|
|
127
|
+
saveDocumentCalls.push(params);
|
|
128
|
+
return saveDocumentResult;
|
|
129
|
+
},
|
|
130
|
+
addDocumentConversation: (surfaceId: string, conversationId: string) => {
|
|
131
|
+
addDocConvCalls.push({ surfaceId, conversationId });
|
|
132
|
+
},
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
// addMessage mock
|
|
136
|
+
let addMessageCalls: Array<{
|
|
137
|
+
conversationId: string;
|
|
138
|
+
role: string;
|
|
139
|
+
content: string;
|
|
140
|
+
metadata: unknown;
|
|
141
|
+
opts: unknown;
|
|
142
|
+
}> = [];
|
|
143
|
+
|
|
144
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
145
|
+
addMessage: async (
|
|
146
|
+
conversationId: string,
|
|
147
|
+
role: string,
|
|
148
|
+
content: string,
|
|
149
|
+
metadata: unknown,
|
|
150
|
+
opts: unknown,
|
|
151
|
+
) => {
|
|
152
|
+
addMessageCalls.push({ conversationId, role, content, metadata, opts });
|
|
153
|
+
return { id: `msg-${addMessageCalls.length}` };
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
// emitNotificationSignal mock
|
|
158
|
+
let emitSignalCalls: Array<Record<string, unknown>> = [];
|
|
159
|
+
|
|
160
|
+
mock.module("../notifications/emit-signal.js", () => ({
|
|
161
|
+
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
162
|
+
emitSignalCalls.push(params);
|
|
163
|
+
return {
|
|
164
|
+
signalId: "signal-1",
|
|
165
|
+
deduplicated: false,
|
|
166
|
+
dispatched: true,
|
|
167
|
+
reason: "ok",
|
|
168
|
+
deliveryResults: [],
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
// findConversation mock
|
|
174
|
+
type MockConversation = {
|
|
175
|
+
processing: boolean;
|
|
176
|
+
messages: unknown[];
|
|
177
|
+
getMessages: () => unknown[];
|
|
178
|
+
};
|
|
179
|
+
let mockConversations: Map<string, MockConversation> = new Map();
|
|
180
|
+
|
|
181
|
+
mock.module("../daemon/conversation-store.js", () => ({
|
|
182
|
+
findConversation: (id: string) => mockConversations.get(id),
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
// createAssistantMessage mock
|
|
186
|
+
mock.module("../agent/message-types.js", () => ({
|
|
187
|
+
createAssistantMessage: (text: string) => ({
|
|
188
|
+
role: "assistant",
|
|
189
|
+
content: [{ type: "text", text }],
|
|
190
|
+
}),
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
// Trigger state mock
|
|
194
|
+
let releaseClaimCalls = 0;
|
|
195
|
+
|
|
196
|
+
mock.module("./trigger-state.js", () => ({
|
|
197
|
+
releaseProactiveArtifactClaim: () => {
|
|
198
|
+
releaseClaimCalls++;
|
|
199
|
+
},
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Trust context mock
|
|
203
|
+
mock.module("../daemon/trust-context.js", () => ({
|
|
204
|
+
INTERNAL_GUARDIAN_TRUST_CONTEXT: {
|
|
205
|
+
sourceChannel: "vellum",
|
|
206
|
+
trustClass: "guardian",
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
// Logger mock
|
|
211
|
+
let logWarnCalls: Array<{ args: unknown[] }> = [];
|
|
212
|
+
|
|
213
|
+
mock.module("../util/logger.js", () => ({
|
|
214
|
+
getLogger: () => ({
|
|
215
|
+
info: () => {},
|
|
216
|
+
warn: (...args: unknown[]) => {
|
|
217
|
+
logWarnCalls.push({ args });
|
|
218
|
+
},
|
|
219
|
+
error: () => {},
|
|
220
|
+
debug: () => {},
|
|
221
|
+
}),
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// uuid mock — deterministic IDs for testing
|
|
225
|
+
let uuidCounter = 0;
|
|
226
|
+
mock.module("uuid", () => ({
|
|
227
|
+
v4: () => `test-uuid-${++uuidCounter}`,
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
// ── Import SUT after mocks ─────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
const { runProactiveArtifactJob } = await import("./job.js");
|
|
233
|
+
const { injectAuxAssistantMessage } = await import("./aux-message-injector.js");
|
|
234
|
+
const { buildMessageCopyPrompt, parseMessageCopy } =
|
|
235
|
+
await import("./message-copy.js");
|
|
236
|
+
|
|
237
|
+
// ── Test helpers ────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
let broadcastCalls: Array<Record<string, unknown>> = [];
|
|
240
|
+
const mockBroadcast: any = (msg: Record<string, unknown>) => {
|
|
241
|
+
broadcastCalls.push(msg);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const defaultTranscript = [
|
|
245
|
+
{ role: "user", content: "Hello there" },
|
|
246
|
+
{ role: "assistant", content: "Hi! How can I help?" },
|
|
247
|
+
{ role: "user", content: "I need a budget tracker" },
|
|
248
|
+
{ role: "assistant", content: "I can help with that." },
|
|
249
|
+
{ role: "user", content: "I spend about $3000 per month" },
|
|
250
|
+
{ role: "assistant", content: "Got it, that is useful context." },
|
|
251
|
+
{ role: "user", content: "Yes, let us track groceries and rent" },
|
|
252
|
+
{
|
|
253
|
+
role: "assistant",
|
|
254
|
+
content: "Great, I will remember those categories.",
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const decisionYesApp = `SHOULD_BUILD: yes
|
|
259
|
+
ARTIFACT_TYPE: app
|
|
260
|
+
ARTIFACT_TITLE: Budget Tracker
|
|
261
|
+
ARTIFACT_DESCRIPTION: A budget tracking app for monthly expenses around $3000, focusing on groceries and rent.`;
|
|
262
|
+
|
|
263
|
+
const decisionYesDocument = `SHOULD_BUILD: yes
|
|
264
|
+
ARTIFACT_TYPE: document
|
|
265
|
+
ARTIFACT_TITLE: Monthly Budget Guide
|
|
266
|
+
ARTIFACT_DESCRIPTION: A structured guide for tracking monthly expenses with categories for groceries and rent.`;
|
|
267
|
+
|
|
268
|
+
const decisionNo = `SHOULD_BUILD: no
|
|
269
|
+
SKIP_REASON: Not enough context to build something specific.`;
|
|
270
|
+
|
|
271
|
+
function resetState() {
|
|
272
|
+
decisionProviderAvailable = true;
|
|
273
|
+
buildProviderAvailable = true;
|
|
274
|
+
decisionResponse = "";
|
|
275
|
+
buildResponse = "";
|
|
276
|
+
copyResponse = "";
|
|
277
|
+
providerSendCalls = [];
|
|
278
|
+
rawAllRows = [];
|
|
279
|
+
bootstrapCalls = [];
|
|
280
|
+
processMessageCalls = [];
|
|
281
|
+
processMessageShouldThrow = false;
|
|
282
|
+
mockApps = [];
|
|
283
|
+
addAppConvCalls = [];
|
|
284
|
+
saveDocumentCalls = [];
|
|
285
|
+
saveDocumentResult = { success: true, surfaceId: "doc-123" };
|
|
286
|
+
addDocConvCalls = [];
|
|
287
|
+
releaseClaimCalls = 0;
|
|
288
|
+
addMessageCalls = [];
|
|
289
|
+
emitSignalCalls = [];
|
|
290
|
+
broadcastCalls = [];
|
|
291
|
+
mockConversations = new Map();
|
|
292
|
+
logWarnCalls = [];
|
|
293
|
+
uuidCounter = 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Tests ───────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
beforeEach(() => {
|
|
299
|
+
resetState();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
afterEach(() => {
|
|
303
|
+
resetState();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("runProactiveArtifactJob", () => {
|
|
307
|
+
describe("Phase 1 — Decision", () => {
|
|
308
|
+
test("shouldBuild:false → releases claim, no Phase 2", async () => {
|
|
309
|
+
rawAllRows = defaultTranscript;
|
|
310
|
+
decisionResponse = decisionNo;
|
|
311
|
+
|
|
312
|
+
await runProactiveArtifactJob({
|
|
313
|
+
conversationId: "conv-1",
|
|
314
|
+
userMessageCutoff: 1000,
|
|
315
|
+
assistantMessageId: "msg-4",
|
|
316
|
+
broadcastMessage: mockBroadcast,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(releaseClaimCalls).toBe(1);
|
|
320
|
+
expect(bootstrapCalls).toHaveLength(0);
|
|
321
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
322
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
323
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
324
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("null (malformed) → releases claim, silent exit", async () => {
|
|
328
|
+
rawAllRows = defaultTranscript;
|
|
329
|
+
decisionResponse = "THIS IS GARBAGE OUTPUT";
|
|
330
|
+
|
|
331
|
+
await runProactiveArtifactJob({
|
|
332
|
+
conversationId: "conv-1",
|
|
333
|
+
userMessageCutoff: 1000,
|
|
334
|
+
assistantMessageId: "msg-4",
|
|
335
|
+
broadcastMessage: mockBroadcast,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(releaseClaimCalls).toBe(1);
|
|
339
|
+
expect(bootstrapCalls).toHaveLength(0);
|
|
340
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
341
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
342
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("provider unavailable → releases claim, silent return", async () => {
|
|
346
|
+
rawAllRows = defaultTranscript;
|
|
347
|
+
decisionProviderAvailable = false;
|
|
348
|
+
|
|
349
|
+
await runProactiveArtifactJob({
|
|
350
|
+
conversationId: "conv-1",
|
|
351
|
+
userMessageCutoff: 1000,
|
|
352
|
+
assistantMessageId: "msg-4",
|
|
353
|
+
broadcastMessage: mockBroadcast,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(releaseClaimCalls).toBe(1);
|
|
357
|
+
expect(providerSendCalls).toHaveLength(0);
|
|
358
|
+
expect(bootstrapCalls).toHaveLength(0);
|
|
359
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
360
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("Phase 2 — Build", () => {
|
|
365
|
+
test("Phase 2 failure → releases claim, no message, no notification", async () => {
|
|
366
|
+
rawAllRows = defaultTranscript;
|
|
367
|
+
decisionResponse = decisionYesApp;
|
|
368
|
+
processMessageShouldThrow = true;
|
|
369
|
+
|
|
370
|
+
await runProactiveArtifactJob({
|
|
371
|
+
conversationId: "conv-1",
|
|
372
|
+
userMessageCutoff: 1000,
|
|
373
|
+
assistantMessageId: "msg-4",
|
|
374
|
+
broadcastMessage: mockBroadcast,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// processMessage was called (the build attempt happened)
|
|
378
|
+
expect(processMessageCalls).toHaveLength(1);
|
|
379
|
+
// But no message injection or notification
|
|
380
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
381
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
382
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
383
|
+
// Claim released so next turn can retry
|
|
384
|
+
expect(releaseClaimCalls).toBe(1);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("successful app: Phase 1 → Phase 2 → app store query → message copy → inject → notify", async () => {
|
|
388
|
+
rawAllRows = defaultTranscript;
|
|
389
|
+
decisionResponse = decisionYesApp;
|
|
390
|
+
copyResponse = "MESSAGE: I built a budget tracker for you!";
|
|
391
|
+
|
|
392
|
+
const buildStartedAt = Date.now();
|
|
393
|
+
mockApps = [
|
|
394
|
+
{
|
|
395
|
+
id: "app-123",
|
|
396
|
+
name: "Budget Tracker",
|
|
397
|
+
createdAt: buildStartedAt + 100,
|
|
398
|
+
updatedAt: buildStartedAt + 100,
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
// Set up an idle conversation so injection works fully
|
|
403
|
+
const convMessages: unknown[] = [];
|
|
404
|
+
mockConversations.set("conv-1", {
|
|
405
|
+
processing: false,
|
|
406
|
+
messages: convMessages,
|
|
407
|
+
getMessages: () => convMessages,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await runProactiveArtifactJob({
|
|
411
|
+
conversationId: "conv-1",
|
|
412
|
+
userMessageCutoff: 1000,
|
|
413
|
+
assistantMessageId: "msg-4",
|
|
414
|
+
broadcastMessage: mockBroadcast,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Phase 1: decision provider called
|
|
418
|
+
expect(
|
|
419
|
+
providerSendCalls.some(
|
|
420
|
+
(c) => c.callSite === "proactiveArtifactDecision",
|
|
421
|
+
),
|
|
422
|
+
).toBe(true);
|
|
423
|
+
|
|
424
|
+
// Phase 2: processMessage called with correct options
|
|
425
|
+
expect(processMessageCalls).toHaveLength(1);
|
|
426
|
+
expect(processMessageCalls[0].prompt).toContain("Budget Tracker");
|
|
427
|
+
expect(processMessageCalls[0].prompt).toContain("auto_open: false");
|
|
428
|
+
const pmOpts = processMessageCalls[0].options as Record<string, unknown>;
|
|
429
|
+
expect(pmOpts.callSite).toBe("proactiveArtifactBuild");
|
|
430
|
+
expect(pmOpts.trustContext).toEqual({
|
|
431
|
+
sourceChannel: "vellum",
|
|
432
|
+
trustClass: "guardian",
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Bootstrap conversation created for app build
|
|
436
|
+
expect(bootstrapCalls).toHaveLength(1);
|
|
437
|
+
expect(bootstrapCalls[0].conversationType).toBe("background");
|
|
438
|
+
expect(bootstrapCalls[0].source).toBe("proactive_artifact");
|
|
439
|
+
|
|
440
|
+
// App associated with user's conversation for Assets chip
|
|
441
|
+
expect(addAppConvCalls).toHaveLength(1);
|
|
442
|
+
expect(addAppConvCalls[0].appId).toBe("app-123");
|
|
443
|
+
expect(addAppConvCalls[0].conversationId).toBe("conv-1");
|
|
444
|
+
|
|
445
|
+
// Message injection: addMessage called with skipIndexing
|
|
446
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
447
|
+
expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
|
|
448
|
+
expect(addMessageCalls[0].conversationId).toBe("conv-1");
|
|
449
|
+
|
|
450
|
+
// Notification emitted
|
|
451
|
+
expect(emitSignalCalls).toHaveLength(1);
|
|
452
|
+
expect(emitSignalCalls[0].sourceEventName).toBe("activity.complete");
|
|
453
|
+
expect(emitSignalCalls[0].sourceChannel).toBe("vellum");
|
|
454
|
+
expect(emitSignalCalls[0].dedupeKey).toBe("proactive-artifact");
|
|
455
|
+
const hints = emitSignalCalls[0].attentionHints as Record<
|
|
456
|
+
string,
|
|
457
|
+
unknown
|
|
458
|
+
>;
|
|
459
|
+
expect(hints.visibleInSourceNow).toBe(false);
|
|
460
|
+
expect(hints.isAsyncBackground).toBe(true);
|
|
461
|
+
expect(hints.requiresAction).toBe(false);
|
|
462
|
+
// No conversationAffinityHint
|
|
463
|
+
expect(emitSignalCalls[0].conversationAffinityHint).toBeUndefined();
|
|
464
|
+
|
|
465
|
+
// Claim NOT released on success — guard stays permanent
|
|
466
|
+
expect(releaseClaimCalls).toBe(0);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("successful document: Phase 1 → content gen → saveDocument → message copy → inject → notify", async () => {
|
|
470
|
+
rawAllRows = defaultTranscript;
|
|
471
|
+
decisionResponse = decisionYesDocument;
|
|
472
|
+
buildResponse =
|
|
473
|
+
"# Monthly Budget Guide\n\nTrack groceries and rent expenses.";
|
|
474
|
+
copyResponse =
|
|
475
|
+
"MESSAGE: I created a monthly budget guide tailored to your needs.";
|
|
476
|
+
|
|
477
|
+
mockConversations.set("conv-1", {
|
|
478
|
+
processing: false,
|
|
479
|
+
messages: [],
|
|
480
|
+
getMessages: () => [],
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await runProactiveArtifactJob({
|
|
484
|
+
conversationId: "conv-1",
|
|
485
|
+
userMessageCutoff: 1000,
|
|
486
|
+
assistantMessageId: "msg-4",
|
|
487
|
+
broadcastMessage: mockBroadcast,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Document saved via saveDocument (not raw file writes)
|
|
491
|
+
expect(saveDocumentCalls).toHaveLength(1);
|
|
492
|
+
expect(saveDocumentCalls[0].title).toBe("Monthly Budget Guide");
|
|
493
|
+
expect(saveDocumentCalls[0].conversationId).toBe("conv-1");
|
|
494
|
+
expect(saveDocumentCalls[0].content).toContain("Monthly Budget Guide");
|
|
495
|
+
expect(
|
|
496
|
+
(saveDocumentCalls[0].surfaceId as string).startsWith("doc-"),
|
|
497
|
+
).toBe(true);
|
|
498
|
+
expect((saveDocumentCalls[0].wordCount as number) > 0).toBe(true);
|
|
499
|
+
|
|
500
|
+
// No bootstrapConversation or processMessage for document path
|
|
501
|
+
expect(bootstrapCalls).toHaveLength(0);
|
|
502
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
503
|
+
|
|
504
|
+
// Message injection and notification
|
|
505
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
506
|
+
expect(emitSignalCalls).toHaveLength(1);
|
|
507
|
+
|
|
508
|
+
// Claim NOT released on success
|
|
509
|
+
expect(releaseClaimCalls).toBe(0);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("app build - no matching app found → releases claim for retry", async () => {
|
|
513
|
+
rawAllRows = defaultTranscript;
|
|
514
|
+
decisionResponse = decisionYesApp;
|
|
515
|
+
mockApps = []; // no apps in store
|
|
516
|
+
|
|
517
|
+
await runProactiveArtifactJob({
|
|
518
|
+
conversationId: "conv-1",
|
|
519
|
+
userMessageCutoff: 1000,
|
|
520
|
+
assistantMessageId: "msg-4",
|
|
521
|
+
broadcastMessage: mockBroadcast,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
expect(processMessageCalls).toHaveLength(1);
|
|
525
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
526
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
527
|
+
expect(releaseClaimCalls).toBe(1);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("app decision with foreground app tool suppresses background build permanently", async () => {
|
|
531
|
+
rawAllRows = defaultTranscript;
|
|
532
|
+
decisionResponse = decisionYesApp;
|
|
533
|
+
|
|
534
|
+
await runProactiveArtifactJob({
|
|
535
|
+
conversationId: "conv-1",
|
|
536
|
+
userMessageCutoff: 1000,
|
|
537
|
+
assistantMessageId: "msg-4",
|
|
538
|
+
suppressAppBuild: true,
|
|
539
|
+
broadcastMessage: mockBroadcast,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(bootstrapCalls).toHaveLength(0);
|
|
543
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
544
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
545
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
546
|
+
expect(releaseClaimCalls).toBe(0);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("app decision with recent app activity in source conversation suppresses background build permanently", async () => {
|
|
550
|
+
rawAllRows = defaultTranscript;
|
|
551
|
+
decisionResponse = decisionYesApp;
|
|
552
|
+
mockApps = [
|
|
553
|
+
{
|
|
554
|
+
id: "app-main",
|
|
555
|
+
name: "Budget Tracker",
|
|
556
|
+
createdAt: 1200,
|
|
557
|
+
updatedAt: 1300,
|
|
558
|
+
conversationIds: ["conv-1"],
|
|
559
|
+
},
|
|
560
|
+
];
|
|
561
|
+
|
|
562
|
+
await runProactiveArtifactJob({
|
|
563
|
+
conversationId: "conv-1",
|
|
564
|
+
userMessageCutoff: 1000,
|
|
565
|
+
assistantMessageId: "msg-4",
|
|
566
|
+
broadcastMessage: mockBroadcast,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
expect(bootstrapCalls).toHaveLength(0);
|
|
570
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
571
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
572
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
573
|
+
expect(releaseClaimCalls).toBe(0);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("document build - build provider unavailable → releases claim for retry", async () => {
|
|
577
|
+
rawAllRows = defaultTranscript;
|
|
578
|
+
decisionResponse = decisionYesDocument;
|
|
579
|
+
buildProviderAvailable = false;
|
|
580
|
+
|
|
581
|
+
await runProactiveArtifactJob({
|
|
582
|
+
conversationId: "conv-1",
|
|
583
|
+
userMessageCutoff: 1000,
|
|
584
|
+
assistantMessageId: "msg-4",
|
|
585
|
+
broadcastMessage: mockBroadcast,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// No message, no notification
|
|
589
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
590
|
+
expect(emitSignalCalls).toHaveLength(0);
|
|
591
|
+
expect(releaseClaimCalls).toBe(1);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe("Message copy", () => {
|
|
596
|
+
test("uses fallback message when copy provider unavailable", async () => {
|
|
597
|
+
rawAllRows = defaultTranscript;
|
|
598
|
+
decisionResponse = decisionYesApp;
|
|
599
|
+
// Build provider is unavailable for copy step, but we need it for
|
|
600
|
+
// the copy call. Since the same callSite is used, we'll test the
|
|
601
|
+
// fallback by making the copy return unparseable output.
|
|
602
|
+
buildProviderAvailable = true;
|
|
603
|
+
copyResponse = "INVALID OUTPUT WITHOUT MESSAGE PREFIX";
|
|
604
|
+
|
|
605
|
+
const buildTime = Date.now();
|
|
606
|
+
mockApps = [
|
|
607
|
+
{ id: "app-456", name: "Budget Tracker", createdAt: buildTime + 50 },
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
mockConversations.set("conv-1", {
|
|
611
|
+
processing: false,
|
|
612
|
+
messages: [],
|
|
613
|
+
getMessages: () => [],
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
await runProactiveArtifactJob({
|
|
617
|
+
conversationId: "conv-1",
|
|
618
|
+
userMessageCutoff: 1000,
|
|
619
|
+
assistantMessageId: "msg-4",
|
|
620
|
+
broadcastMessage: mockBroadcast,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Verify fallback message was used
|
|
624
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
625
|
+
const content = JSON.parse(addMessageCalls[0].content);
|
|
626
|
+
expect(content[0].text).toContain("I made something for you");
|
|
627
|
+
expect(content[0].text).toContain("Budget Tracker");
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe("Transcript collection", () => {
|
|
632
|
+
test("dual-condition transcript query uses userMessageCutoff and assistantMessageId", async () => {
|
|
633
|
+
rawAllRows = defaultTranscript;
|
|
634
|
+
decisionResponse = decisionNo;
|
|
635
|
+
|
|
636
|
+
await runProactiveArtifactJob({
|
|
637
|
+
conversationId: "conv-1",
|
|
638
|
+
userMessageCutoff: 5000,
|
|
639
|
+
assistantMessageId: "asst-msg-99",
|
|
640
|
+
broadcastMessage: mockBroadcast,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// The rawAll mock captures all calls; verify the decision was called
|
|
644
|
+
// (proving transcript was collected and passed to decision)
|
|
645
|
+
expect(
|
|
646
|
+
providerSendCalls.some(
|
|
647
|
+
(c) => c.callSite === "proactiveArtifactDecision",
|
|
648
|
+
),
|
|
649
|
+
).toBe(true);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("empty transcript → early return", async () => {
|
|
653
|
+
rawAllRows = [];
|
|
654
|
+
|
|
655
|
+
await runProactiveArtifactJob({
|
|
656
|
+
conversationId: "conv-1",
|
|
657
|
+
userMessageCutoff: 1000,
|
|
658
|
+
assistantMessageId: "msg-4",
|
|
659
|
+
broadcastMessage: mockBroadcast,
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
expect(releaseClaimCalls).toBe(1);
|
|
663
|
+
expect(providerSendCalls).toHaveLength(0);
|
|
664
|
+
expect(addMessageCalls).toHaveLength(0);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
describe("injectAuxAssistantMessage", () => {
|
|
670
|
+
test("idle conversation: persists with skipIndexing, pushes to getMessages(), broadcasts delta + complete(aux) + list_invalidated", async () => {
|
|
671
|
+
const messages: unknown[] = [];
|
|
672
|
+
mockConversations.set("conv-inject-1", {
|
|
673
|
+
processing: false,
|
|
674
|
+
messages,
|
|
675
|
+
getMessages: () => messages,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await injectAuxAssistantMessage({
|
|
679
|
+
conversationId: "conv-inject-1",
|
|
680
|
+
text: "Here is your artifact!",
|
|
681
|
+
broadcastMessage: mockBroadcast,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Persisted with skipIndexing
|
|
685
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
686
|
+
expect(addMessageCalls[0].conversationId).toBe("conv-inject-1");
|
|
687
|
+
expect(addMessageCalls[0].role).toBe("assistant");
|
|
688
|
+
expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
|
|
689
|
+
|
|
690
|
+
// Pushed to in-memory messages
|
|
691
|
+
expect(messages).toHaveLength(1);
|
|
692
|
+
|
|
693
|
+
// Broadcasts: delta, complete(aux), list_invalidated
|
|
694
|
+
const deltaMsg = broadcastCalls.find(
|
|
695
|
+
(c) => c.type === "assistant_text_delta",
|
|
696
|
+
);
|
|
697
|
+
expect(deltaMsg).toBeDefined();
|
|
698
|
+
expect(deltaMsg!.text).toBe("Here is your artifact!");
|
|
699
|
+
expect(deltaMsg!.conversationId).toBe("conv-inject-1");
|
|
700
|
+
|
|
701
|
+
const completeMsg = broadcastCalls.find(
|
|
702
|
+
(c) => c.type === "message_complete",
|
|
703
|
+
);
|
|
704
|
+
expect(completeMsg).toBeDefined();
|
|
705
|
+
expect(completeMsg!.source).toBe("aux");
|
|
706
|
+
expect(completeMsg!.messageId).toBeDefined();
|
|
707
|
+
|
|
708
|
+
const listMsg = broadcastCalls.find(
|
|
709
|
+
(c) => c.type === "conversation_list_invalidated",
|
|
710
|
+
);
|
|
711
|
+
expect(listMsg).toBeDefined();
|
|
712
|
+
expect(listMsg!.reason).toBe("reordered");
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("processing → idle: waits for processing to become false before persisting", async () => {
|
|
716
|
+
const messages: unknown[] = [];
|
|
717
|
+
let processingFlag = true;
|
|
718
|
+
const conv: MockConversation = {
|
|
719
|
+
get processing() {
|
|
720
|
+
return processingFlag;
|
|
721
|
+
},
|
|
722
|
+
set processing(v: boolean) {
|
|
723
|
+
processingFlag = v;
|
|
724
|
+
},
|
|
725
|
+
messages,
|
|
726
|
+
getMessages: () => messages,
|
|
727
|
+
};
|
|
728
|
+
mockConversations.set("conv-inject-2", conv);
|
|
729
|
+
|
|
730
|
+
// Simulate processing becoming idle after a short delay
|
|
731
|
+
setTimeout(() => {
|
|
732
|
+
processingFlag = false;
|
|
733
|
+
}, 100);
|
|
734
|
+
|
|
735
|
+
await injectAuxAssistantMessage({
|
|
736
|
+
conversationId: "conv-inject-2",
|
|
737
|
+
text: "Deferred message",
|
|
738
|
+
broadcastMessage: mockBroadcast,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Should have waited and then injected
|
|
742
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
743
|
+
expect(messages).toHaveLength(1);
|
|
744
|
+
expect(broadcastCalls.some((c) => c.type === "assistant_text_delta")).toBe(
|
|
745
|
+
true,
|
|
746
|
+
);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("processing → timeout: injects anyway with warning after poll timeout", async () => {
|
|
750
|
+
const messages: unknown[] = [];
|
|
751
|
+
// Conversation stays processing permanently — never becomes idle
|
|
752
|
+
const conv: MockConversation = {
|
|
753
|
+
processing: true,
|
|
754
|
+
messages,
|
|
755
|
+
getMessages: () => messages,
|
|
756
|
+
};
|
|
757
|
+
mockConversations.set("conv-inject-3", conv);
|
|
758
|
+
|
|
759
|
+
// Mock Date.now() to simulate time past the 60s timeout.
|
|
760
|
+
// First call sets `start`, second call must exceed IDLE_TIMEOUT_MS (60_000).
|
|
761
|
+
const realDateNow = Date.now;
|
|
762
|
+
let dateNowCallCount = 0;
|
|
763
|
+
const baseTime = 1_000_000;
|
|
764
|
+
Date.now = () => {
|
|
765
|
+
dateNowCallCount++;
|
|
766
|
+
// First call: start = baseTime
|
|
767
|
+
// Second call onward: past the timeout
|
|
768
|
+
if (dateNowCallCount <= 1) return baseTime;
|
|
769
|
+
return baseTime + 60_001;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
try {
|
|
773
|
+
await injectAuxAssistantMessage({
|
|
774
|
+
conversationId: "conv-inject-3",
|
|
775
|
+
text: "Timeout message",
|
|
776
|
+
broadcastMessage: mockBroadcast,
|
|
777
|
+
});
|
|
778
|
+
} finally {
|
|
779
|
+
Date.now = realDateNow;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Message was still persisted despite timeout
|
|
783
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
784
|
+
expect(addMessageCalls[0].conversationId).toBe("conv-inject-3");
|
|
785
|
+
|
|
786
|
+
// Warning log was emitted about the timeout
|
|
787
|
+
expect(logWarnCalls.length).toBeGreaterThanOrEqual(1);
|
|
788
|
+
const warnMsg = logWarnCalls.find((c) =>
|
|
789
|
+
c.args.some(
|
|
790
|
+
(arg) => typeof arg === "string" && arg.includes("Timed out"),
|
|
791
|
+
),
|
|
792
|
+
);
|
|
793
|
+
expect(warnMsg).toBeDefined();
|
|
794
|
+
|
|
795
|
+
// Since conversation is still processing, no delta/complete broadcasts
|
|
796
|
+
expect(
|
|
797
|
+
broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
|
|
798
|
+
).toHaveLength(0);
|
|
799
|
+
expect(
|
|
800
|
+
broadcastCalls.filter((c) => c.type === "message_complete"),
|
|
801
|
+
).toHaveLength(0);
|
|
802
|
+
|
|
803
|
+
// But list_invalidated IS sent (always sent regardless of processing state)
|
|
804
|
+
expect(
|
|
805
|
+
broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
|
|
806
|
+
).toHaveLength(1);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("inactive/unloaded conversation: persists + list_invalidated only", async () => {
|
|
810
|
+
// No conversation in the store
|
|
811
|
+
await injectAuxAssistantMessage({
|
|
812
|
+
conversationId: "conv-inject-4",
|
|
813
|
+
text: "Offline message",
|
|
814
|
+
broadcastMessage: mockBroadcast,
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// Message persisted
|
|
818
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
819
|
+
expect(addMessageCalls[0].conversationId).toBe("conv-inject-4");
|
|
820
|
+
|
|
821
|
+
// No delta or complete (conversation not loaded)
|
|
822
|
+
expect(
|
|
823
|
+
broadcastCalls.filter((c) => c.type === "assistant_text_delta"),
|
|
824
|
+
).toHaveLength(0);
|
|
825
|
+
expect(
|
|
826
|
+
broadcastCalls.filter((c) => c.type === "message_complete"),
|
|
827
|
+
).toHaveLength(0);
|
|
828
|
+
|
|
829
|
+
// But list_invalidated IS sent
|
|
830
|
+
expect(
|
|
831
|
+
broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
|
|
832
|
+
).toHaveLength(1);
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
describe("message-copy", () => {
|
|
837
|
+
test("buildMessageCopyPrompt includes all parameters", () => {
|
|
838
|
+
const prompt = buildMessageCopyPrompt({
|
|
839
|
+
artifactType: "app",
|
|
840
|
+
artifactTitle: "Budget Tracker",
|
|
841
|
+
artifactId: "app-123",
|
|
842
|
+
transcript: "[User]: I need a budget tool",
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
expect(prompt).toContain("app");
|
|
846
|
+
expect(prompt).toContain("Budget Tracker");
|
|
847
|
+
expect(prompt).toContain("app-123");
|
|
848
|
+
expect(prompt).toContain("I need a budget tool");
|
|
849
|
+
expect(prompt).toContain("MESSAGE:");
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("parseMessageCopy extracts MESSAGE value", () => {
|
|
853
|
+
expect(parseMessageCopy("MESSAGE: Hello there!")).toBe("Hello there!");
|
|
854
|
+
expect(
|
|
855
|
+
parseMessageCopy("MESSAGE: I built something special for you."),
|
|
856
|
+
).toBe("I built something special for you.");
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
test("parseMessageCopy returns null for missing MESSAGE", () => {
|
|
860
|
+
expect(parseMessageCopy("Some random text")).toBeNull();
|
|
861
|
+
expect(parseMessageCopy("")).toBeNull();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("parseMessageCopy returns null for empty MESSAGE", () => {
|
|
865
|
+
expect(parseMessageCopy("MESSAGE: ")).toBeNull();
|
|
866
|
+
});
|
|
867
|
+
});
|