@mycadet/assistant 0.12.7-dev.202610111049.d2b52a8 → 0.12.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 (40) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  2. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  3. package/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  4. package/openapi.yaml +6679 -658
  5. package/package.json +1 -1
  6. package/src/__tests__/config-loader-cloud-defaults.test.ts +0 -233
  7. package/src/__tests__/credential-health-service.test.ts +0 -14
  8. package/src/__tests__/credential-routes.test.ts +0 -51
  9. package/src/__tests__/init-feature-flag-overrides.test.ts +40 -24
  10. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -2
  11. package/src/__tests__/llm-schema-fallback-profile.test.ts +9 -3
  12. package/src/__tests__/requests-mock.ts +231 -9
  13. package/src/__tests__/secret-routes-cloud-proxy.test.ts +8 -24
  14. package/src/__tests__/secret-routes-scrub.test.ts +2 -42
  15. package/src/__tests__/test-preload.ts +8 -5
  16. package/src/__tests__/web-search.test.ts +340 -859
  17. package/src/api/events/cloud-disconnected.ts +2 -2
  18. package/src/config/schemas/cloud.ts +2 -1
  19. package/src/config/schemas/host-browser.ts +1 -0
  20. package/src/config/schemas/ingress.ts +1 -12
  21. package/src/config/schemas/llm.ts +10 -1
  22. package/src/config/schemas/memory-lifecycle.ts +11 -1
  23. package/src/config/schemas/tts.ts +5 -0
  24. package/src/credential-execution/prompted-credential.ts +6 -18
  25. package/src/credential-health/credential-health-service.ts +6 -21
  26. package/src/daemon/credential-transcript-scrub.ts +21 -29
  27. package/src/ipc/gateway-client.test.ts +55 -59
  28. package/src/persistence/compaction-log-store-clickhouse.ts +1 -1
  29. package/src/persistence/llm-request-log-sink-clickhouse.ts +1 -1
  30. package/src/persistence/llm-request-log-source-clickhouse.ts +1 -1
  31. package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
  32. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +10 -7
  33. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +3 -9
  34. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +3 -3
  35. package/src/runtime/routes/cloud-routes.ts +14 -18
  36. package/src/runtime/routes/conversation-query-routes.ts +30 -230
  37. package/src/runtime/routes/migration-routes.ts +5 -6
  38. package/src/runtime/routes/secret-routes.ts +12 -31
  39. package/src/tools/credentials/store.ts +6 -15
  40. package/src/__tests__/mock-gateway-ipc.ts +0 -279
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/assistant",
3
- "version": "0.12.7-dev.202610111049.d2b52a8",
3
+ "version": "0.12.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -39,7 +39,6 @@ afterAll(() => {
39
39
  });
40
40
 
41
41
  import { invalidateConfigCache, loadConfig } from "../config/loader.js";
42
- import { applyContextDefaultsToRawConfig } from "../runtime/routes/conversation-query-routes.js";
43
42
 
44
43
  // ---------------------------------------------------------------------------
45
44
  // Helpers
@@ -372,235 +371,3 @@ describe("cloud-managed config defaults", () => {
372
371
  }
373
372
  });
374
373
  });
