@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
@@ -23,9 +23,11 @@
23
23
  * - A function response receives the recorded request and returns the
24
24
  * response dynamically.
25
25
  * - An unmatched request throws `NoMockMatchError` listing the request and
26
- * the registered handlers (CLI channel + HTTP). The gateway channel falls
27
- * through to the legacy `mock-gateway-ipc` behavior instead, because
28
- * incidental `get_feature_flags` calls are ubiquitous in this suite.
26
+ * the registered handlers (CLI channel + HTTP). The gateway transport is
27
+ * the exception: it answers every method — registrations are consulted
28
+ * first, then per-test transport defaults, then a kernel
29
+ * `get_feature_flags` sentinel — because incidental `get_feature_flags`
30
+ * calls are ubiquitous in this suite.
29
31
  *
30
32
  * Channels reset automatically after every test (`afterEach` registered in
31
33
  * test-preload.ts), so registrations are per-test: anything registered at
@@ -38,6 +40,7 @@
38
40
  * the real-socket tests rely on that.
39
41
  */
40
42
 
43
+ import { EventEmitter } from "node:events";
41
44
  import { mock } from "bun:test";
42
45
 
43
46
  // ---------------------------------------------------------------------------
@@ -299,16 +302,50 @@ export interface IpcMockController {
299
302
  lastCall(method?: string): IpcCallRecord | undefined;
300
303
  }
301
304
 
305
+ /**
306
+ * Transport-level controls unique to the gateway channel. The fake answers
307
+ * every method (it never throws `NoMockMatchError` — incidental gateway
308
+ * calls are ubiquitous in the suite), so tests shape what the transport says
309
+ * to UN-registered requests instead.
310
+ */
311
+ export interface GatewayTransportControls {
312
+ /**
313
+ * Results answered for methods no `.on` registration claimed. A key mapped
314
+ * to `undefined` suppresses even the `get_feature_flags` sentinel for that
315
+ * method. Replaces any previously set defaults.
316
+ */
317
+ setDefaults(results: Record<string, unknown>): void;
318
+ /**
319
+ * Simulate a failed socket connect: the persistent client throws
320
+ * `IpcConnectError`, the one-shot `ipcCall` returns `undefined` (mirroring
321
+ * the real module).
322
+ */
323
+ failConnect(opts?: { code?: string }): void;
324
+ /**
325
+ * Throw a transient (retryable) connect error for the first `n`
326
+ * persistent-client calls; with `ipcCallError`, a non-retryable
327
+ * `IpcCallError` instead. Consulted AFTER `.on` registrations, so a
328
+ * registered response is never "failed" by these knobs.
329
+ */
330
+ failFirstN(n: number, opts?: { ipcCallError?: boolean }): void;
331
+ /** Persistent-client `.call` invocations for `method` since the last reset. */
332
+ persistentCallCount(method: string): number;
333
+ }
334
+
302
335
  export type IpcMockChannel = "cli" | "gateway";
303
336
 
