@vellumai/assistant 0.6.0 → 0.6.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.
Files changed (358) 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 +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -187,6 +187,58 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
187
187
  expect(system[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
188
188
  });
189
189
 
190
+ test("drops static system block cache_control when total would exceed 4", async () => {
191
+ const staticBlock = "You are a helpful assistant.";
192
+ const dynamicBlock = "User workspace files here.";
193
+ const prompt = staticBlock + SYSTEM_PROMPT_CACHE_BOUNDARY + dynamicBlock;
194
+
195
+ // Boundary (2 system) + tools (1) + turn-start (1) + tail (1) = 5 → must cap at 4
196
+ const messages: Message[] = [
197
+ userMsg("Do something"),
198
+ toolUseMsg("tu_1", "bash"),
199
+ toolResultMsg("tu_1", "output"),
200
+ ];
201
+ await provider.sendMessage(messages, sampleTools, prompt);
202
+
203
+ const system = lastStreamParams!.system as Array<{
204
+ type: string;
205
+ text: string;
206
+ cache_control?: { type: string; ttl?: string };
207
+ }>;
208
+ expect(system).toHaveLength(2);
209
+ // Static block's cache_control dropped (small, cheap to re-read)
210
+ expect(system[0].cache_control).toBeUndefined();
211
+ // Dynamic block keeps its cache_control
212
+ expect(system[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
213
+
214
+ // Tools breakpoint still present
215
+ const tools = lastStreamParams!.tools as Array<{
216
+ cache_control?: { type: string; ttl?: string };
217
+ }>;
218
+ expect(tools[tools.length - 1].cache_control).toEqual({
219
+ type: "ephemeral",
220
+ ttl: "1h",
221
+ });
222
+
223
+ // Turn-start + tail breakpoints still present
224
+ const sent = lastStreamParams!.messages as Array<{
225
+ role: string;
226
+ content: Array<{
227
+ type: string;
228
+ cache_control?: { type: string; ttl?: string };
229
+ }>;
230
+ }>;
231
+ const turnStart = sent[0];
232
+ expect(
233
+ turnStart.content[turnStart.content.length - 1].cache_control,
234
+ ).toEqual({ type: "ephemeral", ttl: "1h" });
235
+ const lastMsg = sent[sent.length - 1];
236
+ expect(lastMsg.content[lastMsg.content.length - 1].cache_control).toEqual({
237
+ type: "ephemeral",
238
+ ttl: "5m",
239
+ });
240
+ });
241
+
190
242
  // -----------------------------------------------------------------------
191
243
  // Tool cache control
192
244
  // -----------------------------------------------------------------------
@@ -225,33 +277,22 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
225
277
  });
226
278
 
227
279
  // -----------------------------------------------------------------------
228
- // User turn cache breakpoints — second-to-last user turn only
280
+ // Advancing tail — 5m cache on last block after turn-starting message
229
281
  // -----------------------------------------------------------------------
