@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `platform_disconnected` SSE event.
2
+ * `cloud_disconnected` SSE event.
3
3
  *
4
4
  * Emitted by the cloud-disconnect route after stored platform
5
5
  * credentials are deleted, notifying connected clients that the cloud
@@ -13,7 +13,7 @@
13
13
  import { z } from "zod";
14
14
 
15
15
  export const CloudDisconnectedEventSchema = z.object({
16
- type: z.literal("platform_disconnected"),
16
+ type: z.literal("cloud_disconnected"),
17
17
  });
18
18
 
19
19
  export type CloudDisconnectedEvent = z.infer<
@@ -33,7 +33,8 @@ function timezoneConfigField(path: string) {
33
33
  return z.NEVER;
34
34
  }
35
35
  return canonical;
36
- });
36
+ })
37
+ .pipe(z.string());
37
38
  }
38
39
 
39
40
  export const CloudConfigSchema = z
@@ -61,6 +61,7 @@ const HostBrowserCdpInspectConfigSchema = z
61
61
  .string({ error: "hostBrowser.cdpInspect.host must be a string" })
62
62
  .transform((v) => v || "localhost")
63
63
  .default("localhost")
64
+ .pipe(z.string())
64
65
  .describe(
65
66
  "Host name or IP address where the host Chrome instance exposes its remote debugging endpoint.",
66
67
  ),
@@ -94,14 +94,6 @@ const IngressBaseSchema = z
94
94
  rateLimit: IngressRateLimitConfigSchema.default(
95
95
  IngressRateLimitConfigSchema.parse({}),
96
96
  ),
97
- shutdownDrainMs: z
98
- .number({ error: "ingress.shutdownDrainMs must be a number" })
99
- .int("ingress.shutdownDrainMs must be an integer")
100
- .nonnegative("ingress.shutdownDrainMs must be a non-negative integer")
101
- .default(5_000)
102
- .describe(
103
- "Time in milliseconds to drain in-flight requests during graceful shutdown",
104
- ),
105
97
  })
106
98
  .describe(
107
99
  "Ingress HTTP server configuration — handles incoming webhooks, API requests, and channel events",
@@ -109,10 +101,7 @@ const IngressBaseSchema = z
109
101
 
110
102
  export const IngressConfigSchema = IngressBaseSchema.default(
111
103
  IngressBaseSchema.parse({}),
112
- ).transform((val) => ({
113
- ...val,
114
- enabled: val.enabled,
115
- }));
104
+ );
116
105
 
117
106
  export type IngressWebhookConfig = z.infer<typeof IngressWebhookConfigSchema>;
118
107
  export type IngressRateLimitConfig = z.infer<
@@ -733,7 +733,16 @@ export const ProfileEntry = LLMConfigFragment.extend({
733
733
  * not supported.
734
734
  */
735
735
  fallbackProfile: z.string().min(1).optional().meta({ readOnly: true }),
736
- });
736
+ /**
737
+ * Wire-only capability flags: stamped onto config-read responses by
738
+ * `enrichProfilesForWire` and stripped from incoming writes by
739
+ * `stripWireOnlyProfileKeys` (conversation-query-routes), so they never
740
+ * persist. Declared here so the wire contract (AssistantConfig → SDK
741
+ * types) describes the enriched profile shape clients consume.
742
+ */
743
+ supportsVision: z.boolean().optional(),
744
+ invariant: z.boolean().optional(),
745
+ }).meta({ id: "ProfileEntry" });
737
746
  export type ProfileEntry = z.infer<typeof ProfileEntry>;
738
747
 