375
-
376
- /**
377
- * Regression guard for the `handleGetConfig` route handler in
378
- * `assistant/src/runtime/routes/conversation-query-routes.ts`. That handler
379
- * returns the raw on-disk JSON to clients (macOS, web, CLI) via
380
- * `GET /v1/config`, but first layers deployment-context defaults on top
381
- * via the `applyContextDefaultsToRawConfig` helper.
382
- *
383
- * macOS's `loadServiceModes(config:)` only updates `inferenceMode` when
384
- * `services.inference.mode` is present in the response — without the fill
385
- * pass, freshly-hatched cloud assistants would have no `services`
386
- * key on disk (only `llm.profiles` from `seedInferenceProfiles`) and macOS
387
- * would fall back to its `@Published` default of "your-own". The helper is
388
- * also responsible for guarding against `loadRawConfig()` returning a
389
- * non-object payload from a malformed-but-parseable `config.json`.
390
- */
391
- describe("GET /v1/config handler — context-default fill on raw response", () => {
392
- const originalIsPlatform = process.env.IS_CLOUD;
393
-
394
- afterEach(() => {
395
- if (originalIsPlatform === undefined) {
396
- delete process.env.IS_CLOUD;
397
- } else {
398
- process.env.IS_CLOUD = originalIsPlatform;
399
- }
400
- });
401
-
402
- test("IS_CLOUD=true, raw config has no services key → response includes managed defaults", () => {
403
- process.env.IS_CLOUD = "true";
404
-
405
- // Mirrors the real-world fresh-hatch state: lifecycle wrote
406
- // `llm.profiles` to disk, but never persisted any service modes.
407
- const raw: Record<string, unknown> = {
408
- llm: {
409
- profiles: {
410
- balanced: { provider: "anthropic", model: "claude-sonnet-4.5" },
411
- },
412
- activeProfile: "balanced",
413
- },
414
- };
415
-
416
- const result = applyContextDefaultsToRawConfig(raw) as Record<
417
- string,
418
- unknown
419
- >;
420
- const services = result["services"] as Record<
421
- string,
422
- { mode?: string; provider?: string }
423
- >;
424
- expect(services).toBeDefined();
425
- for (const svc of MANAGED_SERVICES) {
426
- expect(services[svc]!.mode).toBe("managed");
427
- }
428
- // image-generation is filled by provider, not mode.
429
- expect(services["image-generation"]!.provider).toBe("cadet");
430
- expect(services["image-generation"]!.mode).toBeUndefined();
431
- });
432
-
433
- test("IS_CLOUD=true, raw config has explicit services.image-generation.mode='your-own' → preserved", () => {
434
- process.env.IS_CLOUD = "true";
435
-
436
- // A legacy raw shape: mode with no provider leaf. Migration 134 rewrites
437
- // it (pinning provider gemini) before any real request reaches this
438
- // function, so the fill's only obligation here is to be non-destructive:
439
- // existing keys pass through untouched, absent leaves get the cloud
440
- // default.
441
- const raw: Record<string, unknown> = {
442
- services: {
443
- "image-generation": { mode: "your-own" },
444
- },
445
- };
446
-
447
- const result = applyContextDefaultsToRawConfig(raw) as Record<
448
- string,
449
- unknown
450
- >;
451
- const services = result["services"] as Record<
452
- string,
453
- { mode?: string; provider?: string }
454
- >;
455
- expect(services["image-generation"]!.mode).toBe("your-own");
456
- expect(services["image-generation"]!.provider).toBe("cadet");
457
- // web-search is not context-filled: a filled `mode` would override BYOK
458
- // configs on every load.
459
- expect(services["web-search"]).toBeUndefined();
460
- // inference.mode is a legacy backwards-compat wire field — synthesized
461
- // here for old macOS clients (SettingsStore.swift) that still read it.
462
- expect(services["inference"]!.mode).toBe("managed");
463
- });
464
-
465
- test("IS_CLOUD=false, raw config has no services key → response is unchanged", () => {
466
- process.env.IS_CLOUD = "false";
467
-
468
- const raw: Record<string, unknown> = {
469
- llm: {
470
- profiles: {
471
- balanced: { provider: "anthropic", model: "claude-sonnet-4.5" },
472
- },
473
- },
474
- };
475
-
476
- const result = applyContextDefaultsToRawConfig(raw) as Record<
477
- string,
478
- unknown
479
- >;
480
- expect(result["services"]).toBeUndefined();
481
- });
482
-
483
- test("IS_CLOUD=true, raw config has an explicit image-generation provider → preserved, no mode injected", () => {
484
- process.env.IS_CLOUD = "true";
485
-
486
- // User set image-generation.provider explicitly. The fill targets that
487
- // same leaf (`provider: "cadet"`), so the explicit value wins and no
488
- // mode is injected.
489
- const raw: Record<string, unknown> = {
490
- services: {
491
- "image-generation": { provider: "openai" },
492
- },
493
- };
494
-
495
- const result = applyContextDefaultsToRawConfig(raw) as Record<
496
- string,
497
- unknown
498
- >;
499
- const services = result["services"] as Record<
500
- string,
501
- { mode?: string; provider?: string }
502
- >;
503
- expect(services["image-generation"]!.provider).toBe("openai");
504
- expect(services["image-generation"]!.mode).toBeUndefined();
505
- // services.inference.mode is synthesized as a legacy wire-only field for
506
- // older macOS clients during the rollout window (Phase 1.2 schema removal
507
- // landed before the macOS Providers UI ships).
508
- expect(services["inference"]!.mode).toBe("managed");
509
- });
510
-
511
- test("IS_CLOUD=true, raw config has no inference subtree → synthesizes legacy mode='managed'", () => {
512
- process.env.IS_CLOUD = "true";
513
-
514
- const raw: Record<string, unknown> = {
515
- llm: {
516
- profiles: {
517
- balanced: { provider: "anthropic", model: "claude-sonnet-4.5" },
518
- },
519
- },
520
- };
521
-
522
- const result = applyContextDefaultsToRawConfig(raw) as Record<
523
- string,
524
- unknown
525
- >;
526
- const services = result["services"] as Record<string, { mode: string }>;
527
- expect(services["inference"]!.mode).toBe("managed");
528
- });
529
-
530
- test("IS_CLOUD=true, raw config has explicit services.inference.mode='your-own' → preserved (legacy override)", () => {
531
- process.env.IS_CLOUD = "true";
532
-
533
- // Pre-migration upgrade: workspace config still carries the legacy
534
- // mode value. The synthesis only fills when absent, so an explicit
535
- // disk value wins until migration 076 strips it.
536
- const raw: Record<string, unknown> = {
537
- services: {
538
- inference: { mode: "your-own" },
539
- },
540
- };
541
-
542
- const result = applyContextDefaultsToRawConfig(raw) as Record<
543
- string,
544
- unknown
545
- >;
546
- const services = result["services"] as Record<string, { mode: string }>;
547
- expect(services["inference"]!.mode).toBe("your-own");
548
- });
549
-
550
- test("IS_CLOUD=false, raw config has no inference subtree → no synthesis", () => {
551
- process.env.IS_CLOUD = "false";
552
-
553
- const raw: Record<string, unknown> = {
554
- llm: {},
555
- };
556
-
557
- const result = applyContextDefaultsToRawConfig(raw) as Record<
558
- string,
559
- unknown
560
- >;
561
- expect(result["services"]).toBeUndefined();
562
- });
563
-
564
- // -------------------------------------------------------------------------
565
- // Malformed-but-parseable config.json — must not 500 the GET endpoint.
566
- //
567
- // `loadRawConfig()` is typed `Record<string, unknown>` but `JSON.parse`
568
- // will happily return `null`, primitives, or arrays for a syntactically
569
- // valid file like `null` / `42` / `[]`. The helper must return those
570
- // payloads unchanged rather than throwing inside
571
- // `fillContextDefaultsForMissingKeys`.
572
- // -------------------------------------------------------------------------
573
-
574
- test("IS_CLOUD=true, raw config is null → returned unchanged (no throw)", () => {
575
- process.env.IS_CLOUD = "true";
576
- expect(applyContextDefaultsToRawConfig(null)).toBe(null);
577
- });
578
-
579
- test("IS_CLOUD=true, raw config is a primitive number → returned unchanged (no throw)", () => {
580
- process.env.IS_CLOUD = "true";
581
- expect(applyContextDefaultsToRawConfig(42)).toBe(42);
582
- });
583
-
584
- test("IS_CLOUD=true, raw config is an array → returned unchanged (no throw)", () => {
585
- process.env.IS_CLOUD = "true";
586
- const raw: unknown[] = [{ foo: "bar" }];
587
- const result = applyContextDefaultsToRawConfig(raw);
588
- expect(result).toBe(raw);
589
- // No `services` key was synthesized onto the array.
590
- expect((result as { services?: unknown }).services).toBeUndefined();
591
- });
592
-
593
- test("IS_CLOUD=true, raw config is a string → returned unchanged (no throw)", () => {
594
- process.env.IS_CLOUD = "true";
595
- expect(applyContextDefaultsToRawConfig("not-an-object")).toBe(
596
- "not-an-object",
597
- );
598
- });
599
-
600
- test("IS_CLOUD=false, raw config is null → returned unchanged (no throw)", () => {
601
- // Sanity check: when there are no context defaults to apply, the helper
602
- // also short-circuits cleanly on non-object payloads.
603
- process.env.IS_CLOUD = "false";
604
- expect(applyContextDefaultsToRawConfig(null)).toBe(null);
605
- });
606
- });
@@ -773,7 +773,6 @@ describe("credential-health-service", () => {
773
773
 
774
774
  describe("cloud assistant credential", () => {
775
775
  const KEY = "credential/cadet/assistant_api_key";
776
- const ASSISTANT_ID = "credential/cadet/platform_assistant_id";
777
776
 
778
777
  // Checked directly rather than through `checkAllCredentials`: the cloud
779
778
  // credential is not enrolled in the heartbeat pass yet, because the alert
@@ -785,16 +784,7 @@ describe("credential-health-service", () => {
785
784
  expect(await assistantKeyResult()).toBeNull();
786
785
  });
787
786
 
788
- test("is missing_token when a cloud identity exists without a key", async () => {
789
- secureKeyValues.set(ASSISTANT_ID, "11111111-2222-4333-8444-555555555555");
790
-
791
- const result = await assistantKeyResult();
792
- expect(result!.status).toBe("missing_token");
793
- expect(result!.canAutoRecover).toBe(true);
794
- });
795
-
796
787
  test("is healthy when the cloud accepts the stored key", async () => {
797
- secureKeyValues.set(ASSISTANT_ID, "11111111-2222-4333-8444-555555555555");
798
788
  secureKeyValues.set(KEY, "stored-key");
799
789
  assistantKeyVerdict = "valid";
800
790
 
@@ -802,7 +792,6 @@ describe("credential-health-service", () => {
802
792
  });
803
793
 
804
794
  test("is revoked when the cloud rejects the stored key", async () => {
805
- secureKeyValues.set(ASSISTANT_ID, "11111111-2222-4333-8444-555555555555");
806
795
  secureKeyValues.set(KEY, "stored-key");
807
796
  assistantKeyVerdict = "rejected";
808
797
 
@@ -818,7 +807,6 @@ describe("credential-health-service", () => {
818
807
  // error, transport failure) must degrade to `unreachable`, which the
819
808
  // heartbeat drops before notifying.
820
809
  test("an unsettled platform answer is unreachable, never revoked", async () => {
821
- secureKeyValues.set(ASSISTANT_ID, "11111111-2222-4333-8444-555555555555");
822
810
  secureKeyValues.set(KEY, "stored-key");
823
811
  assistantKeyVerdict = "unknown";
824
812
 
@@ -826,14 +814,12 @@ describe("credential-health-service", () => {
826
814
  });
827
815
 
828
816
  test("an unreadable credential store is unreachable, never revoked", async () => {
829
- secureKeyValues.set(ASSISTANT_ID, "11111111-2222-4333-8444-555555555555");
830
817
  markUnreachable(KEY);
831
818
 
832
819
  expect((await assistantKeyResult())!.status).toBe("unreachable");
833
820
  });
834
821
 
835
822
  test("is unreachable when the cloud client cannot be built", async () => {
836
- secureKeyValues.set(ASSISTANT_ID, "11111111-2222-4333-8444-555555555555");
837
823
  secureKeyValues.set(KEY, "stored-key");
838
824
  cloudClientAvailable = false;
839
825
 
@@ -18,7 +18,6 @@ let syncRejects: boolean;
18
18
  let disconnectedProviders: string[];
19
19
  let credentialIdCounter: number;
20
20
  let scrubbedValues: string[];
21
- let scrubRejects: boolean;
22
21
  let liveRecordsUnreachable: boolean;
23
22
 
24
23
  function metaKey(service: string, field: string): string {
@@ -120,9 +119,6 @@ mock.module("../credential-execution/cloud-catalog.js", () => ({
120
119
  mock.module("../daemon/credential-transcript-scrub.js", () => ({
121
120
  scrubStoredCredentialFromTranscripts: mock(async (value: string) => {
122
121
  scrubbedValues.push(value);
123
- if (scrubRejects) {
124
- throw new Error("transcript sweep failed");
125
- }
126
122
  return { dbMessagesScrubbed: 0, residentMessagesScrubbed: 0 };
127
123
  }),
128
124
  }));
@@ -195,7 +191,6 @@ describe("credentials routes", () => {
195
191
  disconnectedProviders = [];
196
192
  credentialIdCounter = 0;
197
193
  scrubbedValues = [];
198
- scrubRejects = false;
199
194
  liveRecordsUnreachable = false;
200
195
  _resetRevealSuccessRegistryForTest();
201
196
  resetForChatMintRegistryForTest();
@@ -618,52 +613,6 @@ describe("credentials routes", () => {
618
613
  expect(scrubbedValues).toEqual([SECRET_VALUE]);
619
614
  });
620
615
 
621
- test("non-secret cadet cloud fields are stored but never scrubbed", async () => {
622
- /**
623
- * Platform identity ids are benign UUIDs that legitimately appear in
624
- * transcripts; scrubbing them would redact ordinary conversation
625
- * content. Shares the exemption with the /v1/secrets credential branch
626
- * via isNonSecretPlatformField.
627
- for (const field of [
628
- "platform_assistant_id",
629
- "platform_organization_id",
630
- "platform_user_id",
631
- ]) {
632
- const result = (await setRoute!.handler({
633
- body: {
634
- service: "cadet",
635
- field,
636
- value: "0198f4c2-1111-2222-3333-444455556666",
637
- },
638
- })) as SetResponse;
639
- expect(result.service).toBe("cadet");
640
- }
641
-
642
- expect(scrubbedValues).toEqual([]);
643
- });
644
-
645
- test("the set still succeeds when the transcript scrub rejects", async () => {
646
- /**
647
- * The scrub is post-store hygiene: the credential IS stored, so a
648
- * scrub failure is logged and must stay invisible to the caller.
649
- */
650
- // GIVEN a scrub that rejects
651
- scrubRejects = true;
652
-
653
- // WHEN a credential is stored
654
- const result = (await setRoute!.handler({
655
- body: { service: "vercel", field: "api_token", value: SECRET_VALUE },
656
- })) as SetResponse;
657
-
658
- // THEN the route still returns success and the secret is persisted
659
- expect(result.service).toBe("vercel");
660
- expect(result.credentialId).toBe("cred-1");
661
- expect(secureStore.get(seedKey("vercel", "api_token"))).toBe(
662
- SECRET_VALUE,
663
- );
664
- expect(scrubbedValues).toEqual([SECRET_VALUE]);
665
- });
666
-
667
616
  test("leaves a non-ACP service unaffected by the OAuth-field guard", async () => {
668
617
  /**
669
618
  * The guard is scoped to the ACP service; the same field name on another
@@ -2,8 +2,8 @@
2
2
  * Tests for initFeatureFlagOverrides() — the async IPC call that
3
3
  * pre-populates the feature flag cache before CLI program construction.
4
4
  *
5
- * Uses the shared mock-gateway-ipc utility (installed in test-preload.ts)
6
- * which mocks node:net so no test connects to a real gateway socket.
5
+ * Uses the requests-mock gateway channel (transport installed in
6
+ * test-preload.ts) so no test connects to a real gateway socket.
7
7
  */
8
8
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
9
9
 
@@ -13,21 +13,23 @@ import {
13
13
  isAssistantFeatureFlagEnabled,
14
14
  refreshOverridesFromGateway,
15
15
  } from "../config/assistant-feature-flags.js";
16
- import { mockGatewayIpc, resetMockGatewayIpc } from "./mock-gateway-ipc.js";
16
+ import { mockIpc, resetRequestMockChannels } from "./requests-mock.js";
17
17
 
18
18
  beforeEach(() => {
19
19
  clearFeatureFlagOverridesCache();
20
- resetMockGatewayIpc();
21
20
  });
22
21
 
23
22
  afterEach(() => {
24
23
  clearFeatureFlagOverridesCache();
25
- resetMockGatewayIpc();
26
24
  });
27
25
 
28
26
  describe("initFeatureFlagOverrides", () => {
29
27
  it("populates cache from gateway IPC response", async () => {
30
- mockGatewayIpc({ "foo-enabled": true, "bar-enabled": true });
28
+ const gateway = await mockIpc("gateway");
29
+ gateway.on("get_feature_flags", {
30
+ "foo-enabled": true,
31
+ "bar-enabled": true,
32
+ });
31
33
 
32
34
  await initFeatureFlagOverrides();
33
35
 
@@ -37,7 +39,8 @@ describe("initFeatureFlagOverrides", () => {
37
39
  });
38
40
 
39
41
  it("falls back gracefully when gateway socket is unavailable", async () => {
40
- mockGatewayIpc(null, { error: true });
42
+ const gateway = await mockIpc("gateway");
43
+ gateway.failConnect();
41
44
 
42
45
  // Disable retries — production retries the IPC fetch on failure to
43
46
  // dodge the daemon-vs-gateway startup race, but here we're explicitly
@@ -51,7 +54,11 @@ describe("initFeatureFlagOverrides", () => {
51
54
  });
52
55
 
53
56
  it("respects false values from gateway IPC", async () => {
54
- mockGatewayIpc({ "gated-feature": true, "disabled-feature": false });
57
+ const gateway = await mockIpc("gateway");
58
+ gateway.on("get_feature_flags", {
59
+ "gated-feature": true,
60
+ "disabled-feature": false,
61
+ });
55
62
 
56
63
  await initFeatureFlagOverrides();
57
64
 
@@ -63,7 +70,8 @@ describe("initFeatureFlagOverrides", () => {
63
70
  });
64
71
 
65
72
  it("does not cache empty gateway response", async () => {
66
- mockGatewayIpc({});
73
+ const gateway = await mockIpc("gateway");
74
+ gateway.on("get_feature_flags", {});
67
75
 
68
76
  // Disable retries — this test explicitly simulates a sustained empty
69
77
  // response (gateway up but reporting zero flags) and should not wait
@@ -80,10 +88,11 @@ describe("initFeatureFlagOverrides", () => {
80
88
  // returns empty (gateway not yet ready), but a later attempt sees
81
89
  // the populated flag map. The retry loop in init should bridge this
82
90
  // gap without losing the flag.
83
- mockGatewayIpc({});
91
+ const gateway = await mockIpc("gateway");
92
+ gateway.on("get_feature_flags", {});
84
93
  setTimeout(() => {
85
- resetMockGatewayIpc();
86
- mockGatewayIpc({ "retry-test-flag": true });
94
+ resetRequestMockChannels();
95
+ gateway.on("get_feature_flags", { "retry-test-flag": true });
87
96
  }, 50);
88
97
 
89
98
  await initFeatureFlagOverrides({ retryBackoffsMs: [200] });
@@ -93,14 +102,18 @@ describe("initFeatureFlagOverrides", () => {
93
102
  });
94
103
 
95
104
  it("does not re-fetch when cache is already populated", async () => {
96
- mockGatewayIpc({ "first-call": true });
105
+ const gateway = await mockIpc("gateway");
106
+ gateway.on("get_feature_flags", { "first-call": true });
97
107
 
98
108
  await initFeatureFlagOverrides();
99
109
 
100
110
  // Change what IPC would return — if the guard is broken and init
101
111
  // re-fetches, "first-call" would flip to false.
102
- resetMockGatewayIpc();
103
- mockGatewayIpc({ "first-call": false, "second-call": true });
112
+ resetRequestMockChannels();
113
+ gateway.on("get_feature_flags", {
114
+ "first-call": false,
115
+ "second-call": true,
116
+ });
104
117
 
105
118
  await initFeatureFlagOverrides();
106
119
 
@@ -114,11 +127,12 @@ describe("initFeatureFlagOverrides", () => {
114
127
 
115
128
  describe("refreshOverridesFromGateway", () => {
116
129
  it("replaces the cache with the new gateway values", async () => {
117
- mockGatewayIpc({ "moving-flag": true });
130
+ const gateway = await mockIpc("gateway");
131
+ gateway.on("get_feature_flags", { "moving-flag": true });
118
132
  await initFeatureFlagOverrides();
119
133
 
120
- resetMockGatewayIpc();
121
- mockGatewayIpc({ "moving-flag": false });
134
+ resetRequestMockChannels();
135
+ gateway.on("get_feature_flags", { "moving-flag": false });
122
136
 
123
137
  expect(await refreshOverridesFromGateway()).toBe(true);
124
138
 
@@ -127,11 +141,12 @@ describe("refreshOverridesFromGateway", () => {
127
141
  });
128
142
 
129
143
  it("keeps the last known values when the refresh fails", async () => {
130
- mockGatewayIpc({ "moving-flag": true });
144
+ const gateway = await mockIpc("gateway");
145
+ gateway.on("get_feature_flags", { "moving-flag": true });
131
146
  await initFeatureFlagOverrides();
132
147
 
133
- resetMockGatewayIpc();
134
- mockGatewayIpc(null, { error: true });
148
+ resetRequestMockChannels();
149
+ gateway.failConnect();
135
150
 
136
151
  expect(await refreshOverridesFromGateway()).toBe(false);
137
152
 
@@ -143,11 +158,12 @@ describe("refreshOverridesFromGateway", () => {
143
158
  });
144
159
 
145
160
  it("keeps the cache readable for the whole call", async () => {
146
- mockGatewayIpc({ "moving-flag": true });
161
+ const gateway = await mockIpc("gateway");
162
+ gateway.on("get_feature_flags", { "moving-flag": true });
147
163
  await initFeatureFlagOverrides();
148
164
 
149
- resetMockGatewayIpc();
150
- mockGatewayIpc({ "moving-flag": false });
165
+ resetRequestMockChannels();
166
+ gateway.on("get_feature_flags", { "moving-flag": false });
151
167
 
152
168
  const config = {} as any;
153
169
  const inFlight = refreshOverridesFromGateway();
@@ -302,7 +302,7 @@ describe("ClickHouseLlmRequestLogSource", () => {
302
302
  );
303
303
  });
304
304
 
305
- test("missing cadet:platform_assistant_id surfaces a clear error", async () => {
305
+ test("a missing Cadet Cloud assistant id surfaces a clear error", async () => {
306
306
  const src = new ClickHouseLlmRequestLogSource(DEFAULT_CONFIG, {
307
307
  resolveUrl: async () => "https://ch.example.test",
308
308
  resolvePassword: async () => "x",
@@ -310,7 +310,7 @@ describe("ClickHouseLlmRequestLogSource", () => {
310
310
  fetchImpl: fakeFetchReturning(""),
311
311
  });
312
312
  await expect(src.getRequestLogById("log-1")).rejects.toThrow(
313
- /cadet:platform_assistant_id credential is required/,
313
+ /Cadet Cloud assistant id is required/,
314
314
  );
315
315
  });
316
316
  });
@@ -130,12 +130,18 @@ describe("LLMSchema fallbackProfile", () => {
130
130
  unrepresentable: "any",
131
131
  io: "input",
132
132
  });
133
+ // ProfileEntry carries a named meta id (#172), so zod v4 hoists it into
134
+ // top-level $defs and the profiles record references it by $ref — the
135
+ // fallbackProfile readOnly marker the docs surface lives in the $defs copy.
133
136
  expect(json).toMatchObject({
134
137
  properties: {
135
138
  profiles: {
136
- additionalProperties: {
137
- properties: { fallbackProfile: { readOnly: true } },
138
- },
139
+ additionalProperties: { $ref: "#/$defs/ProfileEntry" },
140
+ },
141
+ },
142
+ $defs: {
143
+ ProfileEntry: {
144
+ properties: { fallbackProfile: { readOnly: true } },
139
145
  },
140
146
  },
141
147
  });