vellum 0.2.8 → 0.2.10
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/bun.lock +2 -2
- package/package.json +3 -2
- package/scripts/capture-x-graphql.ts +1 -18
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +110 -0
- package/src/__tests__/call-bridge.test.ts +40 -0
- package/src/__tests__/call-state.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +0 -6
- package/src/__tests__/forbidden-legacy-symbols.test.ts +71 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +95 -91
- package/src/__tests__/home-base-bootstrap.test.ts +13 -8
- package/src/__tests__/ingress-url-consistency.test.ts +214 -0
- package/src/__tests__/intent-routing.test.ts +2 -5
- package/src/__tests__/ipc-snapshot.test.ts +64 -14
- package/src/__tests__/oauth2-gateway-transport.test.ts +7 -1
- package/src/__tests__/onboarding-starter-tasks.test.ts +12 -2
- package/src/__tests__/prebuilt-home-base-seed.test.ts +9 -5
- package/src/__tests__/public-ingress-urls.test.ts +50 -34
- package/src/__tests__/relay-server.test.ts +55 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
- package/src/__tests__/runtime-events-sse.test.ts +162 -0
- package/src/__tests__/skills.test.ts +83 -0
- package/src/__tests__/system-prompt.test.ts +2 -24
- package/src/__tests__/twilio-provider.test.ts +37 -1
- package/src/__tests__/twilio-routes.test.ts +112 -4
- package/src/__tests__/twitter-auth-handler.test.ts +87 -2
- package/src/calls/call-domain.ts +8 -6
- package/src/calls/call-orchestrator.ts +25 -5
- package/src/calls/call-state.ts +23 -0
- package/src/calls/relay-server.ts +56 -1
- package/src/calls/twilio-config.ts +11 -16
- package/src/calls/twilio-provider.ts +6 -1
- package/src/calls/twilio-routes.ts +10 -1
- package/src/cli/core-commands.ts +12 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +57 -1
- package/src/config/bundled-skills/document/SKILL.md +11 -3
- package/src/config/bundled-skills/followups/icon.svg +24 -0
- package/src/config/bundled-skills/messaging/SKILL.md +7 -3
- package/src/config/bundled-skills/public-ingress/SKILL.md +183 -0
- package/src/config/bundled-skills/self-upgrade/SKILL.md +4 -10
- package/src/config/bundled-skills/tasks/TOOLS.json +25 -0
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
- package/src/config/defaults.ts +1 -2
- package/src/config/schema.ts +4 -11
- package/src/config/system-prompt.ts +64 -360
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +10 -5
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +9 -3
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -3
- package/src/daemon/handlers/config.ts +44 -50
- package/src/daemon/handlers/home-base.ts +3 -2
- package/src/daemon/handlers/identity.ts +127 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/shared.ts +1 -0
- package/src/daemon/handlers/subagents.ts +85 -2
- package/src/daemon/handlers/twitter-auth.ts +31 -2
- package/src/daemon/handlers/workspace-files.ts +75 -0
- package/src/daemon/ipc-contract-inventory.json +20 -8
- package/src/daemon/ipc-contract.ts +85 -21
- package/src/daemon/lifecycle.ts +9 -4
- package/src/daemon/server.ts +7 -0
- package/src/daemon/session-notifiers.ts +29 -0
- package/src/daemon/session-surfaces.ts +5 -2
- package/src/daemon/session-tool-setup.ts +16 -5
- package/src/home-base/bootstrap.ts +3 -1
- package/src/home-base/prebuilt/seed.ts +16 -5
- package/src/inbound/public-ingress-urls.ts +49 -32
- package/src/memory/db.ts +132 -5
- package/src/memory/llm-usage-store.ts +0 -1
- package/src/memory/runs-store.ts +51 -3
- package/src/memory/schema.ts +2 -2
- package/src/runtime/gateway-client.ts +7 -1
- package/src/runtime/http-server.ts +215 -27
- package/src/runtime/routes/channel-routes.ts +7 -2
- package/src/runtime/routes/events-routes.ts +79 -0
- package/src/runtime/routes/run-routes.ts +43 -0
- package/src/runtime/run-orchestrator.ts +64 -7
- package/src/security/oauth-callback-registry.ts +10 -0
- package/src/security/oauth2.ts +23 -131
- package/src/subagent/manager.ts +3 -1
- package/src/tools/browser/auto-navigate.ts +2 -2
- package/src/tools/browser/x-auto-navigate.ts +1 -1
- package/src/tools/claude-code/claude-code.ts +1 -1
- package/src/tools/system/version.ts +43 -0
- package/src/tools/tasks/work-item-run.ts +78 -0
- package/src/tools/terminal/parser.ts +29 -7
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +9 -2
- package/src/util/platform.ts +1 -1
- package/src/work-items/work-item-runner.ts +171 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -221
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +0 -162
- package/src/calls/twilio-webhook-urls.ts +0 -47
package/bun.lock
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@huggingface/transformers": "^3.8.1",
|
|
12
12
|
"@qdrant/js-client-rest": "^1.16.2",
|
|
13
13
|
"@sentry/node": "^10.38.0",
|
|
14
|
-
"@vellumai/cli": "0.1.
|
|
14
|
+
"@vellumai/cli": "0.1.10",
|
|
15
15
|
"@vellumai/vellum-gateway": "0.1.10",
|
|
16
16
|
"agentmail": "^0.1.0",
|
|
17
17
|
"archiver": "^7.0.1",
|
|
@@ -542,7 +542,7 @@
|
|
|
542
542
|
|
|
543
543
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="],
|
|
544
544
|
|
|
545
|
-
"@vellumai/cli": ["@vellumai/cli@0.1.
|
|
545
|
+
"@vellumai/cli": ["@vellumai/cli@0.1.10", "", { "dependencies": { "ink": "^6.7.0", "react": "^19.2.4" }, "bin": { "vellum-cli": "src/index.ts" } }, "sha512-KBnfQRlt5VAJX6JMMYTWXq3Poi3YfVXE/O+C2249W1j58fxzxoRRENLK9WXDIKCIHlQ3+HRDug7ZAcYaEXURRA=="],
|
|
546
546
|
|
|
547
547
|
"@vellumai/vellum-gateway": ["@vellumai/vellum-gateway@0.1.10", "", { "dependencies": { "file-type": "^21.3.0", "pino": "^9.6.0", "pino-pretty": "^13.1.3", "zod": "^4.3.6" } }, "sha512-a41fGexW8RpWL4RTfZ3EM+XJMvz7t26D1axu2xAtZioXW3ZWMLGuogHnIJsgglzESl49E6VmmUsUGeD+dseV2w=="],
|
|
548
548
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vellum",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vellum": "./src/index.ts"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"dev": "bun run src/index.ts",
|
|
10
|
+
"daemon:restart:http": "RUNTIME_HTTP_PORT=7821 bun run src/index.ts daemon restart",
|
|
10
11
|
"db:generate": "drizzle-kit generate",
|
|
11
12
|
"db:push": "drizzle-kit push",
|
|
12
13
|
"ipc:inventory": "bun run scripts/ipc/check-contract-inventory.ts",
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
"@huggingface/transformers": "^3.8.1",
|
|
29
30
|
"@qdrant/js-client-rest": "^1.16.2",
|
|
30
31
|
"@sentry/node": "^10.38.0",
|
|
31
|
-
"@vellumai/cli": "0.1.
|
|
32
|
+
"@vellumai/cli": "0.1.10",
|
|
32
33
|
"@vellumai/vellum-gateway": "0.1.10",
|
|
33
34
|
"agentmail": "^0.1.0",
|
|
34
35
|
"archiver": "^7.0.1",
|
|
@@ -63,7 +63,7 @@ class CDPClient {
|
|
|
63
63
|
const msg = JSON.parse(String(event.data));
|
|
64
64
|
if (msg.id != null) {
|
|
65
65
|
const cb = this.callbacks.get(msg.id);
|
|
66
|
-
if (cb) { this.callbacks.delete(msg.id); msg.error
|
|
66
|
+
if (cb) { this.callbacks.delete(msg.id); if (msg.error) { cb.reject(new Error(msg.error.message)); } else { cb.resolve(msg.result); } }
|
|
67
67
|
} else if (msg.method) {
|
|
68
68
|
for (const h of this.eventHandlers.get(msg.method) ?? []) h(msg.params ?? {});
|
|
69
69
|
}
|
|
@@ -216,20 +216,6 @@ function notifyQuerySeen(queryName: string) {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function waitForQuery(queryName: string, timeoutMs = 15000): Promise<boolean> {
|
|
220
|
-
if (seenQueries.has(queryName)) return Promise.resolve(true);
|
|
221
|
-
return new Promise(resolve => {
|
|
222
|
-
const timer = setTimeout(() => {
|
|
223
|
-
queryWaiters.delete(queryName);
|
|
224
|
-
resolve(false);
|
|
225
|
-
}, timeoutMs);
|
|
226
|
-
queryWaiters.set(queryName, () => {
|
|
227
|
-
clearTimeout(timer);
|
|
228
|
-
resolve(true);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
219
|
function waitForAnyQuery(queryNames: string[], timeoutMs = 15000): Promise<boolean> {
|
|
234
220
|
if (queryNames.some(q => seenQueries.has(q))) return Promise.resolve(true);
|
|
235
221
|
return new Promise(resolve => {
|
|
@@ -251,7 +237,6 @@ function waitForAnyQuery(queryNames: string[], timeoutMs = 15000): Promise<boole
|
|
|
251
237
|
|
|
252
238
|
// We'll keep a reference to one client that's on an x.com tab for navigation
|
|
253
239
|
let navigationClient: CDPClient | null = null;
|
|
254
|
-
let navigationWsUrl: string | null = null;
|
|
255
240
|
|
|
256
241
|
for (const page of pages) {
|
|
257
242
|
const client = new CDPClient();
|
|
@@ -261,7 +246,6 @@ for (const page of pages) {
|
|
|
261
246
|
// Track which client is on an x.com tab for navigation
|
|
262
247
|
if (page.url.includes('x.com') || page.url.includes('twitter.com')) {
|
|
263
248
|
navigationClient = client;
|
|
264
|
-
navigationWsUrl = page.webSocketDebuggerUrl;
|
|
265
249
|
}
|
|
266
250
|
|
|
267
251
|
client.on('Network.requestWillBeSent', (params) => {
|
|
@@ -347,7 +331,6 @@ for (const page of pages) {
|
|
|
347
331
|
if (!navigationClient && pages.length > 0) {
|
|
348
332
|
navigationClient = new CDPClient();
|
|
349
333
|
await navigationClient.connect(pages[0].webSocketDebuggerUrl);
|
|
350
|
-
navigationWsUrl = pages[0].webSocketDebuggerUrl;
|
|
351
334
|
}
|
|
352
335
|
|
|
353
336
|
// ─── CDP navigation helpers ──────────────────────────────────────────────────
|
|
@@ -530,6 +530,13 @@ exports[`IPC message snapshots ClientMessage types slack_webhook_config serializ
|
|
|
530
530
|
}
|
|
531
531
|
`;
|
|
532
532
|
|
|
533
|
+
exports[`IPC message snapshots ClientMessage types ingress_config serializes to expected JSON 1`] = `
|
|
534
|
+
{
|
|
535
|
+
"action": "get",
|
|
536
|
+
"type": "ingress_config",
|
|
537
|
+
}
|
|
538
|
+
`;
|
|
539
|
+
|
|
533
540
|
exports[`IPC message snapshots ClientMessage types vercel_api_config serializes to expected JSON 1`] = `
|
|
534
541
|
{
|
|
535
542
|
"action": "get",
|
|
@@ -796,6 +803,33 @@ exports[`IPC message snapshots ClientMessage types subagent_message serializes t
|
|
|
796
803
|
}
|
|
797
804
|
`;
|
|
798
805
|
|
|
806
|
+
exports[`IPC message snapshots ClientMessage types subagent_detail_request serializes to expected JSON 1`] = `
|
|
807
|
+
{
|
|
808
|
+
"conversationId": "conv-001",
|
|
809
|
+
"subagentId": "sub-001",
|
|
810
|
+
"type": "subagent_detail_request",
|
|
811
|
+
}
|
|
812
|
+
`;
|
|
813
|
+
|
|
814
|
+
exports[`IPC message snapshots ClientMessage types workspace_files_list serializes to expected JSON 1`] = `
|
|
815
|
+
{
|
|
816
|
+
"type": "workspace_files_list",
|
|
817
|
+
}
|
|
818
|
+
`;
|
|
819
|
+
|
|
820
|
+
exports[`IPC message snapshots ClientMessage types workspace_file_read serializes to expected JSON 1`] = `
|
|
821
|
+
{
|
|
822
|
+
"path": "IDENTITY.md",
|
|
823
|
+
"type": "workspace_file_read",
|
|
824
|
+
}
|
|
825
|
+
`;
|
|
826
|
+
|
|
827
|
+
exports[`IPC message snapshots ClientMessage types identity_get serializes to expected JSON 1`] = `
|
|
828
|
+
{
|
|
829
|
+
"type": "identity_get",
|
|
830
|
+
}
|
|
831
|
+
`;
|
|
832
|
+
|
|
799
833
|
exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
|
|
800
834
|
{
|
|
801
835
|
"success": true,
|
|
@@ -1448,6 +1482,14 @@ exports[`IPC message snapshots ServerMessage types watcher_escalation serializes
|
|
|
1448
1482
|
}
|
|
1449
1483
|
`;
|
|
1450
1484
|
|
|
1485
|
+
exports[`IPC message snapshots ServerMessage types agent_heartbeat_alert serializes to expected JSON 1`] = `
|
|
1486
|
+
{
|
|
1487
|
+
"body": "No activity detected in the last 60 minutes.",
|
|
1488
|
+
"title": "Agent heartbeat stalled",
|
|
1489
|
+
"type": "agent_heartbeat_alert",
|
|
1490
|
+
}
|
|
1491
|
+
`;
|
|
1492
|
+
|
|
1451
1493
|
exports[`IPC message snapshots ServerMessage types watch_started serializes to expected JSON 1`] = `
|
|
1452
1494
|
{
|
|
1453
1495
|
"durationSeconds": 300,
|
|
@@ -1770,6 +1812,16 @@ exports[`IPC message snapshots ServerMessage types slack_webhook_config_response
|
|
|
1770
1812
|
}
|
|
1771
1813
|
`;
|
|
1772
1814
|
|
|
1815
|
+
exports[`IPC message snapshots ServerMessage types ingress_config_response serializes to expected JSON 1`] = `
|
|
1816
|
+
{
|
|
1817
|
+
"enabled": true,
|
|
1818
|
+
"localGatewayTarget": "http://127.0.0.1:7830",
|
|
1819
|
+
"publicBaseUrl": "https://example.com",
|
|
1820
|
+
"success": true,
|
|
1821
|
+
"type": "ingress_config_response",
|
|
1822
|
+
}
|
|
1823
|
+
`;
|
|
1824
|
+
|
|
1773
1825
|
exports[`IPC message snapshots ServerMessage types vercel_api_config_response serializes to expected JSON 1`] = `
|
|
1774
1826
|
{
|
|
1775
1827
|
"hasToken": true,
|
|
@@ -2175,6 +2227,15 @@ exports[`IPC message snapshots ServerMessage types open_tasks_window serializes
|
|
|
2175
2227
|
}
|
|
2176
2228
|
`;
|
|
2177
2229
|
|
|
2230
|
+
exports[`IPC message snapshots ServerMessage types task_run_thread_created serializes to expected JSON 1`] = `
|
|
2231
|
+
{
|
|
2232
|
+
"conversationId": "conv-task-run-001",
|
|
2233
|
+
"title": "Process report",
|
|
2234
|
+
"type": "task_run_thread_created",
|
|
2235
|
+
"workItemId": "wi-001",
|
|
2236
|
+
}
|
|
2237
|
+
`;
|
|
2238
|
+
|
|
2178
2239
|
exports[`IPC message snapshots ServerMessage types subagent_spawned serializes to expected JSON 1`] = `
|
|
2179
2240
|
{
|
|
2180
2241
|
"label": "Research Agent",
|
|
@@ -2204,3 +2265,52 @@ exports[`IPC message snapshots ServerMessage types subagent_event serializes to
|
|
|
2204
2265
|
"type": "subagent_event",
|
|
2205
2266
|
}
|
|
2206
2267
|
`;
|
|
2268
|
+
|
|
2269
|
+
exports[`IPC message snapshots ServerMessage types subagent_detail_response serializes to expected JSON 1`] = `
|
|
2270
|
+
{
|
|
2271
|
+
"events": [
|
|
2272
|
+
{
|
|
2273
|
+
"content": "Reading file...",
|
|
2274
|
+
"isError": false,
|
|
2275
|
+
"toolName": "read_file",
|
|
2276
|
+
"type": "tool_use",
|
|
2277
|
+
},
|
|
2278
|
+
],
|
|
2279
|
+
"objective": "Search for documentation",
|
|
2280
|
+
"subagentId": "sub-001",
|
|
2281
|
+
"type": "subagent_detail_response",
|
|
2282
|
+
}
|
|
2283
|
+
`;
|
|
2284
|
+
|
|
2285
|
+
exports[`IPC message snapshots ServerMessage types workspace_files_list_response serializes to expected JSON 1`] = `
|
|
2286
|
+
{
|
|
2287
|
+
"files": [
|
|
2288
|
+
{
|
|
2289
|
+
"exists": true,
|
|
2290
|
+
"name": "IDENTITY.md",
|
|
2291
|
+
"path": "IDENTITY.md",
|
|
2292
|
+
},
|
|
2293
|
+
],
|
|
2294
|
+
"type": "workspace_files_list_response",
|
|
2295
|
+
}
|
|
2296
|
+
`;
|
|
2297
|
+
|
|
2298
|
+
exports[`IPC message snapshots ServerMessage types workspace_file_read_response serializes to expected JSON 1`] = `
|
|
2299
|
+
{
|
|
2300
|
+
"content": "# My Identity",
|
|
2301
|
+
"path": "IDENTITY.md",
|
|
2302
|
+
"type": "workspace_file_read_response",
|
|
2303
|
+
}
|
|
2304
|
+
`;
|
|
2305
|
+
|
|
2306
|
+
exports[`IPC message snapshots ServerMessage types identity_get_response serializes to expected JSON 1`] = `
|
|
2307
|
+
{
|
|
2308
|
+
"emoji": "✨",
|
|
2309
|
+
"found": true,
|
|
2310
|
+
"home": "~/workspace",
|
|
2311
|
+
"name": "Vex",
|
|
2312
|
+
"personality": "Friendly",
|
|
2313
|
+
"role": "AI assistant",
|
|
2314
|
+
"type": "identity_get_response",
|
|
2315
|
+
}
|
|
2316
|
+
`;
|
|
@@ -92,6 +92,9 @@ import {
|
|
|
92
92
|
import {
|
|
93
93
|
registerCallQuestionNotifier,
|
|
94
94
|
unregisterCallQuestionNotifier,
|
|
95
|
+
registerCallTranscriptNotifier,
|
|
96
|
+
unregisterCallTranscriptNotifier,
|
|
97
|
+
fireCallTranscriptNotifier,
|
|
95
98
|
registerCallCompletionNotifier,
|
|
96
99
|
unregisterCallCompletionNotifier,
|
|
97
100
|
fireCallQuestionNotifier,
|
|
@@ -335,6 +338,43 @@ describe('call-bridge', () => {
|
|
|
335
338
|
unregisterCallQuestionNotifier('conv-notifier-q');
|
|
336
339
|
});
|
|
337
340
|
|
|
341
|
+
// ── Call transcript notifier ─────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
test('call transcript notifier persists transcript line and emits events', () => {
|
|
344
|
+
ensureConversation('conv-notifier-t');
|
|
345
|
+
|
|
346
|
+
const emittedEvents: Array<{ type: string; text?: string }> = [];
|
|
347
|
+
const sendToClient = (msg: { type: string; text?: string }) => {
|
|
348
|
+
emittedEvents.push(msg);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
registerCallTranscriptNotifier('conv-notifier-t', (_callSessionId: string, speaker: 'caller' | 'assistant', text: string) => {
|
|
352
|
+
const speakerLabel = speaker === 'caller' ? 'Caller' : 'Assistant';
|
|
353
|
+
const transcriptText = `**Live call transcript**\n${speakerLabel}: ${text}`;
|
|
354
|
+
conversationStore.addMessage(
|
|
355
|
+
'conv-notifier-t',
|
|
356
|
+
'assistant',
|
|
357
|
+
JSON.stringify([{ type: 'text', text: transcriptText }]),
|
|
358
|
+
);
|
|
359
|
+
sendToClient({ type: 'assistant_text_delta', text: transcriptText });
|
|
360
|
+
sendToClient({ type: 'message_complete' });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
fireCallTranscriptNotifier('conv-notifier-t', 'call-session-1', 'caller', 'Can you confirm the appointment?');
|
|
364
|
+
|
|
365
|
+
const msgs = getMessagesForConversation('conv-notifier-t');
|
|
366
|
+
expect(msgs.length).toBe(1);
|
|
367
|
+
expect(msgs[0].role).toBe('assistant');
|
|
368
|
+
expect(msgs[0].content).toContain('Caller: Can you confirm the appointment?');
|
|
369
|
+
|
|
370
|
+
expect(emittedEvents.length).toBe(2);
|
|
371
|
+
expect(emittedEvents[0].type).toBe('assistant_text_delta');
|
|
372
|
+
expect(emittedEvents[0].text).toContain('Live call transcript');
|
|
373
|
+
expect(emittedEvents[1].type).toBe('message_complete');
|
|
374
|
+
|
|
375
|
+
unregisterCallTranscriptNotifier('conv-notifier-t');
|
|
376
|
+
});
|
|
377
|
+
|
|
338
378
|
// ── Call completion notifier ────────────────────────────────────
|
|
339
379
|
|
|
340
380
|
test('call completion notifier persists summary and emits events', () => {
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
registerCallQuestionNotifier,
|
|
11
11
|
unregisterCallQuestionNotifier,
|
|
12
12
|
fireCallQuestionNotifier,
|
|
13
|
+
registerCallTranscriptNotifier,
|
|
14
|
+
unregisterCallTranscriptNotifier,
|
|
15
|
+
fireCallTranscriptNotifier,
|
|
13
16
|
registerCallCompletionNotifier,
|
|
14
17
|
unregisterCallCompletionNotifier,
|
|
15
18
|
fireCallCompletionNotifier,
|
|
@@ -23,6 +26,7 @@ describe('call-state', () => {
|
|
|
23
26
|
// Clean up notifiers between tests
|
|
24
27
|
beforeEach(() => {
|
|
25
28
|
unregisterCallQuestionNotifier('test-conv');
|
|
29
|
+
unregisterCallTranscriptNotifier('test-conv');
|
|
26
30
|
unregisterCallCompletionNotifier('test-conv');
|
|
27
31
|
unregisterCallOrchestrator('test-session');
|
|
28
32
|
});
|
|
@@ -62,6 +66,43 @@ describe('call-state', () => {
|
|
|
62
66
|
fireCallQuestionNotifier('unregistered-conv', 'session-1', 'question');
|
|
63
67
|
});
|
|
64
68
|
|
|
69
|
+
// ── Transcript notifiers ──────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
test('registerCallTranscriptNotifier + fireCallTranscriptNotifier: callback receives args', () => {
|
|
72
|
+
let receivedSessionId = '';
|
|
73
|
+
let receivedSpeaker = '';
|
|
74
|
+
let receivedText = '';
|
|
75
|
+
|
|
76
|
+
registerCallTranscriptNotifier('test-conv', (callSessionId, speaker, text) => {
|
|
77
|
+
receivedSessionId = callSessionId;
|
|
78
|
+
receivedSpeaker = speaker;
|
|
79
|
+
receivedText = text;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
fireCallTranscriptNotifier('test-conv', 'session-321', 'caller', 'Hello from caller');
|
|
83
|
+
|
|
84
|
+
expect(receivedSessionId).toBe('session-321');
|
|
85
|
+
expect(receivedSpeaker).toBe('caller');
|
|
86
|
+
expect(receivedText).toBe('Hello from caller');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('unregisterCallTranscriptNotifier: fire after unregister does nothing', () => {
|
|
90
|
+
let called = false;
|
|
91
|
+
|
|
92
|
+
registerCallTranscriptNotifier('test-conv', () => {
|
|
93
|
+
called = true;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
unregisterCallTranscriptNotifier('test-conv');
|
|
97
|
+
fireCallTranscriptNotifier('test-conv', 'session-321', 'assistant', 'Test');
|
|
98
|
+
|
|
99
|
+
expect(called).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('fireCallTranscriptNotifier does nothing when no notifier is registered', () => {
|
|
103
|
+
fireCallTranscriptNotifier('unregistered-conv', 'session-1', 'caller', 'text');
|
|
104
|
+
});
|
|
105
|
+
|
|
65
106
|
// ── Completion notifiers ──────────────────────────────────────────
|
|
66
107
|
|
|
67
108
|
test('registerCallCompletionNotifier + fireCallCompletionNotifier: callback receives callSessionId', () => {
|
|
@@ -646,7 +646,6 @@ describe('AssistantConfigSchema', () => {
|
|
|
646
646
|
expect(result.calls).toEqual({
|
|
647
647
|
enabled: true,
|
|
648
648
|
provider: 'twilio',
|
|
649
|
-
webhookBaseUrl: '',
|
|
650
649
|
maxDurationSeconds: 3600,
|
|
651
650
|
userConsultTimeoutSeconds: 120,
|
|
652
651
|
disclosure: {
|
|
@@ -659,11 +658,6 @@ describe('AssistantConfigSchema', () => {
|
|
|
659
658
|
});
|
|
660
659
|
});
|
|
661
660
|
|
|
662
|
-
test('calls.webhookBaseUrl defaults to empty string', () => {
|
|
663
|
-
const result = AssistantConfigSchema.parse({});
|
|
664
|
-
expect(result.calls.webhookBaseUrl).toBe('');
|
|
665
|
-
});
|
|
666
|
-
|
|
667
661
|
test('accepts valid calls config overrides', () => {
|
|
668
662
|
const result = AssistantConfigSchema.parse({
|
|
669
663
|
calls: {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Guard test: fail if any legacy Twilio ingress symbols reappear in
|
|
7
|
+
* production source code, docs, configs, or scripts.
|
|
8
|
+
*
|
|
9
|
+
* Context: As part of the gateway-only ingress migration (#5948, #6000),
|
|
10
|
+
* all Twilio webhook configuration was consolidated into the gateway service.
|
|
11
|
+
* The assistant no longer manages its own Twilio webhook URLs — the gateway
|
|
12
|
+
* is the single ingress point for all telephony webhooks. Re-introducing
|
|
13
|
+
* these symbols in the assistant would bypass that architecture and create
|
|
14
|
+
* a split-brain ingress problem.
|
|
15
|
+
*
|
|
16
|
+
* Forbidden symbols:
|
|
17
|
+
* - legacy uppercase Twilio webhook base env var
|
|
18
|
+
* - twilioWebhookBaseUrl
|
|
19
|
+
* - twilio_webhook_config
|
|
20
|
+
* - calls.webhookBaseUrl
|
|
21
|
+
*
|
|
22
|
+
* Excluded directories:
|
|
23
|
+
* - node_modules — third-party code, not under our control
|
|
24
|
+
* - __tests__ — test files (including this guard test) reference the
|
|
25
|
+
* symbols in grep patterns and assertions
|
|
26
|
+
* - .private — local-only developer notes and scratch files
|
|
27
|
+
*/
|
|
28
|
+
describe('forbidden legacy symbols', () => {
|
|
29
|
+
test('no production code references removed Twilio ingress symbols', () => {
|
|
30
|
+
const legacyEnvVar = ['TWILIO', 'WEBHOOK', 'BASE', 'URL'].join('_');
|
|
31
|
+
const forbiddenSymbols = [
|
|
32
|
+
legacyEnvVar,
|
|
33
|
+
'twilioWebhookBaseUrl',
|
|
34
|
+
'twilio_webhook_config',
|
|
35
|
+
'calls.webhookBaseUrl',
|
|
36
|
+
];
|
|
37
|
+
const escapedPattern = forbiddenSymbols
|
|
38
|
+
.map((symbol) => symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
39
|
+
.join('|');
|
|
40
|
+
|
|
41
|
+
const repoRoot = resolve(__dirname, '..', '..', '..');
|
|
42
|
+
let matches = '';
|
|
43
|
+
try {
|
|
44
|
+
// Use git grep so only tracked files are searched. This automatically
|
|
45
|
+
// excludes untracked local .env files while still scanning committed
|
|
46
|
+
// environment templates like .env.example.
|
|
47
|
+
matches = execSync(
|
|
48
|
+
`git grep -rn -E "${escapedPattern}" --` +
|
|
49
|
+
' "*.ts" "*.tsx" "*.js" "*.mjs" "*.swift"' +
|
|
50
|
+
' "*.json" "*.md" "*.yml" "*.yaml"' +
|
|
51
|
+
' "*.sh" "*.env" "*.env.*"' +
|
|
52
|
+
' ":!node_modules" ":!*/__tests__/*" ":!.private"',
|
|
53
|
+
{ cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
54
|
+
);
|
|
55
|
+
} catch (err: unknown) {
|
|
56
|
+
// grep exits with code 1 when no matches are found — that is the expected (passing) case
|
|
57
|
+
const exitCode = (err as { status?: number }).status;
|
|
58
|
+
if (exitCode === 1) {
|
|
59
|
+
// No matches found — test passes
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Any other error is unexpected
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If we reach here, grep found matches (exit code 0) — fail the test
|
|
67
|
+
expect(matches.trim()).toBe(
|
|
68
|
+
'', // should be empty — if not, the matched lines appear in the failure message
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|