@vellumai/assistant 0.8.11 → 0.8.12-staging.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 (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -175,6 +175,26 @@ function seedRequestLog(
175
175
  .run();
176
176
  }
177
177
 
178
+ function seedRequestLogWithSections(messageId: string, id: string): void {
179
+ getDb()
180
+ .insert(llmRequestLogs)
181
+ .values({
182
+ id,
183
+ conversationId: "conv-1",
184
+ messageId,
185
+ provider: "openai",
186
+ requestPayload: JSON.stringify({
187
+ model: "gpt-4.1",
188
+ messages: [{ role: "user", content: "hello" }],
189
+ }),
190
+ responsePayload: JSON.stringify({
191
+ choices: [{ message: { content: "hi" } }],
192
+ }),
193
+ createdAt: 1_700_000_000_000,
194
+ })
195
+ .run();
196
+ }
197
+
178
198
  function seedConversationKey(
179
199
  conversationKey: string,
180
200
  conversationId: string,
@@ -274,6 +294,118 @@ describe("GET /v1/conversations/llm-context", () => {
274
294
  });
275
295
  });
276
296
 
297
+ describe("llm-context view=summary", () => {
298
+ beforeEach(() => {
299
+ clearTables();
300
+ });
301
+
302
+ function seedConversationWithLog(): void {
303
+ seedConversationAndMessage({
304
+ conversationId: "conv-1",
305
+ messageId: "msg-1",
306
+ source: "user",
307
+ conversationType: "standard",
308
+ });
309
+ seedConversationKey("conv-key", "conv-1");
310
+ seedRequestLogWithSections("msg-1", "log-a");
311
+ }
312
+
313
+ test("conversation endpoint omits sections in summary view but keeps summary fields", async () => {
314
+ seedConversationWithLog();
315
+
316
+ const body = (await dispatchConversationLlmContext({
317
+ conversationKey: "conv-key",
318
+ view: "summary",
319
+ })) as {
320
+ logs: Array<Record<string, unknown>>;
321
+ };
322
+
323
+ expect(body.logs).toHaveLength(1);
324
+ const log = body.logs[0]!;
325
+ expect(log.requestSections).toBeUndefined();
326
+ expect(log.responseSections).toBeUndefined();
327
+ expect(log.id).toBe("log-a");
328
+ expect(log.summary).toBeDefined();
329
+ });
330
+
331
+ test("conversation endpoint includes sections by default", async () => {
332
+ seedConversationWithLog();
333
+
334
+ const body = (await dispatchConversationLlmContext({
335
+ conversationKey: "conv-key",
336
+ })) as {
337
+ logs: Array<Record<string, unknown>>;
338
+ };
339
+
340
+ expect(Array.isArray(body.logs[0]!.requestSections)).toBe(true);
341
+ expect(Array.isArray(body.logs[0]!.responseSections)).toBe(true);
342
+ });
343
+
344
+ test("message endpoint omits sections in summary view", async () => {
345
+ seedConversationWithLog();
346
+
347
+ const body = (await llmContextRoute.handler({
348
+ pathParams: { id: "msg-1" },
349
+ queryParams: { view: "summary" },
350
+ })) as {
351
+ logs: Array<Record<string, unknown>>;
352
+ };
353
+
354
+ expect(body.logs).toHaveLength(1);
355
+ expect(body.logs[0]!.requestSections).toBeUndefined();
356
+ expect(body.logs[0]!.responseSections).toBeUndefined();
357
+ expect(body.logs[0]!.summary).toBeDefined();
358
+ });
359
+
360
+ test("rejects unknown view values", async () => {
361
+ seedConversationWithLog();
362
+
363
+ expect(
364
+ dispatchConversationLlmContext({
365
+ conversationKey: "conv-key",
366
+ view: "compact",
367
+ }),
368
+ ).rejects.toThrow("Invalid view parameter");
369
+ });
370
+ });
371
+
372
+ describe("GET /v1/llm-request-logs/:id/context", () => {
373
+ const logContextRoute = ROUTES.find(
374
+ (r) => r.operationId === "llm_request_logs_context_get",
375
+ )!;
376
+
377
+ beforeEach(() => {
378
+ clearTables();
379
+ });
380
+
381
+ test("returns the normalized entry with sections for a single log", async () => {
382
+ seedConversationAndMessage({
383
+ conversationId: "conv-1",
384
+ messageId: "msg-1",
385
+ source: "user",
386
+ conversationType: "standard",
387
+ });
388
+ seedRequestLogWithSections("msg-1", "log-detail");
389
+
390
+ const body = (await logContextRoute.handler({
391
+ pathParams: { id: "log-detail" },
392
+ })) as Record<string, unknown>;
393
+
394
+ expect(body.id).toBe("log-detail");
395
+ expect(body.requestPayload).toBeNull();
396
+ expect(body.responsePayload).toBeNull();
397
+ expect(body.summary).toBeDefined();
398
+ expect(Array.isArray(body.requestSections)).toBe(true);
399
+ expect(Array.isArray(body.responseSections)).toBe(true);
400
+ });
401
+
402
+ test("throws NotFound for a missing log id", async () => {
403
+ expect(
404
+ logContextRoute.handler({ pathParams: { id: "log-missing" } }),
405
+ ).rejects.toThrow("log not found");
406
+ });
407
+ });
408
+
277
409
  describe("GET /v1/messages/:id/llm-context — memoryV2Activation", () => {
278
410
  beforeEach(() => {
279
411
  clearTables();
@@ -1,69 +1,44 @@
1
1
  /**
2
- * Unit tests for the gateway_logs_tail IPC route handler.
3
- *
4
- * Covers:
5
- * - Happy path: all params → correct URL with querystring
6
- * - All params absent → URL with no querystring
7
- * - Only n provided → querystring has only n
8
- * - Gateway returns 500 with error body → Error thrown with error message
9
- * - level: "INVALID" → ZodError (no gateway call)
10
- * - n: 0 → ZodError (min 1 violation)
11
- * - n: 1001 → ZodError (max 1000 violation)
12
- * - module: "" → accepted (empty string passes zod string validation)
2
+ * Unit tests for the gateway_logs_tail route handler.
13
3
  */
14
4
 
15
5
  import { beforeEach, describe, expect, mock, test } from "bun:test";
16
6
 
17
7
  import { ZodError } from "zod";
18
8
 
19
- // ---------------------------------------------------------------------------
20
- // Mocks — must be defined before importing the module under test
21
- // ---------------------------------------------------------------------------
22
-
23
- mock.module("../../../util/logger.js", () => ({
24
- getLogger: () =>
25
- new Proxy({} as Record<string, unknown>, {
26
- get: () => () => {},
27
- }),
28
- }));
29
-
30
- mock.module("../../../config/env.js", () => ({
31
- getGatewayInternalBaseUrl: () => "http://localhost:9999",
9
+ type IpcCall = {
10
+ method: string;
11
+ params?: Record<string, unknown>;
12
+ };
13
+
14
+ let ipcCalls: IpcCall[] = [];
15
+ let ipcResult: unknown = { lines: [], truncated: false };
16
+ let ipcError: Error | undefined;
17
+
18
+ const ipcCallPersistentMock = mock(
19
+ async (method: string, params?: Record<string, unknown>) => {
20
+ ipcCalls.push({ method, params });
21
+ if (ipcError) throw ipcError;
22
+ return ipcResult;
23
+ },
24
+ );
25
+
26
+ mock.module("../../../ipc/gateway-client.js", () => ({
27
+ ipcCallPersistent: ipcCallPersistentMock,
32
28
  }));
33
29
 
34
- // ---------------------------------------------------------------------------
35
- // Import the module under test AFTER mocks are set up
36
- // ---------------------------------------------------------------------------
37
-
38
30
  import { ROUTES } from "../gateway-log-routes.js";
39
31
 
40
32
  const gatewayLogsTailRoute = ROUTES.find(
41
33
  (r) => r.operationId === "gateway_logs_tail",
42
34
  )!;
43
35
 
44
- // ---------------------------------------------------------------------------
45
- // Helpers
46
- // ---------------------------------------------------------------------------
47
-
48
- function makeFetchMock(
49
- status: number,
50
- body: unknown,
51
- ): ReturnType<typeof mock> {
52
- return mock(async () => ({
53
- ok: status >= 200 && status < 300,
54
- status,
55
- json: async () => body,
56
- }));
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // Tests
61
- // ---------------------------------------------------------------------------
62
-
63
36
  describe("gateway_logs_tail route", () => {
64
37
  beforeEach(() => {
65
- // Reset global fetch to avoid cross-test contamination
66
- globalThis.fetch = undefined as unknown as typeof fetch;
38
+ ipcCalls = [];
39
+ ipcResult = { lines: [], truncated: false };
40
+ ipcError = undefined;
41
+ ipcCallPersistentMock.mockClear();
67
42
  });
68
43
 
69
44
  test("route is registered with correct operationId, method, and endpoint", () => {
@@ -73,170 +48,107 @@ describe("gateway_logs_tail route", () => {
73
48
  expect(gatewayLogsTailRoute.endpoint).toBe("gateway/logs/tail");
74
49
  });
75
50
 
76
- describe("happy path all params provided via body", () => {
77
- test("calls gateway with correct URL including all query params", async () => {
78
- const mockFetch = makeFetchMock(200, { entries: [] });
79
- globalThis.fetch = mockFetch as unknown as typeof fetch;
80
-
81
- await gatewayLogsTailRoute.handler({
82
- body: { n: 5, level: "warn", module: "mcp" },
83
- });
84
-
85
- expect(mockFetch).toHaveBeenCalledTimes(1);
86
- const calledUrl = (mockFetch.mock.calls[0] as [string])[0];
87
- expect(calledUrl).toBe(
88
- "http://localhost:9999/v1/logs/tail?n=5&level=warn&module=mcp",
89
- );
51
+ test("calls gateway IPC with all typed params from body", async () => {
52
+ await gatewayLogsTailRoute.handler({
53
+ body: { n: 5, level: "warn", module: "mcp" },
90
54
  });
91
55
 
92
- test("returns the parsed response body", async () => {
93
- const responseBody = { entries: [{ msg: "hello", level: 40 }] };
94
- globalThis.fetch = makeFetchMock(200, responseBody) as unknown as typeof fetch;
95
-
96
- const result = await gatewayLogsTailRoute.handler({
97
- body: { n: 5, level: "warn", module: "mcp" },
98
- });
99
-
100
- expect(result).toEqual(responseBody);
101
- });
56
+ expect(ipcCalls).toEqual([
57
+ {
58
+ method: "gateway_logs_tail",
59
+ params: { n: 5, level: "warn", module: "mcp" },
60
+ },
61
+ ]);
102
62
  });
103
63
 
104
- describe("all params absent", () => {
105
- test("calls gateway URL with no querystring when body is empty", async () => {
106
- const mockFetch = makeFetchMock(200, { entries: [] });
107
- globalThis.fetch = mockFetch as unknown as typeof fetch;
64
+ test("returns the parsed IPC response body", async () => {
65
+ ipcResult = {
66
+ lines: [{ msg: "hello", level: 40 }],
67
+ truncated: false,
68
+ };
108
69
 
109
- await gatewayLogsTailRoute.handler({ body: {} });
110
-
111
- const calledUrl = (mockFetch.mock.calls[0] as [string])[0];
112
- expect(calledUrl).toBe("http://localhost:9999/v1/logs/tail");
70
+ const result = await gatewayLogsTailRoute.handler({
71
+ body: { n: 5, level: "warn", module: "mcp" },
113
72
  });
114
73
 
115
- test("calls gateway URL with no querystring when no args provided", async () => {
116
- const mockFetch = makeFetchMock(200, { entries: [] });
117
- globalThis.fetch = mockFetch as unknown as typeof fetch;
118
-
119
- await gatewayLogsTailRoute.handler({});
120
-
121
- const calledUrl = (mockFetch.mock.calls[0] as [string])[0];
122
- expect(calledUrl).toBe("http://localhost:9999/v1/logs/tail");
123
- });
74
+ expect(result).toEqual(ipcResult);
124
75
  });
125
76
 
126
- describe("only n provided", () => {
127
- test("querystring contains only n — no spurious level or module keys", async () => {
128
- const mockFetch = makeFetchMock(200, { entries: [] });
129
- globalThis.fetch = mockFetch as unknown as typeof fetch;
77
+ test("calls gateway IPC with empty params when no filters are provided", async () => {
78
+ await gatewayLogsTailRoute.handler({});
130
79
 
131
- await gatewayLogsTailRoute.handler({ body: { n: 5 } });
132
-
133
- const calledUrl = (mockFetch.mock.calls[0] as [string])[0];
134
- expect(calledUrl).toBe("http://localhost:9999/v1/logs/tail?n=5");
135
- expect(calledUrl).not.toContain("level");
136
- expect(calledUrl).not.toContain("module");
137
- });
80
+ expect(ipcCalls).toEqual([{ method: "gateway_logs_tail", params: {} }]);
138
81
  });
139
82
 
140
- describe("gateway error handling", () => {
141
- test("gateway 500 with { error: 'disk error' } throws Error with that message", async () => {
142
- globalThis.fetch = makeFetchMock(500, {
143
- error: "disk error",
144
- }) as unknown as typeof fetch;
83
+ test("sends only n when only n is provided", async () => {
84
+ await gatewayLogsTailRoute.handler({ body: { n: 5 } });
145
85
 
146
- await expect(
147
- gatewayLogsTailRoute.handler({ body: {} }),
148
- ).rejects.toThrow("disk error");
149
- });
150
-
151
- test("gateway 500 with non-string error falls back to generic message", async () => {
152
- globalThis.fetch = makeFetchMock(500, {
153
- error: { nested: true },
154
- }) as unknown as typeof fetch;
155
-
156
- await expect(
157
- gatewayLogsTailRoute.handler({ body: {} }),
158
- ).rejects.toThrow("Gateway request failed (500)");
159
- });
160
-
161
- test("gateway 500 with unparseable JSON falls back to generic message", async () => {
162
- globalThis.fetch = mock(async () => ({
163
- ok: false,
164
- status: 500,
165
- json: async () => {
166
- throw new SyntaxError("Unexpected token");
167
- },
168
- })) as unknown as typeof fetch;
169
-
170
- await expect(
171
- gatewayLogsTailRoute.handler({ body: {} }),
172
- ).rejects.toThrow("Gateway request failed (500)");
173
- });
86
+ expect(ipcCalls).toEqual([
87
+ { method: "gateway_logs_tail", params: { n: 5 } },
88
+ ]);
174
89
  });
175
90
 
176
- describe("zod validation", () => {
177
- test("level: 'INVALID' is rejected with ZodError before calling gateway", async () => {
178
- const mockFetch = makeFetchMock(200, {});
179
- globalThis.fetch = mockFetch as unknown as typeof fetch;
91
+ test("propagates gateway IPC errors", async () => {
92
+ ipcError = new Error("Gateway IPC socket disconnected");
180
93
 
181
- await expect(
182
- gatewayLogsTailRoute.handler({
183
- body: { level: "INVALID" },
184
- }),
185
- ).rejects.toBeInstanceOf(ZodError);
94
+ await expect(gatewayLogsTailRoute.handler({ body: {} })).rejects.toThrow(
95
+ "Gateway IPC socket disconnected",
96
+ );
97
+ });
186
98
 
187
- expect(mockFetch).not.toHaveBeenCalled();
188
- });
99
+ test("rejects malformed gateway IPC responses", async () => {
100
+ ipcResult = { entries: [] };
189
101
 
190
- test("n: 0 is rejected with ZodError (min 1 violation)", async () => {
191
- const mockFetch = makeFetchMock(200, {});
192
- globalThis.fetch = mockFetch as unknown as typeof fetch;
102
+ await expect(
103
+ gatewayLogsTailRoute.handler({ body: {} }),
104
+ ).rejects.toBeInstanceOf(ZodError);
105
+ });
193
106
 
194
- await expect(
195
- gatewayLogsTailRoute.handler({ body: { n: 0 } }),
196
- ).rejects.toBeInstanceOf(ZodError);
107
+ test("level: 'INVALID' is rejected before calling gateway IPC", async () => {
108
+ await expect(
109
+ gatewayLogsTailRoute.handler({
110
+ body: { level: "INVALID" },
111
+ }),
112
+ ).rejects.toBeInstanceOf(ZodError);
197
113
 
198
- expect(mockFetch).not.toHaveBeenCalled();
199
- });
114
+ expect(ipcCalls).toEqual([]);
115
+ });
200
116
 
201
- test("n: 1001 is rejected with ZodError (max 1000 violation)", async () => {
202
- const mockFetch = makeFetchMock(200, {});
203
- globalThis.fetch = mockFetch as unknown as typeof fetch;
117
+ test("n: 0 is rejected before calling gateway IPC", async () => {
118
+ await expect(
119
+ gatewayLogsTailRoute.handler({ body: { n: 0 } }),
120
+ ).rejects.toBeInstanceOf(ZodError);
204
121
 
205
- await expect(
206
- gatewayLogsTailRoute.handler({ body: { n: 1001 } }),
207
- ).rejects.toBeInstanceOf(ZodError);
122
+ expect(ipcCalls).toEqual([]);
123
+ });
208
124
 
209
- expect(mockFetch).not.toHaveBeenCalled();
210
- });
125
+ test("n: 1001 is rejected before calling gateway IPC", async () => {
126
+ await expect(
127
+ gatewayLogsTailRoute.handler({ body: { n: 1001 } }),
128
+ ).rejects.toBeInstanceOf(ZodError);
211
129
 
212
- test("module: '' is accepted (empty string passes zod string validation)", async () => {
213
- const mockFetch = makeFetchMock(200, { entries: [] });
214
- globalThis.fetch = mockFetch as unknown as typeof fetch;
130
+ expect(ipcCalls).toEqual([]);
131
+ });
215
132
 
216
- await expect(
217
- gatewayLogsTailRoute.handler({ body: { module: "" } }),
218
- ).resolves.toBeDefined();
133
+ test("module: '' is accepted", async () => {
134
+ await gatewayLogsTailRoute.handler({ body: { module: "" } });
219
135
 
220
- expect(mockFetch).toHaveBeenCalledTimes(1);
221
- });
136
+ expect(ipcCalls).toEqual([
137
+ { method: "gateway_logs_tail", params: { module: "" } },
138
+ ]);
222
139
  });
223
140
 
224
- describe("queryParams source (HTTP GET path)", () => {
225
- test("uses queryParams when provided (HTTP GET path) — string params", async () => {
226
- const mockFetch = makeFetchMock(200, { entries: [] });
227
- globalThis.fetch = mockFetch as unknown as typeof fetch;
228
-
229
- // When queryParams has values, those take precedence over body.
230
- // Only string-valued params (level, module) are valid from the HTTP layer;
231
- // n is numeric and must come via IPC body.
232
- await gatewayLogsTailRoute.handler({
233
- queryParams: { level: "info", module: "cors" },
234
- body: {},
235
- });
236
-
237
- const calledUrl = (mockFetch.mock.calls[0] as [string])[0];
238
- expect(calledUrl).toContain("level=info");
239
- expect(calledUrl).toContain("module=cors");
141
+ test("uses queryParams when provided and coerces n", async () => {
142
+ await gatewayLogsTailRoute.handler({
143
+ queryParams: { n: "7", level: "info", module: "cors" },
144
+ body: { n: 1, level: "error" },
240
145
  });
146
+
147
+ expect(ipcCalls).toEqual([
148
+ {
149
+ method: "gateway_logs_tail",
150
+ params: { n: 7, level: "info", module: "cors" },
151
+ },
152
+ ]);
241
153
  });
242
154
  });
@@ -65,6 +65,14 @@ mock.module("../../../memory/conversation-crud.js", () => ({
65
65
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
66
66
  }));
67
67
 
68
+ // Stub the home-content revalidation coordinator: the GET handler fires
69
+ // it fire-and-forget, and the real module would reach into the LLM
70
+ // provider layer and checkpoint store.
71
+ const revalidateSpy = mock<() => void>(() => {});
72
+ mock.module("../../../home/home-content-refresh.js", () => ({
73
+ revalidateHomeContentInBackground: revalidateSpy,
74
+ }));
75
+
68
76
  // Dynamic imports so module mocks are wired before evaluation.
69
77
  const {
70
78
  computeGreeting,
@@ -333,6 +341,15 @@ describe("handleGetHomeFeed", () => {
333
341
  expect(res.status).toBe(400);
334
342
  });
335
343
 
344
+ test("triggers fire-and-forget home content revalidation", async () => {
345
+ revalidateSpy.mockClear();
346
+ const res = await handleGetHomeFeed(
347
+ new Request("http://localhost/v1/home/feed?timeAwaySeconds=0"),
348
+ );
349
+ expect(res.status).toBe(200);
350
+ expect(revalidateSpy).toHaveBeenCalledTimes(1);
351
+ });
352
+
336
353
  test("empty feed returns empty items and zero newCount", async () => {
337
354
  const res = await handleGetHomeFeed(
338
355
  new Request("http://localhost/v1/home/feed?timeAwaySeconds=0"),