739
748
  /**
@@ -41,6 +41,14 @@ const MemoryJobsConfigInputSchema = z.object({
41
41
  ),
42
42
  });
43
43
 
44
+ const MemoryJobsConfigOutputSchema = z.object({
45
+ workerConcurrency: positiveInt("workerConcurrency"),
46
+ stalledJobTimeoutMs: positiveInt("stalledJobTimeoutMs"),
47
+ slowLlmConcurrency: positiveInt("slowLlmConcurrency"),
48
+ fastConcurrency: positiveInt("fastConcurrency"),
49
+ embedConcurrency: positiveInt("embedConcurrency"),
50
+ });
51
+
44
52
  export const MemoryJobsConfigSchema = MemoryJobsConfigInputSchema.transform(
45
53
  (input) => {
46
54
  // When `workerConcurrency` is explicitly set but lane caps are not,
@@ -76,7 +84,9 @@ export const MemoryJobsConfigSchema = MemoryJobsConfigInputSchema.transform(
76
84
  embedConcurrency,
77
85
  };
78
86
  },
79
- ).describe("Memory background job processing configuration");
87
+ )
88
+ .pipe(MemoryJobsConfigOutputSchema)
89
+ .describe("Memory background job processing configuration");
80
90
 
81
91
  export const MemoryRetentionConfigSchema = z
82
92
  .object({
@@ -52,6 +52,7 @@ function languageVoicesSchema(providerId: string, valuesDescription: string) {
52
52
  }
53
53
  return Object.fromEntries(normalized);
54
54
  })
55
+ .pipe(z.record(z.string(), z.string()))
55
56
  .optional()
56
57
  .describe(
57
58
  "Per-language voice overrides for live voice. Keys are normalized on " +
@@ -70,6 +71,7 @@ export const TtsElevenLabsProviderConfigSchema = z
70
71
  })
71
72
  .transform((v) => v || DEFAULT_ELEVENLABS_VOICE_ID)
72
73
  .default(DEFAULT_ELEVENLABS_VOICE_ID)
74
+ .pipe(z.string())
73
75
  .describe("ElevenLabs voice ID for text-to-speech"),
74
76
  voiceModelId: z
75
77
  .string({
@@ -194,6 +196,7 @@ export const TtsDeepgramProviderConfigSchema = z
194
196
  })
195
197
  .transform((v) => v || "aura-asteria-en")
196
198
  .default("aura-asteria-en")
199
+ .pipe(z.string())
197
200
  .describe("Deepgram TTS model identifier"),
198
201
  format: z
199
202
  .enum(["mp3", "wav", "opus"], {
@@ -221,6 +224,7 @@ export const TtsXaiProviderConfigSchema = z
221
224
  })
222
225
  .transform((v) => v || "eve")
223
226
  .default("eve")
227
+ .pipe(z.string())
224
228
  .describe(
225
229
  "xAI voice ID — one of: eve, ara, rex, sal, leo (case-insensitive)",
226
230
  ),
@@ -230,6 +234,7 @@ export const TtsXaiProviderConfigSchema = z
230
234
  })
231
235
  .transform((v) => v || "auto")
232
236
  .default("auto")
237
+ .pipe(z.string())
233
238
  .describe(
234
239
  "BCP-47 language code (e.g. 'en-US') or 'auto' for auto-detection",
235
240
  ),
@@ -18,10 +18,7 @@ import {
18
18
  assertAcpCredentialFormat,
19
19
  } from "../acp/acp-credentials.js";
20
20
  import { getConfig } from "../config/loader.js";
21
- import {
22
- isNonSecretPlatformField,
23
- scrubStoredCredentialFromTranscripts,
24
- } from "../daemon/credential-transcript-scrub.js";
21
+ import { scrubStoredCredentialFromTranscripts } from "../daemon/credential-transcript-scrub.js";
25
22
  import {
26
23
  setSlackChannelConfig,
27
24
  type SlackChannelConfigResult,
@@ -209,20 +206,11 @@ export async function persistPromptedCredential(args: {
209
206
  // (the 06-credential-security rail promises that pasted message is
210
207
  // scrubbed after storage). Run the scrub right after the store succeeds,
211
208
  // best-effort and invisible to the caller, mirroring credentials_set.
212
- if (!isNonSecretPlatformField(service, field)) {
213
- try {
214
- const scrubbed = await scrubStoredCredentialFromTranscripts(value);
215
- log.info(
216
- { service, field, ...scrubbed },
217
- "Prompted credential stored; scrubbed value from recent transcripts",
218
- );
219
- } catch (err) {
220
- log.warn(
221
- { service, field, err },
222
- "Prompted credential stored, but transcript scrub failed",
223
- );
224
- }
225
- }
209
+ const scrubbed = await scrubStoredCredentialFromTranscripts(value);
210
+ log.info(
211
+ { service, field, ...scrubbed },
212
+ "Prompted credential stored; scrubbed value from recent transcripts",
213
+ );
226
214
 
227
215
  try {
228
216
  upsertCredentialMetadata(service, field, {
@@ -36,10 +36,7 @@ import {
36
36
  } from "../oauth/oauth-store.js";
37
37
  import { missingScopesForStoredToken } from "../oauth/scope-utils.js";
38
38
  import { credentialKey } from "../security/credential-key.js";
39
- import {
40
- getSecureKeyAsync,
41
- getSecureKeyResultAsync,
42
- } from "../security/secure-keys.js";
39
+ import { getSecureKeyResultAsync } from "../security/secure-keys.js";
43
40
  import {
44
41
  TokenExpiredError,
45
42
  withValidToken,
@@ -636,13 +633,12 @@ const ASSISTANT_API_KEY_CONNECTION_ID = "cadet:assistant_api_key";
636
633
  * observed here and acted on by a client.
637
634
  *
638
635
  * Statuses map onto the shared vocabulary:
639
- * - `missing_token`: no key is stored, so managed inference cannot run.
640
636
  * - `revoked`: the cloud settled that the stored key is not accepted.
641
637
  * - `unreachable`: the credential store or the cloud did not answer, so
642
638
  * health is unknown. The heartbeat drops these before notifying, which is
643
639
  * what keeps a transient outage from raising a credential alert.
644
640
  *
645
- * `canAutoRecover` is true for both failure statuses: recovery needs no
641
+ * `canAutoRecover` is true for the revoked status: recovery needs no
646
642
  * re-authorization from the user, only a signed-in client to rotate the key.
647
643
  */
