@vellumai/assistant 0.11.0-staging.2 → 0.11.0
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/docs/architecture/memory.md +7 -0
- package/openapi.yaml +12 -1
- package/package.json +1 -1
- package/src/__tests__/channel-readiness-service.test.ts +107 -1
- package/src/__tests__/platform-callback-registration.test.ts +132 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
- package/src/__tests__/telegram-config.test.ts +96 -2
- package/src/config/__tests__/webhook-routing.test.ts +229 -0
- package/src/config/webhook-routing.ts +55 -14
- package/src/daemon/handlers/config-telegram.ts +12 -7
- package/src/inbound/platform-callback-registration.ts +44 -10
- package/src/inbound/public-ingress-urls.ts +40 -9
- package/src/messaging/providers/slack/binding-metadata.test.ts +37 -8
- package/src/messaging/providers/slack/binding-metadata.ts +21 -21
- package/src/messaging/providers/slack/deep-link.test.ts +91 -0
- package/src/messaging/providers/slack/deep-link.ts +27 -8
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +22 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.ts +22 -3
- package/src/runtime/channel-readiness-service.ts +8 -10
- package/src/runtime/routes/webhook-routes.ts +1 -13
- package/src/telegram/__tests__/webhook-health.test.ts +76 -13
- package/src/telegram/webhook-health.ts +1 -1
|
@@ -123,6 +123,13 @@ build can never drift. The same predicate gates procedural-memory-as-skills,
|
|
|
123
123
|
so both v3-tier features honor the Memory opt-out identically. `GET /v1/memory-graph-node` serves node detail,
|
|
124
124
|
including `buffer:` ids for pending entries.
|
|
125
125
|
|
|
126
|
+
`GET /v1/memory/stats` also reports `tier` (`memoryTier()`: `off` / `v1` /
|
|
127
|
+
`v2` / `v3`), which is what lets the web Memory tab explain an unavailable
|
|
128
|
+
graph instead of stating a bare "not available": `off` is the user's own
|
|
129
|
+
Memory opt-out and points at Settings, while `v1`/`v2` point at the v3
|
|
130
|
+
migration. `graph_supported` is exactly `tier === "v3"`, so the capability
|
|
131
|
+
bit and its explanation are derived from one gate.
|
|
132
|
+
|
|
126
133
|
## Capture beyond `remember`
|
|
127
134
|
|
|
128
135
|
- **Retrospective** (`memory-retrospective-*.ts`): periodic per-conversation
|
package/openapi.yaml
CHANGED
|
@@ -17747,7 +17747,9 @@ paths:
|
|
|
17747
17747
|
"Return a cheap count of concept pages from the cached memory page index, for glanceable surfaces like the
|
|
17748
17748
|
identity Memory card. Counts concept pages only and never builds the memory-concept graph. Also reports
|
|
17749
17749
|
graph_supported: whether the memory-concept graph is available for this assistant (memory enabled and v3 live),
|
|
17750
|
-
so callers can gate the graph entry point without building the graph
|
|
17750
|
+
so callers can gate the graph entry point without building the graph, plus tier: the coarse memory tier
|
|
17751
|
+
explaining why the graph is unavailable (off = the user's Memory opt-out, v1/v2 = a legacy engine that has not
|
|
17752
|
+
migrated to v3)."
|
|
17751
17753
|
tags:
|
|
17752
17754
|
- memory
|
|
17753
17755
|
responses:
|
|
@@ -17764,9 +17766,18 @@ paths:
|
|
|
17764
17766
|
graph_supported:
|
|
17765
17767
|
type: boolean
|
|
17766
17768
|
description: Whether the memory-concept graph is available (memory enabled and v3 live)
|
|
17769
|
+
tier:
|
|
17770
|
+
type: string
|
|
17771
|
+
enum:
|
|
17772
|
+
- off
|
|
17773
|
+
- v1
|
|
17774
|
+
- v2
|
|
17775
|
+
- v3
|
|
17776
|
+
description: Coarse memory tier for this assistant; graph_supported is exactly tier === 'v3'
|
|
17767
17777
|
required:
|
|
17768
17778
|
- concepts
|
|
17769
17779
|
- graph_supported
|
|
17780
|
+
- tier
|
|
17770
17781
|
additionalProperties: false
|
|
17771
17782
|
/v1/memory/v2/backfill:
|
|
17772
17783
|
post:
|
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@ import { setConfig } from "./helpers/set-config.js";
|
|
|
6
6
|
let mockSecureKeys: Record<string, string>;
|
|
7
7
|
let mockHasTwilioCredentials: boolean;
|
|
8
8
|
let mockGetIsPlatform: boolean;
|
|
9
|
+
/** Platform credentials present: base URL + assistant ID + assistant API key. */
|
|
10
|
+
let mockPlatformConnected: boolean;
|
|
9
11
|
|
|
10
12
|
mock.module("../calls/twilio-rest.js", () => ({
|
|
11
13
|
getPhoneNumberSid: async () => null,
|
|
@@ -24,7 +26,22 @@ mock.module("../config/env-registry.js", () => ({
|
|
|
24
26
|
getIsPlatform: () => mockGetIsPlatform,
|
|
25
27
|
}));
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
// Spread the real module: replacing it wholesale drops the exports peer test
|
|
30
|
+
// files import from it, which breaks their named-import validation whenever
|
|
31
|
+
// this mock wins evaluation in a combined run.
|
|
32
|
+
const actualRegistration =
|
|
33
|
+
await import("../inbound/platform-callback-registration.js");
|
|
34
|
+
mock.module("../inbound/platform-callback-registration.js", () => ({
|
|
35
|
+
...actualRegistration,
|
|
36
|
+
resolvePlatformCallbackRegistrationContext: async () => ({
|
|
37
|
+
isPlatform: mockGetIsPlatform,
|
|
38
|
+
platformBaseUrl: "https://api.vellum.ai",
|
|
39
|
+
assistantId: mockPlatformConnected ? "assistant-123" : "",
|
|
40
|
+
hasAssistantApiKey: mockPlatformConnected,
|
|
41
|
+
authHeader: mockPlatformConnected ? "Api-Key secret" : null,
|
|
42
|
+
enabled: mockPlatformConnected,
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
28
45
|
|
|
29
46
|
mock.module("../security/secure-keys.js", () => ({
|
|
30
47
|
getSecureKeyAsync: async (key: string) => mockSecureKeys[key] ?? null,
|
|
@@ -85,6 +102,7 @@ describe("ChannelReadinessService", () => {
|
|
|
85
102
|
mockSecureKeys = {};
|
|
86
103
|
mockHasTwilioCredentials = false;
|
|
87
104
|
mockGetIsPlatform = false;
|
|
105
|
+
mockPlatformConnected = false;
|
|
88
106
|
});
|
|
89
107
|
|
|
90
108
|
test("local checks run on every call (no caching of local results)", async () => {
|
|
@@ -278,6 +296,94 @@ describe("ChannelReadinessService", () => {
|
|
|
278
296
|
});
|
|
279
297
|
});
|
|
280
298
|
|
|
299
|
+
test("telegram readiness accepts a platform-connected assistant with no ingress", async () => {
|
|
300
|
+
// LUM-2882: `webhooks register telegram` resolves a platform callback URL
|
|
301
|
+
// in this configuration, so reporting missing ingress here would hide a
|
|
302
|
+
// registration that is really in place (or really broken).
|
|
303
|
+
mockPlatformConnected = true;
|
|
304
|
+
mockSecureKeys[credentialKey("telegram", "bot_token")] = "123:abc";
|
|
305
|
+
mockSecureKeys[credentialKey("telegram", "webhook_secret")] = "secret";
|
|
306
|
+
|
|
307
|
+
const readiness = createReadinessService();
|
|
308
|
+
const [snapshot] = await readiness.getReadiness("telegram");
|
|
309
|
+
|
|
310
|
+
expect(snapshot.ready).toBe(true);
|
|
311
|
+
expect(snapshot.localChecks).toContainEqual({
|
|
312
|
+
name: "ingress",
|
|
313
|
+
passed: true,
|
|
314
|
+
message: "Managed platform callback routing is configured",
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("phone readiness accepts a platform-connected assistant with no ingress", async () => {
|
|
319
|
+
mockPlatformConnected = true;
|
|
320
|
+
mockHasTwilioCredentials = true;
|
|
321
|
+
setConfig("twilio", { phoneNumber: "+15550123" });
|
|
322
|
+
|
|
323
|
+
const readiness = createReadinessService();
|
|
324
|
+
const [snapshot] = await readiness.getReadiness("phone");
|
|
325
|
+
|
|
326
|
+
expect(snapshot.ready).toBe(true);
|
|
327
|
+
expect(snapshot.localChecks).toContainEqual({
|
|
328
|
+
name: "ingress",
|
|
329
|
+
passed: true,
|
|
330
|
+
message: "Managed platform callback routing is configured",
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("configured ingress beats the platform-connected fallback", async () => {
|
|
335
|
+
// Any logged-in local assistant holds platform credentials for the LLM
|
|
336
|
+
// proxy. Reporting managed routing here would mislabel a webhook that
|
|
337
|
+
// `webhooks register` resolves to the self-hosted URL.
|
|
338
|
+
mockPlatformConnected = true;
|
|
339
|
+
mockSecureKeys[credentialKey("telegram", "bot_token")] = "123:abc";
|
|
340
|
+
mockSecureKeys[credentialKey("telegram", "webhook_secret")] = "secret";
|
|
341
|
+
setConfig("ingress", { publicBaseUrl: "https://tunnel.example.com" });
|
|
342
|
+
|
|
343
|
+
const readiness = createReadinessService();
|
|
344
|
+
const [snapshot] = await readiness.getReadiness("telegram");
|
|
345
|
+
|
|
346
|
+
expect(snapshot.ready).toBe(true);
|
|
347
|
+
expect(snapshot.localChecks).toContainEqual({
|
|
348
|
+
name: "ingress",
|
|
349
|
+
passed: true,
|
|
350
|
+
message: "Public ingress URL is configured",
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("explicit ingress opt-out blocks the platform-connected fallback", async () => {
|
|
355
|
+
// Switching ingress off is a decision not to accept inbound webhooks, not
|
|
356
|
+
// an invitation to route them through the platform instead.
|
|
357
|
+
mockPlatformConnected = true;
|
|
358
|
+
mockSecureKeys[credentialKey("telegram", "bot_token")] = "123:abc";
|
|
359
|
+
mockSecureKeys[credentialKey("telegram", "webhook_secret")] = "secret";
|
|
360
|
+
setConfig("ingress", { enabled: false });
|
|
361
|
+
|
|
362
|
+
const readiness = createReadinessService();
|
|
363
|
+
const [snapshot] = await readiness.getReadiness("telegram");
|
|
364
|
+
|
|
365
|
+
expect(snapshot.ready).toBe(false);
|
|
366
|
+
expect(snapshot.reasons).toContainEqual({
|
|
367
|
+
code: "ingress",
|
|
368
|
+
text: "No public ingress URL or managed callback route is configured",
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("email readiness ignores platform connectivity", async () => {
|
|
373
|
+
// Email passes `allowManagedCallbacks: false`, so the managed tiers are
|
|
374
|
+
// not offered to it and only a real ingress URL counts.
|
|
375
|
+
mockPlatformConnected = true;
|
|
376
|
+
|
|
377
|
+
const readiness = createReadinessService();
|
|
378
|
+
const [snapshot] = await readiness.getReadiness("email");
|
|
379
|
+
|
|
380
|
+
expect(snapshot.localChecks).toContainEqual({
|
|
381
|
+
name: "ingress",
|
|
382
|
+
passed: false,
|
|
383
|
+
message: "Public ingress URL is not configured or disabled",
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
281
387
|
test("phone readiness accepts configured public ingress", async () => {
|
|
282
388
|
mockHasTwilioCredentials = true;
|
|
283
389
|
setConfig("twilio", { phoneNumber: "+15550123" });
|
|
@@ -33,8 +33,14 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
33
33
|
const originalFetch = globalThis.fetch;
|
|
34
34
|
const originalEnvCredential = process.env.ASSISTANT_API_KEY;
|
|
35
35
|
|
|
36
|
-
const {
|
|
37
|
-
|
|
36
|
+
const {
|
|
37
|
+
registerCallbackRoute,
|
|
38
|
+
resolveCallbackUrl,
|
|
39
|
+
resolvePlatformCallbackRegistrationContext,
|
|
40
|
+
} = await import("../inbound/platform-callback-registration.js");
|
|
41
|
+
|
|
42
|
+
const { PublicIngressDisabledError } =
|
|
43
|
+
await import("../inbound/public-ingress-urls.js");
|
|
38
44
|
|
|
39
45
|
describe("platform callback registration", () => {
|
|
40
46
|
beforeEach(() => {
|
|
@@ -150,3 +156,127 @@ describe("platform callback registration", () => {
|
|
|
150
156
|
).resolves.toBe("https://platform.example.com/v1/gateway/callbacks/x/");
|
|
151
157
|
});
|
|
152
158
|
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `resolveCallbackUrl` drives Twilio voice/status callbacks and OAuth redirect
|
|
162
|
+
* URIs. Its tier order has to match `handleWebhooksRegister` in
|
|
163
|
+
* `runtime/routes/webhook-routes.ts` and `hasWebhookRoutingConfigured` in
|
|
164
|
+
* `config/webhook-routing.ts` (LUM-2882).
|
|
165
|
+
*/
|
|
166
|
+
describe("resolveCallbackUrl resolution order", () => {
|
|
167
|
+
const PLATFORM_URL = "https://platform.example.com/v1/gateway/callbacks/x/";
|
|
168
|
+
|
|
169
|
+
/** Stand in for a URL builder that cannot resolve a public ingress URL. */
|
|
170
|
+
function noIngress(): string {
|
|
171
|
+
throw new Error(
|
|
172
|
+
"No public base URL configured. Set ingress.publicBaseUrl in config.",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Stand in for a URL builder reached while ingress is switched off. */
|
|
177
|
+
function ingressDisabled(): string {
|
|
178
|
+
throw new PublicIngressDisabledError();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function seedPlatformCredentials(): void {
|
|
182
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
|
|
183
|
+
"https://platform.example.com";
|
|
184
|
+
mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
|
|
185
|
+
"11111111-2222-4333-8444-555555555555";
|
|
186
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
187
|
+
"ast-managed-key";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let registerCalls: number;
|
|
191
|
+
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
mockIsPlatform = false;
|
|
194
|
+
mockPlatformBaseUrl = "";
|
|
195
|
+
mockPlatformAssistantId = "";
|
|
196
|
+
mockSecureKeys = {};
|
|
197
|
+
delete process.env.ASSISTANT_API_KEY;
|
|
198
|
+
registerCalls = 0;
|
|
199
|
+
globalThis.fetch = mock(async () => {
|
|
200
|
+
registerCalls++;
|
|
201
|
+
return new Response(
|
|
202
|
+
JSON.stringify({
|
|
203
|
+
callback_url: PLATFORM_URL,
|
|
204
|
+
callback_path: "webhooks/twilio/voice",
|
|
205
|
+
type: "twilio_voice",
|
|
206
|
+
assistant_id: "11111111-2222-4333-8444-555555555555",
|
|
207
|
+
}),
|
|
208
|
+
{ status: 201, headers: { "content-type": "application/json" } },
|
|
209
|
+
);
|
|
210
|
+
}) as unknown as typeof fetch;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
afterEach(() => {
|
|
214
|
+
globalThis.fetch = originalFetch;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("platform pods register with the platform gateway", async () => {
|
|
218
|
+
mockIsPlatform = true;
|
|
219
|
+
seedPlatformCredentials();
|
|
220
|
+
|
|
221
|
+
// The direct supplier is never evaluated on a pod: there is no ingress of
|
|
222
|
+
// its own to advertise, and evaluating it would throw.
|
|
223
|
+
await expect(
|
|
224
|
+
resolveCallbackUrl(noIngress, "webhooks/twilio/voice", "twilio_voice"),
|
|
225
|
+
).resolves.toBe(PLATFORM_URL);
|
|
226
|
+
expect(registerCalls).toBe(1);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a configured ingress wins over platform connectivity", async () => {
|
|
230
|
+
seedPlatformCredentials();
|
|
231
|
+
|
|
232
|
+
await expect(
|
|
233
|
+
resolveCallbackUrl(
|
|
234
|
+
() => "https://tunnel.example.com/webhooks/twilio/voice",
|
|
235
|
+
"webhooks/twilio/voice",
|
|
236
|
+
"twilio_voice",
|
|
237
|
+
),
|
|
238
|
+
).resolves.toBe("https://tunnel.example.com/webhooks/twilio/voice");
|
|
239
|
+
expect(registerCalls).toBe(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a platform-connected assistant with no ingress registers with the platform", async () => {
|
|
243
|
+
// LUM-2882: this used to return the direct builder's throw because the
|
|
244
|
+
// platform branch was gated on IS_PLATFORM, which is only true on a pod.
|
|
245
|
+
seedPlatformCredentials();
|
|
246
|
+
|
|
247
|
+
await expect(
|
|
248
|
+
resolveCallbackUrl(noIngress, "webhooks/twilio/voice", "twilio_voice"),
|
|
249
|
+
).resolves.toBe(PLATFORM_URL);
|
|
250
|
+
expect(registerCalls).toBe(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("query parameters are appended to the platform URL", async () => {
|
|
254
|
+
seedPlatformCredentials();
|
|
255
|
+
|
|
256
|
+
await expect(
|
|
257
|
+
resolveCallbackUrl(noIngress, "webhooks/twilio/voice", "twilio_voice", {
|
|
258
|
+
callSessionId: "conv-xyz",
|
|
259
|
+
}),
|
|
260
|
+
).resolves.toBe(`${PLATFORM_URL}?callSessionId=conv-xyz`);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("an explicit ingress opt-out is not routed around", async () => {
|
|
264
|
+
seedPlatformCredentials();
|
|
265
|
+
|
|
266
|
+
await expect(
|
|
267
|
+
resolveCallbackUrl(
|
|
268
|
+
ingressDisabled,
|
|
269
|
+
"webhooks/twilio/voice",
|
|
270
|
+
"twilio_voice",
|
|
271
|
+
),
|
|
272
|
+
).rejects.toThrow("Public ingress is disabled");
|
|
273
|
+
expect(registerCalls).toBe(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("without platform credentials the ingress error surfaces unchanged", async () => {
|
|
277
|
+
await expect(
|
|
278
|
+
resolveCallbackUrl(noIngress, "webhooks/twilio/voice", "twilio_voice"),
|
|
279
|
+
).rejects.toThrow("No public base URL configured");
|
|
280
|
+
expect(registerCalls).toBe(0);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -103,6 +103,12 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
103
103
|
"../../../../config/assistant-feature-flags.js",
|
|
104
104
|
"../../../../config/default-profile-catalog.js",
|
|
105
105
|
"../../../../config/loader.js",
|
|
106
|
+
// Sibling of the already-sanctioned memory-v3-gate: `memoryTier()` is
|
|
107
|
+
// defined as fully derived from that module's predicates, and the stats
|
|
108
|
+
// route reports both (`graph_supported` is exactly `tier === "v3"`).
|
|
109
|
+
// Re-deriving the tier inside the plugin would reintroduce exactly the
|
|
110
|
+
// drift the shared module exists to prevent. No plugin-api equivalent.
|
|
111
|
+
"../../../../config/memory-tier.js",
|
|
106
112
|
"../../../../config/memory-v3-gate.js",
|
|
107
113
|
"../../../../config/schema.js",
|
|
108
114
|
"../../../../config/schemas/memory-v2.js",
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
invalidateConfigCache,
|
|
5
|
+
loadRawConfig,
|
|
6
|
+
saveRawConfig,
|
|
7
|
+
setNestedValue,
|
|
8
|
+
} from "../config/loader.js";
|
|
3
9
|
import { credentialKey } from "../security/credential-key.js";
|
|
4
10
|
|
|
5
11
|
let secureKeyStore: Record<string, string> = {};
|
|
@@ -8,9 +14,28 @@ let oauthConnectionStore: Record<
|
|
|
8
14
|
{ id: string; status: string; accountInfo?: string | null }
|
|
9
15
|
> = {};
|
|
10
16
|
const syncCalls: Array<{ provider: string; accountInfo?: string }> = [];
|
|
17
|
+
let platformContextEnabled = false;
|
|
11
18
|
|
|
19
|
+
const registerCallbackRouteMock = mock(
|
|
20
|
+
async (callbackPath: string, _type: string) =>
|
|
21
|
+
`https://gateway.vellum.ai/assistant-123/${callbackPath}`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Spread the real module: it is a shared barrel and replacing it wholesale
|
|
25
|
+
// breaks unrelated importers pulled in by the module under test.
|
|
26
|
+
const actualPlatformCallbackRegistration =
|
|
27
|
+
await import("../inbound/platform-callback-registration.js");
|
|
12
28
|
mock.module("../inbound/platform-callback-registration.js", () => ({
|
|
13
|
-
|
|
29
|
+
...actualPlatformCallbackRegistration,
|
|
30
|
+
registerCallbackRoute: registerCallbackRouteMock,
|
|
31
|
+
resolvePlatformCallbackRegistrationContext: async () => ({
|
|
32
|
+
isPlatform: false,
|
|
33
|
+
platformBaseUrl: platformContextEnabled ? "https://api.vellum.ai" : "",
|
|
34
|
+
assistantId: platformContextEnabled ? "assistant-123" : "",
|
|
35
|
+
hasAssistantApiKey: platformContextEnabled,
|
|
36
|
+
authHeader: platformContextEnabled ? "Api-Key secret" : null,
|
|
37
|
+
enabled: platformContextEnabled,
|
|
38
|
+
}),
|
|
14
39
|
}));
|
|
15
40
|
|
|
16
41
|
mock.module("../daemon/handlers/shared.js", () => ({
|
|
@@ -77,13 +102,37 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
|
77
102
|
|
|
78
103
|
const originalFetch = globalThis.fetch;
|
|
79
104
|
|
|
80
|
-
|
|
105
|
+
const { getTelegramConfig, setTelegramConfig } =
|
|
106
|
+
await import("../daemon/handlers/config-telegram.js");
|
|
107
|
+
|
|
108
|
+
function mockTelegramApi(): typeof fetch {
|
|
109
|
+
return (async (input: RequestInfo | URL) => {
|
|
110
|
+
const url = String(input);
|
|
111
|
+
if (url.includes("/getMe")) {
|
|
112
|
+
return new Response(
|
|
113
|
+
JSON.stringify({ ok: true, result: { id: 42, username: "testbot" } }),
|
|
114
|
+
{ status: 200 },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
118
|
+
}) as typeof fetch;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setIngressPublicBaseUrl(url: string): void {
|
|
122
|
+
const raw = loadRawConfig();
|
|
123
|
+
setNestedValue(raw, "ingress.publicBaseUrl", url);
|
|
124
|
+
saveRawConfig(raw);
|
|
125
|
+
invalidateConfigCache();
|
|
126
|
+
}
|
|
81
127
|
|
|
82
128
|
describe("Telegram config handler", () => {
|
|
83
129
|
beforeEach(() => {
|
|
84
130
|
secureKeyStore = {};
|
|
85
131
|
oauthConnectionStore = {};
|
|
86
132
|
syncCalls.length = 0;
|
|
133
|
+
platformContextEnabled = false;
|
|
134
|
+
registerCallbackRouteMock.mockClear();
|
|
135
|
+
setIngressPublicBaseUrl("");
|
|
87
136
|
globalThis.fetch = originalFetch;
|
|
88
137
|
});
|
|
89
138
|
|
|
@@ -105,4 +154,49 @@ describe("Telegram config handler", () => {
|
|
|
105
154
|
]);
|
|
106
155
|
expect(oauthConnectionStore["telegram"]?.accountInfo).toBe("@testbot");
|
|
107
156
|
});
|
|
157
|
+
|
|
158
|
+
// A platform-connected local assistant (IS_PLATFORM unset, valid platform
|
|
159
|
+
// credentials, no public ingress) receives Telegram webhooks only through
|
|
160
|
+
// managed platform callbacks, so saving the bot token must register the
|
|
161
|
+
// route.
|
|
162
|
+
test("set registers the platform callback route for a platform-connected local assistant", async () => {
|
|
163
|
+
platformContextEnabled = true;
|
|
164
|
+
globalThis.fetch = mockTelegramApi();
|
|
165
|
+
|
|
166
|
+
const result = await setTelegramConfig(
|
|
167
|
+
"123456789:AAtesttoken_testtoken_testtoken_test",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(result.success).toBe(true);
|
|
171
|
+
expect(registerCallbackRouteMock).toHaveBeenCalledWith(
|
|
172
|
+
"webhooks/telegram",
|
|
173
|
+
"telegram",
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("set does not register a platform callback route when not platform-connected", async () => {
|
|
178
|
+
globalThis.fetch = mockTelegramApi();
|
|
179
|
+
|
|
180
|
+
const result = await setTelegramConfig(
|
|
181
|
+
"123456789:AAtesttoken_testtoken_testtoken_test",
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(result.success).toBe(true);
|
|
185
|
+
expect(registerCallbackRouteMock).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// A logged-in local assistant holds platform credentials for the LLM proxy,
|
|
189
|
+
// so credential presence must not override an explicitly configured ingress.
|
|
190
|
+
test("set does not register a platform callback route when a public ingress is configured", async () => {
|
|
191
|
+
platformContextEnabled = true;
|
|
192
|
+
setIngressPublicBaseUrl("https://abc.ngrok.io");
|
|
193
|
+
globalThis.fetch = mockTelegramApi();
|
|
194
|
+
|
|
195
|
+
const result = await setTelegramConfig(
|
|
196
|
+
"123456789:AAtesttoken_testtoken_testtoken_test",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(result.success).toBe(true);
|
|
200
|
+
expect(registerCallbackRouteMock).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
108
202
|
});
|