vellum 0.2.13 → 0.2.14

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 (207) hide show
  1. package/README.md +32 -0
  2. package/bun.lock +2 -2
  3. package/docs/skills.md +4 -4
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
  6. package/src/__tests__/app-git-history.test.ts +176 -0
  7. package/src/__tests__/app-git-service.test.ts +169 -0
  8. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  9. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
  10. package/src/__tests__/browser-skill-endstate.test.ts +6 -6
  11. package/src/__tests__/call-bridge.test.ts +105 -13
  12. package/src/__tests__/call-domain.test.ts +163 -0
  13. package/src/__tests__/call-orchestrator.test.ts +113 -0
  14. package/src/__tests__/call-routes-http.test.ts +246 -6
  15. package/src/__tests__/channel-approval-routes.test.ts +438 -0
  16. package/src/__tests__/channel-approval.test.ts +266 -0
  17. package/src/__tests__/channel-approvals.test.ts +393 -0
  18. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  19. package/src/__tests__/checker.test.ts +607 -1048
  20. package/src/__tests__/cli.test.ts +1 -56
  21. package/src/__tests__/config-schema.test.ts +137 -18
  22. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  23. package/src/__tests__/conflict-policy.test.ts +121 -0
  24. package/src/__tests__/conflict-store.test.ts +2 -0
  25. package/src/__tests__/contacts-tools.test.ts +3 -3
  26. package/src/__tests__/contradiction-checker.test.ts +99 -1
  27. package/src/__tests__/credential-security-invariants.test.ts +22 -6
  28. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  29. package/src/__tests__/elevenlabs-client.test.ts +62 -0
  30. package/src/__tests__/ephemeral-permissions.test.ts +73 -23
  31. package/src/__tests__/filesystem-tools.test.ts +579 -0
  32. package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
  33. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  34. package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
  35. package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
  36. package/src/__tests__/handlers-slack-config.test.ts +2 -1
  37. package/src/__tests__/handlers-telegram-config.test.ts +855 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +141 -1
  39. package/src/__tests__/hooks-runner.test.ts +6 -2
  40. package/src/__tests__/host-file-edit-tool.test.ts +124 -0
  41. package/src/__tests__/host-file-read-tool.test.ts +62 -0
  42. package/src/__tests__/host-file-write-tool.test.ts +59 -0
  43. package/src/__tests__/host-shell-tool.test.ts +251 -0
  44. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +100 -41
  46. package/src/__tests__/ipc-validate.test.ts +50 -0
  47. package/src/__tests__/key-migration.test.ts +23 -0
  48. package/src/__tests__/memory-regressions.test.ts +99 -0
  49. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  50. package/src/__tests__/oauth-callback-registry.test.ts +11 -4
  51. package/src/__tests__/playbook-execution.test.ts +502 -0
  52. package/src/__tests__/playbook-tools.test.ts +4 -6
  53. package/src/__tests__/public-ingress-urls.test.ts +34 -0
  54. package/src/__tests__/qdrant-manager.test.ts +267 -0
  55. package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
  56. package/src/__tests__/recurrence-engine.test.ts +9 -0
  57. package/src/__tests__/recurrence-types.test.ts +8 -0
  58. package/src/__tests__/registry.test.ts +1 -1
  59. package/src/__tests__/runtime-runs.test.ts +1 -25
  60. package/src/__tests__/schedule-store.test.ts +16 -14
  61. package/src/__tests__/schedule-tools.test.ts +83 -0
  62. package/src/__tests__/scheduler-recurrence.test.ts +111 -10
  63. package/src/__tests__/secret-allowlist.test.ts +18 -17
  64. package/src/__tests__/secret-ingress-handler.test.ts +11 -0
  65. package/src/__tests__/secret-scanner.test.ts +43 -0
  66. package/src/__tests__/session-conflict-gate.test.ts +442 -6
  67. package/src/__tests__/session-init.benchmark.test.ts +3 -0
  68. package/src/__tests__/session-process-bridge.test.ts +242 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -1
  70. package/src/__tests__/shell-identity.test.ts +256 -0
  71. package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
  72. package/src/__tests__/subagent-tools.test.ts +637 -54
  73. package/src/__tests__/task-management-tools.test.ts +936 -0
  74. package/src/__tests__/task-runner.test.ts +2 -2
  75. package/src/__tests__/terminal-tools.test.ts +840 -0
  76. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  77. package/src/__tests__/tool-executor.test.ts +85 -151
  78. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  79. package/src/__tests__/trust-store.test.ts +27 -453
  80. package/src/__tests__/twilio-provider.test.ts +153 -3
  81. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  82. package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
  83. package/src/__tests__/twilio-routes.test.ts +17 -262
  84. package/src/__tests__/twitter-auth-handler.test.ts +2 -1
  85. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  86. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  87. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  88. package/src/__tests__/workspace-policy.test.ts +213 -0
  89. package/src/calls/call-bridge.ts +92 -19
  90. package/src/calls/call-domain.ts +157 -5
  91. package/src/calls/call-orchestrator.ts +93 -7
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +8 -0
  94. package/src/calls/elevenlabs-config.ts +7 -5
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +32 -37
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +29 -7
  99. package/src/cli/twitter.ts +200 -21
  100. package/src/cli.ts +1 -20
  101. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
  102. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
  103. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
  104. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  105. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
  106. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  107. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
  108. package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
  109. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
  110. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
  111. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
  112. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
  113. package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
  114. package/src/config/bundled-skills/twitter/SKILL.md +103 -17
  115. package/src/config/defaults.ts +10 -4
  116. package/src/config/schema.ts +80 -21
  117. package/src/config/types.ts +1 -0
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
  119. package/src/daemon/assistant-attachments.ts +4 -2
  120. package/src/daemon/handlers/apps.ts +69 -0
  121. package/src/daemon/handlers/config.ts +543 -24
  122. package/src/daemon/handlers/index.ts +1 -0
  123. package/src/daemon/handlers/sessions.ts +22 -6
  124. package/src/daemon/handlers/shared.ts +2 -1
  125. package/src/daemon/handlers/skills.ts +5 -20
  126. package/src/daemon/ipc-contract-inventory.json +28 -0
  127. package/src/daemon/ipc-contract.ts +168 -10
  128. package/src/daemon/ipc-validate.ts +17 -0
  129. package/src/daemon/lifecycle.ts +2 -0
  130. package/src/daemon/server.ts +78 -72
  131. package/src/daemon/session-attachments.ts +1 -1
  132. package/src/daemon/session-conflict-gate.ts +62 -6
  133. package/src/daemon/session-notifiers.ts +1 -1
  134. package/src/daemon/session-process.ts +62 -3
  135. package/src/daemon/session-tool-setup.ts +1 -2
  136. package/src/daemon/tls-certs.ts +189 -0
  137. package/src/daemon/video-thumbnail.ts +5 -3
  138. package/src/hooks/manager.ts +5 -9
  139. package/src/memory/app-git-service.ts +295 -0
  140. package/src/memory/app-store.ts +21 -0
  141. package/src/memory/conflict-intent.ts +47 -4
  142. package/src/memory/conflict-policy.ts +73 -0
  143. package/src/memory/conflict-store.ts +9 -1
  144. package/src/memory/contradiction-checker.ts +28 -0
  145. package/src/memory/conversation-key-store.ts +15 -0
  146. package/src/memory/db.ts +81 -0
  147. package/src/memory/embedding-local.ts +3 -13
  148. package/src/memory/external-conversation-store.ts +234 -0
  149. package/src/memory/job-handlers/conflict.ts +22 -2
  150. package/src/memory/jobs-worker.ts +67 -28
  151. package/src/memory/runs-store.ts +54 -7
  152. package/src/memory/schema.ts +20 -0
  153. package/src/messaging/provider.ts +9 -0
  154. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  155. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  156. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  157. package/src/messaging/registry.ts +1 -0
  158. package/src/permissions/checker.ts +48 -44
  159. package/src/permissions/prompter.ts +0 -4
  160. package/src/permissions/shell-identity.ts +227 -0
  161. package/src/permissions/trust-store.ts +76 -53
  162. package/src/permissions/types.ts +0 -19
  163. package/src/permissions/workspace-policy.ts +114 -0
  164. package/src/providers/retry.ts +12 -37
  165. package/src/runtime/assistant-event-hub.ts +41 -4
  166. package/src/runtime/channel-approval-parser.ts +60 -0
  167. package/src/runtime/channel-approval-types.ts +71 -0
  168. package/src/runtime/channel-approvals.ts +145 -0
  169. package/src/runtime/gateway-client.ts +16 -0
  170. package/src/runtime/http-server.ts +29 -9
  171. package/src/runtime/routes/call-routes.ts +52 -2
  172. package/src/runtime/routes/channel-routes.ts +296 -16
  173. package/src/runtime/routes/events-routes.ts +97 -28
  174. package/src/runtime/routes/run-routes.ts +2 -7
  175. package/src/runtime/run-orchestrator.ts +0 -3
  176. package/src/schedule/recurrence-engine.ts +26 -2
  177. package/src/schedule/recurrence-types.ts +1 -1
  178. package/src/schedule/schedule-store.ts +12 -3
  179. package/src/security/secret-scanner.ts +7 -0
  180. package/src/tasks/ephemeral-permissions.ts +0 -2
  181. package/src/tasks/task-scheduler.ts +2 -1
  182. package/src/tools/calls/call-start.ts +8 -0
  183. package/src/tools/execution-target.ts +21 -0
  184. package/src/tools/execution-timeout.ts +49 -0
  185. package/src/tools/executor.ts +6 -135
  186. package/src/tools/network/web-search.ts +9 -32
  187. package/src/tools/policy-context.ts +29 -0
  188. package/src/tools/schedule/update.ts +8 -1
  189. package/src/tools/terminal/parser.ts +16 -18
  190. package/src/tools/types.ts +4 -11
  191. package/src/twitter/oauth-client.ts +102 -0
  192. package/src/twitter/router.ts +101 -0
  193. package/src/util/debounce.ts +88 -0
  194. package/src/util/network-info.ts +47 -0
  195. package/src/util/platform.ts +29 -4
  196. package/src/util/promise-guard.ts +37 -0
  197. package/src/util/retry.ts +98 -0
  198. package/src/util/truncate.ts +1 -1
  199. package/src/workspace/git-service.ts +129 -112
  200. package/src/tools/contacts/contact-merge.ts +0 -55
  201. package/src/tools/contacts/contact-search.ts +0 -58
  202. package/src/tools/contacts/contact-upsert.ts +0 -64
  203. package/src/tools/playbooks/index.ts +0 -4
  204. package/src/tools/playbooks/playbook-create.ts +0 -96
  205. package/src/tools/playbooks/playbook-delete.ts +0 -52
  206. package/src/tools/playbooks/playbook-list.ts +0 -74
  207. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -4,7 +4,6 @@ import type {
4
4
  ClientMessage,
5
5
  ServerMessage,
6
6
  } from '../daemon/ipc-protocol.js';