648
644
  export async function checkAssistantApiKey(): Promise<CredentialHealthResult | null> {
@@ -672,21 +668,10 @@ export async function checkAssistantApiKey(): Promise<CredentialHealthResult | n
672
668
 
673
669
  if (stored.value == null) {
674
670
  // A workspace that was never connected to the cloud has no managed
675
- // credential and is not unhealthy. Only an install that already carries
676
- // the rest of a cloud identity is missing something.
677
- const assistantId = await getSecureKeyAsync(
678
- credentialKey("cadet", "platform_assistant_id"),
679
- );
680
- if (!assistantId) {
681
- return null;
682
- }
683
- return {
684
- ...base,
685
- status: "missing_token",
686
- details:
687
- "No Cadet Cloud credentials are stored yet. Log in to Cadet Cloud to set them up.",
688
- canAutoRecover: true,
689
- };
671
+ // credential and is not unhealthy. The identity ids resolve from the
672
+ // api key via the validate endpoint, so a missing key means nothing is
673
+ // bound — there is nothing to check.
674
+ return null;
690
675
  }
691
676
 
692
677
  const { CadetCloudClient } = await import("../cloud/client.js");
@@ -93,44 +93,36 @@ export async function scrubStoredCredentialFromTranscripts(
93
93
  dbMessagesScrubbed: 0,
94
94
  residentMessagesScrubbed: 0,
95
95
  };
96
- if (value.trim().length < MIN_SCRUB_VALUE_LENGTH) {
97
- return result;
98
- }
99
- const targets = buildSearchTargets(value);
100
96
  try {
101
- result.dbMessagesScrubbed = sweepDbMessages(targets);
102
- } catch (err) {
103
- log.warn(
104
- { err, valueLength: value.length },
105
- "Credential transcript DB sweep failed; continuing with in-memory sweep",
106
- );
107
- }
108
- try {
109
- result.residentMessagesScrubbed = sweepResidentConversations(targets);
97
+ if (value.trim().length < MIN_SCRUB_VALUE_LENGTH) {
98
+ return result;
99
+ }
100
+ const targets = buildSearchTargets(value);
101
+ try {
102
+ result.dbMessagesScrubbed = sweepDbMessages(targets);
103
+ } catch (err) {
104
+ log.warn(
105
+ { err, valueLength: value.length },
106
+ "Credential transcript DB sweep failed; continuing with in-memory sweep",
107
+ );
108
+ }
109
+ try {
110
+ result.residentMessagesScrubbed = sweepResidentConversations(targets);
111
+ } catch (err) {
112
+ log.warn(
113
+ { err, valueLength: value.length },
114
+ "Credential transcript in-memory sweep failed",
115
+ );
116
+ }
110
117
  } catch (err) {
111
118
  log.warn(
112
119
  { err, valueLength: value.length },
113
- "Credential transcript in-memory sweep failed",
120
+ "Credential transcript scrub failed",
114
121
  );
115
122
  }
116
123
  return result;
117
124
  }
