@vellumai/assistant 0.5.14 → 0.5.16

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 (175) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/integrations.md +15 -14
  3. package/knip.json +3 -1
  4. package/openapi.yaml +11 -43
  5. package/package.json +1 -1
  6. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
  7. package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
  8. package/src/__tests__/checker.test.ts +59 -0
  9. package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
  10. package/src/__tests__/cli-memory.test.ts +372 -0
  11. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
  12. package/src/__tests__/config-schema.test.ts +0 -2
  13. package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
  15. package/src/__tests__/conversation-slash-commands.test.ts +2 -6
  16. package/src/__tests__/conversation-usage.test.ts +1 -0
  17. package/src/__tests__/credential-security-e2e.test.ts +4 -1
  18. package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
  20. package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
  21. package/src/__tests__/heartbeat-service.test.ts +1 -3
  22. package/src/__tests__/intent-routing.test.ts +6 -18
  23. package/src/__tests__/log-export-workspace.test.ts +2 -28
  24. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
  25. package/src/__tests__/managed-store.test.ts +2 -10
  26. package/src/__tests__/messaging-send-tool.test.ts +6 -6
  27. package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
  28. package/src/__tests__/migration-export-http.test.ts +3 -34
  29. package/src/__tests__/migration-import-commit-http.test.ts +1 -29
  30. package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
  31. package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
  32. package/src/__tests__/oauth-apps-routes.test.ts +120 -10
  33. package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
  34. package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
  35. package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
  36. package/src/__tests__/oauth-providers-routes.test.ts +5 -2
  37. package/src/__tests__/oauth-store.test.ts +0 -5
  38. package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
  39. package/src/__tests__/path-policy.test.ts +2 -17
  40. package/src/__tests__/permission-types.test.ts +0 -1
  41. package/src/__tests__/platform-callback-registration.test.ts +3 -7
  42. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  43. package/src/__tests__/provider-error-scenarios.test.ts +0 -2
  44. package/src/__tests__/qdrant-manager.test.ts +68 -21
  45. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  46. package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
  47. package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
  48. package/src/__tests__/secret-allowlist.test.ts +20 -35
  49. package/src/__tests__/shell-credential-ref.test.ts +0 -5
  50. package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
  51. package/src/__tests__/skill-load-inline-command.test.ts +3 -65
  52. package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
  53. package/src/__tests__/skill-load-tool.test.ts +3 -67
  54. package/src/__tests__/skill-memory.test.ts +362 -119
  55. package/src/__tests__/skills.test.ts +22 -49
  56. package/src/__tests__/slack-channel-config.test.ts +2 -21
  57. package/src/__tests__/starter-bundle.test.ts +2 -8
  58. package/src/__tests__/stt-hints.test.ts +7 -2
  59. package/src/__tests__/system-prompt.test.ts +25 -45
  60. package/src/__tests__/task-compiler.test.ts +0 -21
  61. package/src/__tests__/task-management-tools.test.ts +0 -21
  62. package/src/__tests__/task-memory-cleanup.test.ts +0 -21
  63. package/src/__tests__/task-runner.test.ts +0 -21
  64. package/src/__tests__/task-scheduler.test.ts +0 -21
  65. package/src/__tests__/terminal-tools.test.ts +1 -17
  66. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
  67. package/src/__tests__/tool-approval-handler.test.ts +1 -20
  68. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
  69. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
  70. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  71. package/src/__tests__/tool-executor.test.ts +0 -1
  72. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
  73. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
  74. package/src/__tests__/trust-store.test.ts +9 -41
  75. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
  76. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
  77. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
  78. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
  79. package/src/__tests__/trusted-contact-verification.test.ts +0 -22
  80. package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
  81. package/src/__tests__/twilio-provider.test.ts +0 -16
  82. package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
  83. package/src/__tests__/twilio-routes.test.ts +0 -24
  84. package/src/__tests__/update-bulletin.test.ts +17 -89
  85. package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
  86. package/src/__tests__/usage-routes.test.ts +0 -21
  87. package/src/__tests__/user-reference.test.ts +1 -5
  88. package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
  89. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
  90. package/src/__tests__/voice-invite-redemption.test.ts +0 -21
  91. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
  92. package/src/__tests__/voice-session-bridge.test.ts +0 -21
  93. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
  94. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
  96. package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
  97. package/src/acp/client-handler.ts +1 -2
  98. package/src/cli/__tests__/notifications.test.ts +0 -22
  99. package/src/cli/cli-memory.ts +176 -0
  100. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  101. package/src/cli/commands/oauth/connect.ts +15 -0
  102. package/src/cli/commands/oauth/providers.ts +49 -42
  103. package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
  104. package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
  105. package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
  106. package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
  107. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  108. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  109. package/src/config/feature-flag-registry.json +16 -0
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/security.ts +0 -6
  112. package/src/config/schemas/services.ts +8 -0
  113. package/src/context/window-manager.ts +28 -9
  114. package/src/credential-execution/approval-bridge.ts +0 -1
  115. package/src/daemon/config-watcher.ts +51 -0
  116. package/src/daemon/conversation-agent-loop.ts +3 -2
  117. package/src/daemon/conversation-process.ts +1 -0
  118. package/src/daemon/conversation-usage.ts +1 -0
  119. package/src/daemon/handlers/skills.ts +9 -1
  120. package/src/daemon/lifecycle.ts +13 -4
  121. package/src/daemon/message-types/conversations.ts +1 -0
  122. package/src/daemon/providers-setup.ts +2 -0
  123. package/src/daemon/server.ts +26 -22
  124. package/src/events/domain-events.ts +1 -2
  125. package/src/memory/db-init.ts +9 -0
  126. package/src/memory/job-handlers/batch-extraction.ts +16 -4
  127. package/src/memory/job-handlers/embedding.test.ts +3 -27
  128. package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
  129. package/src/memory/llm-usage-store.ts +35 -2
  130. package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
  131. package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
  132. package/src/memory/migrations/index.ts +2 -0
  133. package/src/memory/qdrant-manager.ts +26 -5
  134. package/src/memory/query-expansion.ts +1 -1
  135. package/src/memory/retriever.test.ts +22 -20
  136. package/src/memory/retriever.ts +10 -2
  137. package/src/memory/schema/oauth.ts +1 -1
  138. package/src/memory/search/mmr.ts +8 -5
  139. package/src/memory/slack-thread-store.ts +17 -0
  140. package/src/messaging/providers/outlook/adapter.ts +193 -0
  141. package/src/messaging/providers/outlook/client.ts +311 -0
  142. package/src/messaging/providers/outlook/types.ts +83 -0
  143. package/src/notifications/adapters/slack.ts +1 -1
  144. package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
  145. package/src/oauth/connect-orchestrator.ts +10 -3
  146. package/src/oauth/oauth-store.ts +10 -11
  147. package/src/oauth/provider-serializer.ts +3 -0
  148. package/src/oauth/provider-visibility.ts +16 -0
  149. package/src/oauth/seed-providers.ts +49 -17
  150. package/src/permissions/checker.ts +39 -7
  151. package/src/permissions/types.ts +2 -4
  152. package/src/prompts/journal-context.ts +9 -11
  153. package/src/prompts/system-prompt.ts +3 -64
  154. package/src/prompts/templates/UPDATES.md +6 -0
  155. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
  156. package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
  157. package/src/runtime/auth/route-policy.ts +0 -4
  158. package/src/runtime/guardian-reply-router.ts +6 -2
  159. package/src/runtime/routes/conversation-query-routes.ts +2 -58
  160. package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
  161. package/src/runtime/routes/memory-item-routes.test.ts +0 -17
  162. package/src/runtime/routes/memory-item-routes.ts +103 -12
  163. package/src/runtime/routes/oauth-apps.ts +18 -1
  164. package/src/runtime/routes/oauth-providers.ts +13 -1
  165. package/src/runtime/routes/settings-routes.ts +1 -0
  166. package/src/runtime/routes/usage-routes.ts +19 -2
  167. package/src/runtime/routes/work-items-routes.test.ts +0 -21
  168. package/src/runtime/routes/workspace-routes.test.ts +3 -27
  169. package/src/security/secret-allowlist.ts +4 -4
  170. package/src/skills/skill-memory.ts +62 -23
  171. package/src/tools/memory/handlers.test.ts +1 -29
  172. package/src/tools/permission-checker.ts +0 -18
  173. package/src/tools/skills/skill-script-runner.ts +1 -1
  174. package/src/util/device-id.ts +3 -65
  175. package/src/workspace/git-service.ts +27 -6