337
+ export async function mockIpc(
338
+ channel: "gateway",
339
+ ): Promise<IpcMockController & GatewayTransportControls>;
340
+ export async function mockIpc(channel: "cli"): Promise<IpcMockController>;
304
341
  export async function mockIpc(
305
342
  channel: IpcMockChannel,
306
- ): Promise<IpcMockController> {
343
+ ): Promise<IpcMockController & Partial<GatewayTransportControls>> {
307
344
  if (channel === "cli") {
308
345
  await ensureCliClientMock();
309
346
  }
310
347
  const target = channel === "cli" ? cliChannel : gatewayChannel;
311
- return {
348
+ const base: IpcMockController = {
312
349
  on: (matcher, response) => target.on(matcher, response),
313
350
  calls: (method) => filterIpcCalls(target, method),
314
351
  callCount: (method) => filterIpcCalls(target, method).length,
@@ -317,6 +354,26 @@ export async function mockIpc(
317
354
  return list[list.length - 1];
318
355
  },
319
356
  };
357
+ if (channel !== "gateway") {
358
+ return base;
359
+ }
360
+ return {
361
+ ...base,
362
+ setDefaults: (results) => {
363
+ gatewayDefaults = { ...results };
364
+ },
365
+ failConnect: (opts) => {
366
+ gatewayConnectError = true;
367
+ if (opts?.code !== undefined) {
368
+ gatewayConnectErrorCode = opts.code;
369
+ }
370
+ },
371
+ failFirstN: (n, opts) => {
372
+ gatewayFailFirstN = n;
373
+ gatewayFailFirstNIpcCallError = opts?.ipcCallError ?? false;
374
+ },
375
+ persistentCallCount: (method) => gatewayPersistentCallCounts[method] ?? 0,
376
+ };
320
377
  }
321
378
 
322
379
  /**
@@ -346,10 +403,10 @@ export function ipcError(
346
403
  }
347
404
 
348
405
  /**
349
- * Hook for `mock-gateway-ipc.ts`: consult the gateway channel FIRST, falling
350
- * through to the legacy sentinel/results-map behavior when nothing matches.
351
- * An `Error`-instance response comes back as the resolved value so the caller
352
- * can apply the one-shot/persistent failure contract.
406
+ * Hook for the gateway transport fake (below): consult the gateway channel
407
+ * FIRST, falling through to the per-test transport defaults when nothing
408
+ * matches. An `Error`-instance response comes back as the resolved value so
409
+ * the caller can apply the one-shot/persistent failure contract.
353
410
  */
354
411
  export function dispatchGatewayIpcRequest(
355
412
  method: string,
@@ -371,6 +428,163 @@ export function dispatchGatewayIpcRequest(
371
428
  }
372
429
  }
373
430
 
431
+ // ---------------------------------------------------------------------------
432
+ // Gateway transport fake (installed globally once by test-preload.ts)
433
+ // ---------------------------------------------------------------------------
434
+
435
+ /**
436
+ * Sentinel answered for `get_feature_flags` when neither a registration nor
437
+ * a per-test default covers it. Keeps the response non-empty so
438
+ * `initFeatureFlagOverrides()` does not enter its 7.75 s empty-result retry
439
+ * loop in tests that build the CLI program or otherwise trigger flag
440
+ * initialization. The key starts with a double underscore so it cannot
441
+ * collide with any real registry-declared flag.
442
+ */
443
+ const GET_FEATURE_FLAGS_SENTINEL: Record<string, boolean> = {
444
+ __test_default__: false,
445
+ };
446
+
447
+ class FakeIpcCallError extends Error {
448
+ readonly statusCode?: number;
449
+ readonly errorCode?: string;
450
+ readonly errorDetails?: unknown;
451
+
452
+ constructor(
453
+ message: string,
454
+ fields: {
455
+ statusCode?: number;
456
+ errorCode?: string;
457
+ errorDetails?: unknown;
458
+ } = {},
459
+ ) {
460
+ super(message);
461
+ this.name = "IpcCallError";
462
+ if (fields.statusCode !== undefined) {
463
+ this.statusCode = fields.statusCode;
464
+ }
465
+ if (fields.errorCode !== undefined) {
466
+ this.errorCode = fields.errorCode;
467
+ }
468
+ if (fields.errorDetails !== undefined) {
469
+ this.errorDetails = fields.errorDetails;
470
+ }
471
+ }
472
+ }
473
+
474
+ class FakeIpcConnectError extends Error {
475
+ readonly code?: string;
476
+
477
+ constructor(message: string, code?: string) {
478
+ super(message);
479
+ this.name = "IpcConnectError";
480
+ if (code !== undefined) {
481
+ this.code = code;
482
+ }
483
+ }
484
+ }
485
+
486
+ const RETRYABLE_CONNECT_CODES = new Set(["ENOENT", "ECONNREFUSED"]);
487
+
488
+ function isRetryableIpcConnectError(err: unknown): boolean {
489
+ if (typeof err === "object" && err !== null && "code" in err) {
490
+ const code = (err as { code?: unknown }).code;
491
+ return typeof code === "string" && RETRYABLE_CONNECT_CODES.has(code);
492
+ }
493
+ return false;
494
+ }
495
+
496
+ // Per-test transport state — cleared by resetRequestMockChannels.
497
+ let gatewayDefaults: Record<string, unknown> = {};
498
+ let gatewayConnectError = false;
499
+ let gatewayConnectErrorCode: string | undefined;
500
+ let gatewayFailFirstN = 0;
501
+ let gatewayFailFirstNIpcCallError = false;
502
+ const gatewayPersistentCallCounts: Record<string, number> = {};
503
+
504
+ function mockConnectError(): FakeIpcConnectError {
505
+ return new FakeIpcConnectError(
506
+ "Mock IPC socket error",
507
+ gatewayConnectErrorCode ?? "ENOENT",
508
+ );
509
+ }
510
+
511
+ /** The transport's answer for a method no `.on` registration claimed. */
512
+ function gatewayDefaultAnswer(method: string): unknown {
513
+ if (method in gatewayDefaults) {
514
+ return gatewayDefaults[method];
515
+ }
516
+ if (method === "get_feature_flags") {
517
+ return GET_FEATURE_FLAGS_SENTINEL;
518
+ }
519
+ return undefined;
520
+ }
521
+
522
+ class FakePersistentIpcClient extends EventEmitter {
523
+ async call(
524
+ method: string,
525
+ _params?: Record<string, unknown>,
526
+ _timeoutMs?: number,
527
+ ): Promise<unknown> {
528
+ gatewayPersistentCallCounts[method] =
529
+ (gatewayPersistentCallCounts[method] ?? 0) + 1;
530
+ const dispatched = dispatchGatewayIpcRequest(method, _params);
531
+ if (dispatched.handled) {
532
+ if (dispatched.value instanceof Error) {
533
+ throw dispatched.value;
534
+ }
535
+ return dispatched.value;
536
+ }
537
+ if (gatewayFailFirstN > 0) {
538
+ gatewayFailFirstN--;
539
+ if (gatewayFailFirstNIpcCallError) {
540
+ throw new FakeIpcCallError("Mock deterministic gateway error");
541
+ }
542
+ throw mockConnectError();
543
+ }
544
+ if (gatewayConnectError) {
545
+ throw mockConnectError();
546
+ }
547
+ return gatewayDefaultAnswer(method);
548
+ }
549
+
550
+ destroy(): void {
551
+ /* no-op */
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Swap `@mycadet/gateway-client/ipc-client` for the channel-backed fake.
557
+ * Registered once in the test preload so every test file gets a no-op IPC
558
+ * layer by default — no test accidentally connects to a real gateway socket.
559
+ */
560
+ export function installGatewayIpcMock(): void {
561
+ // Named exports here must cover every ipc-client symbol the package barrel
562
+ // re-exports. Bun's mock replaces that module for relative and package
563
+ // imports, so a missing name fails assistant tests at load time.
564
+ mock.module("@mycadet/gateway-client/ipc-client", () => ({
565
+ ipcCall: async (
566
+ _socketPath: string,
567
+ method: string,
568
+ params?: Record<string, unknown>,
569
+ ): Promise<unknown> => {
570
+ const dispatched = dispatchGatewayIpcRequest(method, params);
571
+ if (dispatched.handled) {
572
+ // Real ipcCall returns undefined on failure — mirror that behavior.
573
+ return dispatched.value instanceof Error ? undefined : dispatched.value;
574
+ }
575
+ if (gatewayConnectError) {
576
+ // Real ipcCall returns undefined on failure — mirror that behavior.
577
+ return undefined;
578
+ }
579
+ return gatewayDefaultAnswer(method);
580
+ },
581
+ IpcCallError: FakeIpcCallError,
582
+ IpcConnectError: FakeIpcConnectError,
583
+ isRetryableIpcConnectError,
584
+ PersistentIpcClient: FakePersistentIpcClient,
585
+ }));
586
+ }
587
+
374
588
  // ---------------------------------------------------------------------------
375
589
  // CLI client transport fake (lazy — only files that call mockIpc("cli"))
376
590
  // ---------------------------------------------------------------------------
@@ -742,6 +956,14 @@ export function mockHttp(): HttpMockController {
742
956
  export function resetRequestMockChannels(): void {
743
957
  cliChannel.reset();
744
958
  gatewayChannel.reset();
959
+ gatewayDefaults = {};
960
+ gatewayConnectError = false;
961
+ gatewayConnectErrorCode = undefined;
962
+ gatewayFailFirstN = 0;
963
+ gatewayFailFirstNIpcCallError = false;
964
+ for (const key of Object.keys(gatewayPersistentCallCounts)) {
965
+ delete gatewayPersistentCallCounts[key];
966
+ }
745
967
  if (originalFetch !== undefined) {
746
968
  globalThis.fetch = originalFetch;
747
969
  originalFetch = undefined;
@@ -349,30 +349,14 @@ describe("secret routes cloud proxy registry sync", () => {
349
349
  cloudOrganizationIdOverride = "existing-org";
350
350
  platformUserIdOverride = "existing-user";
351
351
 
352
+ // Identity ids are not credentials at all — they resolve from the
353
+ // assistant_api_key via validate and live in the in-memory identity.
352
354
  await expect(
353
- addCredential("cadet:platform_assistant_id", "asst-1"),
354
- ).rejects.toThrow("Unknown cadet credential: platform_assistant_id");
355
- await expect(
356
- addCredential("cadet:platform_organization_id", "org-1"),
357
- ).rejects.toThrow("Unknown cadet credential: platform_organization_id");
358
- await expect(
359
- addCredential("cadet:platform_user_id", "user-1"),
360
- ).rejects.toThrow("Unknown cadet credential: platform_user_id");
361
- await expect(
362
- addCredential("cadet:platform_base_url", "https://managed.example.com"),
363
- ).rejects.toThrow("Unknown cadet credential: platform_base_url");
355
+ addCredential("cadet:cloud_assistant_id", "asst-1"),
356
+ ).rejects.toThrow("Unknown cadet credential: cloud_assistant_id");
364
357
 
365
358
  expect(
366
- secureKeyStore.get(credentialKey("cadet", "platform_assistant_id")),
367
- ).toBeUndefined();
368
- expect(
369
- secureKeyStore.get(credentialKey("cadet", "platform_organization_id")),
370
- ).toBeUndefined();
371
- expect(
372
- secureKeyStore.get(credentialKey("cadet", "platform_user_id")),
373
- ).toBeUndefined();
374
- expect(
375
- secureKeyStore.get(credentialKey("cadet", "platform_base_url")),
359
+ secureKeyStore.get(credentialKey("cadet", "cloud_assistant_id")),
376
360
  ).toBeUndefined();
377
361
  expect(metadataUpserts).toEqual([]);
378
362
  expect(cloudAssistantIdOverride).toBe("existing-asst");
@@ -383,13 +367,13 @@ describe("secret routes cloud proxy registry sync", () => {
383
367
  });
384
368
 
385
369
  test("an unknown cadet credential does not delete leftover vault copies", async () => {
386
- const key = credentialKey("cadet", "platform_assistant_id");
370
+ const key = credentialKey("cadet", "cloud_assistant_id");
387
371
  secureKeyStore.set(key, "leftover-asst");
388
372
  cloudAssistantIdOverride = "asst-1";
389
373
 
390
374
  await expect(
391
- addCredential("cadet:platform_assistant_id", " "),
392
- ).rejects.toThrow("Unknown cadet credential: platform_assistant_id");
375
+ addCredential("cadet:cloud_assistant_id", " "),
376
+ ).rejects.toThrow("Unknown cadet credential: cloud_assistant_id");
393
377
 
394
378
  expect(secureKeyStore.get(key)).toBe("leftover-asst");
395
379
  expect(cloudAssistantIdOverride).toBe("asst-1");
@@ -6,7 +6,6 @@
6
6
  * - scrubs the stored value exactly once, immediately after the
7
7
  * secure-store write
8
8
  * - never scrubs when validation rejects the request (nothing was stored)
9
- * - still succeeds when the scrub itself rejects (best-effort hygiene)
10
9
  */
11
10
 
12
11
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -21,7 +20,6 @@ import { useMemorySecureKeyMap } from "./helpers/secure-keys.js";
21
20
  const secureStore = useMemorySecureKeyMap();
22
21
 
23
22
  let scrubbedValues: string[];
24
- let scrubRejects: boolean;
25
23
  let anthropicKeyValid: boolean;
26
24
  let providersRefreshed: number;
27
25
 
@@ -32,9 +30,6 @@ let providersRefreshed: number;
32
30
  mock.module("../daemon/credential-transcript-scrub.js", () => ({
33
31
  scrubStoredCredentialFromTranscripts: mock(async (value: string) => {
34
32
  scrubbedValues.push(value);
35
- if (scrubRejects) {
36
- throw new Error("transcript sweep failed");
37
- }
38
33
  return { dbMessagesScrubbed: 0, residentMessagesScrubbed: 0 };
39
34
  }),
40
35
  }));
@@ -87,7 +82,6 @@ describe("secrets_add api_key transcript scrub", () => {
87
82
  beforeEach(() => {
88
83
  secureStore.clear();
89
84
  scrubbedValues = [];
90
- scrubRejects = false;
91
85
  anthropicKeyValid = true;
92
86
  providersRefreshed = 0;
93
87
  });
@@ -104,6 +98,8 @@ describe("secrets_add api_key transcript scrub", () => {
104
98
  API_KEY_VALUE,
105
99
  );
106
100
  expect(scrubbedValues).toEqual([API_KEY_VALUE]);
101
+ // The provider refresh still happens after the scrub.
102
+ expect(providersRefreshed).toBe(1);
107
103
  });
108
104
 
109
105
  test("an unknown provider is rejected and never triggers a scrub", async () => {
@@ -134,23 +130,6 @@ describe("secrets_add api_key transcript scrub", () => {
134
130
  expect(secureStore.size).toBe(0);
135
131
  expect(scrubbedValues).toEqual([]);
136
132
  });
137
-
138
- test("the route still succeeds (and refreshes providers) when the scrub rejects", async () => {
139
- scrubRejects = true;
140
-
141
- const result = await addRoute.handler({
142
- body: { type: "api_key", name: "fireworks", value: API_KEY_VALUE },
143
- });
144
-
145
- expect(result).toEqual(expect.objectContaining({ success: true }));
146
- expect(secureStore.get(credentialKey("fireworks", "api_key"))).toBe(
147
- API_KEY_VALUE,
148
- );
149
- // The scrub ran (once) even though it failed, and the failure stayed
150
- // invisible to the caller — the provider refresh still happened.
151
- expect(scrubbedValues).toEqual([API_KEY_VALUE]);
152
- expect(providersRefreshed).toBe(1);
153
- });
154
133
  });
155
134
 
156
135
  const CREDENTIAL_VALUE = "synthetic-credential-value-0987654321";
@@ -159,7 +138,6 @@ describe("secrets_add credential transcript scrub", () => {
159
138
  beforeEach(() => {
160
139
  secureStore.clear();
161
140
  scrubbedValues = [];
162
- scrubRejects = false;
163
141
  anthropicKeyValid = true;
164
142
  providersRefreshed = 0;
165
143
  });
@@ -180,24 +158,6 @@ describe("secrets_add credential transcript scrub", () => {
180
158
  expect(scrubbedValues).toEqual([CREDENTIAL_VALUE]);
181
159
  });
182
160
 
183
- test("the route still succeeds when the credential scrub rejects", async () => {
184
- scrubRejects = true;
185
-
186
- const result = await addRoute.handler({
187
- body: {
188
- type: "credential",
189
- name: "github:api_token",
190
- value: CREDENTIAL_VALUE,
191
- },
192
- });
193
-
194
- expect(result).toEqual(expect.objectContaining({ success: true }));
195
- expect(secureStore.get(credentialKey("github", "api_token"))).toBe(
196
- CREDENTIAL_VALUE,
197
- );
198
- expect(scrubbedValues).toEqual([CREDENTIAL_VALUE]);
199
- });
200
-
201
161
  test("an unknown cadet credential is rejected and never triggers a scrub", async () => {
202
162
  await expect(
203
163
  addRoute.handler({
@@ -22,8 +22,10 @@ import { tmpdir } from "node:os";
22
22
  import { join } from "node:path";
23
23
  import { afterAll, afterEach } from "bun:test";
24
24
 
25
- import { installGatewayIpcMock } from "./mock-gateway-ipc.js";
26
- import { resetRequestMockChannels } from "./requests-mock.js";
25
+ import {
26
+ installGatewayIpcMock,
27
+ resetRequestMockChannels,
28
+ } from "./requests-mock.js";
27
29
 
28
30
  // --- Phase 1: env override (zero source-module imports above this point) ---
29
31
 
@@ -67,9 +69,10 @@ delete process.env.CES_CREDENTIAL_URL;
67
69
  // --- Phase 2: install the IPC mock (no source-module imports) ---
68
70
 
69
71
  // Mock gateway IPC so no test accidentally connects to a real gateway socket.
70
- // The mock returns a sentinel for `get_feature_flags` to short-circuit the
71
- // retry loop in `initFeatureFlagOverrides()`. Tests that need specific IPC
72
- // responses use `mockGatewayIpc()` / `resetMockGatewayIpc()`.
72
+ // The transport answers a sentinel for `get_feature_flags` to short-circuit
73
+ // the retry loop in `initFeatureFlagOverrides()`. Tests that need specific
74
+ // IPC behavior use `mockIpc("gateway")` (`.on`, `.setDefaults`,
75
+ // `.failConnect`, `.failFirstN`).
73
76
  installGatewayIpcMock();
74
77
 
75
78
  // requests-mock channels (mockIpc/mockHttp) are per-test by construction: