@vellumai/vellum-gateway 0.5.6 → 0.5.8

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 (35) hide show
  1. package/ARCHITECTURE.md +9 -8
  2. package/bun.lock +0 -3
  3. package/package.json +2 -2
  4. package/src/__tests__/feature-flags-route.test.ts +76 -72
  5. package/src/__tests__/guardian-init-lockfile.test.ts +217 -4
  6. package/src/__tests__/probes.test.ts +29 -1
  7. package/src/__tests__/route-schema-guard.test.ts +0 -2
  8. package/src/__tests__/slack-errors.test.ts +85 -1
  9. package/src/auth/token-service.ts +17 -2
  10. package/src/credential-reader.ts +4 -4
  11. package/src/credential-watcher.ts +28 -7
  12. package/src/feature-flag-registry.json +49 -9
  13. package/src/feature-flag-store.ts +120 -0
  14. package/src/feature-flag-watcher.ts +79 -0
  15. package/src/http/routes/audio-proxy.ts +63 -0
  16. package/src/http/routes/channel-verification-session-proxy.ts +141 -24
  17. package/src/http/routes/feature-flags.ts +14 -82
  18. package/src/http/routes/migration-proxy.ts +164 -0
  19. package/src/http/routes/migration-rollback-proxy.ts +93 -0
  20. package/src/http/routes/slack-deliver.ts +114 -5
  21. package/src/http/routes/vercel-control-plane-proxy.ts +112 -0
  22. package/src/http/routes/workspace-commit-proxy.ts +88 -0
  23. package/src/index.ts +111 -13
  24. package/src/schema.ts +217 -0
  25. package/src/slack/block-kit-builder.test.ts +10 -3
  26. package/src/slack/block-kit-builder.ts +3 -0
  27. package/src/slack/errors.ts +53 -0
  28. package/src/slack/socket-mode.ts +27 -58
  29. package/src/telegram/api.ts +40 -18
  30. package/src/whatsapp/api.ts +26 -12
  31. package/workspace/config.json +1 -5
  32. package/src/__tests__/signing-key-bootstrap.test.ts +0 -143
  33. package/src/__tests__/slack-app-home.test.ts +0 -155
  34. package/src/http/routes/signing-key-bootstrap.ts +0 -59
  35. package/src/slack/app-home.ts +0 -120
package/ARCHITECTURE.md CHANGED
@@ -40,16 +40,16 @@ The gateway exposes a REST API for reading and mutating assistant feature flags.
40
40
 
41
41
  **Endpoints (GET/PATCH contract):**
42
42
 
43
- | Method | Path | Description |
44
- | ------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | GET | `/v1/feature-flags` | List all declared assistant feature flags from the defaults registry, merged with persisted values from workspace config. Returns `{ flags: FeatureFlagEntry[] }` where each entry has `key`, `enabled`, `defaultEnabled`, and `description`. |
46
- | PATCH | `/v1/feature-flags/:key` | Set a single assistant feature flag. Body: `{ "enabled": true\|false }`. Key must match `feature_flags.<flagId>.enabled` and be declared in the defaults registry. Writes to the `assistantFeatureFlagValues` config section. |
43
+ | Method | Path | Description |
44
+ | ------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | GET | `/v1/feature-flags` | List all declared assistant feature flags from the defaults registry, merged with persisted values from the feature flag store. Returns `{ flags: FeatureFlagEntry[] }` where each entry has `key`, `enabled`, `defaultEnabled`, and `description`. |
46
+ | PATCH | `/v1/feature-flags/:key` | Set a single assistant feature flag. Body: `{ "enabled": true\|false }`. Key must match `feature_flags.<flagId>.enabled` and be declared in the defaults registry. Writes to `~/.vellum/protected/feature-flags.json`. |
47
47
 
48
48
  **Unified registry:** All declared feature flags and their default values are defined in the unified registry at `meta/feature-flags/feature-flag-registry.json` (bundled copy at `gateway/src/feature-flag-registry.json`). The gateway loads this registry on startup via `gateway/src/feature-flag-defaults.ts`, filtering to `scope: "assistant"` flags. Labels come from the registry. The GET endpoint merges persisted overrides with registry defaults to produce the full flag list. The PATCH endpoint validates that the target flag key exists in the registry before accepting a write. Only declared keys are exposed by this API.
49
49
 
50
- **Flag key format:** The canonical key format is `feature_flags.<flagId>.enabled`. Only keys matching this pattern are accepted by the PATCH endpoint; other patterns are rejected with 400. All writes use the canonical format and are stored in the `assistantFeatureFlagValues` config section.
50
+ **Flag key format:** The canonical key format is `feature_flags.<flagId>.enabled`. Only keys matching this pattern are accepted by the PATCH endpoint; other patterns are rejected with 400. All writes use the canonical format and are stored in the protected feature flag store (`~/.vellum/protected/feature-flags.json`).
51
51
 