118
125
 
119
- /**
120
- * The Cadet Cloud identity ids and base URL are stored through the
121
- * credential routes but are not secrets — they are UUIDs/URLs that
122
- * legitimately appear in conversation text, so scrubbing them would redact
123
- * benign transcript content. Both credential write paths
124
- * (`credentials_set` and the `/v1/secrets` credential branch) consult this
125
- * one exemption so they cannot drift.
126
- */
127
- export function isNonSecretPlatformField(
128
- service: string,
129
- field: string,
130
- ): boolean {
131
- return service === "cadet" && field === "platform_assistant_id";
132
- }
133
-
134
126
  /**
135
127
  * Every transcript encoding of the value ({@link credentialValueEncodings}:
136
128
  * raw plus JSON-escaped — tool_use inputs are stored as JSON, and string
@@ -8,11 +8,7 @@
8
8
  */
9
9
  import { afterEach, describe, expect, test } from "bun:test";
10
10
 
11
- import {
12
- getMockPersistentCallCount,
13
- mockGatewayIpc,
14
- resetMockGatewayIpc,
15
- } from "../__tests__/mock-gateway-ipc.js";
11
+ import { mockIpc } from "../__tests__/requests-mock.js";
16
12
  import { ServiceUnavailableError } from "../runtime/routes/errors.js";
