@vellumai/assistant 0.4.22 → 0.4.25
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 +3 -0
- package/package.json +2 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +55 -44
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -90
- package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
- package/src/__tests__/config-schema.test.ts +38 -178
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
- package/src/__tests__/credential-security-invariants.test.ts +0 -2
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
- package/src/__tests__/headless-browser-interactions.test.ts +0 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -63
- package/src/__tests__/onboarding-template-contract.test.ts +10 -20
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/resolve-guardian-trust-class.test.ts +61 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
- package/src/__tests__/runtime-events-sse.test.ts +7 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -4
- package/src/__tests__/session-runtime-assembly.test.ts +34 -8
- package/src/__tests__/system-prompt.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
- package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/voice-quality.test.ts +21 -132
- package/src/calls/relay-server.ts +11 -5
- package/src/calls/twilio-routes.ts +4 -38
- package/src/calls/voice-quality.ts +7 -63
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +143 -82
- package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
- package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
- package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
- package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
- package/src/config/calls-schema.ts +3 -53
- package/src/config/elevenlabs-schema.ts +33 -0
- package/src/config/schema.ts +183 -137
- package/src/config/types.ts +0 -1
- package/src/daemon/daemon-control.ts +3 -0
- package/src/daemon/handlers/browser.ts +2 -53
- package/src/daemon/ipc-contract/browser.ts +5 -84
- package/src/daemon/ipc-contract/surfaces.ts +51 -48
- package/src/daemon/ipc-contract-inventory.json +0 -9
- package/src/daemon/session-agent-loop-handlers.ts +3 -0
- package/src/daemon/session-agent-loop.ts +2 -1
- package/src/daemon/session-runtime-assembly.ts +9 -7
- package/src/daemon/session-tool-setup.ts +27 -13
- package/src/mcp/client.ts +2 -1
- package/src/memory/conversation-crud.ts +339 -166
- package/src/memory/migrations/102-alter-table-columns.ts +254 -37
- package/src/memory/schema.ts +1227 -1035
- package/src/runtime/routes/events-routes.ts +7 -0
- package/src/runtime/routes/inbound-message-handler.ts +3 -4
- package/src/schedule/scheduler.ts +159 -45
- package/src/security/secure-keys.ts +3 -3
- package/src/tools/browser/browser-execution.ts +314 -331
- package/src/tools/browser/browser-handoff.ts +11 -37
- package/src/tools/browser/browser-manager.ts +203 -352
- package/src/tools/browser/browser-screencast.ts +15 -76
- package/src/tools/network/script-proxy/certs.ts +7 -237
- package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
- package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
- package/src/tools/network/script-proxy/logging.ts +12 -196
- package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
- package/src/tools/network/script-proxy/policy.ts +4 -152
- package/src/tools/network/script-proxy/router.ts +2 -60
- package/src/tools/network/script-proxy/server.ts +5 -137
- package/src/tools/network/script-proxy/types.ts +19 -125
- package/src/tools/system/voice-config.ts +23 -1
- package/src/util/logger.ts +4 -1
- package/src/__tests__/elevenlabs-config.test.ts +0 -95
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
- package/src/calls/elevenlabs-config.ts +0 -32
|
@@ -84,7 +84,10 @@ mock.module("../runtime/guardian-context-resolver.js", () => ({
|
|
|
84
84
|
trustClass: "guardian",
|
|
85
85
|
sourceChannel: "vellum",
|
|
86
86
|
}),
|
|
87
|
-
toGuardianRuntimeContext: (ctx: unknown) =>
|
|
87
|
+
toGuardianRuntimeContext: (sourceChannel: unknown, ctx: unknown) => ({
|
|
88
|
+
...(ctx as Record<string, unknown>),
|
|
89
|
+
sourceChannel,
|
|
90
|
+
}),
|
|
88
91
|
}));
|
|
89
92
|
|
|
90
93
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
@@ -226,7 +226,6 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
226
226
|
"tools/network/script-proxy/session-manager.ts", // proxy credential injection at runtime
|
|
227
227
|
"messaging/registry.ts", // checks stored credentials for connected providers
|
|
228
228
|
"calls/call-domain.ts", // caller identity resolution (user phone number lookup)
|
|
229
|
-
"calls/elevenlabs-config.ts", // ElevenLabs voice quality API key lookup
|
|
230
229
|
"calls/twilio-config.ts", // call infrastructure credential lookup
|
|
231
230
|
"calls/twilio-provider.ts", // call infrastructure credential lookup
|
|
232
231
|
"calls/twilio-rest.ts", // Twilio REST API credential lookup
|
|
@@ -234,7 +233,6 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
234
233
|
"runtime/http-server.ts", // HTTP server credential lookup
|
|
235
234
|
"daemon/handlers/twitter-auth.ts", // Twitter OAuth token storage
|
|
236
235
|
"twitter/oauth-client.ts", // Twitter OAuth API client (reads access token for API calls)
|
|
237
|
-
"calls/elevenlabs-config.ts", // ElevenLabs credential lookup
|
|
238
236
|
"cli/config-commands.ts", // CLI config management
|
|
239
237
|
"messaging/providers/telegram-bot/adapter.ts", // Telegram bot token lookup for connectivity check
|
|
240
238
|
"messaging/providers/sms/adapter.ts", // Twilio credential lookup for SMS connectivity check
|
|
@@ -119,13 +119,13 @@ describe("guardian-verify-setup skill — voice auto-followup", () => {
|
|
|
119
119
|
expect(pollingSection).toContain("Non-rebind flows");
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
test("polling is voice-only — does not apply to
|
|
122
|
+
test("polling is voice-only — does not apply to Telegram", () => {
|
|
123
123
|
const pollingSection =
|
|
124
124
|
skillContent
|
|
125
125
|
.split("## Voice Auto-Check Polling")[1]
|
|
126
126
|
?.split("## Step 6")[0] ?? "";
|
|
127
127
|
expect(pollingSection).toContain("voice-only");
|
|
128
|
-
expect(pollingSection).toContain("Do NOT poll for
|
|
128
|
+
expect(pollingSection).toContain("Do NOT poll for Telegram");
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
test('no instruction requires waiting for user to ask "did it work?"', () => {
|
|
@@ -64,10 +64,6 @@ mock.module("../tools/browser/browser-screencast.js", () => ({
|
|
|
64
64
|
stopBrowserScreencast: async () => {},
|
|
65
65
|
stopAllScreencasts: async () => {},
|
|
66
66
|
ensureScreencast: async () => {},
|
|
67
|
-
updateBrowserStatus: () => {},
|
|
68
|
-
updatePagesList: async () => {},
|
|
69
|
-
getElementBounds: async () => null,
|
|
70
|
-
updateHighlights: () => {},
|
|
71
67
|
}));
|
|
72
68
|
|
|
73
69
|
import {
|
|
@@ -476,39 +476,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
476
476
|
service: "gmail",
|
|
477
477
|
requestedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
|
|
478
478
|
},
|
|
479
|
-
browser_cdp_response: {
|
|
480
|
-
type: "browser_cdp_response",
|
|
481
|
-
sessionId: "test-session",
|
|
482
|
-
success: true,
|
|
483
|
-
},
|
|
484
|
-
browser_user_click: {
|
|
485
|
-
type: "browser_user_click",
|
|
486
|
-
sessionId: "test-session",
|
|
487
|
-
surfaceId: "test-surface",
|
|
488
|
-
x: 100,
|
|
489
|
-
y: 200,
|
|
490
|
-
},
|
|
491
|
-
browser_user_scroll: {
|
|
492
|
-
type: "browser_user_scroll",
|
|
493
|
-
sessionId: "test-session",
|
|
494
|
-
surfaceId: "test-surface",
|
|
495
|
-
deltaX: 0,
|
|
496
|
-
deltaY: -100,
|
|
497
|
-
x: 100,
|
|
498
|
-
y: 200,
|
|
499
|
-
},
|
|
500
|
-
browser_user_keypress: {
|
|
501
|
-
type: "browser_user_keypress",
|
|
502
|
-
sessionId: "test-session",
|
|
503
|
-
surfaceId: "test-surface",
|
|
504
|
-
key: "Enter",
|
|
505
|
-
},
|
|
506
|
-
browser_interactive_mode: {
|
|
507
|
-
type: "browser_interactive_mode",
|
|
508
|
-
sessionId: "test-session",
|
|
509
|
-
surfaceId: "test-surface",
|
|
510
|
-
enabled: true,
|
|
511
|
-
},
|
|
512
479
|
work_items_list: {
|
|
513
480
|
type: "work_items_list",
|
|
514
481
|
status: "queued",
|
|
@@ -1590,19 +1557,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1590
1557
|
type: "app_files_changed",
|
|
1591
1558
|
appId: "app-001",
|
|
1592
1559
|
},
|
|
1593
|
-
browser_frame: {
|
|
1594
|
-
type: "browser_frame",
|
|
1595
|
-
sessionId: "sess-001",
|
|
1596
|
-
surfaceId: "surface-001",
|
|
1597
|
-
frame: "base64-jpeg-data",
|
|
1598
|
-
metadata: {
|
|
1599
|
-
offsetTop: 0,
|
|
1600
|
-
pageScaleFactor: 1,
|
|
1601
|
-
scrollOffsetX: 0,
|
|
1602
|
-
scrollOffsetY: 0,
|
|
1603
|
-
timestamp: 1700000000,
|
|
1604
|
-
},
|
|
1605
|
-
},
|
|
1606
1560
|
diagnostics_export_response: {
|
|
1607
1561
|
type: "diagnostics_export_response",
|
|
1608
1562
|
success: true,
|
|
@@ -1633,23 +1587,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1633
1587
|
grantedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
|
|
1634
1588
|
accountInfo: "user@example.com",
|
|
1635
1589
|
},
|
|
1636
|
-
browser_cdp_request: {
|
|
1637
|
-
type: "browser_cdp_request",
|
|
1638
|
-
sessionId: "test-session",
|
|
1639
|
-
},
|
|
1640
|
-
browser_interactive_mode_changed: {
|
|
1641
|
-
type: "browser_interactive_mode_changed",
|
|
1642
|
-
sessionId: "test-session",
|
|
1643
|
-
surfaceId: "test-surface",
|
|
1644
|
-
enabled: true,
|
|
1645
|
-
},
|
|
1646
|
-
browser_handoff_request: {
|
|
1647
|
-
type: "browser_handoff_request",
|
|
1648
|
-
sessionId: "test-session",
|
|
1649
|
-
surfaceId: "test-surface",
|
|
1650
|
-
reason: "auth" as const,
|
|
1651
|
-
message: "Login required",
|
|
1652
|
-
},
|
|
1653
1590
|
document_editor_show: {
|
|
1654
1591
|
type: "document_editor_show",
|
|
1655
1592
|
sessionId: "sess-001",
|
|
@@ -14,11 +14,11 @@ describe("onboarding template contracts", () => {
|
|
|
14
14
|
expect(lower).toContain("who am i");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
test("infers personality
|
|
17
|
+
test("infers personality organically instead of asking directly", () => {
|
|
18
18
|
const lower = bootstrap.toLowerCase();
|
|
19
|
-
// Personality step must instruct
|
|
19
|
+
// Personality step must instruct organic discovery via conversation
|
|
20
20
|
expect(lower).toContain("personality");
|
|
21
|
-
expect(lower).toContain("
|
|
21
|
+
expect(lower).toContain("emerge");
|
|
22
22
|
expect(lower).toContain("vibe");
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -41,15 +41,15 @@ describe("onboarding template contracts", () => {
|
|
|
41
41
|
const lower = bootstrap.toLowerCase();
|
|
42
42
|
// The template must prompt the assistant to ask about names.
|
|
43
43
|
expect(lower).toContain("name");
|
|
44
|
-
// The first step should be about
|
|
45
|
-
expect(lower).toContain("
|
|
44
|
+
// The first step should be about the assistant's name
|
|
45
|
+
expect(lower).toContain("your name");
|
|
46
46
|
// The conversation sequence must include identity/naming
|
|
47
47
|
expect(lower).toContain("who am i");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test("asks user name AFTER assistant identity is established", () => {
|
|
51
|
-
// Step 1 is
|
|
52
|
-
const assistantNameIdx = bootstrap.indexOf("
|
|
51
|
+
// Step 1 is the assistant's name, step 4 is asking the user's name
|
|
52
|
+
const assistantNameIdx = bootstrap.indexOf("Your name:");
|
|
53
53
|
const userNameIdx = bootstrap.indexOf("who am I talking to?");
|
|
54
54
|
expect(assistantNameIdx).toBeGreaterThan(-1);
|
|
55
55
|
expect(userNameIdx).toBeGreaterThan(-1);
|
|
@@ -87,10 +87,8 @@ describe("onboarding template contracts", () => {
|
|
|
87
87
|
expect(lower).toContain("home base");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
test("contains
|
|
90
|
+
test("contains refusal policy", () => {
|
|
91
91
|
const lower = bootstrap.toLowerCase();
|
|
92
|
-
// Must have a privacy section
|
|
93
|
-
expect(lower).toContain("privacy");
|
|
94
92
|
// Assistant name is hard-required, user details are best-effort
|
|
95
93
|
expect(lower).toContain("hard-required");
|
|
96
94
|
expect(lower).toContain("best-effort");
|
|
@@ -107,16 +105,8 @@ describe("onboarding template contracts", () => {
|
|
|
107
105
|
expect(lower).toContain("declined");
|
|
108
106
|
});
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expect(lower).toContain("em dashes");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("preserves no technical jargon instruction", () => {
|
|
116
|
-
const lower = bootstrap.toLowerCase();
|
|
117
|
-
expect(lower).toContain("technical jargon");
|
|
118
|
-
expect(lower).toContain("system internals");
|
|
119
|
-
});
|
|
108
|
+
// em-dash and technical jargon instructions are now hardcoded in the system
|
|
109
|
+
// prompt builder (buildSystemPrompt) rather than in the BOOTSTRAP.md template.
|
|
120
110
|
|
|
121
111
|
test("preserves comment line format instruction", () => {
|
|
122
112
|
// The template must start with the comment format explanation
|
|
@@ -2276,7 +2276,7 @@ describe("relay-server", () => {
|
|
|
2276
2276
|
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
2277
2277
|
.filter((m) => m.type === "text");
|
|
2278
2278
|
const promptText = textMessages.map((m) => m.token ?? "").join("");
|
|
2279
|
-
expect(promptText).toContain("Hi, this is my
|
|
2279
|
+
expect(promptText).toContain("Hi, this is my human's assistant.");
|
|
2280
2280
|
expect(promptText).not.toContain("Vellum");
|
|
2281
2281
|
expect(promptText).toContain("don't recognize this number");
|
|
2282
2282
|
expect(promptText).toContain("Can I get your name");
|
|
@@ -2326,13 +2326,13 @@ describe("relay-server", () => {
|
|
|
2326
2326
|
// Should have transitioned to awaiting guardian decision
|
|
2327
2327
|
expect(relay.getConnectionState()).toBe("awaiting_guardian_decision");
|
|
2328
2328
|
|
|
2329
|
-
// Should have sent the hold message (guardian label defaults to "my
|
|
2329
|
+
// Should have sent the hold message (guardian label defaults to "my human")
|
|
2330
2330
|
const textMessages = ws.sentMessages
|
|
2331
2331
|
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
2332
2332
|
.filter((m) => m.type === "text");
|
|
2333
2333
|
expect(
|
|
2334
2334
|
textMessages.some((m) =>
|
|
2335
|
-
(m.token ?? "").includes("I've let my
|
|
2335
|
+
(m.token ?? "").includes("I've let my human know"),
|
|
2336
2336
|
),
|
|
2337
2337
|
).toBe(true);
|
|
2338
2338
|
expect(
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { GuardianRuntimeContext } from "../daemon/session-runtime-assembly.js";
|
|
4
|
+
|
|
5
|
+
// ── Module mocks ─────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
let fakeHttpAuthDisabled = false;
|
|
8
|
+
|
|
9
|
+
mock.module("../config/env.js", () => ({
|
|
10
|
+
isHttpAuthDisabled: () => fakeHttpAuthDisabled,
|
|
11
|
+
hasUngatedHttpAuthDisabled: () => false,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// ── Real imports (after mocks) ───────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
import { resolveGuardianTrustClass } from "../daemon/session-tool-setup.js";
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
mock.restore();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe("resolveGuardianTrustClass", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
fakeHttpAuthDisabled = false;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("returns guardian context trust class when auth is enabled", () => {
|
|
30
|
+
const ctx: Pick<GuardianRuntimeContext, "trustClass"> = {
|
|
31
|
+
trustClass: "trusted_contact",
|
|
32
|
+
};
|
|
33
|
+
expect(resolveGuardianTrustClass(ctx as GuardianRuntimeContext)).toBe(
|
|
34
|
+
"trusted_contact",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("defaults to guardian when no guardian context and auth is enabled", () => {
|
|
39
|
+
expect(resolveGuardianTrustClass(undefined)).toBe("guardian");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("forces guardian when HTTP auth is disabled, regardless of context trust class", () => {
|
|
43
|
+
fakeHttpAuthDisabled = true;
|
|
44
|
+
const ctx: Pick<GuardianRuntimeContext, "trustClass"> = {
|
|
45
|
+
trustClass: "trusted_contact",
|
|
46
|
+
};
|
|
47
|
+
expect(resolveGuardianTrustClass(ctx as GuardianRuntimeContext)).toBe(
|
|
48
|
+
"guardian",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("forces guardian for unknown trust class when HTTP auth is disabled", () => {
|
|
53
|
+
fakeHttpAuthDisabled = true;
|
|
54
|
+
const ctx: Pick<GuardianRuntimeContext, "trustClass"> = {
|
|
55
|
+
trustClass: "unknown",
|
|
56
|
+
};
|
|
57
|
+
expect(resolveGuardianTrustClass(ctx as GuardianRuntimeContext)).toBe(
|
|
58
|
+
"guardian",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -106,6 +106,11 @@ async function publishAndReadFrame(
|
|
|
106
106
|
await assistantEventHub.publish(event);
|
|
107
107
|
|
|
108
108
|
const reader = response.body!.getReader();
|
|
109
|
+
|
|
110
|
+
// The first chunk is the immediate heartbeat comment enqueued in start().
|
|
111
|
+
await reader.read();
|
|
112
|
+
|
|
113
|
+
// The second chunk is the actual assistant event.
|
|
109
114
|
const { value } = await reader.read();
|
|
110
115
|
ac.abort();
|
|
111
116
|
|
|
@@ -366,6 +371,11 @@ describe("SSE IPC parity — streaming/delta message types", () => {
|
|
|
366
371
|
await assistantEventHub.publish(published);
|
|
367
372
|
|
|
368
373
|
const reader = response.body!.getReader();
|
|
374
|
+
|
|
375
|
+
// The first chunk is the immediate heartbeat comment enqueued in start().
|
|
376
|
+
await reader.read();
|
|
377
|
+
|
|
378
|
+
// The second chunk is the actual assistant event.
|
|
369
379
|
const { value } = await reader.read();
|
|
370
380
|
ac.abort();
|
|
371
381
|
|
|
@@ -168,6 +168,13 @@ describe("SSE assistant-events endpoint", () => {
|
|
|
168
168
|
|
|
169
169
|
// Read the first frame directly from the response body stream.
|
|
170
170
|
const reader = response.body!.getReader();
|
|
171
|
+
|
|
172
|
+
// The first chunk is the immediate heartbeat comment enqueued in start().
|
|
173
|
+
const initial = await reader.read();
|
|
174
|
+
expect(initial.done).toBe(false);
|
|
175
|
+
expect(new TextDecoder().decode(initial.value)).toBe(": heartbeat\n\n");
|
|
176
|
+
|
|
177
|
+
// The second chunk is the actual assistant event.
|
|
171
178
|
const { value, done } = await reader.read();
|
|
172
179
|
ac.abort();
|
|
173
180
|
|
|
@@ -277,11 +277,7 @@ mock.module("../tools/browser/browser-screencast.js", () => ({
|
|
|
277
277
|
registerSessionSender: () => {},
|
|
278
278
|
unregisterSessionSender: () => {},
|
|
279
279
|
ensureScreencast: () => Promise.resolve(),
|
|
280
|
-
updateBrowserStatus: () => {},
|
|
281
|
-
updatePagesList: () => Promise.resolve(),
|
|
282
280
|
stopBrowserScreencast: () => Promise.resolve(),
|
|
283
|
-
getElementBounds: () => Promise.resolve(null),
|
|
284
|
-
updateHighlights: () => {},
|
|
285
281
|
stopAllScreencasts: () => Promise.resolve(),
|
|
286
282
|
isScreencastActive: () => false,
|
|
287
283
|
getSender: () => undefined,
|
|
@@ -44,6 +44,14 @@ describe("resolveChannelCapabilities", () => {
|
|
|
44
44
|
expect(caps.supportsVoiceInput).toBe(true);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
test("vellum channel with vellum interface supports dynamic UI", () => {
|
|
48
|
+
const caps = resolveChannelCapabilities("vellum", "vellum");
|
|
49
|
+
expect(caps.channel).toBe("vellum");
|
|
50
|
+
expect(caps.dashboardCapable).toBe(false);
|
|
51
|
+
expect(caps.supportsDynamicUi).toBe(true);
|
|
52
|
+
expect(caps.supportsVoiceInput).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
47
55
|
test("defaults to vellum for null source channel", () => {
|
|
48
56
|
const caps = resolveChannelCapabilities(null);
|
|
49
57
|
expect(caps.channel).toBe("vellum");
|
|
@@ -407,6 +415,24 @@ describe("trust-gating via channel capabilities", () => {
|
|
|
407
415
|
expect(injected).toContain("Present information as well-formatted text");
|
|
408
416
|
expect(injected).toContain("desktop app");
|
|
409
417
|
});
|
|
418
|
+
|
|
419
|
+
test("vellum web interface allows dynamic UI but constrains dashboard references", () => {
|
|
420
|
+
const caps = resolveChannelCapabilities("vellum", "vellum");
|
|
421
|
+
const message: Message = {
|
|
422
|
+
role: "user",
|
|
423
|
+
content: [{ type: "text", text: "Show me a form" }],
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const result = injectChannelCapabilityContext(message, caps);
|
|
427
|
+
const injected = (result.content[0] as { type: "text"; text: string }).text;
|
|
428
|
+
|
|
429
|
+
expect(injected).toContain("CHANNEL CONSTRAINTS");
|
|
430
|
+
expect(injected).toContain("Do NOT reference the dashboard UI");
|
|
431
|
+
expect(injected).not.toContain("Do NOT use ui_show");
|
|
432
|
+
expect(injected).not.toContain("Present information as well-formatted text");
|
|
433
|
+
expect(injected).toContain("supports_dynamic_ui: true");
|
|
434
|
+
expect(injected).toContain("dashboard_capable: false");
|
|
435
|
+
});
|
|
410
436
|
});
|
|
411
437
|
|
|
412
438
|
// ---------------------------------------------------------------------------
|
|
@@ -994,12 +1020,12 @@ describe("sanitizePttActivationKey", () => {
|
|
|
994
1020
|
expect(sanitizePttActivationKey("none")).toBe("none");
|
|
995
1021
|
});
|
|
996
1022
|
|
|
997
|
-
test(
|
|
998
|
-
expect(
|
|
999
|
-
"
|
|
1000
|
-
);
|
|
1001
|
-
expect(sanitizePttActivationKey("arbitrary_value")).
|
|
1002
|
-
expect(sanitizePttActivationKey("")).
|
|
1023
|
+
test("returns undefined for invalid keys", () => {
|
|
1024
|
+
expect(
|
|
1025
|
+
sanitizePttActivationKey("malicious\nprompt injection"),
|
|
1026
|
+
).toBeUndefined();
|
|
1027
|
+
expect(sanitizePttActivationKey("arbitrary_value")).toBeUndefined();
|
|
1028
|
+
expect(sanitizePttActivationKey("")).toBeUndefined();
|
|
1003
1029
|
});
|
|
1004
1030
|
});
|
|
1005
1031
|
|
|
@@ -1015,11 +1041,11 @@ describe("resolveChannelCapabilities with PTT metadata", () => {
|
|
|
1015
1041
|
expect(caps.pttActivationKey).toBe("fn");
|
|
1016
1042
|
});
|
|
1017
1043
|
|
|
1018
|
-
test("sanitizes invalid pttActivationKey to
|
|
1044
|
+
test("sanitizes invalid pttActivationKey to undefined", () => {
|
|
1019
1045
|
const caps = resolveChannelCapabilities("macos", "macos", {
|
|
1020
1046
|
pttActivationKey: "evil\nprompt",
|
|
1021
1047
|
});
|
|
1022
|
-
expect(caps.pttActivationKey).
|
|
1048
|
+
expect(caps.pttActivationKey).toBeUndefined();
|
|
1023
1049
|
});
|
|
1024
1050
|
|
|
1025
1051
|
test("passes through microphonePermissionGranted", () => {
|
|
@@ -75,9 +75,15 @@ const {
|
|
|
75
75
|
buildPhoneCallsRoutingSection,
|
|
76
76
|
} = await import("../config/system-prompt.js");
|
|
77
77
|
|
|
78
|
-
/** Strip the Configuration and
|
|
78
|
+
/** Strip the Configuration, Skills, and hardcoded preamble sections so base-prompt tests stay focused. */
|
|
79
79
|
function basePrompt(result: string): string {
|
|
80
80
|
let s = result;
|
|
81
|
+
// Strip the hardcoded em-dash instruction preamble
|
|
82
|
+
const emDashLine =
|
|
83
|
+
"IMPORTANT: Never use em dashes (\u2014) in your messages. Use commas, periods, or just start a new sentence instead.";
|
|
84
|
+
if (s.startsWith(emDashLine)) {
|
|
85
|
+
s = s.slice(emDashLine.length).replace(/^\n\n/, "");
|
|
86
|
+
}
|
|
81
87
|
for (const heading of [
|
|
82
88
|
"## Configuration",
|
|
83
89
|
"## Skills Catalog",
|
|
@@ -120,8 +120,14 @@ mock.module("../config/env.js", () => ({
|
|
|
120
120
|
getGatewayInternalBaseUrl: () => "http://localhost:3000",
|
|
121
121
|
}));
|
|
122
122
|
|
|
123
|
+
// ── User reference mock ──
|
|
124
|
+
mock.module("../config/user-reference.js", () => ({
|
|
125
|
+
resolveUserReference: () => "my human",
|
|
126
|
+
}));
|
|
127
|
+
|
|
123
128
|
// Import module under test AFTER mocks are set up
|
|
124
129
|
import type { ChannelId } from "../channels/types.js";
|
|
130
|
+
import { resolveUserReference } from "../config/user-reference.js";
|
|
125
131
|
import type { GuardianContext } from "../runtime/guardian-context-resolver.js";
|
|
126
132
|
|
|
127
133
|
// We need to test the private functions by importing the module.
|
|
@@ -220,9 +226,7 @@ async function simulateNotifierPoll(params: {
|
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
|
|
223
|
-
const waitingText = guardianName
|
|
224
|
-
? `Waiting for ${guardianName}'s approval...`
|
|
225
|
-
: "Waiting for your guardian's approval...";
|
|
229
|
+
const waitingText = `Waiting for ${guardianName ?? resolveUserReference()}'s approval...`;
|
|
226
230
|
|
|
227
231
|
try {
|
|
228
232
|
await deliverChannelReply(
|
|
@@ -330,7 +334,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
330
334
|
);
|
|
331
335
|
});
|
|
332
336
|
|
|
333
|
-
test("
|
|
337
|
+
test("falls back to user reference when no guardian name is available", async () => {
|
|
334
338
|
mockPendingApprovals = [
|
|
335
339
|
{
|
|
336
340
|
requestId: "req-3",
|
|
@@ -359,11 +363,11 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
359
363
|
|
|
360
364
|
expect(deliveredReplies).toHaveLength(1);
|
|
361
365
|
expect(deliveredReplies[0].payload.text).toBe(
|
|
362
|
-
"Waiting for
|
|
366
|
+
"Waiting for my human's approval...",
|
|
363
367
|
);
|
|
364
368
|
});
|
|
365
369
|
|
|
366
|
-
test("
|
|
370
|
+
test("falls back to user reference when no guardian binding exists", async () => {
|
|
367
371
|
mockPendingApprovals = [
|
|
368
372
|
{
|
|
369
373
|
requestId: "req-4",
|
|
@@ -388,7 +392,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
388
392
|
|
|
389
393
|
expect(deliveredReplies).toHaveLength(1);
|
|
390
394
|
expect(deliveredReplies[0].payload.text).toBe(
|
|
391
|
-
"Waiting for
|
|
395
|
+
"Waiting for my human's approval...",
|
|
392
396
|
);
|
|
393
397
|
});
|
|
394
398
|
|
|
@@ -736,7 +740,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
736
740
|
expect(deliveredReplies).toHaveLength(1);
|
|
737
741
|
// Falls back to generic phrasing
|
|
738
742
|
expect(deliveredReplies[0].payload.text).toBe(
|
|
739
|
-
"Waiting for
|
|
743
|
+
"Waiting for my human's approval...",
|
|
740
744
|
);
|
|
741
745
|
});
|
|
742
746
|
});
|
|
@@ -46,7 +46,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
46
46
|
expect(twiml).toContain('voice="voice123-turbo_v2_5-1_0.5_0.75"');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
test("voice attribute reflects configured voice
|
|
49
|
+
test("voice attribute reflects configured Google voice", () => {
|
|
50
50
|
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
|
|
51
51
|
language: "en-US",
|
|
52
52
|
transcriptionProvider: "Deepgram",
|
|
@@ -57,7 +57,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
57
57
|
expect(twiml).toContain('voice="Google.en-US-Journey-O"');
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test("voice attribute reflects configured voice
|
|
60
|
+
test("voice attribute reflects configured ElevenLabs voice", () => {
|
|
61
61
|
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
|
|
62
62
|
language: "en-US",
|
|
63
63
|
transcriptionProvider: "Deepgram",
|
|
@@ -60,13 +60,12 @@ const mockConfigObj = {
|
|
|
60
60
|
memory: { enabled: false },
|
|
61
61
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
62
62
|
secretDetection: { enabled: false },
|
|
63
|
+
elevenlabs: { voiceId: "21m00Tcm4TlvDq8ikWAM" },
|
|
63
64
|
calls: {
|
|
64
65
|
voice: {
|
|
65
|
-
mode: "twilio_standard",
|
|
66
66
|
language: "en-US",
|
|
67
67
|
transcriptionProvider: "Deepgram",
|
|
68
|
-
|
|
69
|
-
elevenlabs: { voiceId: "" },
|
|
68
|
+
elevenlabs: {},
|
|
70
69
|
},
|
|
71
70
|
},
|
|
72
71
|
};
|