@vellumai/assistant 0.6.0 → 0.6.1

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.
Files changed (285) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,291 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ getMockFetchCalls,
5
+ mockFetch,
6
+ resetMockFetch,
7
+ } from "../../../__tests__/mock-fetch.js";
8
+ import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js";
9
+ import { setPlatformAssistantId } from "../../../config/env.js";
10
+ import { credentialKey } from "../../../security/credential-key.js";
11
+ import {
12
+ _resetBackend,
13
+ setSecureKeyAsync,
14
+ } from "../../../security/secure-keys.js";
15
+ import { runAssistantCommand } from "../../__tests__/run-assistant-command.js";
16
+
17
+ const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
18
+ const ADDRESS_ID = "550e8400-e29b-41d4-a716-446655440000";
19
+ const ADDRESS = "mybot@vellum.me";
20
+ const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key");
21
+
22
+ function mockListAddresses(
23
+ addresses: { id: string; address: string }[] = [
24
+ { id: ADDRESS_ID, address: ADDRESS },
25
+ ],
26
+ status = 200,
27
+ ): void {
28
+ mockFetch("/email-addresses/", {}, { body: { results: addresses }, status });
29
+ }
30
+
31
+ function mockSendSuccess(deliveryId = "del_abc123", status = 202): void {
32
+ mockFetch(
33
+ "/runtime-proxy/email/send/",
34
+ { method: "POST" },
35
+ { body: { delivery_id: deliveryId, status: "accepted" }, status },
36
+ );
37
+ }
38
+
39
+ beforeEach(async () => {
40
+ process.exitCode = 0;
41
+ _resetBackend();
42
+ resetMockFetch();
43
+ _setOverridesForTesting({ "email-channel": true });
44
+ setPlatformAssistantId(ASSISTANT_ID);
45
+ await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key");
46
+ });
47
+
48
+ afterEach(() => {
49
+ resetMockFetch();
50
+ _setOverridesForTesting({});
51
+ setPlatformAssistantId(undefined);
52
+ _resetBackend();
53
+ });
54
+
55
+ describe("assistant email send", () => {
56
+ test("successful send with --body and --subject", async () => {
57
+ mockListAddresses();
58
+ mockSendSuccess();
59
+
60
+ await runAssistantCommand(
61
+ "email",
62
+ "send",
63
+ "user@example.com",
64
+ "-s",
65
+ "Hello",
66
+ "-b",
67
+ "Hi there",
68
+ );
69
+
70
+ const calls = getMockFetchCalls();
71
+ expect(calls).toHaveLength(2);
72
+
73
+ // First call: list addresses to resolve "from"
74
+ expect(calls[0].path).toBe(
75
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/`,
76
+ );
77
+
78
+ // Second call: send via runtime proxy
79
+ expect(calls[1].path).toBe("/v1/runtime-proxy/email/send/");
80
+ expect(calls[1].init.method).toBe("POST");
81
+
82
+ const payload = JSON.parse(calls[1].init.body as string);
83
+ expect(payload.to).toBe("user@example.com");
84
+ expect(payload.from_address).toBe(ADDRESS);
85
+ expect(payload.subject).toBe("Hello");
86
+ expect(payload.text).toBe("Hi there");
87
+
88
+ expect(process.exitCode).toBe(0);
89
+ });
90
+
91
+ test("--json outputs structured response", async () => {
92
+ mockListAddresses();
93
+ mockSendSuccess("del_xyz789");
94
+
95
+ const output = await runAssistantCommand(
96
+ "email",
97
+ "--json",
98
+ "send",
99
+ "user@example.com",
100
+ "-s",
101
+ "Test",
102
+ "-b",
103
+ "Body",
104
+ );
105
+
106
+ const parsed = JSON.parse(output.trim());
107
+ expect(parsed.delivery_id).toBe("del_xyz789");
108
+ expect(parsed.status).toBe("accepted");
109
+ expect(process.exitCode).toBe(0);
110
+ });
111
+
112
+ test("no registered address returns error", async () => {
113
+ mockListAddresses([]);
114
+
115
+ const output = await runAssistantCommand(
116
+ "email",
117
+ "--json",
118
+ "send",
119
+ "user@example.com",
120
+ "-s",
121
+ "Hello",
122
+ "-b",
123
+ "Body",
124
+ );
125
+
126
+ expect(process.exitCode).toBe(1);
127
+ const parsed = JSON.parse(output.trim());
128
+ expect(parsed.error).toContain("No email address registered");
129
+ });
130
+
131
+ test("missing body returns error", async () => {
132
+ // Force isTTY to true so stdin fallback is skipped
133
+ const origIsTTY = process.stdin.isTTY;
134
+ process.stdin.isTTY = true as unknown as boolean;
135
+
136
+ mockListAddresses();
137
+
138
+ const output = await runAssistantCommand(
139
+ "email",
140
+ "--json",
141
+ "send",
142
+ "user@example.com",
143
+ "-s",
144
+ "Hello",
145
+ );
146
+
147
+ process.stdin.isTTY = origIsTTY;
148
+
149
+ expect(process.exitCode).toBe(1);
150
+ const parsed = JSON.parse(output.trim());
151
+ expect(parsed.error).toContain("Email body is required");
152
+ });
153
+
154
+ test("send endpoint failure surfaces error detail", async () => {
155
+ mockListAddresses();
156
+ mockFetch(
157
+ "/runtime-proxy/email/send/",
158
+ { method: "POST" },
159
+ {
160
+ body: { detail: "From address not owned by this assistant." },
161
+ status: 403,
162
+ },
163
+ );
164
+
165
+ const output = await runAssistantCommand(
166
+ "email",
167
+ "--json",
168
+ "send",
169
+ "user@example.com",
170
+ "-s",
171
+ "Hello",
172
+ "-b",
173
+ "Body",
174
+ );
175
+
176
+ expect(process.exitCode).toBe(1);
177
+ const parsed = JSON.parse(output.trim());
178
+ expect(parsed.error).toContain("not owned by this assistant");
179
+ });
180
+
181
+ test("list addresses failure returns error", async () => {
182
+ mockFetch(
183
+ "/email-addresses/",
184
+ {},
185
+ { body: { detail: "Internal server error" }, status: 500 },
186
+ );
187
+
188
+ const output = await runAssistantCommand(
189
+ "email",
190
+ "--json",
191
+ "send",
192
+ "user@example.com",
193
+ "-s",
194
+ "Hello",
195
+ "-b",
196
+ "Body",
197
+ );
198
+
199
+ expect(process.exitCode).toBe(1);
200
+ const parsed = JSON.parse(output.trim());
201
+ expect(parsed.error).toContain("Failed to list email addresses");
202
+ });
203
+
204
+ test("missing platform credentials returns error", async () => {
205
+ _resetBackend();
206
+ setPlatformAssistantId(undefined);
207
+
208
+ const output = await runAssistantCommand(
209
+ "email",
210
+ "--json",
211
+ "send",
212
+ "user@example.com",
213
+ "-s",
214
+ "Hello",
215
+ "-b",
216
+ "Body",
217
+ );
218
+
219
+ expect(process.exitCode).toBe(1);
220
+ const parsed = JSON.parse(output.trim());
221
+ expect(parsed.error).toContain("Platform credentials not configured");
222
+ });
223
+
224
+ test("missing assistant ID returns error", async () => {
225
+ setPlatformAssistantId("");
226
+
227
+ const output = await runAssistantCommand(
228
+ "email",
229
+ "--json",
230
+ "send",
231
+ "user@example.com",
232
+ "-s",
233
+ "Hello",
234
+ "-b",
235
+ "Body",
236
+ );
237
+
238
+ expect(process.exitCode).toBe(1);
239
+ const parsed = JSON.parse(output.trim());
240
+ expect(parsed.error).toContain("Assistant ID");
241
+ });
242
+
243
+ test("send without subject omits subject from payload", async () => {
244
+ mockListAddresses();
245
+ mockSendSuccess();
246
+
247
+ await runAssistantCommand(
248
+ "email",
249
+ "send",
250
+ "user@example.com",
251
+ "-b",
252
+ "Body only, no subject",
253
+ );
254
+
255
+ const calls = getMockFetchCalls();
256
+ const payload = JSON.parse(calls[1].init.body as string);
257
+ expect(payload.subject).toBeUndefined();
258
+ expect(payload.text).toBe("Body only, no subject");
259
+ expect(process.exitCode).toBe(0);
260
+ });
261
+
262
+ test("suppressed recipient error is surfaced", async () => {
263
+ mockListAddresses();
264
+ mockFetch(
265
+ "/runtime-proxy/email/send/",
266
+ { method: "POST" },
267
+ {
268
+ body: {
269
+ detail:
270
+ "Recipient user@example.com is suppressed due to prior bounce or spam complaint.",
271
+ },
272
+ status: 422,
273
+ },
274
+ );
275
+
276
+ const output = await runAssistantCommand(
277
+ "email",
278
+ "--json",
279
+ "send",
280
+ "user@example.com",
281
+ "-s",
282
+ "Hello",
283
+ "-b",
284
+ "Body",
285
+ );
286
+
287
+ expect(process.exitCode).toBe(1);
288
+ const parsed = JSON.parse(output.trim());
289
+ expect(parsed.error).toContain("suppressed");
290
+ });
291
+ });
@@ -0,0 +1,181 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ getMockFetchCalls,
5
+ mockFetch,
6
+ resetMockFetch,
7
+ } from "../../../__tests__/mock-fetch.js";
8
+ import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js";
9
+ import { setPlatformAssistantId } from "../../../config/env.js";
10
+ import { credentialKey } from "../../../security/credential-key.js";
11
+ import {
12
+ _resetBackend,
13
+ deleteSecureKeyAsync,
14
+ setSecureKeyAsync,
15
+ } from "../../../security/secure-keys.js";
16
+ import { runAssistantCommand } from "../../__tests__/run-assistant-command.js";
17
+
18
+ const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
19
+ const ADDRESS_ID = "550e8400-e29b-41d4-a716-446655440000";
20
+ const ADDRESS = "mybot@vellum.me";
21
+ const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key");
22
+
23
+ function mockListAddresses(
24
+ addresses: { id: string; address: string }[] = [
25
+ { id: ADDRESS_ID, address: ADDRESS },
26
+ ],
27
+ status = 200,
28
+ ): void {
29
+ mockFetch("/email-addresses/", {}, { body: { results: addresses }, status });
30
+ }
31
+
32
+ function mockStatusSuccess(
33
+ overrides?: Partial<{
34
+ address: string;
35
+ status: string;
36
+ usage: { sent_today: number; daily_limit: number; received_today: number };
37
+ }>,
38
+ ): void {
39
+ mockFetch(
40
+ `/email-addresses/${ADDRESS_ID}/status/`,
41
+ {},
42
+ {
43
+ body: {
44
+ address: overrides?.address ?? ADDRESS,
45
+ status: overrides?.status ?? "active",
46
+ usage: overrides?.usage ?? {
47
+ sent_today: 12,
48
+ daily_limit: 100,
49
+ received_today: 5,
50
+ },
51
+ },
52
+ status: 200,
53
+ },
54
+ );
55
+ }
56
+
57
+ let savedCesUrl: string | undefined;
58
+ let savedContainerized: string | undefined;
59
+
60
+ beforeEach(async () => {
61
+ process.exitCode = 0;
62
+
63
+ // Force encrypted-store backend so setSecureKeyAsync works in sandbox
64
+ savedCesUrl = process.env.CES_CREDENTIAL_URL;
65
+ savedContainerized = process.env.IS_CONTAINERIZED;
66
+ delete process.env.CES_CREDENTIAL_URL;
67
+ delete process.env.IS_CONTAINERIZED;
68
+
69
+ _resetBackend();
70
+ resetMockFetch();
71
+ _setOverridesForTesting({ "email-channel": true });
72
+ setPlatformAssistantId(ASSISTANT_ID);
73
+ await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key");
74
+ });
75
+
76
+ afterEach(() => {
77
+ resetMockFetch();
78
+ _setOverridesForTesting({});
79
+ setPlatformAssistantId(undefined);
80
+ _resetBackend();
81
+
82
+ // Restore env
83
+ if (savedCesUrl !== undefined) process.env.CES_CREDENTIAL_URL = savedCesUrl;
84
+ else delete process.env.CES_CREDENTIAL_URL;
85
+ if (savedContainerized !== undefined)
86
+ process.env.IS_CONTAINERIZED = savedContainerized;
87
+ else delete process.env.IS_CONTAINERIZED;
88
+ });
89
+
90
+ describe("assistant email status", () => {
91
+ test("successful status shows address and usage", async () => {
92
+ mockListAddresses();
93
+ mockStatusSuccess();
94
+
95
+ const output = await runAssistantCommand("email", "--json", "status");
96
+
97
+ const parsed = JSON.parse(output.trim());
98
+ expect(parsed.address).toBe(ADDRESS);
99
+ expect(parsed.status).toBe("active");
100
+ expect(parsed.usage.sent_today).toBe(12);
101
+ expect(parsed.usage.daily_limit).toBe(100);
102
+ expect(parsed.usage.received_today).toBe(5);
103
+ expect(process.exitCode).toBe(0);
104
+ });
105
+
106
+ test("calls correct URLs in order", async () => {
107
+ mockListAddresses();
108
+ mockStatusSuccess();
109
+
110
+ await runAssistantCommand("email", "--json", "status");
111
+
112
+ const calls = getMockFetchCalls();
113
+ expect(calls).toHaveLength(2);
114
+ expect(calls[0].path).toContain(
115
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/`,
116
+ );
117
+ expect(calls[1].path).toContain(
118
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/${ADDRESS_ID}/status/`,
119
+ );
120
+ });
121
+
122
+ test("no registered address returns error", async () => {
123
+ mockListAddresses([]);
124
+
125
+ const output = await runAssistantCommand("email", "--json", "status");
126
+
127
+ expect(process.exitCode).toBe(1);
128
+ const parsed = JSON.parse(output.trim());
129
+ expect(parsed.error).toContain("No email address registered");
130
+ });
131
+
132
+ test("list endpoint failure returns error", async () => {
133
+ mockFetch(
134
+ "/email-addresses/",
135
+ {},
136
+ { body: { detail: "Internal server error" }, status: 500 },
137
+ );
138
+
139
+ const output = await runAssistantCommand("email", "--json", "status");
140
+
141
+ expect(process.exitCode).toBe(1);
142
+ const parsed = JSON.parse(output.trim());
143
+ expect(parsed.error).toContain("Failed to list email addresses");
144
+ });
145
+
146
+ test("status endpoint failure returns error", async () => {
147
+ mockListAddresses();
148
+ mockFetch(
149
+ `/email-addresses/${ADDRESS_ID}/status/`,
150
+ {},
151
+ { body: { detail: "Service unavailable" }, status: 503 },
152
+ );
153
+
154
+ const output = await runAssistantCommand("email", "--json", "status");
155
+
156
+ expect(process.exitCode).toBe(1);
157
+ const parsed = JSON.parse(output.trim());
158
+ expect(parsed.error).toContain("Service unavailable");
159
+ });
160
+
161
+ test("missing platform credentials returns error", async () => {
162
+ // Delete the API key so create() returns null
163
+ await deleteSecureKeyAsync(API_KEY_CREDENTIAL);
164
+
165
+ const output = await runAssistantCommand("email", "--json", "status");
166
+
167
+ expect(process.exitCode).toBe(1);
168
+ const parsed = JSON.parse(output.trim());
169
+ expect(parsed.error).toContain("Platform credentials not configured");
170
+ });
171
+
172
+ test("missing assistant ID returns error", async () => {
173
+ setPlatformAssistantId("");
174
+
175
+ const output = await runAssistantCommand("email", "--json", "status");
176
+
177
+ expect(process.exitCode).toBe(1);
178
+ const parsed = JSON.parse(output.trim());
179
+ expect(parsed.error).toContain("Assistant ID");
180
+ });
181
+ });
@@ -0,0 +1,139 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ getMockFetchCalls,
5
+ mockFetch,
6
+ resetMockFetch,
7
+ } from "../../../__tests__/mock-fetch.js";
8
+ import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js";
9
+ import { setPlatformAssistantId } from "../../../config/env.js";
10
+ import { credentialKey } from "../../../security/credential-key.js";
11
+ import {
12
+ _resetBackend,
13
+ setSecureKeyAsync,
14
+ } from "../../../security/secure-keys.js";
15
+ import { runAssistantCommand } from "../../__tests__/run-assistant-command.js";
16
+
17
+ const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
18
+ const ADDRESS_ID = "550e8400-e29b-41d4-a716-446655440000";
19
+ const ADDRESS = "mybot@vellum.me";
20
+ const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key");
21
+
22
+ beforeEach(async () => {
23
+ process.exitCode = 0;
24
+ _resetBackend();
25
+ resetMockFetch();
26
+ _setOverridesForTesting({ "email-channel": true });
27
+ setPlatformAssistantId(ASSISTANT_ID);
28
+ await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key");
29
+ });
30
+
31
+ afterEach(() => {
32
+ resetMockFetch();
33
+ _setOverridesForTesting({});
34
+ setPlatformAssistantId(undefined);
35
+ _resetBackend();
36
+ });
37
+
38
+ function standardEmailMockFetches(
39
+ deleteStatus = 204,
40
+ deleteBody: unknown = null,
41
+ ): void {
42
+ mockFetch(
43
+ "/email-addresses/",
44
+ {},
45
+ {
46
+ body: { results: [{ id: ADDRESS_ID, address: ADDRESS }] },
47
+ status: 200,
48
+ },
49
+ );
50
+ mockFetch(
51
+ `/email-addresses/${ADDRESS_ID}/`,
52
+ { method: "DELETE" },
53
+ { body: deleteBody, status: deleteStatus },
54
+ );
55
+ }
56
+
57
+ describe("assistant email unregister", () => {
58
+ test("successful unregister with --confirm lists then deletes", async () => {
59
+ standardEmailMockFetches();
60
+
61
+ await runAssistantCommand("email", "unregister", "--confirm");
62
+
63
+ const calls = getMockFetchCalls();
64
+ expect(calls).toHaveLength(2);
65
+ expect(calls[0].path).toBe(
66
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/`,
67
+ );
68
+ expect(calls[1].path).toBe(
69
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/${ADDRESS_ID}/`,
70
+ );
71
+ expect(calls[1].init.method).toBe("DELETE");
72
+ expect(process.exitCode).toBe(0);
73
+ });
74
+
75
+ test("--json outputs structured response", async () => {
76
+ standardEmailMockFetches();
77
+
78
+ const output = await runAssistantCommand("email", "--json", "unregister");
79
+
80
+ const parsed = JSON.parse(output.trim());
81
+ expect(parsed.unregistered).toBe(ADDRESS);
82
+ expect(process.exitCode).toBe(0);
83
+ });
84
+
85
+ test("no registered address returns error", async () => {
86
+ mockFetch("/email-addresses/", {}, { body: { results: [] }, status: 200 });
87
+
88
+ const output = await runAssistantCommand("email", "--json", "unregister");
89
+
90
+ expect(process.exitCode).toBe(1);
91
+ const parsed = JSON.parse(output.trim());
92
+ expect(parsed.error).toContain("No email address registered");
93
+ });
94
+
95
+ test("list endpoint failure returns error", async () => {
96
+ mockFetch(
97
+ "/email-addresses/",
98
+ {},
99
+ { body: { detail: "Internal server error" }, status: 500 },
100
+ );
101
+
102
+ const output = await runAssistantCommand("email", "--json", "unregister");
103
+
104
+ expect(process.exitCode).toBe(1);
105
+ const parsed = JSON.parse(output.trim());
106
+ expect(parsed.error).toContain("Failed to list email addresses");
107
+ });
108
+
109
+ test("delete endpoint failure returns error", async () => {
110
+ standardEmailMockFetches(500, { detail: "Cannot delete address" });
111
+
112
+ const output = await runAssistantCommand("email", "--json", "unregister");
113
+
114
+ expect(process.exitCode).toBe(1);
115
+ const parsed = JSON.parse(output.trim());
116
+ expect(parsed.error).toContain("Cannot delete address");
117
+ });
118
+
119
+ test("missing platform credentials returns error", async () => {
120
+ _resetBackend();
121
+ setPlatformAssistantId(undefined);
122
+
123
+ const output = await runAssistantCommand("email", "--json", "unregister");
124
+
125
+ expect(process.exitCode).toBe(1);
126
+ const parsed = JSON.parse(output.trim());
127
+ expect(parsed.error).toContain("Platform credentials not configured");
128
+ });
129
+
130
+ test("missing assistant ID returns error", async () => {
131
+ setPlatformAssistantId("");
132
+
133
+ const output = await runAssistantCommand("email", "--json", "unregister");
134
+
135
+ expect(process.exitCode).toBe(1);
136
+ const parsed = JSON.parse(output.trim());
137
+ expect(parsed.error).toContain("Assistant ID");
138
+ });
139
+ });