@@ -5,11 +5,11 @@ import {
5
5
  readdirSync,
6
6
  readFileSync,
7
7
  rmSync,
8
+ symlinkSync,
8
9
  writeFileSync,
9
10
  } from "node:fs";
10
11
  import { join } from "node:path";
11
12
  import {
12
- afterAll,
13
13
  afterEach,
14
14
  beforeAll,
15
15
  beforeEach,
@@ -19,11 +19,7 @@ import {
19
19
  test,
20
20
  } from "bun:test";
21
21
 
22
- const testDataDir = "/tmp/qdrant-manager-test-" + process.pid;
23
-
24
- mock.module("../util/platform.js", () => ({
25
- getDataDir: () => testDataDir,
26
- }));
22
+ const testDataDir = process.env.VELLUM_WORKSPACE_DIR!;
27
23
 
28
24
  mock.module("../util/logger.js", () => ({
29
25
  getLogger: () =>
@@ -42,7 +38,7 @@ const FAST_TIMEOUTS = {
42
38
  } as const;
43
39
 
44
40
  function placeFakeBinary(script: string): string {
45
- const binaryPath = join(testDataDir, "qdrant", "bin", "qdrant");
41
+ const binaryPath = join(testDataDir, "data", "qdrant", "bin", "qdrant");
46
42
  writeFileSync(binaryPath, script);
47
43
  chmodSync(binaryPath, 0o755);
48
44
  return binaryPath;
@@ -53,7 +49,7 @@ function getTestPort(): number {
53
49
  return nextPort++;
54
50
  }
55
51
 
56
- const qdrantDir = join(testDataDir, "qdrant");
52
+ const qdrantDir = join(testDataDir, "data", "qdrant");
57
53
  const qdrantBinDir = join(qdrantDir, "bin");
58
54
 
59
55
  beforeAll(() => {
@@ -79,10 +75,6 @@ afterEach(() => {
79
75
  delete process.env.QDRANT_URL;
80
76
  });
81
77
 
82
- afterAll(() => {
83
- rmSync(testDataDir, { recursive: true, force: true });
84
- });
85
-
86
78
  describe("QdrantManager", () => {
87
79
  // ── Constructor ──────────────────────────────────────────────
88
80
 
@@ -147,7 +139,7 @@ describe("QdrantManager", () => {
147
139
 
148
140
  describe("stop() without running process", () => {
149
141
  test("removes stale PID file", async () => {
150
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
142
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
151
143
  writeFileSync(pidPath, "99999");
152
144
 
153
145
  const mgr = new QdrantManager({ url: "http://127.0.0.1:6333" });
@@ -166,7 +158,7 @@ describe("QdrantManager", () => {
166
158
 
167
159
  describe("stale PID cleanup during start()", () => {
168
160
  test("removes PID file for non-existent process", async () => {
169
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
161
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
170
162
  writeFileSync(pidPath, "2147483647");
171
163
 
172
164
  placeFakeBinary("#!/bin/sh\nexit 1");
@@ -187,7 +179,7 @@ describe("QdrantManager", () => {
187
179
  }, 10_000);
188
180
 
189
181
  test("handles invalid PID file contents", async () => {
190
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
182
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
191
183
  writeFileSync(pidPath, "garbage");
192
184
 
193
185
  placeFakeBinary("#!/bin/sh\nexit 1");
@@ -208,7 +200,7 @@ describe("QdrantManager", () => {
208
200
  }, 10_000);
209
201
 
210
202
  test("handles empty PID file", async () => {
211
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
203
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
212
204
  writeFileSync(pidPath, "");
213
205
 
214
206
  placeFakeBinary("#!/bin/sh\nexit 1");
@@ -233,7 +225,7 @@ describe("QdrantManager", () => {
233
225
 
234
226
  describe("process lifecycle", () => {
235
227
  test("writes PID file after spawning", async () => {
236
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
228
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
237
229
 
238
230
  // Binary that stays alive. We'll stop it before readyz times out.
239
231
  placeFakeBinary("#!/bin/sh\nexec sleep 300");
@@ -265,7 +257,7 @@ describe("QdrantManager", () => {
265
257
  }, 10_000);
266
258
 
267
259
  test("stop() escalates to SIGKILL after grace period", async () => {
268
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
260
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
269
261
 
270
262
  // Binary that ignores SIGTERM
271
263
  placeFakeBinary('#!/bin/sh\ntrap "" TERM\nexec sleep 300');
@@ -297,7 +289,7 @@ describe("QdrantManager", () => {
297
289
 
298
290
  describe("start failure cleanup", () => {
299
291
  test("cleans up process on readyz timeout", async () => {
300
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
292
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
301
293
 
302
294
  // Binary that stays alive but never serves readyz
303
295
  placeFakeBinary("#!/bin/sh\nexec sleep 300");
@@ -313,7 +305,7 @@ describe("QdrantManager", () => {
313
305
  }, 10_000);
314
306
 
315
307
  test("fails fast with exit code when process exits immediately", async () => {
316
- const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
308
+ const pidPath = join(testDataDir, "data", "qdrant", "qdrant.pid");
317
309
 
318
310
  // GIVEN a Qdrant binary that exits immediately with code 1
319
311
  placeFakeBinary("#!/bin/sh\nexit 1");
@@ -372,8 +364,63 @@ describe("QdrantManager", () => {
372
364
  /* readyz timeout */
373
365
  }
374
366
 
375
- const binaryPath = join(testDataDir, "qdrant", "bin", "qdrant");
367
+ const binaryPath = join(testDataDir, "data", "qdrant", "bin", "qdrant");
376
368
  expect(existsSync(binaryPath)).toBe(true);
377
369
  }, 10_000);
378
370
  });
371
+
372
+ // ── Symlink Safety ────────────────────────────────────────────
373
+
374
+ describe("vellum-qdrant symlink safety", () => {
375
+ test("ignores pre-existing non-symlink vellum-qdrant file", async () => {
376
+ const realMarkerPath = join(qdrantDir, "real-executed.txt");
377
+ const hijackMarkerPath = join(qdrantDir, "hijack-executed.txt");
378
+
379
+ placeFakeBinary(`#!/bin/sh\necho real > "${realMarkerPath}"\nexit 1`);
380
+
381
+ const hijackPath = join(qdrantBinDir, "vellum-qdrant");
382
+ writeFileSync(
383
+ hijackPath,
384
+ `#!/bin/sh\necho hijack > "${hijackMarkerPath}"\nexit 0`,
385
+ );
386
+ chmodSync(hijackPath, 0o755);
387
+
388
+ const port = getTestPort();
389
+ const mgr = new QdrantManager({
390
+ url: `http://127.0.0.1:${port}`,
391
+ ...FAST_TIMEOUTS,
392
+ });
393
+
394
+ await expect(mgr.start()).rejects.toThrow("before becoming ready");
395
+ expect(existsSync(realMarkerPath)).toBe(true);
396
+ expect(existsSync(hijackMarkerPath)).toBe(false);
397
+ }, 10_000);
398
+
399
+ test("ignores symlink that does not point to real qdrant binary", async () => {
400
+ const realMarkerPath = join(qdrantDir, "real-executed.txt");
401
+ const hijackMarkerPath = join(qdrantDir, "hijack-executed.txt");
402
+
403
+ placeFakeBinary(`#!/bin/sh\necho real > "${realMarkerPath}"\nexit 1`);
404
+
405
+ const evilBinaryPath = join(qdrantBinDir, "evil-qdrant");
406
+ writeFileSync(
407
+ evilBinaryPath,
408
+ `#!/bin/sh\necho hijack > "${hijackMarkerPath}"\nexit 0`,
409
+ );
410
+ chmodSync(evilBinaryPath, 0o755);
411
+
412
+ const vellumQdrantPath = join(qdrantBinDir, "vellum-qdrant");
413
+ symlinkSync(evilBinaryPath, vellumQdrantPath);
414
+
415
+ const port = getTestPort();
416
+ const mgr = new QdrantManager({
417
+ url: `http://127.0.0.1:${port}`,
418
+ ...FAST_TIMEOUTS,
419
+ });
420
+
421
+ await expect(mgr.start()).rejects.toThrow("before becoming ready");
422
+ expect(existsSync(realMarkerPath)).toBe(true);
423
+ expect(existsSync(hijackMarkerPath)).toBe(false);
424
+ }, 10_000);
425
+ });
379
426
  });
@@ -60,7 +60,6 @@ const mockConfig = {
60
60
  },
61
61
  permissions: {
62
62
  mode: "workspace" as const,
63
- dangerouslySkipPermissions: false,
64
63
  },
65
64
  };
66
65
 
@@ -1,6 +1,5 @@
1
1
  import * as realChildProcess from "node:child_process";
2
- import { join } from "node:path";
3
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
3
 
5
4
  const execSyncMock = mock(
6
5
  (_command: string, _opts?: unknown): unknown => undefined,
@@ -11,25 +10,6 @@ mock.module("node:child_process", () => ({
11
10
  execSync: execSyncMock,
12
11
  }));
13
12
 
14
- // Mock platform detection — default to macOS
15
- let mockIsMacOS = true;
16
- let mockIsLinux = false;
17
-
18
- mock.module("../util/platform.js", () => ({
19
- isMacOS: () => mockIsMacOS,
20
- isLinux: () => mockIsLinux,
21
- getProtectedDir: () => join("/tmp/vellum-test", "protected"),
22
- getDataDir: () => "/tmp/vellum-test/data",
23
- getDbPath: () => "/tmp/vellum-test/data/db/assistant.db",
24
- getLogPath: () => "/tmp/vellum-test/data/logs/daemon.log",
25
- getSandboxRootDir: () => "/tmp/vellum-test/sandbox",
26
- getSandboxWorkingDir: () => "/tmp/vellum-test/workspace",
27
- ensureDataDir: () => {},
28
- getHistoryPath: () => "/tmp/vellum-test/data/history",
29
- getHooksDir: () => "/tmp/vellum-test/hooks",
30
- getPidPath: () => "/tmp/vellum-test/data/daemon.pid",
31
- }));
32
-
33
13
  // Mock config loader — return a config with sandbox settings
34
14
  let mockSandboxConfig: {
35
15
  enabled: boolean;
@@ -58,10 +38,17 @@ mock.module("../util/logger.js", () => ({
58
38
  const { runSandboxDiagnostics } =
59
39
  await import("../tools/terminal/sandbox-diagnostics.js");
60
40
 
41
+ // ---------------------------------------------------------------------------
42
+ // Helper: temporarily override process.platform for a test
43
+ // ---------------------------------------------------------------------------
44
+ const originalPlatform = process.platform;
45
+
46
+ function setPlatform(platform: string): void {
47
+ Object.defineProperty(process, "platform", { value: platform });
48
+ }
49
+
61
50
  beforeEach(() => {
62
51
  execSyncMock.mockReset();
63
- mockIsMacOS = true;
64
- mockIsLinux = false;
65
52
  mockSandboxConfig = {
66
53
  enabled: true,
67
54
  };
@@ -69,6 +56,11 @@ beforeEach(() => {
69
56
  execSyncMock.mockImplementation(() => undefined);
70
57
  });
71
58
 
59
+ afterEach(() => {
60
+ // Restore the real platform after every test
61
+ setPlatform(originalPlatform);
62
+ });
63
+
72
64
  describe("runSandboxDiagnostics — config reporting", () => {
73
65
  test("reports sandbox enabled state", () => {
74
66
  const result = runSandboxDiagnostics();
@@ -97,6 +89,7 @@ describe("runSandboxDiagnostics — active backend reason", () => {
97
89
 
98
90
  describe("runSandboxDiagnostics — native backend check (macOS)", () => {
99
91
  test("passes when sandbox-exec works on macOS", () => {
92
+ setPlatform("darwin");
100
93
  const result = runSandboxDiagnostics();
101
94
  const nativeCheck = result.checks.find((c) =>
102
95
  c.label.includes("Native sandbox"),
@@ -107,6 +100,7 @@ describe("runSandboxDiagnostics — native backend check (macOS)", () => {
107
100
  });
108
101
 
109
102
  test("fails when sandbox-exec does not work on macOS", () => {
103
+ setPlatform("darwin");
110
104
  execSyncMock.mockImplementation((cmd: string) => {
111
105
  if (typeof cmd === "string" && cmd.includes("sandbox-exec")) {
112
106
  throw new Error("not available");
@@ -124,8 +118,7 @@ describe("runSandboxDiagnostics — native backend check (macOS)", () => {
124
118
 
125
119
  describe("runSandboxDiagnostics — native backend check (Linux)", () => {
126
120
  test("passes when bwrap works on Linux", () => {
127
- mockIsMacOS = false;
128
- mockIsLinux = true;
121
+ setPlatform("linux");
129
122
  const result = runSandboxDiagnostics();
130
123
  const nativeCheck = result.checks.find((c) =>
131
124
  c.label.includes("Native sandbox"),
@@ -136,8 +129,7 @@ describe("runSandboxDiagnostics — native backend check (Linux)", () => {
136
129
  });
137
130
 
138
131
  test("fails when bwrap is not available on Linux", () => {
139
- mockIsMacOS = false;
140
- mockIsLinux = true;
132
+ setPlatform("linux");
141
133
  execSyncMock.mockImplementation((cmd: string) => {
142
134
  if (typeof cmd === "string" && cmd.includes("bwrap")) {
143
135
  throw new Error("not found");
@@ -156,8 +148,7 @@ describe("runSandboxDiagnostics — native backend check (Linux)", () => {
156
148
 
157
149
  describe("runSandboxDiagnostics — native backend check (unsupported OS)", () => {
158
150
  test("reports unsupported when neither macOS nor Linux", () => {
159
- mockIsMacOS = false;
160
- mockIsLinux = false;
151
+ setPlatform("win32");
161
152
  const result = runSandboxDiagnostics();
162
153
  const nativeCheck = result.checks.find((c) =>
163
154
  c.label.includes("Native sandbox"),
@@ -1,15 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
- import { mkdtempSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
2
  import { join } from "node:path";
5
3
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
6
4
 
7
- let TEST_DIR = "";
8
-
9
- mock.module("../util/platform.js", () => ({
10
- getProtectedDir: () => join(TEST_DIR, "protected"),
11
- getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
12
- }));
5
+ const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
13
6
 
14
7
  mock.module("../util/logger.js", () => ({
15
8
  getLogger: () =>
@@ -30,12 +23,11 @@ function makeContext(): ToolContext {
30
23
  }
31
24
 
32
25
  beforeEach(() => {
33
- TEST_DIR = mkdtempSync(join(tmpdir(), "scaffold-tool-test-"));
34
26
  mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
35
27
  });
36
28
 
37
29
  afterEach(() => {
38
- rmSync(TEST_DIR, { recursive: true, force: true });
30
+ rmSync(join(TEST_DIR, "skills"), { recursive: true, force: true });
39
31
  });
40
32
 
41
33
  describe("scaffold_managed_skill tool", () => {
@@ -1,10 +1,8 @@
1
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
2
  import { join } from "node:path";
4
3
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
5
4
 
6
- // Mock the data dir to a temp directory so tests don't touch ~/.vellum/
7
- let testDir: string;
5
+ const testDir = process.env.VELLUM_WORKSPACE_DIR!;
8
6
 
9
7
  mock.module("../util/logger.js", () => ({
10
8
  getLogger: () =>
@@ -13,11 +11,6 @@ mock.module("../util/logger.js", () => ({
13
11
  }),
14
12
  }));
15
13
 
16
- mock.module("../util/platform.js", () => ({
17
- getProtectedDir: () => join(testDir, "protected"),
18
- getDataDir: () => testDir,
19
- }));
20
-
21
14
  import {
22
15
  isAllowlisted,
23
16
  loadAllowlist,
@@ -27,21 +20,13 @@ import { scanText } from "../security/secret-scanner.js";
27
20
 
28
21
  describe("secret-allowlist", () => {
29
22
  beforeEach(() => {
30
- testDir = join(
31
- tmpdir(),
32
- `vellum-allowlist-test-${Date.now()}-${Math.random()
33
- .toString(36)
34
- .slice(2)}`,
35
- );
36
- mkdirSync(join(testDir, "protected"), { recursive: true });
23
+ mkdirSync(join(testDir, "data"), { recursive: true });
37
24
  resetAllowlist();
38
25
  });
39
26
 
40
27
  afterEach(() => {
41
28
  resetAllowlist();
42
- if (existsSync(testDir)) {
43
- rmSync(testDir, { recursive: true, force: true });
44
- }
29
+ rmSync(join(testDir, "data"), { recursive: true, force: true });
45
30
  });
46
31
 
47
32
  // -----------------------------------------------------------------------
@@ -56,7 +41,7 @@ describe("secret-allowlist", () => {
56
41
  // -----------------------------------------------------------------------
57
42
  test("[experimental] suppresses exact values", () => {
58
43
  writeFileSync(
59
- join(testDir, "protected", "secret-allowlist.json"),
44
+ join(testDir, "data", "secret-allowlist.json"),
60
45
  JSON.stringify({ values: ["my-test-api-key-12345"] }),
61
46
  );
62
47
  expect(isAllowlisted("my-test-api-key-12345")).toBe(true);
@@ -65,7 +50,7 @@ describe("secret-allowlist", () => {
65
50
 
66
51
  test("[experimental] exact values are case-sensitive", () => {
67
52
  writeFileSync(
68
- join(testDir, "protected", "secret-allowlist.json"),
53
+ join(testDir, "data", "secret-allowlist.json"),
69
54
  JSON.stringify({ values: ["MyTestKey"] }),
70
55
  );
71
56
  expect(isAllowlisted("MyTestKey")).toBe(true);
@@ -77,7 +62,7 @@ describe("secret-allowlist", () => {
77
62
  // -----------------------------------------------------------------------
78
63
  test("[experimental] suppresses values matching a prefix", () => {
79
64
  writeFileSync(
80
- join(testDir, "protected", "secret-allowlist.json"),
65
+ join(testDir, "data", "secret-allowlist.json"),
81
66
  JSON.stringify({ prefixes: ["my-internal-"] }),
82
67
  );
83
68
  expect(isAllowlisted("my-internal-key-abc123")).toBe(true);
@@ -89,7 +74,7 @@ describe("secret-allowlist", () => {
89
74
  // -----------------------------------------------------------------------
90
75
  test("[experimental] suppresses values matching a regex pattern", () => {
91
76
  writeFileSync(
92
- join(testDir, "protected", "secret-allowlist.json"),
77
+ join(testDir, "data", "secret-allowlist.json"),
93
78
  JSON.stringify({ patterns: ["^ci-test-[a-z0-9]+$"] }),
94
79
  );
95
80
  expect(isAllowlisted("ci-test-abc123")).toBe(true);
@@ -98,7 +83,7 @@ describe("secret-allowlist", () => {
98
83
 
99
84
  test("[experimental] invalid regex is skipped without crashing", () => {
100
85
  writeFileSync(
101
- join(testDir, "protected", "secret-allowlist.json"),
86
+ join(testDir, "data", "secret-allowlist.json"),
102
87
  JSON.stringify({ patterns: ["[invalid", "^valid$"] }),
103
88
  );
104
89
  loadAllowlist();
@@ -113,7 +98,7 @@ describe("secret-allowlist", () => {
113
98
  // -----------------------------------------------------------------------
114
99
  test("[experimental] combines values, prefixes, and patterns", () => {
115
100
  writeFileSync(
116
- join(testDir, "protected", "secret-allowlist.json"),
101
+ join(testDir, "data", "secret-allowlist.json"),
117
102
  JSON.stringify({
118
103
  values: ["exact-match-value"],
119
104
  prefixes: ["test-prefix-"],
@@ -131,7 +116,7 @@ describe("secret-allowlist", () => {
131
116
  // -----------------------------------------------------------------------
132
117
  test("handles malformed JSON gracefully", () => {
133
118
  writeFileSync(
134
- join(testDir, "protected", "secret-allowlist.json"),
119
+ join(testDir, "data", "secret-allowlist.json"),
135
120
  "not json{{{",
136
121
  );
137
122
  loadAllowlist();
@@ -141,7 +126,7 @@ describe("secret-allowlist", () => {
141
126
 
142
127
  test("handles non-array fields gracefully", () => {
143
128
  writeFileSync(
144
- join(testDir, "protected", "secret-allowlist.json"),
129
+ join(testDir, "data", "secret-allowlist.json"),
145
130
  JSON.stringify({ values: "not-an-array", prefixes: 42 }),
146
131
  );
147
132
  loadAllowlist();
@@ -154,7 +139,7 @@ describe("secret-allowlist", () => {
154
139
  test("[experimental] allowlisted values are suppressed by scanText", () => {
155
140
  const awsKey = "AKIAIOSFODNN7REALKEY";
156
141
  writeFileSync(
157
- join(testDir, "protected", "secret-allowlist.json"),
142
+ join(testDir, "data", "secret-allowlist.json"),
158
143
  JSON.stringify({ values: [awsKey] }),
159
144
  );
160
145
  resetAllowlist();
@@ -166,7 +151,7 @@ describe("secret-allowlist", () => {
166
151
 
167
152
  test("non-allowlisted values are still detected by scanText", () => {
168
153
  writeFileSync(
169
- join(testDir, "protected", "secret-allowlist.json"),
154
+ join(testDir, "data", "secret-allowlist.json"),
170
155
  JSON.stringify({ values: ["AKIAIOSFODNN7OTHERKE"] }),
171
156
  );
172
157
  resetAllowlist();
@@ -178,7 +163,7 @@ describe("secret-allowlist", () => {
178
163
 
179
164
  test("[experimental] prefix allowlist suppresses pattern matches", () => {
180
165
  writeFileSync(
181
- join(testDir, "protected", "secret-allowlist.json"),
166
+ join(testDir, "data", "secret-allowlist.json"),
182
167
  JSON.stringify({ prefixes: ["ghp_AAAA"] }),
183
168
  );
184
169
  resetAllowlist();
@@ -198,7 +183,7 @@ describe("secret-allowlist", () => {
198
183
 
199
184
  // Create the file — but fileChecked is cached, so it won't be seen
200
185
  writeFileSync(
201
- join(testDir, "protected", "secret-allowlist.json"),
186
+ join(testDir, "data", "secret-allowlist.json"),
202
187
  JSON.stringify({ values: ["test-key"] }),
203
188
  );
204
189
  expect(isAllowlisted("test-key")).toBe(false);
@@ -211,7 +196,7 @@ describe("secret-allowlist", () => {
211
196
  test("[experimental] retries loading when file was malformed on first call", () => {
212
197
  // First call with malformed JSON
213
198
  writeFileSync(
214
- join(testDir, "protected", "secret-allowlist.json"),
199
+ join(testDir, "data", "secret-allowlist.json"),
215
200
  "not json{{{",
216
201
  );
217
202
  loadAllowlist();
@@ -219,7 +204,7 @@ describe("secret-allowlist", () => {
219
204
 
220
205
  // Fix the file
221
206
  writeFileSync(
222
- join(testDir, "protected", "secret-allowlist.json"),
207
+ join(testDir, "data", "secret-allowlist.json"),
223
208
  JSON.stringify({ values: ["test-key"] }),
224
209
  );
225
210
 
@@ -232,7 +217,7 @@ describe("secret-allowlist", () => {
232
217
  // -----------------------------------------------------------------------
233
218
  test("[experimental] resetAllowlist clears cached state", () => {
234
219
  writeFileSync(
235
- join(testDir, "protected", "secret-allowlist.json"),
220
+ join(testDir, "data", "secret-allowlist.json"),
236
221
  JSON.stringify({ values: ["test-value"] }),
237
222
  );
238
223
  loadAllowlist();
@@ -240,7 +225,7 @@ describe("secret-allowlist", () => {
240
225
 
241
226
  // Reset and remove file — should no longer be allowlisted
242
227
  resetAllowlist();
243
- rmSync(join(testDir, "protected", "secret-allowlist.json"));
228
+ rmSync(join(testDir, "data", "secret-allowlist.json"));
244
229
  expect(isAllowlisted("test-value")).toBe(false);
245
230
  });
246
231
  });
@@ -45,11 +45,6 @@ mock.module("../tools/terminal/safe-env.js", () => ({
45
45
  buildSanitizedEnv: () => ({ PATH: "/usr/bin" }),
46
46
  }));
47
47
 
48
- // Mock platform
49
- mock.module("../util/platform.js", () => ({
50
- getDataDir: () => "/tmp/test-data",
51
- }));
52
-
53
48
  // Mock proxy session manager
54
49
  const mockGetOrStartSession = mock((_convId: string, _credIds: string[]) =>
55
50
  Promise.resolve({
@@ -2,57 +2,19 @@
2
2
  * Tests that skill_load rejects loading a skill whose feature flag is OFF
3
3
  * with a deterministic error message.
4
4
  */
5
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
6
- import { tmpdir } from "node:os";
5
+ import { mkdirSync, writeFileSync } from "node:fs";
7
6
  import { join } from "node:path";
8
7
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
9
8
 
10
9
  import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
11
10
 
12
- const TEST_DIR = join(
13
- tmpdir(),
14
- `vellum-skill-load-flag-test-${crypto.randomUUID()}`,
15
- );
11
+ const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
16
12
 
17
13
  let currentConfig: Record<string, unknown> = {};
18
14
 
19
15
  const DECLARED_SKILL_ID = "contacts";
20
16
  const DECLARED_FLAG_KEY = "contacts";
21
17
 
22
- const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
23
- getRootDir: () => TEST_DIR,
24
- getDataDir: () => TEST_DIR,
25
- ensureDataDir: () => {},
26
- getPidPath: () => join(TEST_DIR, "vellum.pid"),
27
- getDbPath: () => join(TEST_DIR, "data", "assistant.db"),
28
- getLogPath: () => join(TEST_DIR, "logs", "vellum.log"),
29
- getWorkspaceDir: () => join(TEST_DIR, "workspace"),
30
- getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
31
- getWorkspaceConfigPath: () => join(TEST_DIR, "workspace", "config.json"),
32
- getWorkspaceHooksDir: () => join(TEST_DIR, "workspace", "hooks"),
33
- getWorkspacePromptPath: (f: unknown) =>
34
- join(TEST_DIR, "workspace", String(f)),
35
- getInterfacesDir: () => join(TEST_DIR, "interfaces"),
36
- getHooksDir: () => join(TEST_DIR, "hooks"),
37
-
38
- getSandboxRootDir: () => join(TEST_DIR, "sandbox"),
39
- getSandboxWorkingDir: () => join(TEST_DIR, "sandbox", "work"),
40
- getHistoryPath: () => join(TEST_DIR, "history"),
41
- getSessionTokenPath: () => join(TEST_DIR, "session-token"),
42
- readSessionToken: () => null,
43
- getClipboardCommand: () => null,
44
- isMacOS: () => process.platform === "darwin",
45
- isLinux: () => process.platform === "linux",
46
- isWindows: () => process.platform === "win32",
47
- getPlatformName: () => process.platform,
48
- };
49
- // eslint-disable-next-line @typescript-eslint/no-require-imports
50
- const realPlatform = require("../util/platform.js");
51
- mock.module("../util/platform.js", () => ({
52
- ...realPlatform,
53
- ...platformOverrides,
54
- }));
55
-
56
18
  const noopLogger = new Proxy({} as Record<string, unknown>, {
57
19
  get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
58
20
  });
@@ -119,9 +81,6 @@ describe("skill_load feature flag enforcement", () => {
119
81
 
120
82
  afterEach(() => {
121
83
  _setOverridesForTesting({});
122
- if (existsSync(TEST_DIR)) {
123
- rmSync(TEST_DIR, { recursive: true, force: true });
124
- }
125
84
  });
126
85
 
127
86
  test("returns deterministic error for flag OFF skill", async () => {