@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
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("
|
|
16
|
+
type: z.literal("cloud_disconnected"),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
export type CloudDisconnectedEvent = z.infer<
|
|
@@ -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
|
-
)
|
|
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
|
-
)
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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.
|
|
676
|
-
// the
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
119
|
-
// (which exists to short-circuit `initFeatureFlagOverrides()`'s
|
|
120
|
-
// loop) so this test exercises the underlying-undefined path.
|
|
121
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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(
|
|
199
|
+
expect(gateway.persistentCallCount("classify_risk")).toBe(2);
|
|
204
200
|
});
|
|
205
201
|
|
|
206
202
|
test("returns undefined after exhausting retries (3 attempts)", async () => {
|
|
207
|
-
|
|
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(
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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(
|
|
218
|
+
expect(gateway.persistentCallCount("classify_risk")).toBe(1);
|
|
224
219
|
});
|
|
225
220
|
|
|
226
221
|
test("stops retrying once the signal is aborted", async () => {
|
|
227
|
-
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
1065
|
-
// `cadet:assistant_api_key` / `
|
|
1066
|
-
//
|
|
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
|
|
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: {
|
|
2215
|
+
conversations: {
|
|
2216
|
+
autoArchive: { enabled: false, afterDays: 7 },
|
|
2217
|
+
skipAutoRetitling: false,
|
|
2218
|
+
},
|
|
2216
2219
|
notifications: { newMessageEnabled: true },
|
|
2217
2220
|
});
|
|
2218
|
-
|
|
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
|
|
2241
|
+
autoArchive: { enabled: true, afterDays: 30 },
|
|
2239
2242
|
},
|
|
2240
|
-
notifications: { newMessageEnabled: false
|
|
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
|
|
2284
|
+
autoArchive: { enabled: false, afterDays: 7 },
|
|
2282
2285
|
},
|
|
2283
|
-
notifications: { newMessageEnabled: true
|
|
2286
|
+
notifications: { newMessageEnabled: true },
|
|
2284
2287
|
});
|
|
2285
2288
|
expect(loadRawConfig()).toMatchObject({
|
|
2286
2289
|
conversations: { autoArchive: { futureOption: "keep" } },
|