17
13
  import {
18
14
  ipcCall,
@@ -25,7 +21,6 @@ import {
25
21
 
26
22
  afterEach(() => {
27
23
  resetPersistentClient();
28
- resetMockGatewayIpc();
29
24
  });
30
25
 
31
26
  // ---------------------------------------------------------------------------
@@ -34,22 +29,24 @@ afterEach(() => {
34
29
 
35
30
  describe("ipcCall", () => {
36
31
  test("delegates to package ipcCall and returns result", async () => {
37
- mockGatewayIpc(null, {
38
- results: { test_method: { key: "value" } },
39
- });
32
+ const gateway = await mockIpc("gateway");
33
+ gateway.setDefaults({ test_method: { key: "value" } });
40
34
 
41
35
  const result = await ipcCall("test_method");
42
36
  expect(result).toEqual({ key: "value" });
43
37
  });
44
38
 
45
39
  test("returns undefined when mock simulates error", async () => {
46
- mockGatewayIpc(null, { error: true });
40
+ const gateway = await mockIpc("gateway");
41
+ gateway.failConnect();
47
42
 
48
43
  const result = await ipcCall("test_method");
49
44
  expect(result).toBeUndefined();
50
45
  });
51
46
 
52
47
  test("returns undefined for unconfigured methods", async () => {
48
+ await mockIpc("gateway");
49
+
53
50
  const result = await ipcCall("unconfigured_method");
54
51
  expect(result).toBeUndefined();
55
52
  });
@@ -61,16 +58,16 @@ describe("ipcCall", () => {
61
58
 
62
59
  describe("ipcCallPersistent", () => {
63
60
  test("returns result from persistent client", async () => {
64
- mockGatewayIpc(null, {
65
- results: { persistent_test: "persistent-result" },
66
- });
61
+ const gateway = await mockIpc("gateway");
62
+ gateway.setDefaults({ persistent_test: "persistent-result" });
67
63
 
68
64
  const result = await ipcCallPersistent("persistent_test");
69
65
  expect(result).toBe("persistent-result");
70
66
  });
71
67
 
72
68
  test("maps a missing gateway socket to ServiceUnavailableError", async () => {
73
- mockGatewayIpc(null, { error: true });
69
+ const gateway = await mockIpc("gateway");
70
+ gateway.failConnect();
74
71
 
75
72
  await expect(ipcCallPersistent("any_method")).rejects.toBeInstanceOf(
76
73
  ServiceUnavailableError,
@@ -92,21 +89,23 @@ describe("ipcCallPersistent", () => {
92
89
 
93
90
  describe("ipcGetFeatureFlags", () => {
94
91
  test("parses boolean flag values from IPC response", async () => {
95
- mockGatewayIpc({ "flag-a": true, "flag-b": false });
92
+ const gateway = await mockIpc("gateway");
93
+ gateway.setDefaults({
94
+ get_feature_flags: { "flag-a": true, "flag-b": false },
95
+ });
96
96
 
97
97
  const flags = await ipcGetFeatureFlags();
98
98
  expect(flags).toEqual({ "flag-a": true, "flag-b": false });
99
99
  });
100
100
 
101
101
  test("accepts boolean and string values, filters other types from response", async () => {
102
- mockGatewayIpc(null, {
103
- results: {
104
- get_feature_flags: {
105
- valid: true,
106
- number: 42,
107
- string: "yes",
108
- nested: { deep: true },
109
- },
102
+ const gateway = await mockIpc("gateway");
103
+ gateway.setDefaults({
104
+ get_feature_flags: {
105
+ valid: true,
106
+ number: 42,
107
+ string: "yes",
108
+ nested: { deep: true },
110
109
  },
111
110
  });
112
111
 
@@ -115,25 +114,26 @@ describe("ipcGetFeatureFlags", () => {
115
114
  });
116
115
 
117
116
  test("returns empty record when IPC returns undefined", async () => {
118
- // Explicitly suppress the mock's default `get_feature_flags` sentinel
119
- // (which exists to short-circuit `initFeatureFlagOverrides()`'s retry
120
- // loop) so this test exercises the underlying-undefined path.
121
- mockGatewayIpc(null, { results: { get_feature_flags: undefined } });
117
+ // Explicitly suppress the transport's default `get_feature_flags`
118
+ // sentinel (which exists to short-circuit `initFeatureFlagOverrides()`'s
119
+ // retry loop) so this test exercises the underlying-undefined path.
120
+ const gateway = await mockIpc("gateway");
121
+ gateway.setDefaults({ get_feature_flags: undefined });
122
122
  const flags = await ipcGetFeatureFlags();
123
123
  expect(flags).toEqual({});
124
124
  });
125
125
 
126
126
  test("returns empty record when IPC returns array", async () => {
127
- mockGatewayIpc(null, {
128
- results: { get_feature_flags: [true, false] },
129
- });
127
+ const gateway = await mockIpc("gateway");
128
+ gateway.setDefaults({ get_feature_flags: [true, false] });
130
129
 
131
130
  const flags = await ipcGetFeatureFlags();
132
131
  expect(flags).toEqual({});
133
132
  });
134
133
 
135
134
  test("returns empty record on error", async () => {
136
- mockGatewayIpc(null, { error: true });
135
+ const gateway = await mockIpc("gateway");
136
+ gateway.failConnect();
137
137
 
138
138
  const flags = await ipcGetFeatureFlags();
139
139
  expect(flags).toEqual({});
@@ -142,12 +142,11 @@ describe("ipcGetFeatureFlags", () => {
142
142
 
143
143
  describe("ipcGetTunnelStatus", () => {
144
144
  test("parses the shared Tunnel status response", async () => {
145
- mockGatewayIpc(null, {
146
- results: {
147
- get_tunnel_status: {
148
- connected: true,
149
- publicUrl: "https://example.test",
150
- },
145
+ const gateway = await mockIpc("gateway");
146
+ gateway.setDefaults({
147
+ get_tunnel_status: {
148
+ connected: true,
149
+ publicUrl: "https://example.test",
151
150
  },
152
151
  });
153
152
 
@@ -158,10 +157,9 @@ describe("ipcGetTunnelStatus", () => {
158
157
  });
159
158
 
160
159
  test("keeps connected status when publicUrl is malformed", async () => {
161
- mockGatewayIpc(null, {
162
- results: {
163
- get_tunnel_status: { connected: false, publicUrl: 42 },
164
- },
160
+ const gateway = await mockIpc("gateway");
161
+ gateway.setDefaults({
162
+ get_tunnel_status: { connected: false, publicUrl: 42 },
165
163
  });
166
164
 
167
165
  expect(await ipcGetTunnelStatus()).toEqual({
@@ -171,9 +169,8 @@ describe("ipcGetTunnelStatus", () => {
171
169
  });
172
170
 
173
171
  test("returns null when connected is missing or malformed", async () => {
174
- mockGatewayIpc(null, {
175
- results: { get_tunnel_status: { connected: "yes" } },
176
- });
172
+ const gateway = await mockIpc("gateway");
173
+ gateway.setDefaults({ get_tunnel_status: { connected: "yes" } });
177
174
 
178
175
  expect(await ipcGetTunnelStatus()).toBeNull();
179
176
  });
@@ -193,38 +190,37 @@ describe("ipcClassifyRisk retry", () => {
193
190
  const emptyParams = {} as unknown as Parameters<typeof ipcClassifyRisk>[0];
194
191
 
195
192
  test("retries a transient failure and then succeeds", async () => {
196
- mockGatewayIpc(null, {
197
- failFirstN: 1,
198
- results: { classify_risk: CLASSIFY_OK },
199
- });
193
+ const gateway = await mockIpc("gateway");
194
+ gateway.failFirstN(1);
195
+ gateway.setDefaults({ classify_risk: CLASSIFY_OK });
200
196
 
201
197
  const result = await ipcClassifyRisk(emptyParams);
202
198
  expect(result?.risk).toBe("low");
203
- expect(getMockPersistentCallCount("classify_risk")).toBe(2);
199
+ expect(gateway.persistentCallCount("classify_risk")).toBe(2);
204
200
  });
205
201
 
206
202
  test("returns undefined after exhausting retries (3 attempts)", async () => {
207
- mockGatewayIpc(null, { error: true });
203
+ const gateway = await mockIpc("gateway");
204
+ gateway.failConnect();
208
205
 
209
206
  const result = await ipcClassifyRisk(emptyParams);
210
207
  expect(result).toBeUndefined();
211
- expect(getMockPersistentCallCount("classify_risk")).toBe(3);
208
+ expect(gateway.persistentCallCount("classify_risk")).toBe(3);
212
209
  });
213
210
 
214
211
  test("does NOT retry a structured IpcCallError (fails after 1 attempt)", async () => {
215
- mockGatewayIpc(null, {
216
- failFirstN: 3,
217
- ipcCallError: true,
218
- results: { classify_risk: CLASSIFY_OK },
219
- });
212
+ const gateway = await mockIpc("gateway");
213
+ gateway.failFirstN(3, { ipcCallError: true });
214
+ gateway.setDefaults({ classify_risk: CLASSIFY_OK });
220
215
 
221
216
  const result = await ipcClassifyRisk(emptyParams);
222
217
  expect(result).toBeUndefined();
223
- expect(getMockPersistentCallCount("classify_risk")).toBe(1);
218
+ expect(gateway.persistentCallCount("classify_risk")).toBe(1);
224
219
  });
225
220
 
226
221
  test("stops retrying once the signal is aborted", async () => {
227
- mockGatewayIpc(null, { error: true });
222
+ const gateway = await mockIpc("gateway");
223
+ gateway.failConnect();
228
224
  const controller = new AbortController();
229
225
  controller.abort();
230
226
 
@@ -232,6 +228,6 @@ describe("ipcClassifyRisk retry", () => {
232
228
  expect(result).toBeUndefined();
233
229
  // An already-aborted signal short-circuits the retry loop after the first
234
230
  // failed attempt rather than exhausting all three.
235
- expect(getMockPersistentCallCount("classify_risk")).toBe(1);
231
+ expect(gateway.persistentCallCount("classify_risk")).toBe(1);
236
232
  });
237
233
  });
@@ -408,7 +408,7 @@ export class ClickHouseCompactionLogStore {
408
408
  const val = await this.resolveAssistantId();
409
409
  if (!val) {
410
410
  throw new Error(
411
- "cadet:platform_assistant_id credential is required when compactionLogs.destination=clickhouse",
411
+ "Cadet Cloud assistant id is required when compactionLogs.destination=clickhouse",
412
412
  );
413
413
  }
414
414
  this.cachedAssistantId = val;
@@ -258,7 +258,7 @@ export class ClickHouseLlmRequestLogSink implements LlmRequestLogWriter {
258
258
  const val = await this.resolveAssistantId();
259
259
  if (!val) {
260
260
  throw new Error(
261
- "cadet:platform_assistant_id credential is required when readSource=clickhouse",
261
+ "Cadet Cloud assistant id is required when readSource=clickhouse",
262
262
  );
263
263
  }
264
264
  this.cachedAssistantId = val;
@@ -500,7 +500,7 @@ export class ClickHouseLlmRequestLogSource implements LlmRequestLogSource {
500
500
  const val = await this.resolveAssistantId();
501
501
  if (!val) {
502
502
  throw new Error(
503
- "cadet:platform_assistant_id credential is required when readSource=clickhouse",
503
+ "Cadet Cloud assistant id is required when readSource=clickhouse",
504
504
  );
505
505
  }
506
506
  this.cachedAssistantId = val;
@@ -1061,9 +1061,9 @@ export async function streamCommitImport(
1061
1061
  }
1062
1062
 
1063
1063
  // Preserve the target's `cadet:*` credential metadata entries across
1064
- // the swap. Django's post-hatch provisioning on the cloud writes
1065
- // `cadet:assistant_api_key` / `platform_assistant_id` / `webhook_secret`
1066
- // via POST /v1/secrets, which upserts into the live
1064
+ // the swap. Django's post-hatch provisioning writes
1065
+ // `cadet:assistant_api_key` / `cadet:webhook_secret` via POST /v1/secrets,
1066
+ // which upserts into the live
1067
1067
  // workspace's `data/credentials/metadata.json`. Without this merge the
1068
1068
  // swap would replace that file with the source's copy (which has no
1069
1069
  // cadet entries on local sources), and the gateway's
@@ -2206,16 +2206,19 @@ describe("persisted chat settings", () => {
2206
2206
  });
2207
2207
  });
2208
2208
 
2209
- test("sparse GET exposes effective defaults without materializing unrelated fields", async () => {
2209
+ test("sparse GET returns the fully materialized typed config with effective chat settings", async () => {
2210
2210
  rawConfigFixture = {};
2211
2211
  seedRawConfig();
2212
2212
  invalidateConfigCache();
2213
2213
  const result = await getRoute.handler({});
2214
2214
  expect(result).toMatchObject({
2215
- conversations: { autoArchive: { enabled: false, afterDays: 7 } },
2215
+ conversations: {
2216
+ autoArchive: { enabled: false, afterDays: 7 },
2217
+ skipAutoRetitling: false,
2218
+ },
2216
2219
  notifications: { newMessageEnabled: true },
2217
2220
  });
2218
- expect(result).not.toHaveProperty("conversations.skipAutoRetitling");
2221
+ // Materialization is wire-only: nothing is written back to disk.
2219
2222
  expect(loadRawConfig()).not.toHaveProperty("conversations");
2220
2223
  expect(loadRawConfig()).not.toHaveProperty("notifications");
2221
2224
  });
@@ -2235,9 +2238,9 @@ describe("persisted chat settings", () => {
2235
2238
  expect(result).toMatchObject({
2236
2239
  conversations: {
2237
2240
  skipAutoRetitling: true,
2238
- autoArchive: { enabled: true, afterDays: 30, futureOption: "keep" },
2241
+ autoArchive: { enabled: true, afterDays: 30 },
2239
2242
  },
2240
- notifications: { newMessageEnabled: false, futureOption: "keep" },
2243
+ notifications: { newMessageEnabled: false },
2241
2244
  maxStepsPerSession: 75,
2242
2245
  });
2243
2246
  expect(loadRawConfig()).toEqual({
@@ -2278,9 +2281,9 @@ describe("persisted chat settings", () => {
2278
2281
  expect(result).toMatchObject({
2279
2282
  conversations: {
2280
2283
  skipAutoRetitling: true,
2281
- autoArchive: { enabled: false, afterDays: 7, futureOption: "keep" },
2284
+ autoArchive: { enabled: false, afterDays: 7 },
2282
2285
  },
2283
- notifications: { newMessageEnabled: true, futureOption: "keep" },
2286
+ notifications: { newMessageEnabled: true },
2284
2287
  });
2285
2288
  expect(loadRawConfig()).toMatchObject({
2286
2289
  conversations: { autoArchive: { futureOption: "keep" } },