@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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/openapi.yaml +6679 -658
- package/package.json +1 -1
- package/src/__tests__/config-loader-cloud-defaults.test.ts +0 -233
- package/src/__tests__/credential-health-service.test.ts +0 -14
- package/src/__tests__/credential-routes.test.ts +0 -51
- package/src/__tests__/init-feature-flag-overrides.test.ts +40 -24
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -2
- package/src/__tests__/llm-schema-fallback-profile.test.ts +9 -3
- package/src/__tests__/requests-mock.ts +231 -9
- package/src/__tests__/secret-routes-cloud-proxy.test.ts +8 -24
- package/src/__tests__/secret-routes-scrub.test.ts +2 -42
- package/src/__tests__/test-preload.ts +8 -5
- package/src/__tests__/web-search.test.ts +340 -859
- package/src/api/events/cloud-disconnected.ts +2 -2
- package/src/config/schemas/cloud.ts +2 -1
- package/src/config/schemas/host-browser.ts +1 -0
- package/src/config/schemas/ingress.ts +1 -12
- package/src/config/schemas/llm.ts +10 -1
- package/src/config/schemas/memory-lifecycle.ts +11 -1
- package/src/config/schemas/tts.ts +5 -0
- package/src/credential-execution/prompted-credential.ts +6 -18
- package/src/credential-health/credential-health-service.ts +6 -21
- package/src/daemon/credential-transcript-scrub.ts +21 -29
- package/src/ipc/gateway-client.test.ts +55 -59
- package/src/persistence/compaction-log-store-clickhouse.ts +1 -1
- package/src/persistence/llm-request-log-sink-clickhouse.ts +1 -1
- package/src/persistence/llm-request-log-source-clickhouse.ts +1 -1
- package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +10 -7
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +3 -9
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +3 -3
- package/src/runtime/routes/cloud-routes.ts +14 -18
- package/src/runtime/routes/conversation-query-routes.ts +30 -230
- package/src/runtime/routes/migration-routes.ts +5 -6
- package/src/runtime/routes/secret-routes.ts +12 -31
- package/src/tools/credentials/store.ts +6 -15
- package/src/__tests__/mock-gateway-ipc.ts +0 -279
package/package.json
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
const gateway = await mockIpc("gateway");
|
|
92
|
+
gateway.on("get_feature_flags", {});
|
|
84
93
|
setTimeout(() => {
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
130
|
+
const gateway = await mockIpc("gateway");
|
|
131
|
+
gateway.on("get_feature_flags", { "moving-flag": true });
|
|
118
132
|
await initFeatureFlagOverrides();
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
144
|
+
const gateway = await mockIpc("gateway");
|
|
145
|
+
gateway.on("get_feature_flags", { "moving-flag": true });
|
|
131
146
|
await initFeatureFlagOverrides();
|
|
132
147
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
161
|
+
const gateway = await mockIpc("gateway");
|
|
162
|
+
gateway.on("get_feature_flags", { "moving-flag": true });
|
|
147
163
|
await initFeatureFlagOverrides();
|
|
148
164
|
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
-
/
|
|
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
|
-
|
|
138
|
-
|
|
139
|
+
additionalProperties: { $ref: "#/$defs/ProfileEntry" },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
$defs: {
|
|
143
|
+
ProfileEntry: {
|
|
144
|
+
properties: { fallbackProfile: { readOnly: true } },
|
|
139
145
|
},
|
|
140
146
|
},
|
|
141
147
|
});
|