52
- **Storage:** Flag overrides are persisted in `~/.vellum/workspace/config.json`. Writes go to the `assistantFeatureFlagValues` section as a `Record<string, boolean>`. The GET endpoint reads from `assistantFeatureFlagValues` and merges with registry defaults. The gateway writes atomically (temp file + rename). The daemon's config watcher hot-reloads changes, so flag mutations take effect on the next session or tool resolution without a restart.
52
+ **Storage:** Flag overrides are persisted in `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store uses a versioned JSON format (`{ version: 1, values: Record<string, boolean> }`). The GET endpoint reads from the feature flag store and merges with registry defaults. The gateway writes atomically (temp file + rename, 0o600 permissions). The daemon's config watcher monitors the protected directory and hot-reloads changes, so flag mutations take effect on the next session or tool resolution without a restart.
53
53
 
54
54
  **Token separation (authentication boundary):**
55
55
 
@@ -62,13 +62,14 @@ The assistant feature flags API uses a dedicated feature-flag token stored at `~
62
62
 
63
63
  The feature-flag token is auto-generated on first gateway startup if the file does not exist. The gateway watches the token file for changes and hot-reloads without restart.
64
64
 
65
- **`assistantFeatureFlagValues` config section:** This is the canonical storage location for assistant feature flag overrides. It is a `Record<string, boolean>` keyed by canonical flag keys (`feature_flags.<id>.enabled`). The gateway's PATCH handler writes exclusively to this section. The daemon's resolver reads it with highest priority, falling back to the defaults registry. Undeclared keys are ignored by the resolver.
65
+ **Protected feature flag store:** The canonical storage for assistant feature flag overrides is `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store is managed by `gateway/src/feature-flag-store.ts` and uses a versioned JSON format with `Record<string, boolean>` values keyed by canonical flag keys (`feature_flags.<id>.enabled`). The gateway's PATCH handler writes exclusively to this store. The daemon's resolver reads it with highest priority, falling back to the defaults registry. Undeclared keys are ignored by the resolver.
66
66
 
67
67
  **Key source files:**
68
68
 
69
69
  | File | Purpose |
70
70
  | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
71
- | `gateway/src/http/routes/feature-flags.ts` | GET and PATCH handlers; config read/write logic; legacy key mapping; key format validation |
71
+ | `gateway/src/http/routes/feature-flags.ts` | GET and PATCH handlers; key format validation; delegates to feature-flag-store for persistence |
72
+ | `gateway/src/feature-flag-store.ts` | File-backed persistence: `readPersistedFeatureFlags()`, `writeFeatureFlag()`, atomic writes to protected directory |
72
73
  | `gateway/src/feature-flag-defaults.ts` | `loadFeatureFlagDefaults()` — loads the shared defaults registry; `isFlagDeclared()` — validates flag keys |
73
74
  | `gateway/src/config.ts` | `readOrGenerateFeatureFlagToken()` — token provisioning; `featureFlagToken` config field |
74
75
  | `gateway/src/index.ts` | Route registration, auth enforcement (dual-token for GET, flag-token-only for PATCH), token file watcher |
package/bun.lock CHANGED
@@ -13,7 +13,6 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/bun": "^1.2.4",
16
- "@types/uuid": "^11.0.0",
17
16
  "eslint": "^10.0.0",
18
17
  "knip": "^5.83.1",
19
18
  "prettier": "^3.8.1",
@@ -121,8 +120,6 @@
121
120
 
122
121
  "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
123
122
 
124
- "@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
125
-
126
123
  "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="],
127
124
 
128
125
  "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg=="],
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "exports": {
6
7
  "./twilio/verify": "./src/twilio/verify.ts",
@@ -30,7 +31,6 @@
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/bun": "^1.2.4",
33
- "@types/uuid": "^11.0.0",
34
34
  "eslint": "^10.0.0",
35
35
  "knip": "^5.83.1",
36
36
  "prettier": "^3.8.1",
@@ -16,8 +16,8 @@ const testDir = join(
16
16
  `vellum-ff-test-${randomBytes(6).toString("hex")}`,
17
17
  );
18
18
  const vellumRoot = join(testDir, ".vellum");
19
- const workspaceDir = join(vellumRoot, "workspace");
20
- const configPath = join(workspaceDir, "config.json");
19
+ const protectedDir = join(vellumRoot, "protected");
20
+ const featureFlagStorePath = join(protectedDir, "feature-flags.json");
21
21
 
22
22
  // Write the test registry to an isolated temp path so we never touch
23
23
  // the committed gateway/src/feature-flag-registry.json file.
@@ -57,11 +57,12 @@ const savedBaseDataDir = process.env.BASE_DATA_DIR;
57
57
 
58
58
  beforeEach(() => {
59
59
  process.env.BASE_DATA_DIR = testDir;
60
- mkdirSync(workspaceDir, { recursive: true });
60
+ mkdirSync(protectedDir, { recursive: true });
61
61
  writeFileSync(defaultsPath, JSON.stringify(TEST_REGISTRY, null, 2));
62
62
  // Point registry resolution at the isolated test file first
63
63
  _setRegistryCandidateOverrides([defaultsPath]);
64
64
  resetFeatureFlagDefaultsCache();
65
+ clearFeatureFlagStoreCache();
65
66
  });
66
67
 
67
68
  afterEach(() => {
@@ -78,6 +79,7 @@ afterEach(() => {
78
79
  // Clear the test-only candidate override and reset the defaults cache
79
80
  _setRegistryCandidateOverrides(null);
80
81
  resetFeatureFlagDefaultsCache();
82
+ clearFeatureFlagStoreCache();
81
83
  });
82
84
 
83
85
  const { createFeatureFlagsGetHandler, createFeatureFlagsPatchHandler } =
@@ -87,12 +89,14 @@ const {
87
89
  resetFeatureFlagDefaultsCache,
88
90
  _setRegistryCandidateOverrides,
89
91
  } = await import("../feature-flag-defaults.js");
92
+ const { clearFeatureFlagStoreCache, readPersistedFeatureFlags } =
93
+ await import("../feature-flag-store.js");
90
94
 
91
95
  describe("GET /v1/feature-flags handler", () => {
92
- test("returns all declared assistant-scope flags with defaults when config file does not exist", async () => {
93
- // Don't create the config file
94
- if (existsSync(configPath)) {
95
- rmSync(configPath);
96
+ test("returns all declared assistant-scope flags with defaults when no persisted file exists", async () => {
97
+ // Don't create the feature-flags.json file
98
+ if (existsSync(featureFlagStorePath)) {
99
+ rmSync(featureFlagStorePath);
96
100
  }
97
101
 
98
102
  const handler = createFeatureFlagsGetHandler();
@@ -170,11 +174,13 @@ describe("GET /v1/feature-flags handler", () => {
170
174
  expect(macosFlag).toBeUndefined();
171
175
  });
172
176
 
173
- test("returns all declared flags even when config has no persisted values", async () => {
177
+ test("returns all declared flags even when store has no persisted values", async () => {
178
+ // Write an empty feature-flags.json store
174
179
  writeFileSync(
175
- configPath,
176
- JSON.stringify({ twilio: { phoneNumber: "+1234" } }),
180
+ featureFlagStorePath,
181
+ JSON.stringify({ version: 1, values: {} }),
177
182
  );
183
+ clearFeatureFlagStoreCache();
178
184
 
179
185
  const handler = createFeatureFlagsGetHandler();
180
186
  const res = await handler(
@@ -189,15 +195,17 @@ describe("GET /v1/feature-flags handler", () => {
189
195
  expect(body.flags.length).toBe(declaredKeys.length);
190
196
  });
191
197
 
192
- test("merges persisted values from assistantFeatureFlagValues with defaults", async () => {
198
+ test("merges persisted values from feature-flags.json with defaults", async () => {
193
199
  writeFileSync(
194
- configPath,
200
+ featureFlagStorePath,
195
201
  JSON.stringify({
196
- assistantFeatureFlagValues: {
202
+ version: 1,
203
+ values: {
197
204
  "feature_flags.browser.enabled": false,
198
205
  },
199
206
  }),
200
207
  );
208
+ clearFeatureFlagStoreCache();
201
209
 
202
210
  const handler = createFeatureFlagsGetHandler();
203
211
  const res = await handler(
@@ -215,15 +223,18 @@ describe("GET /v1/feature-flags handler", () => {
215
223
  expect(browserFlag.defaultEnabled).toBe(true);
216
224
  });
217
225
 
218
- test("ignores non-boolean values in assistantFeatureFlagValues", async () => {
226
+ test("ignores non-boolean values in persisted feature flags", async () => {
227
+ // Write a feature-flags.json with an invalid non-boolean value manually
219
228
  writeFileSync(
220
- configPath,
229
+ featureFlagStorePath,
221
230
  JSON.stringify({
222
- assistantFeatureFlagValues: {
231
+ version: 1,
232
+ values: {
223
233
  "feature_flags.browser.enabled": "no",
224
234
  },
225
235
  }),
226
236
  );
237
+ clearFeatureFlagStoreCache();
227
238
 
228
239
  const handler = createFeatureFlagsGetHandler();
229
240
  const res = await handler(
@@ -233,19 +244,20 @@ describe("GET /v1/feature-flags handler", () => {
233
244
  expect(res.status).toBe(200);
234
245
  const body = await res.json();
235
246
 
236
- // browser should fall back to default since non-boolean was ignored
247
+ // readPersistedFeatureFlags filters out non-boolean values, so the
248
+ // invalid "no" string is dropped and the flag falls back to its
249
+ // registry default (true).
237
250
  const browserFlag = body.flags.find(
238
251
  (f: { key: string }) => f.key === "feature_flags.browser.enabled",
239
252
  );
240
253
  expect(browserFlag).toBeDefined();
241
- expect(browserFlag.enabled).toBe(browserFlag.defaultEnabled);
254
+ expect(browserFlag.enabled).toBe(true);
255
+ expect(browserFlag.defaultEnabled).toBe(true);
242
256
  });
243
257
  });
244
258
 
245
259
  describe("PATCH /v1/feature-flags/:flagKey handler", () => {
246
- test("writes to assistantFeatureFlagValues", async () => {
247
- writeFileSync(configPath, JSON.stringify({}));
248
-
260
+ test("writes to feature-flags.json store", async () => {
249
261
  const handler = createFeatureFlagsPatchHandler();
250
262
  const res = await handler(
251
263
  new Request(
@@ -266,24 +278,24 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
266
278
  enabled: false,
267
279
  });
268
280
 
269
- // Verify persistence to the assistantFeatureFlagValues section
270
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
271
- expect(
272
- config.assistantFeatureFlagValues["feature_flags.browser.enabled"],
273
- ).toBe(false);
281
+ // Verify persistence to the feature-flags.json store
282
+ clearFeatureFlagStoreCache();
283
+ const persisted = readPersistedFeatureFlags();
284
+ expect(persisted["feature_flags.browser.enabled"]).toBe(false);
274
285
  });
275
286
 
276
- test("preserves existing config keys when writing", async () => {
287
+ test("preserves existing persisted flags when writing", async () => {
288
+ // Pre-seed a flag value
277
289
  writeFileSync(
278
- configPath,
290
+ featureFlagStorePath,
279
291
  JSON.stringify({
280
- twilio: { phoneNumber: "+1234567890" },
281
- email: { address: "test@example.com" },
282
- assistantFeatureFlagValues: {
292
+ version: 1,
293
+ values: {
283
294
  "feature_flags.contacts.enabled": true,
284
295
  },
285
296
  }),
286
297
  );
298
+ clearFeatureFlagStoreCache();
287
299
 
288
300
  const handler = createFeatureFlagsPatchHandler();
289
301
  await handler(
@@ -298,21 +310,16 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
298
310
  "feature_flags.browser.enabled",
299
311
  );
300
312
 
301
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
302
- expect(config.twilio).toMatchObject({ phoneNumber: "+1234567890" });
303
- expect(config.email).toEqual({ address: "test@example.com" });
304
- // New section should have both old and new values
305
- expect(
306
- config.assistantFeatureFlagValues["feature_flags.contacts.enabled"],
307
- ).toBe(true);
308
- expect(
309
- config.assistantFeatureFlagValues["feature_flags.browser.enabled"],
310
- ).toBe(true);
313
+ // Both old and new values should be persisted
314
+ clearFeatureFlagStoreCache();
315
+ const persisted = readPersistedFeatureFlags();
316
+ expect(persisted["feature_flags.contacts.enabled"]).toBe(true);
317
+ expect(persisted["feature_flags.browser.enabled"]).toBe(true);
311
318
  });
312
319
 
313
- test("creates config file and directories when they do not exist", async () => {
314
- // Remove the workspace dir to test directory creation
315
- rmSync(workspaceDir, { recursive: true, force: true });
320
+ test("creates feature-flags.json and directories when they do not exist", async () => {
321
+ // Remove the protected dir to test directory creation
322
+ rmSync(protectedDir, { recursive: true, force: true });
316
323
 
317
324
  const handler = createFeatureFlagsPatchHandler();
318
325
  const res = await handler(
@@ -328,12 +335,11 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
328
335
  );
329
336
 
330
337
  expect(res.status).toBe(200);
331
- expect(existsSync(configPath)).toBe(true);
338
+ expect(existsSync(featureFlagStorePath)).toBe(true);
332
339
 
333
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
334
- expect(
335
- config.assistantFeatureFlagValues["feature_flags.browser.enabled"],
336
- ).toBe(true);
340
+ clearFeatureFlagStoreCache();
341
+ const persisted = readPersistedFeatureFlags();
342
+ expect(persisted["feature_flags.browser.enabled"]).toBe(true);
337
343
  });
338
344
 
339
345
  // Validation tests
@@ -408,7 +414,6 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
408
414
  });
409
415
 
410
416
  test("rejects undeclared keys (not in defaults registry)", async () => {
411
- writeFileSync(configPath, JSON.stringify({}));
412
417
  const handler = createFeatureFlagsPatchHandler();
413
418
 
414
419
  const res = await handler(
@@ -429,7 +434,6 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
429
434
  });
430
435
 
431
436
  test("accepts valid declared feature_flags.* key formats", async () => {
432
- writeFileSync(configPath, JSON.stringify({}));
433
437
  const handler = createFeatureFlagsPatchHandler();
434
438
 
435
439
  const validKeys = [
@@ -438,6 +442,7 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
438
442
  ];
439
443
 
440
444
  for (const key of validKeys) {
445
+ clearFeatureFlagStoreCache();
441
446
  const res = await handler(
442
447
  new Request(`http://gateway.test/v1/feature-flags/${key}`, {
443
448
  method: "PATCH",
@@ -508,13 +513,16 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
508
513
  expect(res.status).toBe(400);
509
514
  });
510
515
 
511
- test("atomic write does not corrupt config on successful write", async () => {
512
- // Write initial config
513
- const initial = {
514
- twilio: { phoneNumber: "+1234" },
515
- assistantFeatureFlagValues: { "feature_flags.contacts.enabled": true },
516
- };
517
- writeFileSync(configPath, JSON.stringify(initial));
516
+ test("atomic write does not corrupt store on successful write", async () => {
517
+ // Pre-seed the store
518
+ writeFileSync(
519
+ featureFlagStorePath,
520
+ JSON.stringify({
521
+ version: 1,
522
+ values: { "feature_flags.contacts.enabled": true },
523
+ }),
524
+ );
525
+ clearFeatureFlagStoreCache();
518
526
 
519
527
  const handler = createFeatureFlagsPatchHandler();
520
528
  await handler(
@@ -530,25 +538,20 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
530
538
  );
531
539
 
532
540
  // Verify the file is valid JSON and contains all expected data
533
- const raw = readFileSync(configPath, "utf-8");
534
- const config = JSON.parse(raw);
535
- expect(config.twilio).toMatchObject({ phoneNumber: "+1234" });
536
- expect(
537
- config.assistantFeatureFlagValues["feature_flags.contacts.enabled"],
538
- ).toBe(true);
539
- expect(
540
- config.assistantFeatureFlagValues["feature_flags.browser.enabled"],
541
- ).toBe(false);
541
+ const raw = readFileSync(featureFlagStorePath, "utf-8");
542
+ const data = JSON.parse(raw);
543
+ expect(data.version).toBe(1);
544
+ expect(data.values["feature_flags.contacts.enabled"]).toBe(true);
545
+ expect(data.values["feature_flags.browser.enabled"]).toBe(false);
542
546
 
543
547
  // Verify no temp files left behind
544
548
  const { readdirSync } = await import("node:fs");
545
- const files = readdirSync(workspaceDir);
546
- const tmpFiles = files.filter((f: string) => f.endsWith(".tmp"));
549
+ const files = readdirSync(protectedDir);
550
+ const tmpFiles = files.filter((f: string) => f.includes(".tmp"));
547
551
  expect(tmpFiles.length).toBe(0);
548
552
  });
549
553
 
550
554
  test("concurrent writes are serialized and no flag change is lost", async () => {
551
- writeFileSync(configPath, JSON.stringify({}));
552
555
  const handler = createFeatureFlagsPatchHandler();
553
556
 
554
557
  // Fire multiple concurrent PATCH requests at the same time
@@ -576,9 +579,10 @@ describe("PATCH /v1/feature-flags/:flagKey handler", () => {
576
579
  }
577
580
 
578
581
  // All flags should be persisted — none should be lost to a race
579
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
582
+ clearFeatureFlagStoreCache();
583
+ const persisted = readPersistedFeatureFlags();
580
584
  for (const key of flagKeys) {
581
- expect(config.assistantFeatureFlagValues[key]).toBe(false);
585
+ expect(persisted[key]).toBe(false);
582
586
  }
583
587
  });
584
588
  });