7
- import type { ConfirmationRequest } from '../daemon/ipc-contract.js';
8
7
 
9
8
  /**
10
9
  * Snapshot tests for every IPC message type.
@@ -218,6 +217,8 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
218
217
  pattern: 'git *',
219
218
  scope: '/projects/my-app',
220
219
  decision: 'allow',
220
+ allowHighRisk: true,
221
+ executionTarget: 'host',
221
222
  },
222
223
  trust_rules_list: {
223
224
  type: 'trust_rules_list',
@@ -327,6 +328,28 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
327
328
  type: 'app_preview_request',
328
329
  appId: 'app-001',
329
330
  },
331
+ app_history_request: {
332
+ type: 'app_history_request',
333
+ appId: 'app-001',
334
+ limit: 25,
335
+ },
336
+ app_diff_request: {
337
+ type: 'app_diff_request',
338
+ appId: 'app-001',
339
+ fromCommit: 'abc123def456',
340
+ toCommit: '789abc123def',
341
+ },
342
+ app_file_at_version_request: {
343
+ type: 'app_file_at_version_request',
344
+ appId: 'app-001',
345
+ path: 'index.html',
346
+ commitHash: 'abc123def456',
347
+ },
348
+ app_restore_request: {
349
+ type: 'app_restore_request',
350
+ appId: 'app-001',
351
+ commitHash: 'abc123def456',
352
+ },
330
353
  share_app_cloud: {
331
354
  type: 'share_app_cloud',
332
355
  appId: 'app-001',
@@ -351,6 +374,10 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
351
374
  type: 'twitter_integration_config',
352
375
  action: 'get',
353
376
  },
377
+ telegram_config: {
378
+ type: 'telegram_config',
379
+ action: 'get',
380
+ },
354
381
  twitter_auth_start: {
355
382
  type: 'twitter_auth_start',
356
383
  },
@@ -516,6 +543,17 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
516
543
  identity_get: {
517
544
  type: 'identity_get',
518
545
  },
546
+ tool_permission_simulate: {
547
+ type: 'tool_permission_simulate',
548
+ toolName: 'bash',
549
+ input: { command: 'rm -rf /tmp/test' },
550
+ workingDir: '/projects/my-app',
551
+ isInteractive: true,
552
+ forcePromptSideEffects: false,
553
+ },
554
+ tool_names_list: {
555
+ type: 'tool_names_list',
556
+ },
519
557
  };
520
558
 
521
559
  // ---------------------------------------------------------------------------
@@ -605,9 +643,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
605
643
  },
606
644
  sandboxed: false,
607
645
  sessionId: 'sess-001',
608
- principalKind: 'skill',
609
- principalId: 'my-skill',
610
- principalVersion: 'sha256:abcdef1234567890',
611
646
  },
612
647
  message_complete: {
613
648
  type: 'message_complete',
@@ -1169,6 +1204,14 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1169
1204
  localClientConfigured: true,
1170
1205
  connected: false,
1171
1206
  },
1207
+ telegram_config_response: {
1208
+ type: 'telegram_config_response',
1209
+ success: true,
1210
+ hasBotToken: true,
1211
+ botUsername: 'my_test_bot',
1212
+ connected: true,
1213
+ hasWebhookSecret: true,
1214
+ },
1172
1215
  twitter_auth_result: {
1173
1216
  type: 'twitter_auth_result',
1174
1217
  success: true,
@@ -1195,6 +1238,37 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1195
1238
  appId: 'app-001',
1196
1239
  preview: 'base64-png-data',
1197
1240
  },
1241
+ app_history_response: {
1242
+ type: 'app_history_response',
1243
+ appId: 'app-001',
1244
+ versions: [
1245
+ {
1246
+ commitHash: 'abc123def456',
1247
+ message: 'Initial app commit',
1248
+ timestamp: 1700000000,
1249
+ },
1250
+ {
1251
+ commitHash: '789abc123def',
1252
+ message: 'Update landing page',
1253
+ timestamp: 1700001000,
1254
+ },
1255
+ ],
1256
+ },
1257
+ app_diff_response: {
1258
+ type: 'app_diff_response',
1259
+ appId: 'app-001',
1260
+ diff: 'diff --git a/index.html b/index.html',
1261
+ },
1262
+ app_file_at_version_response: {
1263
+ type: 'app_file_at_version_response',
1264
+ appId: 'app-001',
1265
+ path: 'index.html',
1266
+ content: '<html><body>Hello</body></html>',
1267
+ },
1268
+ app_restore_response: {
1269
+ type: 'app_restore_response',
1270
+ success: true,
1271
+ },
1198
1272
  ui_surface_undo_result: {
1199
1273
  type: 'ui_surface_undo_result',
1200
1274
  sessionId: 'sess-001',
@@ -1490,6 +1564,28 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1490
1564
  emoji: '✨',
1491
1565
  home: '~/workspace',
1492
1566
  },
1567
+ tool_permission_simulate_response: {
1568
+ type: 'tool_permission_simulate_response',
1569
+ success: true,
1570
+ decision: 'prompt',
1571
+ riskLevel: 'high',
1572
+ reason: 'No matching trust rule; tool requires approval',
1573
+ promptPayload: {
1574
+ allowlistOptions: [
1575
+ { label: 'Allow rm commands', description: 'Allow rm commands', pattern: 'bash:rm *' },
1576
+ ],
1577
+ scopeOptions: [
1578
+ { label: 'In /projects/my-app', scope: '/projects/my-app' },
1579
+ ],
1580
+ persistentDecisionsAllowed: true,
1581
+ },
1582
+ executionTarget: 'host',
1583
+ matchedRuleId: undefined,
1584
+ },
1585
+ tool_names_list_response: {
1586
+ type: 'tool_names_list_response',
1587
+ names: ['bash', 'file_read', 'file_write'],
1588
+ },
1493
1589
  };
1494
1590
 
1495
1591
  // ---------------------------------------------------------------------------
@@ -1601,43 +1697,6 @@ describe('IPC message snapshots', () => {
1601
1697
  });
1602
1698
  });
1603
1699
 
1604
- // Baseline assertions for principal context in confirmation_request.
1605
- describe('confirmation principal context baselines', () => {
1606
- test('confirmation_request includes principal context fields', () => {
1607
- const req = serverMessages.confirmation_request as ConfirmationRequest;
1608
- expect(req.principalKind).toBe('skill');
1609
- expect(req.principalId).toBe('my-skill');
1610
- expect(req.principalVersion).toBe('sha256:abcdef1234567890');
1611
- });
1612
-
1613
- test('confirmation_request principal fields are optional (backward-compatible)', () => {
1614
- // Core tools omit principal fields — verify the contract allows it
1615
- const minimal: typeof serverMessages.confirmation_request = {
1616
- type: 'confirmation_request',
1617
- requestId: 'req-minimal',
1618
- toolName: 'bash',
1619
- input: { command: 'ls' },
1620
- riskLevel: 'low',
1621
- allowlistOptions: [],
1622
- scopeOptions: [],
1623
- };
1624
- const serialized = serialize(minimal);
1625
- const parsed = JSON.parse(serialized.trimEnd());
1626
- expect(parsed.principalKind).toBeUndefined();
1627
- expect(parsed.principalId).toBeUndefined();
1628
- expect(parsed.principalVersion).toBeUndefined();
1629
- });
1630
-
1631
- test('confirmation_request round-trips with all principal fields', () => {
1632
- const req = serverMessages.confirmation_request;
1633
- const serialized = serialize(req);
1634
- const parsed = JSON.parse(serialized.trimEnd());
1635
- expect(parsed.principalKind).toBe('skill');
1636
- expect(parsed.principalId).toBe('my-skill');
1637
- expect(parsed.principalVersion).toBe('sha256:abcdef1234567890');
1638
- });
1639
- });
1640
-
1641
1700
  // Baseline assertions for error-related message contracts.
1642
1701
  // These document the current shape before error handling modernization.
1643
1702
  describe('error message baselines', () => {
@@ -295,6 +295,55 @@ describe('IPC Validate', () => {
295
295
  if (!result.valid) expect(result.reason).toContain('non-empty string "actionId"');
296
296
  });
297
297
 
298
+ // ─── High-risk: add_trust_rule ──────────────────────────────────────
299
+
300
+ test('accepts valid add_trust_rule', () => {
301
+ const result = validateClientMessage({
302
+ type: 'add_trust_rule',
303
+ toolName: 'Bash',
304
+ pattern: '*',
305
+ scope: 'global',
306
+ decision: 'allow',
307
+ });
308
+ expect(result.valid).toBe(true);
309
+ });
310
+
311
+ test('accepts all valid add_trust_rule decisions', () => {
312
+ for (const decision of ['allow', 'deny', 'ask']) {
313
+ const result = validateClientMessage({
314
+ type: 'add_trust_rule',
315
+ toolName: 'Bash',
316
+ pattern: '*',
317
+ scope: 'global',
318
+ decision,
319
+ });
320
+ expect(result.valid).toBe(true);
321
+ }
322
+ });
323
+
324
+ test('rejects add_trust_rule without toolName', () => {
325
+ const result = validateClientMessage({
326
+ type: 'add_trust_rule',
327
+ pattern: '*',
328
+ scope: 'global',
329
+ decision: 'allow',
330
+ });
331
+ expect(result.valid).toBe(false);
332
+ if (!result.valid) expect(result.reason).toContain('non-empty string "toolName"');
333
+ });
334
+
335
+ test('rejects add_trust_rule with invalid decision', () => {
336
+ const result = validateClientMessage({
337
+ type: 'add_trust_rule',
338
+ toolName: 'Bash',
339
+ pattern: '*',
340
+ scope: 'global',
341
+ decision: 'always_allow',
342
+ });
343
+ expect(result.valid).toBe(false);
344
+ if (!result.valid) expect(result.reason).toContain('"decision" must be one of');
345
+ });
346
+
298
347
  // ─── Extra properties are tolerated ─────────────────────────────────
299
348
 
300
349
  test('allows extra properties on known types', () => {
@@ -335,6 +384,7 @@ describe('IPC Validate', () => {
335
384
  secret_response: { requestId: 'r1' },
336
385
  ui_surface_action: { sessionId: 's1', surfaceId: 'sf1', actionId: 'a1' },
337
386
  ipc_blob_probe: { probeId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', nonceSha256: 'abc123' },
387
+ add_trust_rule: { toolName: 'Bash', pattern: '*', scope: 'global', decision: 'allow' },
338
388
  document_save: { surfaceId: 'doc1', conversationId: 'c1', title: 'Doc', content: 'text', wordCount: 1 },
339
389
  document_load: { surfaceId: 'doc1' },
340
390
  document_list: {},
@@ -31,6 +31,9 @@ mock.module('../util/platform.js', () => ({
31
31
  const logsDir = join(TEST_DIR, 'logs');
32
32
  if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
33
33
  },
34
+ migrateToWorkspaceLayout: () => {},
35
+ migrateToDataLayout: () => {},
36
+ migratePath: () => {},
34
37
  isMacOS: () => false,
35
38
  isLinux: () => false,
36
39
  isWindows: () => false,
@@ -45,8 +48,23 @@ import { getSecureKey } from '../security/secure-keys.js';
45
48
  // Tests
46
49
  // ---------------------------------------------------------------------------
47
50
 
51
+ // API key env vars that loadConfig checks — must be cleared during tests
52
+ // so they don't override the migrated values under test.
53
+ const API_KEY_ENV_VARS = [
54
+ 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY',
55
+ 'OLLAMA_API_KEY', 'FIREWORKS_API_KEY', 'OPENROUTER_API_KEY',
56
+ 'BRAVE_API_KEY', 'PERPLEXITY_API_KEY',
57
+ ];
58
+
48
59
  describe('key migration', () => {
60
+ const savedEnv: Record<string, string | undefined> = {};
61
+
49
62
  beforeEach(() => {
63
+ // Save and clear API key env vars
64
+ for (const key of API_KEY_ENV_VARS) {
65
+ savedEnv[key] = process.env[key];
66
+ delete process.env[key];
67
+ }
50
68
  if (existsSync(TEST_DIR)) {
51
69
  rmSync(TEST_DIR, { recursive: true, force: true });
52
70
  }
@@ -62,6 +80,11 @@ describe('key migration', () => {
62
80
  _setStorePath(null);
63
81
  _setBackend(undefined);
64
82
  invalidateConfigCache();
83
+ // Restore API key env vars
84
+ for (const key of API_KEY_ENV_VARS) {
85
+ if (savedEnv[key] === undefined) delete process.env[key];
86
+ else process.env[key] = savedEnv[key]!;
87
+ }
65
88
  });
66
89
 
67
90
  test('[experimental] migrates plaintext apiKeys from config.json to secure storage', () => {
@@ -1216,6 +1216,105 @@ describe('Memory regressions', () => {
1216
1216
  }
1217
1217
  });
1218
1218
 
1219
+ test('background conflict resolver dismisses transient/non-durable conflicts without LLM call', async () => {
1220
+ const db = getDb();
1221
+ const now = 1_700_001_500_000;
1222
+ const originalConflictsEnabled = TEST_CONFIG.memory.conflicts.enabled;
1223
+ TEST_CONFIG.memory.conflicts.enabled = true;
1224
+
1225
+ try {
1226
+ db.insert(conversations).values({
1227
+ id: 'conv-conflicts-transient',
1228
+ title: null,
1229
+ createdAt: now,
1230
+ updatedAt: now,
1231
+ totalInputTokens: 0,
1232
+ totalOutputTokens: 0,
1233
+ totalEstimatedCost: 0,
1234
+ contextSummary: null,
1235
+ contextCompactedMessageCount: 0,
1236
+ contextCompactedAt: null,
1237
+ }).run();
1238
+
1239
+ db.insert(messages).values({
1240
+ id: 'msg-conflicts-transient',
1241
+ conversationId: 'conv-conflicts-transient',
1242
+ role: 'user',
1243
+ content: JSON.stringify([{ type: 'text', text: 'Keep the new one instead.' }]),
1244
+ createdAt: now + 1,
1245
+ }).run();
1246
+
1247
+ // Create a transient conflict: PR tracking statements should be dismissed
1248
+ db.insert(memoryItems).values([
1249
+ {
1250
+ id: 'item-conflict-existing-transient',
1251
+ kind: 'preference',
1252
+ subject: 'pr-tracking',
1253
+ statement: 'Currently tracking PR #42 for review.',
1254
+ status: 'active',
1255
+ confidence: 0.8,
1256
+ fingerprint: 'fp-conflict-existing-transient',
1257
+ verificationState: 'assistant_inferred',
1258
+ scopeId: 'scope-conflicts-transient',
1259
+ firstSeenAt: now - 10_000,
1260
+ lastSeenAt: now - 5_000,
1261
+ validFrom: now - 10_000,
1262
+ invalidAt: null,
1263
+ },
1264
+ {
1265
+ id: 'item-conflict-candidate-transient',
1266
+ kind: 'preference',
1267
+ subject: 'pr-tracking',
1268
+ statement: 'Currently tracking PR #99 for review.',
1269
+ status: 'pending_clarification',
1270
+ confidence: 0.8,
1271
+ fingerprint: 'fp-conflict-candidate-transient',
1272
+ verificationState: 'assistant_inferred',
1273
+ scopeId: 'scope-conflicts-transient',
1274
+ firstSeenAt: now - 9_000,
1275
+ lastSeenAt: now - 4_000,
1276
+ validFrom: now - 9_000,
1277
+ invalidAt: null,
1278
+ },
1279
+ ]).run();
1280
+
1281
+ const conflict = createOrUpdatePendingConflict({
1282
+ scopeId: 'scope-conflicts-transient',
1283
+ existingItemId: 'item-conflict-existing-transient',
1284
+ candidateItemId: 'item-conflict-candidate-transient',
1285
+ relationship: 'ambiguous_contradiction',
1286
+ });
1287
+ db.update(memoryItemConflicts)
1288
+ .set({ createdAt: now, updatedAt: now })
1289
+ .where(eq(memoryItemConflicts.id, conflict.id))
1290
+ .run();
1291
+
1292
+ enqueueResolvePendingConflictsForMessageJob('msg-conflicts-transient', 'scope-conflicts-transient');
1293
+ const processed = await runMemoryJobsOnce();
1294
+ expect(processed).toBe(1);
1295
+
1296
+ const updatedConflict = getConflictById(conflict.id);
1297
+ expect(updatedConflict?.status).toBe('dismissed');
1298
+ expect(updatedConflict?.resolutionNote).toContain('conflict policy');
1299
+
1300
+ // Memory items should remain untouched (no LLM resolution was attempted)
1301
+ const existing = db
1302
+ .select()
1303
+ .from(memoryItems)
1304
+ .where(eq(memoryItems.id, 'item-conflict-existing-transient'))
1305
+ .get();
1306
+ const candidate = db
1307
+ .select()
1308
+ .from(memoryItems)
1309
+ .where(eq(memoryItems.id, 'item-conflict-candidate-transient'))
1310
+ .get();
1311
+ expect(existing?.status).toBe('active');
1312
+ expect(candidate?.status).toBe('pending_clarification');
1313
+ } finally {
1314
+ TEST_CONFIG.memory.conflicts.enabled = originalConflictsEnabled;
1315
+ }
1316
+ });
1317
+
1219
1318
  test('cleanup job enqueue is deduped and retention overrides upgrade payload', () => {
1220
1319
  const db = getDb();
1221
1320
 
@@ -398,7 +398,7 @@ describe('Memory retrieval benchmark', () => {
398
398
  }
399
399
  });
400
400
 
401
- test('recall.latencyMs tracks wall-clock within 20% tolerance', async () => {
401
+ test('recall.latencyMs tracks wall-clock within 50% tolerance', async () => {
402
402
  const conversationId = 'conv-bench-wallclock';
403
403
  const now = 1_700_500_000_000;
404
404
  seedMemoryItems(conversationId, 500, now);
@@ -61,13 +61,20 @@ describe('OAuth callback registry', () => {
61
61
 
62
62
  test('TTL expiry rejects callback with timeout error', async () => {
63
63
  const promise = new Promise<string>((resolve, reject) => {
64
- registerPendingCallback('state-ttl', resolve, reject, 50);
64
+ registerPendingCallback('state-ttl', resolve, reject, 100);
65
65
  });
66
66
 
67
- // Wait for the TTL to expire
68
- await new Promise((r) => setTimeout(r, 100));
67
+ // Attach a catch handler immediately to prevent unhandled rejection
68
+ // during the sleep. We capture the error and verify it afterwards.
69
+ let caughtError: Error | undefined;
70
+ const guarded = promise.catch((err) => { caughtError = err; });
69
71
 
70
- await expect(promise).rejects.toThrow('OAuth callback timed out');
72
+ // Wait for the TTL to expire (generous margin)
73
+ await new Promise((r) => setTimeout(r, 300));
74
+ await guarded;
75
+
76
+ expect(caughtError).toBeDefined();
77
+ expect(caughtError!.message).toBe('OAuth callback timed out');
71
78
 
72
79
  // After expiry, consume should return false
73
80
  const consumed = consumeCallback('state-ttl', 'late-code');