230
- test("single user turn does NOT get cache_control (no second-to-last)", async () => {
282
+ test("no advancing tail cache when turn-starting user message is last", async () => {
231
283
  await provider.sendMessage([userMsg("Hello")]);
232
284
 
233
- const messages = lastStreamParams!.messages as Array<{
234
- role: string;
235
- content: Array<{
236
- type: string;
237
- text: string;
238
- cache_control?: { type: string; ttl?: string };
239
- }>;
240
- }>;
241
- const lastUser = messages[messages.length - 1];
242
- expect(lastUser.role).toBe("user");
285
+ // No top-level cache_control would conflict with the 1h block breakpoint
243
286
  expect(
244
- lastUser.content[lastUser.content.length - 1].cache_control,
287
+ (lastStreamParams as Record<string, unknown>).cache_control,
245
288
  ).toBeUndefined();
246
289
  });
247
290
 
248
- test("second-to-last user turn gets cache_control, others do not", async () => {
291
+ test("advancing tail: 5m cache on last block when tool results follow turn-starting message", async () => {
249
292
  const messages: Message[] = [
250
- userMsg("Turn 1"), // user turn 0 — no cache
251
- assistantMsg("Response 1"),
252
- userMsg("Turn 2"), // user turn 1 — cache (second-to-last)
253
- assistantMsg("Response 2"),
254
- userMsg("Turn 3"), // user turn 2 — no cache (last)
293
+ userMsg("Do something"),
294
+ toolUseMsg("tu_1", "bash"),
295
+ toolResultMsg("tu_1", "output"),
255
296
  ];
256
297
  await provider.sendMessage(messages);
257
298
 
@@ -259,33 +300,30 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
259
300
  role: string;
260
301
  content: Array<{
261
302
  type: string;
262
- text: string;
263
303
  cache_control?: { type: string; ttl?: string };
264
304
  }>;
265
305
  }>;
266
306
 
267
- // Find user messages in order
268
- const userMessages = sent.filter((m) => m.role === "user");
269
- expect(userMessages).toHaveLength(3);
270
-
271
- // First user turn: no cache_control
272
- const firstUserLastBlock =
273
- userMessages[0].content[userMessages[0].content.length - 1];
274
- expect(firstUserLastBlock.cache_control).toBeUndefined();
275
-
276
- // Second user turn (second-to-last): cache_control ephemeral
277
- const secondUserLastBlock =
278
- userMessages[1].content[userMessages[1].content.length - 1];
279
- expect(secondUserLastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
280
-
281
- // Third user turn (last): no cache_control
282
- const thirdUserLastBlock =
283
- userMessages[2].content[userMessages[2].content.length - 1];
284
- expect(thirdUserLastBlock.cache_control).toBeUndefined();
307
+ // Turn-starting user message (first) keeps 1h
308
+ const turnStart = sent[0];
309
+ const turnStartLast = turnStart.content[turnStart.content.length - 1];
310
+ expect(turnStartLast.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
311
+
312
+ // Last message (tool_result) gets 5m advancing tail
313
+ const lastMessage = sent[sent.length - 1];
314
+ const lastBlock = lastMessage.content[lastMessage.content.length - 1];
315
+ expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "5m" });
285
316
  });
286
317
 
287
- test("single user turn does NOT get cache_control (only one user = no second-to-last)", async () => {
288
- await provider.sendMessage([userMsg("Only turn")]);
318
+ test("turn-starting user message gets 1h cache on last block", async () => {
319
+ const messages: Message[] = [
320
+ userMsg("Turn 1"),
321
+ assistantMsg("Response 1"),
322
+ userMsg("Turn 2"),
323
+ assistantMsg("Response 2"),
324
+ userMsg("Turn 3"),
325
+ ];
326
+ await provider.sendMessage(messages);
289
327
 
290
328
  const sent = lastStreamParams!.messages as Array<{
291
329
  role: string;
@@ -295,35 +333,17 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
295
333
  cache_control?: { type: string; ttl?: string };
296
334
  }>;
297
335
  }>;
298
- const userMessages = sent.filter((m) => m.role === "user");
299
- expect(userMessages).toHaveLength(1);
300
- expect(
301
- userMessages[0].content[userMessages[0].content.length - 1].cache_control,
302
- ).toBeUndefined();
303
- });
304
336
 
305
- // -----------------------------------------------------------------------
306
- // User turn with tool_result cache breakpoint on second-to-last
307
- // -----------------------------------------------------------------------
308
- test("user turn containing tool_result gets cache_control on second-to-last user turn only", async () => {
309
- const messages: Message[] = [
310
- userMsg("Read file"),
311
- toolUseMsg("tu_1", "file_read"),
312
- toolResultMsg("tu_1", "file contents here"),
313
- ];
314
- await provider.sendMessage(messages);
315
-
316
- const sent = lastStreamParams!.messages as Array<{
317
- role: string;
318
- content: Array<{ type: string; cache_control?: { type: string; ttl?: string } }>;
319
- }>;
320
- const userMsgs = sent.filter((m) => m.role === "user");
321
- // First user msg (second-to-last) should get cache
322
- const firstLast = userMsgs[0].content[userMsgs[0].content.length - 1];
323
- expect(firstLast.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
324
- // tool_result msg (last) should NOT get cache
325
- const secondLast = userMsgs[1].content[userMsgs[1].content.length - 1];
326
- expect(secondLast.cache_control).toBeUndefined();
337
+ const userMessages = sent.filter((m) => m.role === "user");
338
+ // Only the last user message (turn-starting) gets cache_control
339
+ for (const user of userMessages.slice(0, -1)) {
340
+ for (const block of user.content) {
341
+ expect(block.cache_control).toBeUndefined();
342
+ }
343
+ }
344
+ const lastUser = userMessages[userMessages.length - 1];
345
+ const lastBlock = lastUser.content[lastUser.content.length - 1];
346
+ expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
327
347
  });
328
348
 
329
349
  // -----------------------------------------------------------------------
@@ -358,7 +378,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
358
378
  // -----------------------------------------------------------------------
359
379
  // Multi-block user message: cache lands on LAST block
360
380
  // -----------------------------------------------------------------------
361
- test("multi-block single user message does NOT get cache (no second-to-last)", async () => {
381
+ test("multi-block single user message gets cache on last block", async () => {
362
382
  const multiBlockUser: Message = {
363
383
  role: "user",
364
384
  content: [
@@ -378,7 +398,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
378
398
  }>;
379
399
  const user = sent[0];
380
400
  expect(user.content[0].cache_control).toBeUndefined();
381
- expect(user.content[1].cache_control).toBeUndefined();
401
+ expect(user.content[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
382
402
  });
383
403
 
384
404
  // -----------------------------------------------------------------------
@@ -395,14 +415,14 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
395
415
  // -----------------------------------------------------------------------
396
416
  // Cache compatibility with workspace context injection
397
417
  // -----------------------------------------------------------------------
398
- test("workspace-prepended single user message does NOT get cache (no second-to-last)", async () => {
418
+ test("workspace-prepended single user message gets cache on last block", async () => {
399
419
  // Simulates what applyRuntimeInjections does: prepend workspace block, keep user text as trailing
400
420
  const workspaceInjectedUser: Message = {
401
421
  role: "user",
402
422
  content: [
403
423
  {
404
424
  type: "text",
405
- text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, tests\n</workspace_top_level>",
425
+ text: "<workspace>\nRoot: /sandbox\nDirectories: src, tests\n</workspace>",
406
426
  },
407
427
  { type: "text", text: "What files are in src?" },
408
428
  ],
@@ -421,18 +441,18 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
421
441
  expect(user.content).toHaveLength(2);
422
442
  // Workspace block (first): no cache_control
423
443
  expect(user.content[0].cache_control).toBeUndefined();
424
- // User text (last): no cache_control (single user turn = no second-to-last)
425
- expect(user.content[1].cache_control).toBeUndefined();
444
+ // User text (last): cache_control with 1h TTL
445
+ expect(user.content[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
426
446
  });
427
447
 
428
- test("workspace + multi-block single user message: no cache (no second-to-last)", async () => {
448
+ test("workspace + multi-block single user message: cache on last block only", async () => {
429
449
  // Simulates workspace prepended + extra context block appended
430
450
  const injectedUser: Message = {
431
451
  role: "user",
432
452
  content: [
433
453
  {
434
454
  type: "text",
435
- text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, tests\n</workspace_top_level>",
455
+ text: "<workspace>\nRoot: /sandbox\nDirectories: src, tests\n</workspace>",
436
456
  },
437
457
  { type: "text", text: "Help me debug this" },
438
458
  {
@@ -453,10 +473,10 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
453
473
  }>;
454
474
  const user = sent[0];
455
475
  expect(user.content).toHaveLength(3);
456
- // No blocks get cache_control (single user turn = no second-to-last)
476
+ // Only last block gets cache_control
457
477
  expect(user.content[0].cache_control).toBeUndefined();
458
478
  expect(user.content[1].cache_control).toBeUndefined();
459
- expect(user.content[2].cache_control).toBeUndefined();
479
+ expect(user.content[2].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
460
480
  });
461
481
 
462
482
  // -----------------------------------------------------------------------
@@ -550,7 +570,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
550
570
  content: [
551
571
  {
552
572
  type: "text",
553
- text: "<workspace_top_level>\nRoot: /sandbox\n</workspace_top_level>",
573
+ text: "<workspace>\nRoot: /sandbox\n</workspace>",
554
574
  },
555
575
  {
556
576
  type: "tool_result",
@@ -1330,39 +1350,36 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
1330
1350
  expect(sent[4].content[0].text).toBe("Follow-up question");
1331
1351
  });
1332
1352
 
1333
- test("multi-turn with workspace injection: cache on second-to-last user turn only", async () => {
1353
+ test("multi-turn with workspace injection: only last user message gets 1h cache", async () => {
1334
1354
  const messages: Message[] = [
1335
- // Turn 1: workspace + user text (no cache - 3rd-to-last)
1336
1355
  {
1337
1356
  role: "user",
1338
1357
  content: [
1339
1358
  {
1340
1359
  type: "text",
1341
- text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src\n</workspace_top_level>",
1360
+ text: "<workspace>\nRoot: /sandbox\nDirectories: src\n</workspace>",
1342
1361
  },
1343
1362
  { type: "text", text: "Turn 1" },
1344
1363
  ],
1345
1364
  },
1346
1365
  assistantMsg("Response 1"),
1347
- // Turn 2: workspace + user text (cache - second-to-last)
1348
1366
  {
1349
1367
  role: "user",
1350
1368
  content: [
1351
1369
  {
1352
1370
  type: "text",
1353
- text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, lib\n</workspace_top_level>",
1371
+ text: "<workspace>\nRoot: /sandbox\nDirectories: src, lib\n</workspace>",
1354
1372
  },
1355
1373
  { type: "text", text: "Turn 2" },
1356
1374
  ],
1357
1375
  },
1358
1376
  assistantMsg("Response 2"),
1359
- // Turn 3: workspace + user text (no cache - last)
1360
1377
  {
1361
1378
  role: "user",
1362
1379
  content: [
1363
1380
  {
1364
1381
  type: "text",
1365
- text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, lib, docs\n</workspace_top_level>",
1382
+ text: "<workspace>\nRoot: /sandbox\nDirectories: src, lib, docs\n</workspace>",
1366
1383
  },
1367
1384
  { type: "text", text: "Turn 3" },
1368
1385
  ],
@@ -1381,17 +1398,65 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
1381
1398
  const userMsgs = sent.filter((m) => m.role === "user");
1382
1399
  expect(userMsgs).toHaveLength(3);
1383
1400
 
1384
- // Turn 1: no cache on any block
1385
- expect(userMsgs[0].content[0].cache_control).toBeUndefined();
1386
- expect(userMsgs[0].content[1].cache_control).toBeUndefined();
1401
+ // Earlier user messages: no cache_control
1402
+ for (const user of userMsgs.slice(0, -1)) {
1403
+ for (const block of user.content) {
1404
+ expect(block.cache_control).toBeUndefined();
1405
+ }
1406
+ }
1407
+
1408
+ // Last user message (turn 3): 1h cache on last block only
1409
+ const lastUser = userMsgs[userMsgs.length - 1];
1410
+ expect(lastUser.content[0].cache_control).toBeUndefined();
1411
+ expect(lastUser.content[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
1412
+
1413
+ // No top-level cache_control — breakpoints are set directly on blocks
1414
+ expect(
1415
+ (lastStreamParams as Record<string, unknown>).cache_control,
1416
+ ).toBeUndefined();
1417
+ });
1418
+
1419
+ test("tool loop: turn-starting user message gets 1h cache, last tool_result gets 5m advancing tail", async () => {
1420
+ const messages: Message[] = [
1421
+ userMsg("Read the config file"),
1422
+ toolUseMsg("tu_1", "file_read"),
1423
+ toolResultMsg("tu_1", "config contents here"),
1424
+ toolUseMsg("tu_2", "file_read"),
1425
+ toolResultMsg("tu_2", "more contents"),
1426
+ ];
1427
+ await provider.sendMessage(messages);
1387
1428
 
1388
- // Turn 2 (second-to-last): cache on last block only
1389
- expect(userMsgs[1].content[0].cache_control).toBeUndefined();
1390
- expect(userMsgs[1].content[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
1429
+ const sent = lastStreamParams!.messages as Array<{
1430
+ role: string;
1431
+ content: Array<{
1432
+ type: string;
1433
+ text?: string;
1434
+ cache_control?: { type: string; ttl?: string };
1435
+ }>;
1436
+ }>;
1437
+
1438
+ // First message is the turn-starting user text — gets 1h cache
1439
+ expect(sent[0].role).toBe("user");
1440
+ expect(sent[0].content[0].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
1441
+
1442
+ // Non-last tool result messages do NOT get cache_control
1443
+ const toolResultMsgs = sent.filter(
1444
+ (m) =>
1445
+ m.role === "user" &&
1446
+ Array.isArray(m.content) &&
1447
+ m.content.every((b) => typeof b !== "string" && b.type === "tool_result"),
1448
+ );
1449
+ expect(toolResultMsgs.length).toBeGreaterThan(0);
1450
+ for (const tr of toolResultMsgs.slice(0, -1)) {
1451
+ for (const block of tr.content) {
1452
+ expect(block.cache_control).toBeUndefined();
1453
+ }
1454
+ }
1391
1455
 
1392
- // Turn 3 (last): no cache
1393
- expect(userMsgs[2].content[0].cache_control).toBeUndefined();
1394
- expect(userMsgs[2].content[1].cache_control).toBeUndefined();
1456
+ // Last message gets 5m advancing tail cache on its last block
1457
+ const lastMsg = sent[sent.length - 1];
1458
+ const lastBlock = lastMsg.content[lastMsg.content.length - 1];
1459
+ expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "5m" });
1395
1460
  });
1396
1461
 
1397
1462
  // -----------------------------------------------------------------------
@@ -18,6 +18,7 @@ import { describe, expect, test } from "bun:test";
18
18
  const ALLOWLIST = new Set([
19
19
  "assistant/src/memory/app-store.ts", // defines getAppsDir
20
20
  "assistant/src/memory/app-git-service.ts", // uses getAppsDir for git repo root, not per-app paths
21
+ "assistant/src/daemon/app-source-watcher.ts", // uses getAppsDir for recursive fs.watch root, not per-app paths
21
22
  ]);
22
23
 
23
24
  function isTestFile(filePath: string): boolean {
@@ -11,7 +11,10 @@ mock.module("../bundler/app-compiler.js", () => ({
11
11
 
12
12
  import type { AppDefinition } from "../memory/app-store.js";
13
13
  import type { AppStore } from "../tools/apps/executors.js";
14
- import { executeAppCreate } from "../tools/apps/executors.js";
14
+ import {
15
+ executeAppCreate,
16
+ executeAppRefresh,
17
+ } from "../tools/apps/executors.js";
15
18
 
16
19
  // ---------------------------------------------------------------------------
17
20
  // Helpers
@@ -158,3 +161,46 @@ describe("executeAppCreate", () => {
158
161
  expect(files["src/main.tsx"]).toBeDefined();
159
162
  });
160
163
  });
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // executeAppRefresh
167
+ // ---------------------------------------------------------------------------
168
+
169
+ describe("executeAppRefresh", () => {
170
+ test("legacy app: bumps updatedAt without compiling", async () => {
171
+ const app = makeLegacyApp();
172
+ const store = mockStore(app);
173
+ const result = await executeAppRefresh({ app_id: app.id }, store);
174
+
175
+ expect(result.isError).toBe(false);
176
+ const parsed = JSON.parse(result.content);
177
+ expect(parsed.refreshed).toBe(true);
178
+ expect(parsed.appId).toBe(app.id);
179
+ // Legacy apps should not have compile-related fields
180
+ expect(parsed.compiled).toBeUndefined();
181
+ expect(parsed.compile_errors).toBeUndefined();
182
+ });
183
+
184
+ test("multifile app: compiles src/ and returns result", async () => {
185
+ const app = makeMultifileApp();
186
+ const store = mockStore(app);
187
+ const result = await executeAppRefresh({ app_id: app.id }, store);
188
+
189
+ expect(result.isError).toBe(false);
190
+ const parsed = JSON.parse(result.content);
191
+ expect(parsed.refreshed).toBe(true);
192
+ expect(parsed.appId).toBe(app.id);
193
+ expect(parsed.compiled).toBe(true);
194
+ expect(parsed.compile_duration_ms).toBeDefined();
195
+ });
196
+
197
+ test("returns error for unknown app", async () => {
198
+ const app = makeLegacyApp();
199
+ const store = mockStore(app);
200
+ const result = await executeAppRefresh({ app_id: "nonexistent" }, store);
201
+
202
+ expect(result.isError).toBe(true);
203
+ const parsed = JSON.parse(result.content);
204
+ expect(parsed.error).toContain("not found");
205
+ });
206
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Tests for AppSourceWatcher — filesystem watcher that detects app source
3
+ * file changes and triggers debounced recompile + surface refresh.
4
+ */
5
+
6
+ import {
7
+ afterEach,
8
+ beforeEach,
9
+ describe,
10
+ expect,
11
+ mock,
12
+ test,
13
+ } from "bun:test";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Mocks — must be set up before importing the module under test
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const TEST_APPS_DIR = "/tmp/test-apps";
20
+ const testDirNameMap = new Map<string, string>([["my-app", "app-id-1"]]);
21
+
22
+ let capturedWatchCallback: ((eventType: string, filename: string | null) => void) | null = null;
23
+ const mockWatcher = { close: mock(() => {}) };
24
+
25
+ mock.module("node:fs", () => {
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const actualFs = require("node:fs");
28
+ return {
29
+ ...actualFs,
30
+ existsSync: mock((p: string) => p === TEST_APPS_DIR),
31
+ watch: mock(
32
+ (
33
+ _path: string,
34
+ _opts: Record<string, unknown>,
35
+ callback: (eventType: string, filename: string | null) => void,
36
+ ) => {
37
+ capturedWatchCallback = callback;
38
+ return mockWatcher;
39
+ },
40
+ ),
41
+ };
42
+ });
43
+
44
+ mock.module("../memory/app-store.js", () => ({
45
+ getAppsDir: mock(() => TEST_APPS_DIR),
46
+ resolveAppIdByDirName: mock(
47
+ (dirName: string) => testDirNameMap.get(dirName) ?? null,
48
+ ),
49
+ }));
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Import after mocks
53
+ // ---------------------------------------------------------------------------
54
+
55
+ import { AppSourceWatcher } from "../daemon/app-source-watcher.js";
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Tests
59
+ // ---------------------------------------------------------------------------
60
+
61
+ describe("AppSourceWatcher", () => {
62
+ let watcher: AppSourceWatcher;
63
+ let onChangeSpy: ReturnType<typeof mock>;
64
+
65
+ beforeEach(() => {
66
+ watcher = new AppSourceWatcher();
67
+ onChangeSpy = mock(() => {});
68
+ capturedWatchCallback = null;
69
+ mockWatcher.close.mockClear();
70
+ });
71
+
72
+ afterEach(() => {
73
+ watcher.stop();
74
+ });
75
+
76
+ test("start() creates a recursive watcher on the apps directory", () => {
77
+ watcher.start(onChangeSpy);
78
+ expect(capturedWatchCallback).not.toBeNull();
79
+ });
80
+
81
+ test("source file change triggers callback with resolved appId", async () => {
82
+ watcher.start(onChangeSpy);
83
+ capturedWatchCallback!("change", "my-app/src/main.tsx");
84
+
85
+ // Wait for debounce (500ms + margin)
86
+ await new Promise((r) => setTimeout(r, 600));
87
+ expect(onChangeSpy).toHaveBeenCalledTimes(1);
88
+ expect(onChangeSpy).toHaveBeenCalledWith("app-id-1");
89
+ });
90
+
91
+ test("root-level app file triggers callback", async () => {
92
+ watcher.start(onChangeSpy);
93
+ capturedWatchCallback!("change", "my-app/index.html");
94
+
95
+ await new Promise((r) => setTimeout(r, 600));
96
+ expect(onChangeSpy).toHaveBeenCalledTimes(1);
97
+ expect(onChangeSpy).toHaveBeenCalledWith("app-id-1");
98
+ });
99
+
100
+ test("dist/ files are filtered out", async () => {
101
+ watcher.start(onChangeSpy);
102
+ capturedWatchCallback!("change", "my-app/dist/index.html");
103
+
104
+ await new Promise((r) => setTimeout(r, 600));
105
+ expect(onChangeSpy).not.toHaveBeenCalled();
106
+ });
107
+
108
+ test("records/ files are filtered out", async () => {
109
+ watcher.start(onChangeSpy);
110
+ capturedWatchCallback!("change", "my-app/records/rec-1.json");
111
+
112
+ await new Promise((r) => setTimeout(r, 600));
113
+ expect(onChangeSpy).not.toHaveBeenCalled();
114
+ });
115
+
116
+ test("files directly in apps/ (no subdirectory) are filtered out", async () => {
117
+ watcher.start(onChangeSpy);
118
+ capturedWatchCallback!("change", "my-app.json");
119
+
120
+ await new Promise((r) => setTimeout(r, 600));
121
+ expect(onChangeSpy).not.toHaveBeenCalled();
122
+ });
123
+
124
+ test("unknown app directory is filtered out", async () => {
125
+ watcher.start(onChangeSpy);
126
+ capturedWatchCallback!("change", "unknown-app/src/main.tsx");
127
+
128
+ await new Promise((r) => setTimeout(r, 600));
129
+ expect(onChangeSpy).not.toHaveBeenCalled();
130
+ });
131
+
132
+ test("null filename is ignored", async () => {
133
+ watcher.start(onChangeSpy);
134
+ capturedWatchCallback!("change", null);
135
+
136
+ await new Promise((r) => setTimeout(r, 600));
137
+ expect(onChangeSpy).not.toHaveBeenCalled();
138
+ });
139
+
140
+ test("rapid changes to same app are debounced into single callback", async () => {
141
+ watcher.start(onChangeSpy);
142
+
143
+ capturedWatchCallback!("change", "my-app/src/main.tsx");
144
+ capturedWatchCallback!("change", "my-app/src/styles.css");
145
+ capturedWatchCallback!("change", "my-app/src/utils.ts");
146
+
147
+ await new Promise((r) => setTimeout(r, 600));
148
+ expect(onChangeSpy).toHaveBeenCalledTimes(1);
149
+ });
150
+
151
+ test("stop() closes watcher and cancels pending timers", () => {
152
+ watcher.start(onChangeSpy);
153
+ capturedWatchCallback!("change", "my-app/src/main.tsx");
154
+
155
+ watcher.stop();
156
+
157
+ expect(mockWatcher.close).toHaveBeenCalledTimes(1);
158
+ });
159
+ });
@@ -102,6 +102,36 @@ describe("AssistantEventHub — fanout", () => {
102
102
  const hub = new AssistantEventHub();
103
103
  await expect(hub.publish(makeEvent())).resolves.toBeUndefined();
104
104
  });
105
+
106
+ test("hasSubscribersForEvent returns true for assistant-wide subscribers", () => {
107
+ const hub = new AssistantEventHub();
108
+ hub.subscribe({ assistantId: "ast_1" }, () => {});
109
+
110
+ expect(
111
+ hub.hasSubscribersForEvent({
112
+ assistantId: "ast_1",
113
+ conversationId: "sess_A",
114
+ }),
115
+ ).toBe(true);
116
+ });
117
+
118
+ test("hasSubscribersForEvent honors conversation scoping", () => {
119
+ const hub = new AssistantEventHub();
120
+ hub.subscribe({ assistantId: "ast_1", conversationId: "sess_A" }, () => {});
121
+
122
+ expect(
123
+ hub.hasSubscribersForEvent({
124
+ assistantId: "ast_1",
125
+ conversationId: "sess_A",
126
+ }),
127
+ ).toBe(true);
128
+ expect(
129
+ hub.hasSubscribersForEvent({
130
+ assistantId: "ast_1",
131
+ conversationId: "sess_B",
132
+ }),
133
+ ).toBe(false);
134
+ });
105
135
  });
106
136
 
107
137
  // ── Unsubscribe / cleanup ────────────────────────────────────────────────────