@vellumai/assistant 0.4.16 → 0.4.17
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/Dockerfile +6 -6
- package/README.md +1 -2
- package/package.json +1 -1
- package/src/__tests__/call-controller.test.ts +1074 -751
- package/src/__tests__/call-routes-http.test.ts +329 -279
- package/src/__tests__/channel-approval-routes.test.ts +0 -11
- package/src/__tests__/channel-approvals.test.ts +227 -182
- package/src/__tests__/channel-guardian.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
- package/src/__tests__/conversation-routes.test.ts +71 -41
- package/src/__tests__/daemon-server-session-init.test.ts +258 -191
- package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
- package/src/__tests__/extract-email.test.ts +42 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
- package/src/__tests__/gateway-only-guard.test.ts +54 -55
- package/src/__tests__/gmail-integration.test.ts +48 -46
- package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
- package/src/__tests__/guardian-outbound-http.test.ts +334 -208
- package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
- package/src/__tests__/guardian-routing-state.test.ts +257 -209
- package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
- package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
- package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
- package/src/__tests__/ingress-reconcile.test.ts +184 -142
- package/src/__tests__/non-member-access-request.test.ts +291 -247
- package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
- package/src/__tests__/recording-intent-handler.test.ts +422 -291
- package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
- package/src/__tests__/runtime-events-sse.test.ts +67 -50
- package/src/__tests__/send-endpoint-busy.test.ts +314 -232
- package/src/__tests__/session-approval-overrides.test.ts +93 -91
- package/src/__tests__/sms-messaging-provider.test.ts +74 -47
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
- package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
- package/src/__tests__/twilio-config.test.ts +49 -41
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
- package/src/__tests__/twilio-routes.test.ts +389 -280
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -4
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +11 -7
- package/src/config/env.ts +39 -29
- package/src/daemon/handlers/skills.ts +18 -10
- package/src/daemon/ipc-contract/messages.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +7 -1
- package/src/daemon/session-agent-loop-handlers.ts +5 -0
- package/src/daemon/session-agent-loop.ts +1 -1
- package/src/daemon/session-process.ts +1 -1
- package/src/daemon/session-surfaces.ts +42 -2
- package/src/runtime/auth/token-service.ts +74 -47
- package/src/sequence/reply-matcher.ts +10 -6
- package/src/skills/frontmatter.ts +9 -6
- package/src/tools/ui-surface/definitions.ts +2 -1
- package/src/util/platform.ts +0 -12
- package/docs/architecture/http-token-refresh.md +0 -274
|
@@ -5,52 +5,57 @@
|
|
|
5
5
|
* POST /v1/calls/:id/cancel, and POST /v1/calls/:id/answer
|
|
6
6
|
* through RuntimeHttpServer.
|
|
7
7
|
*/
|
|
8
|
-
import { mkdtempSync, realpathSync,rmSync } from
|
|
9
|
-
import { tmpdir } from
|
|
10
|
-
import { join } from
|
|
8
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
-
import { afterAll, beforeEach, describe, expect, mock,test } from
|
|
12
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const testDir = realpathSync(
|
|
17
|
+
mkdtempSync(join(tmpdir(), "call-routes-http-test-")),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
mock.module("../util/platform.js", () => ({
|
|
17
21
|
getRootDir: () => testDir,
|
|
18
22
|
getDataDir: () => testDir,
|
|
19
|
-
isMacOS: () => process.platform ===
|
|
20
|
-
isLinux: () => process.platform ===
|
|
21
|
-
isWindows: () => process.platform ===
|
|
22
|
-
getSocketPath: () => join(testDir,
|
|
23
|
-
getPidPath: () => join(testDir,
|
|
24
|
-
getDbPath: () => join(testDir,
|
|
25
|
-
getLogPath: () => join(testDir,
|
|
23
|
+
isMacOS: () => process.platform === "darwin",
|
|
24
|
+
isLinux: () => process.platform === "linux",
|
|
25
|
+
isWindows: () => process.platform === "win32",
|
|
26
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
27
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
28
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
29
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
26
30
|
ensureDataDir: () => {},
|
|
27
31
|
readHttpToken: () => null,
|
|
28
32
|
}));
|
|
29
33
|
|
|
30
|
-
mock.module(
|
|
31
|
-
getLogger: () =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
mock.module("../util/logger.js", () => ({
|
|
35
|
+
getLogger: () =>
|
|
36
|
+
new Proxy({} as Record<string, unknown>, {
|
|
37
|
+
get: () => () => {},
|
|
38
|
+
}),
|
|
34
39
|
}));
|
|
35
40
|
|
|
36
41
|
const mockCallsConfig = {
|
|
37
42
|
enabled: true,
|
|
38
|
-
provider:
|
|
43
|
+
provider: "twilio",
|
|
39
44
|
maxDurationSeconds: 3600,
|
|
40
45
|
userConsultTimeoutSeconds: 120,
|
|
41
|
-
disclosure: { enabled: false, text:
|
|
46
|
+
disclosure: { enabled: false, text: "" },
|
|
42
47
|
safety: { denyCategories: [] },
|
|
43
48
|
callerIdentity: {
|
|
44
49
|
allowPerCallOverride: true,
|
|
45
50
|
},
|
|
46
51
|
};
|
|
47
52
|
|
|
48
|
-
mock.module(
|
|
53
|
+
mock.module("../config/loader.js", () => ({
|
|
49
54
|
getConfig: () => ({
|
|
50
55
|
ui: {},
|
|
51
|
-
|
|
52
|
-
model:
|
|
53
|
-
provider:
|
|
56
|
+
|
|
57
|
+
model: "test",
|
|
58
|
+
provider: "test",
|
|
54
59
|
apiKeys: {},
|
|
55
60
|
memory: { enabled: false },
|
|
56
61
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
@@ -58,8 +63,8 @@ mock.module('../config/loader.js', () => ({
|
|
|
58
63
|
calls: mockCallsConfig,
|
|
59
64
|
}),
|
|
60
65
|
loadConfig: () => ({
|
|
61
|
-
model:
|
|
62
|
-
provider:
|
|
66
|
+
model: "test",
|
|
67
|
+
provider: "test",
|
|
63
68
|
apiKeys: {},
|
|
64
69
|
memory: { enabled: false },
|
|
65
70
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
@@ -67,34 +72,42 @@ mock.module('../config/loader.js', () => ({
|
|
|
67
72
|
calls: mockCallsConfig,
|
|
68
73
|
ingress: {
|
|
69
74
|
enabled: true,
|
|
70
|
-
publicBaseUrl:
|
|
75
|
+
publicBaseUrl: "https://test.example.com",
|
|
71
76
|
},
|
|
72
77
|
}),
|
|
73
78
|
}));
|
|
74
79
|
|
|
75
80
|
// Mock Twilio provider to avoid real API calls
|
|
76
|
-
mock.module(
|
|
81
|
+
mock.module("../calls/twilio-provider.js", () => ({
|
|
77
82
|
TwilioConversationRelayProvider: class {
|
|
78
|
-
static getAuthToken() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
static getAuthToken() {
|
|
84
|
+
return "mock-auth-token";
|
|
85
|
+
}
|
|
86
|
+
static verifyWebhookSignature() {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
async initiateCall() {
|
|
90
|
+
return { callSid: "CA_mock_sid_123" };
|
|
91
|
+
}
|
|
92
|
+
async endCall() {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
82
95
|
},
|
|
83
96
|
}));
|
|
84
97
|
|
|
85
98
|
// Mock Twilio config
|
|
86
|
-
mock.module(
|
|
99
|
+
mock.module("../calls/twilio-config.js", () => ({
|
|
87
100
|
getTwilioConfig: (assistantId?: string) => ({
|
|
88
|
-
accountSid:
|
|
89
|
-
authToken:
|
|
90
|
-
phoneNumber: assistantId ===
|
|
91
|
-
webhookBaseUrl:
|
|
92
|
-
wssBaseUrl:
|
|
101
|
+
accountSid: "AC_test",
|
|
102
|
+
authToken: "test_token",
|
|
103
|
+
phoneNumber: assistantId === "asst-alpha" ? "+15550009999" : "+15550001111",
|
|
104
|
+
webhookBaseUrl: "https://test.example.com",
|
|
105
|
+
wssBaseUrl: "wss://test.example.com",
|
|
93
106
|
}),
|
|
94
107
|
}));
|
|
95
108
|
|
|
96
109
|
// Mock secure keys
|
|
97
|
-
mock.module(
|
|
110
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
98
111
|
getSecureKey: () => null,
|
|
99
112
|
}));
|
|
100
113
|
|
|
@@ -102,12 +115,12 @@ import {
|
|
|
102
115
|
createCallSession,
|
|
103
116
|
createPendingQuestion,
|
|
104
117
|
updateCallSession,
|
|
105
|
-
} from
|
|
106
|
-
import { getDb, initializeDb, resetDb } from
|
|
107
|
-
import { conversations } from
|
|
108
|
-
import { RuntimeHttpServer } from
|
|
118
|
+
} from "../calls/call-store.js";
|
|
119
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
120
|
+
import { conversations } from "../memory/schema.js";
|
|
121
|
+
import { RuntimeHttpServer } from "../runtime/http-server.js";
|
|
109
122
|
|
|
110
|
-
import
|
|
123
|
+
import "../calls/call-state.js";
|
|
111
124
|
|
|
112
125
|
initializeDb();
|
|
113
126
|
|
|
@@ -115,7 +128,7 @@ initializeDb();
|
|
|
115
128
|
// Helpers
|
|
116
129
|
// ---------------------------------------------------------------------------
|
|
117
130
|
|
|
118
|
-
const TEST_TOKEN =
|
|
131
|
+
const TEST_TOKEN = "test-bearer-token-calls";
|
|
119
132
|
const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
|
|
120
133
|
|
|
121
134
|
let ensuredConvIds = new Set<string>();
|
|
@@ -124,25 +137,27 @@ function ensureConversation(id: string): void {
|
|
|
124
137
|
if (ensuredConvIds.has(id)) return;
|
|
125
138
|
const db = getDb();
|
|
126
139
|
const now = Date.now();
|
|
127
|
-
db.insert(conversations)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
db.insert(conversations)
|
|
141
|
+
.values({
|
|
142
|
+
id,
|
|
143
|
+
title: `Test conversation ${id}`,
|
|
144
|
+
createdAt: now,
|
|
145
|
+
updatedAt: now,
|
|
146
|
+
})
|
|
147
|
+
.run();
|
|
133
148
|
ensuredConvIds.add(id);
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
function resetTables() {
|
|
137
152
|
const db = getDb();
|
|
138
|
-
db.run(
|
|
139
|
-
db.run(
|
|
140
|
-
db.run(
|
|
141
|
-
db.run(
|
|
142
|
-
db.run(
|
|
143
|
-
db.run(
|
|
144
|
-
db.run(
|
|
145
|
-
db.run(
|
|
153
|
+
db.run("DELETE FROM guardian_action_deliveries");
|
|
154
|
+
db.run("DELETE FROM guardian_action_requests");
|
|
155
|
+
db.run("DELETE FROM call_pending_questions");
|
|
156
|
+
db.run("DELETE FROM call_events");
|
|
157
|
+
db.run("DELETE FROM call_sessions");
|
|
158
|
+
db.run("DELETE FROM tool_invocations");
|
|
159
|
+
db.run("DELETE FROM messages");
|
|
160
|
+
db.run("DELETE FROM conversations");
|
|
146
161
|
ensuredConvIds = new Set();
|
|
147
162
|
}
|
|
148
163
|
|
|
@@ -150,7 +165,7 @@ function resetTables() {
|
|
|
150
165
|
// Tests
|
|
151
166
|
// ---------------------------------------------------------------------------
|
|
152
167
|
|
|
153
|
-
describe(
|
|
168
|
+
describe("runtime call routes — HTTP layer", () => {
|
|
154
169
|
let server: RuntimeHttpServer;
|
|
155
170
|
let port: number;
|
|
156
171
|
|
|
@@ -160,7 +175,11 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
160
175
|
|
|
161
176
|
afterAll(() => {
|
|
162
177
|
resetDb();
|
|
163
|
-
try {
|
|
178
|
+
try {
|
|
179
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
180
|
+
} catch {
|
|
181
|
+
/* best effort */
|
|
182
|
+
}
|
|
164
183
|
});
|
|
165
184
|
|
|
166
185
|
async function startServer(): Promise<void> {
|
|
@@ -173,29 +192,29 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
173
192
|
await server?.stop();
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
function callsUrl(path =
|
|
195
|
+
function callsUrl(path = ""): string {
|
|
177
196
|
return `http://127.0.0.1:${port}/v1/calls${path}`;
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
// ── POST /v1/calls/start ────────────────────────────────────────────
|
|
181
200
|
|
|
182
|
-
test(
|
|
201
|
+
test("POST /v1/calls/start returns 201 with call session", async () => {
|
|
183
202
|
await startServer();
|
|
184
|
-
ensureConversation(
|
|
203
|
+
ensureConversation("conv-start-1");
|
|
185
204
|
|
|
186
|
-
const res = await fetch(callsUrl(
|
|
187
|
-
method:
|
|
188
|
-
headers: {
|
|
205
|
+
const res = await fetch(callsUrl("/start"), {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
189
208
|
body: JSON.stringify({
|
|
190
|
-
phoneNumber:
|
|
191
|
-
task:
|
|
192
|
-
conversationId:
|
|
209
|
+
phoneNumber: "+15559998888",
|
|
210
|
+
task: "Book a table for two",
|
|
211
|
+
conversationId: "conv-start-1",
|
|
193
212
|
}),
|
|
194
213
|
});
|
|
195
214
|
|
|
196
215
|
expect(res.status).toBe(201);
|
|
197
216
|
|
|
198
|
-
const body = await res.json() as {
|
|
217
|
+
const body = (await res.json()) as {
|
|
199
218
|
callSessionId: string;
|
|
200
219
|
callSid: string;
|
|
201
220
|
status: string;
|
|
@@ -204,111 +223,119 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
204
223
|
};
|
|
205
224
|
|
|
206
225
|
expect(body.callSessionId).toBeDefined();
|
|
207
|
-
expect(body.callSid).toBe(
|
|
208
|
-
expect(body.status).toBe(
|
|
209
|
-
expect(body.toNumber).toBe(
|
|
210
|
-
expect(body.fromNumber).toBe(
|
|
226
|
+
expect(body.callSid).toBe("CA_mock_sid_123");
|
|
227
|
+
expect(body.status).toBe("initiated");
|
|
228
|
+
expect(body.toNumber).toBe("+15559998888");
|
|
229
|
+
expect(body.fromNumber).toBe("+15550001111");
|
|
211
230
|
|
|
212
231
|
await stopServer();
|
|
213
232
|
});
|
|
214
233
|
|
|
215
|
-
test(
|
|
234
|
+
test("POST /v1/calls/start returns 400 when conversationId missing", async () => {
|
|
216
235
|
await startServer();
|
|
217
236
|
|
|
218
|
-
const res = await fetch(callsUrl(
|
|
219
|
-
method:
|
|
220
|
-
headers: {
|
|
237
|
+
const res = await fetch(callsUrl("/start"), {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
221
240
|
body: JSON.stringify({
|
|
222
|
-
phoneNumber:
|
|
223
|
-
task:
|
|
241
|
+
phoneNumber: "+15559998888",
|
|
242
|
+
task: "Book a table",
|
|
224
243
|
}),
|
|
225
244
|
});
|
|
226
245
|
|
|
227
246
|
expect(res.status).toBe(400);
|
|
228
|
-
const body = await res.json() as {
|
|
229
|
-
|
|
247
|
+
const body = (await res.json()) as {
|
|
248
|
+
error: { message: string; code?: string };
|
|
249
|
+
};
|
|
250
|
+
expect(body.error.message).toContain("conversationId");
|
|
230
251
|
|
|
231
252
|
await stopServer();
|
|
232
253
|
});
|
|
233
254
|
|
|
234
|
-
test(
|
|
255
|
+
test("POST /v1/calls/start returns 400 for invalid phone number", async () => {
|
|
235
256
|
await startServer();
|
|
236
|
-
ensureConversation(
|
|
257
|
+
ensureConversation("conv-start-2");
|
|
237
258
|
|
|
238
|
-
const res = await fetch(callsUrl(
|
|
239
|
-
method:
|
|
240
|
-
headers: {
|
|
259
|
+
const res = await fetch(callsUrl("/start"), {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
241
262
|
body: JSON.stringify({
|
|
242
|
-
phoneNumber:
|
|
243
|
-
task:
|
|
244
|
-
conversationId:
|
|
263
|
+
phoneNumber: "not-a-number",
|
|
264
|
+
task: "Book a table",
|
|
265
|
+
conversationId: "conv-start-2",
|
|
245
266
|
}),
|
|
246
267
|
});
|
|
247
268
|
|
|
248
269
|
expect(res.status).toBe(400);
|
|
249
|
-
const body = await res.json() as {
|
|
250
|
-
|
|
270
|
+
const body = (await res.json()) as {
|
|
271
|
+
error: { message: string; code?: string };
|
|
272
|
+
};
|
|
273
|
+
expect(body.error.message).toContain("E.164");
|
|
251
274
|
|
|
252
275
|
await stopServer();
|
|
253
276
|
});
|
|
254
277
|
|
|
255
|
-
test(
|
|
278
|
+
test("POST /v1/calls/start returns 400 for malformed JSON", async () => {
|
|
256
279
|
await startServer();
|
|
257
280
|
|
|
258
|
-
const res = await fetch(callsUrl(
|
|
259
|
-
method:
|
|
260
|
-
headers: {
|
|
261
|
-
body:
|
|
281
|
+
const res = await fetch(callsUrl("/start"), {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
284
|
+
body: "not-json{{",
|
|
262
285
|
});
|
|
263
286
|
|
|
264
287
|
expect(res.status).toBe(400);
|
|
265
|
-
const body = await res.json() as {
|
|
266
|
-
|
|
288
|
+
const body = (await res.json()) as {
|
|
289
|
+
error: { message: string; code?: string };
|
|
290
|
+
};
|
|
291
|
+
expect(body.error.message).toContain("Invalid JSON");
|
|
267
292
|
|
|
268
293
|
await stopServer();
|
|
269
294
|
});
|
|
270
295
|
|
|
271
|
-
test(
|
|
296
|
+
test("POST /v1/calls/start with callerIdentityMode user_number is accepted", async () => {
|
|
272
297
|
await startServer();
|
|
273
|
-
ensureConversation(
|
|
298
|
+
ensureConversation("conv-start-identity-1");
|
|
274
299
|
|
|
275
|
-
const res = await fetch(callsUrl(
|
|
276
|
-
method:
|
|
277
|
-
headers: {
|
|
300
|
+
const res = await fetch(callsUrl("/start"), {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
278
303
|
body: JSON.stringify({
|
|
279
|
-
phoneNumber:
|
|
280
|
-
task:
|
|
281
|
-
conversationId:
|
|
282
|
-
callerIdentityMode:
|
|
304
|
+
phoneNumber: "+15559998888",
|
|
305
|
+
task: "Book a table for two",
|
|
306
|
+
conversationId: "conv-start-identity-1",
|
|
307
|
+
callerIdentityMode: "user_number",
|
|
283
308
|
}),
|
|
284
309
|
});
|
|
285
310
|
|
|
286
311
|
// user_number mode requires a configured user phone number;
|
|
287
312
|
// since we haven't set one, this should return a 400 explaining why
|
|
288
313
|
expect(res.status).toBe(400);
|
|
289
|
-
const body = await res.json() as {
|
|
290
|
-
|
|
314
|
+
const body = (await res.json()) as {
|
|
315
|
+
error: { message: string; code?: string };
|
|
316
|
+
};
|
|
317
|
+
expect(body.error.message).toContain("user_number");
|
|
291
318
|
|
|
292
319
|
await stopServer();
|
|
293
320
|
});
|
|
294
321
|
|
|
295
|
-
test(
|
|
322
|
+
test("POST /v1/calls/start without callerIdentityMode defaults to assistant_number", async () => {
|
|
296
323
|
await startServer();
|
|
297
|
-
ensureConversation(
|
|
324
|
+
ensureConversation("conv-start-identity-2");
|
|
298
325
|
|
|
299
|
-
const res = await fetch(callsUrl(
|
|
300
|
-
method:
|
|
301
|
-
headers: {
|
|
326
|
+
const res = await fetch(callsUrl("/start"), {
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
302
329
|
body: JSON.stringify({
|
|
303
|
-
phoneNumber:
|
|
304
|
-
task:
|
|
305
|
-
conversationId:
|
|
330
|
+
phoneNumber: "+15559998888",
|
|
331
|
+
task: "Book a table for two",
|
|
332
|
+
conversationId: "conv-start-identity-2",
|
|
306
333
|
}),
|
|
307
334
|
});
|
|
308
335
|
|
|
309
336
|
expect(res.status).toBe(201);
|
|
310
337
|
|
|
311
|
-
const body = await res.json() as {
|
|
338
|
+
const body = (await res.json()) as {
|
|
312
339
|
callSessionId: string;
|
|
313
340
|
callSid: string;
|
|
314
341
|
status: string;
|
|
@@ -318,50 +345,52 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
318
345
|
};
|
|
319
346
|
|
|
320
347
|
expect(body.callSessionId).toBeDefined();
|
|
321
|
-
expect(body.callSid).toBe(
|
|
322
|
-
expect(body.fromNumber).toBe(
|
|
323
|
-
expect(body.callerIdentityMode).toBe(
|
|
348
|
+
expect(body.callSid).toBe("CA_mock_sid_123");
|
|
349
|
+
expect(body.fromNumber).toBe("+15550001111");
|
|
350
|
+
expect(body.callerIdentityMode).toBe("assistant_number");
|
|
324
351
|
|
|
325
352
|
await stopServer();
|
|
326
353
|
});
|
|
327
354
|
|
|
328
|
-
test(
|
|
355
|
+
test("POST /v1/calls/start returns 400 for invalid callerIdentityMode", async () => {
|
|
329
356
|
await startServer();
|
|
330
|
-
ensureConversation(
|
|
357
|
+
ensureConversation("conv-start-identity-bogus");
|
|
331
358
|
|
|
332
|
-
const res = await fetch(callsUrl(
|
|
333
|
-
method:
|
|
334
|
-
headers: {
|
|
359
|
+
const res = await fetch(callsUrl("/start"), {
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
335
362
|
body: JSON.stringify({
|
|
336
|
-
phoneNumber:
|
|
337
|
-
task:
|
|
338
|
-
conversationId:
|
|
339
|
-
callerIdentityMode:
|
|
363
|
+
phoneNumber: "+15559998888",
|
|
364
|
+
task: "Book a table for two",
|
|
365
|
+
conversationId: "conv-start-identity-bogus",
|
|
366
|
+
callerIdentityMode: "bogus",
|
|
340
367
|
}),
|
|
341
368
|
});
|
|
342
369
|
|
|
343
370
|
expect(res.status).toBe(400);
|
|
344
|
-
const body = await res.json() as {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
expect(body.error.message).toContain(
|
|
348
|
-
expect(body.error.message).toContain(
|
|
371
|
+
const body = (await res.json()) as {
|
|
372
|
+
error: { message: string; code?: string };
|
|
373
|
+
};
|
|
374
|
+
expect(body.error.message).toContain("Invalid callerIdentityMode");
|
|
375
|
+
expect(body.error.message).toContain("bogus");
|
|
376
|
+
expect(body.error.message).toContain("assistant_number");
|
|
377
|
+
expect(body.error.message).toContain("user_number");
|
|
349
378
|
|
|
350
379
|
await stopServer();
|
|
351
380
|
});
|
|
352
381
|
|
|
353
382
|
// ── GET /v1/calls/:id ───────────────────────────────────────────────
|
|
354
383
|
|
|
355
|
-
test(
|
|
384
|
+
test("GET /v1/calls/:id returns call status", async () => {
|
|
356
385
|
await startServer();
|
|
357
|
-
ensureConversation(
|
|
386
|
+
ensureConversation("conv-get-1");
|
|
358
387
|
|
|
359
388
|
const session = createCallSession({
|
|
360
|
-
conversationId:
|
|
361
|
-
provider:
|
|
362
|
-
fromNumber:
|
|
363
|
-
toNumber:
|
|
364
|
-
task:
|
|
389
|
+
conversationId: "conv-get-1",
|
|
390
|
+
provider: "twilio",
|
|
391
|
+
fromNumber: "+15550001111",
|
|
392
|
+
toNumber: "+15559998888",
|
|
393
|
+
task: "Test task",
|
|
365
394
|
});
|
|
366
395
|
|
|
367
396
|
const res = await fetch(callsUrl(`/${session.id}`), {
|
|
@@ -370,7 +399,7 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
370
399
|
|
|
371
400
|
expect(res.status).toBe(200);
|
|
372
401
|
|
|
373
|
-
const body = await res.json() as {
|
|
402
|
+
const body = (await res.json()) as {
|
|
374
403
|
callSessionId: string;
|
|
375
404
|
status: string;
|
|
376
405
|
toNumber: string;
|
|
@@ -380,19 +409,19 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
380
409
|
};
|
|
381
410
|
|
|
382
411
|
expect(body.callSessionId).toBe(session.id);
|
|
383
|
-
expect(body.status).toBe(
|
|
384
|
-
expect(body.toNumber).toBe(
|
|
385
|
-
expect(body.fromNumber).toBe(
|
|
386
|
-
expect(body.task).toBe(
|
|
412
|
+
expect(body.status).toBe("initiated");
|
|
413
|
+
expect(body.toNumber).toBe("+15559998888");
|
|
414
|
+
expect(body.fromNumber).toBe("+15550001111");
|
|
415
|
+
expect(body.task).toBe("Test task");
|
|
387
416
|
expect(body.pendingQuestion).toBeNull();
|
|
388
417
|
|
|
389
418
|
await stopServer();
|
|
390
419
|
});
|
|
391
420
|
|
|
392
|
-
test(
|
|
421
|
+
test("GET /v1/calls/:id returns 404 for unknown session", async () => {
|
|
393
422
|
await startServer();
|
|
394
423
|
|
|
395
|
-
const res = await fetch(callsUrl(
|
|
424
|
+
const res = await fetch(callsUrl("/nonexistent-id"), {
|
|
396
425
|
headers: AUTH_HEADERS,
|
|
397
426
|
});
|
|
398
427
|
|
|
@@ -403,48 +432,51 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
403
432
|
|
|
404
433
|
// ── POST /v1/calls/:id/cancel ──────────────────────────────────────
|
|
405
434
|
|
|
406
|
-
test(
|
|
435
|
+
test("POST /v1/calls/:id/cancel transitions to cancelled", async () => {
|
|
407
436
|
await startServer();
|
|
408
|
-
ensureConversation(
|
|
437
|
+
ensureConversation("conv-cancel-1");
|
|
409
438
|
|
|
410
439
|
const session = createCallSession({
|
|
411
|
-
conversationId:
|
|
412
|
-
provider:
|
|
413
|
-
fromNumber:
|
|
414
|
-
toNumber:
|
|
440
|
+
conversationId: "conv-cancel-1",
|
|
441
|
+
provider: "twilio",
|
|
442
|
+
fromNumber: "+15550001111",
|
|
443
|
+
toNumber: "+15559998888",
|
|
415
444
|
});
|
|
416
445
|
|
|
417
446
|
const res = await fetch(callsUrl(`/${session.id}/cancel`), {
|
|
418
|
-
method:
|
|
419
|
-
headers: {
|
|
420
|
-
body: JSON.stringify({ reason:
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
449
|
+
body: JSON.stringify({ reason: "User requested" }),
|
|
421
450
|
});
|
|
422
451
|
|
|
423
452
|
expect(res.status).toBe(200);
|
|
424
453
|
|
|
425
|
-
const body = await res.json() as {
|
|
454
|
+
const body = (await res.json()) as {
|
|
455
|
+
callSessionId: string;
|
|
456
|
+
status: string;
|
|
457
|
+
};
|
|
426
458
|
expect(body.callSessionId).toBe(session.id);
|
|
427
|
-
expect(body.status).toBe(
|
|
459
|
+
expect(body.status).toBe("cancelled");
|
|
428
460
|
|
|
429
461
|
await stopServer();
|
|
430
462
|
});
|
|
431
463
|
|
|
432
|
-
test(
|
|
464
|
+
test("POST /v1/calls/:id/cancel returns 409 for already-ended call", async () => {
|
|
433
465
|
await startServer();
|
|
434
|
-
ensureConversation(
|
|
466
|
+
ensureConversation("conv-cancel-2");
|
|
435
467
|
|
|
436
468
|
const session = createCallSession({
|
|
437
|
-
conversationId:
|
|
438
|
-
provider:
|
|
439
|
-
fromNumber:
|
|
440
|
-
toNumber:
|
|
469
|
+
conversationId: "conv-cancel-2",
|
|
470
|
+
provider: "twilio",
|
|
471
|
+
fromNumber: "+15550001111",
|
|
472
|
+
toNumber: "+15559998888",
|
|
441
473
|
});
|
|
442
474
|
|
|
443
|
-
updateCallSession(session.id, { status:
|
|
475
|
+
updateCallSession(session.id, { status: "completed", endedAt: Date.now() });
|
|
444
476
|
|
|
445
477
|
const res = await fetch(callsUrl(`/${session.id}/cancel`), {
|
|
446
|
-
method:
|
|
447
|
-
headers: {
|
|
478
|
+
method: "POST",
|
|
479
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
448
480
|
body: JSON.stringify({}),
|
|
449
481
|
});
|
|
450
482
|
|
|
@@ -453,12 +485,12 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
453
485
|
await stopServer();
|
|
454
486
|
});
|
|
455
487
|
|
|
456
|
-
test(
|
|
488
|
+
test("POST /v1/calls/:id/cancel returns 404 for unknown session", async () => {
|
|
457
489
|
await startServer();
|
|
458
490
|
|
|
459
|
-
const res = await fetch(callsUrl(
|
|
460
|
-
method:
|
|
461
|
-
headers: {
|
|
491
|
+
const res = await fetch(callsUrl("/nonexistent-id/cancel"), {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
462
494
|
body: JSON.stringify({}),
|
|
463
495
|
});
|
|
464
496
|
|
|
@@ -469,69 +501,73 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
469
501
|
|
|
470
502
|
// ── POST /v1/calls/:id/answer ──────────────────────────────────────
|
|
471
503
|
|
|
472
|
-
test(
|
|
504
|
+
test("POST /v1/calls/:id/answer returns 400 for malformed JSON", async () => {
|
|
473
505
|
await startServer();
|
|
474
|
-
ensureConversation(
|
|
506
|
+
ensureConversation("conv-answer-badjson");
|
|
475
507
|
|
|
476
508
|
const session = createCallSession({
|
|
477
|
-
conversationId:
|
|
478
|
-
provider:
|
|
479
|
-
fromNumber:
|
|
480
|
-
toNumber:
|
|
509
|
+
conversationId: "conv-answer-badjson",
|
|
510
|
+
provider: "twilio",
|
|
511
|
+
fromNumber: "+15550001111",
|
|
512
|
+
toNumber: "+15559998888",
|
|
481
513
|
});
|
|
482
514
|
|
|
483
515
|
const res = await fetch(callsUrl(`/${session.id}/answer`), {
|
|
484
|
-
method:
|
|
485
|
-
headers: {
|
|
486
|
-
body:
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
518
|
+
body: "not-json{{",
|
|
487
519
|
});
|
|
488
520
|
|
|
489
521
|
expect(res.status).toBe(400);
|
|
490
|
-
const body = await res.json() as {
|
|
491
|
-
|
|
522
|
+
const body = (await res.json()) as {
|
|
523
|
+
error: { message: string; code?: string };
|
|
524
|
+
};
|
|
525
|
+
expect(body.error.message).toContain("Invalid JSON");
|
|
492
526
|
|
|
493
527
|
await stopServer();
|
|
494
528
|
});
|
|
495
529
|
|
|
496
|
-
test(
|
|
530
|
+
test("POST /v1/calls/:id/answer returns 404 when no pending question", async () => {
|
|
497
531
|
await startServer();
|
|
498
|
-
ensureConversation(
|
|
532
|
+
ensureConversation("conv-answer-1");
|
|
499
533
|
|
|
500
534
|
const session = createCallSession({
|
|
501
|
-
conversationId:
|
|
502
|
-
provider:
|
|
503
|
-
fromNumber:
|
|
504
|
-
toNumber:
|
|
535
|
+
conversationId: "conv-answer-1",
|
|
536
|
+
provider: "twilio",
|
|
537
|
+
fromNumber: "+15550001111",
|
|
538
|
+
toNumber: "+15559998888",
|
|
505
539
|
});
|
|
506
540
|
|
|
507
541
|
const res = await fetch(callsUrl(`/${session.id}/answer`), {
|
|
508
|
-
method:
|
|
509
|
-
headers: {
|
|
510
|
-
body: JSON.stringify({ answer:
|
|
542
|
+
method: "POST",
|
|
543
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
544
|
+
body: JSON.stringify({ answer: "Yes, please" }),
|
|
511
545
|
});
|
|
512
546
|
|
|
513
547
|
expect(res.status).toBe(409);
|
|
514
|
-
const body = await res.json() as {
|
|
515
|
-
|
|
548
|
+
const body = (await res.json()) as {
|
|
549
|
+
error: { message: string; code?: string };
|
|
550
|
+
};
|
|
551
|
+
expect(body.error.message).toContain("No active controller");
|
|
516
552
|
|
|
517
553
|
await stopServer();
|
|
518
554
|
});
|
|
519
555
|
|
|
520
|
-
test(
|
|
556
|
+
test("POST /v1/calls/:id/answer returns 400 when answer is empty", async () => {
|
|
521
557
|
await startServer();
|
|
522
|
-
ensureConversation(
|
|
558
|
+
ensureConversation("conv-answer-2");
|
|
523
559
|
|
|
524
560
|
const session = createCallSession({
|
|
525
|
-
conversationId:
|
|
526
|
-
provider:
|
|
527
|
-
fromNumber:
|
|
528
|
-
toNumber:
|
|
561
|
+
conversationId: "conv-answer-2",
|
|
562
|
+
provider: "twilio",
|
|
563
|
+
fromNumber: "+15550001111",
|
|
564
|
+
toNumber: "+15559998888",
|
|
529
565
|
});
|
|
530
566
|
|
|
531
567
|
const res = await fetch(callsUrl(`/${session.id}/answer`), {
|
|
532
|
-
method:
|
|
533
|
-
headers: {
|
|
534
|
-
body: JSON.stringify({ answer:
|
|
568
|
+
method: "POST",
|
|
569
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
570
|
+
body: JSON.stringify({ answer: "" }),
|
|
535
571
|
});
|
|
536
572
|
|
|
537
573
|
expect(res.status).toBe(400);
|
|
@@ -539,169 +575,183 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
539
575
|
await stopServer();
|
|
540
576
|
});
|
|
541
577
|
|
|
542
|
-
test(
|
|
578
|
+
test("POST /v1/calls/:id/answer returns 409 when no orchestrator", async () => {
|
|
543
579
|
await startServer();
|
|
544
|
-
ensureConversation(
|
|
580
|
+
ensureConversation("conv-answer-3");
|
|
545
581
|
|
|
546
582
|
const session = createCallSession({
|
|
547
|
-
conversationId:
|
|
548
|
-
provider:
|
|
549
|
-
fromNumber:
|
|
550
|
-
toNumber:
|
|
583
|
+
conversationId: "conv-answer-3",
|
|
584
|
+
provider: "twilio",
|
|
585
|
+
fromNumber: "+15550001111",
|
|
586
|
+
toNumber: "+15559998888",
|
|
551
587
|
});
|
|
552
588
|
|
|
553
589
|
// Create a pending question but no orchestrator
|
|
554
|
-
createPendingQuestion(session.id,
|
|
590
|
+
createPendingQuestion(session.id, "What date do you prefer?");
|
|
555
591
|
|
|
556
592
|
const res = await fetch(callsUrl(`/${session.id}/answer`), {
|
|
557
|
-
method:
|
|
558
|
-
headers: {
|
|
559
|
-
body: JSON.stringify({ answer:
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
595
|
+
body: JSON.stringify({ answer: "Tomorrow" }),
|
|
560
596
|
});
|
|
561
597
|
|
|
562
598
|
expect(res.status).toBe(409);
|
|
563
|
-
const body = await res.json() as {
|
|
564
|
-
|
|
599
|
+
const body = (await res.json()) as {
|
|
600
|
+
error: { message: string; code?: string };
|
|
601
|
+
};
|
|
602
|
+
expect(body.error.message).toContain("No active controller");
|
|
565
603
|
|
|
566
604
|
await stopServer();
|
|
567
605
|
});
|
|
568
606
|
|
|
569
607
|
// ── POST /v1/calls/:id/instruction ────────────────────────────────
|
|
570
608
|
|
|
571
|
-
test(
|
|
609
|
+
test("POST /v1/calls/:id/instruction returns 400 for malformed JSON", async () => {
|
|
572
610
|
await startServer();
|
|
573
|
-
ensureConversation(
|
|
611
|
+
ensureConversation("conv-instr-badjson");
|
|
574
612
|
|
|
575
613
|
const session = createCallSession({
|
|
576
|
-
conversationId:
|
|
577
|
-
provider:
|
|
578
|
-
fromNumber:
|
|
579
|
-
toNumber:
|
|
614
|
+
conversationId: "conv-instr-badjson",
|
|
615
|
+
provider: "twilio",
|
|
616
|
+
fromNumber: "+15550001111",
|
|
617
|
+
toNumber: "+15559998888",
|
|
580
618
|
});
|
|
581
619
|
|
|
582
620
|
const res = await fetch(callsUrl(`/${session.id}/instruction`), {
|
|
583
|
-
method:
|
|
584
|
-
headers: {
|
|
585
|
-
body:
|
|
621
|
+
method: "POST",
|
|
622
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
623
|
+
body: "not-json{{",
|
|
586
624
|
});
|
|
587
625
|
|
|
588
626
|
expect(res.status).toBe(400);
|
|
589
|
-
const body = await res.json() as {
|
|
590
|
-
|
|
627
|
+
const body = (await res.json()) as {
|
|
628
|
+
error: { message: string; code?: string };
|
|
629
|
+
};
|
|
630
|
+
expect(body.error.message).toContain("Invalid JSON");
|
|
591
631
|
|
|
592
632
|
await stopServer();
|
|
593
633
|
});
|
|
594
634
|
|
|
595
|
-
test(
|
|
635
|
+
test("POST /v1/calls/:id/instruction returns 400 when instruction is empty", async () => {
|
|
596
636
|
await startServer();
|
|
597
|
-
ensureConversation(
|
|
637
|
+
ensureConversation("conv-instr-empty");
|
|
598
638
|
|
|
599
639
|
const session = createCallSession({
|
|
600
|
-
conversationId:
|
|
601
|
-
provider:
|
|
602
|
-
fromNumber:
|
|
603
|
-
toNumber:
|
|
640
|
+
conversationId: "conv-instr-empty",
|
|
641
|
+
provider: "twilio",
|
|
642
|
+
fromNumber: "+15550001111",
|
|
643
|
+
toNumber: "+15559998888",
|
|
604
644
|
});
|
|
605
645
|
|
|
606
646
|
const res = await fetch(callsUrl(`/${session.id}/instruction`), {
|
|
607
|
-
method:
|
|
608
|
-
headers: {
|
|
609
|
-
body: JSON.stringify({ instruction:
|
|
647
|
+
method: "POST",
|
|
648
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
649
|
+
body: JSON.stringify({ instruction: "" }),
|
|
610
650
|
});
|
|
611
651
|
|
|
612
652
|
expect(res.status).toBe(400);
|
|
613
|
-
const body = await res.json() as {
|
|
614
|
-
|
|
653
|
+
const body = (await res.json()) as {
|
|
654
|
+
error: { message: string; code?: string };
|
|
655
|
+
};
|
|
656
|
+
expect(body.error.message).toContain("instructionText");
|
|
615
657
|
|
|
616
658
|
await stopServer();
|
|
617
659
|
});
|
|
618
660
|
|
|
619
|
-
test(
|
|
661
|
+
test("POST /v1/calls/:id/instruction returns 400 when instruction field is missing", async () => {
|
|
620
662
|
await startServer();
|
|
621
|
-
ensureConversation(
|
|
663
|
+
ensureConversation("conv-instr-missing");
|
|
622
664
|
|
|
623
665
|
const session = createCallSession({
|
|
624
|
-
conversationId:
|
|
625
|
-
provider:
|
|
626
|
-
fromNumber:
|
|
627
|
-
toNumber:
|
|
666
|
+
conversationId: "conv-instr-missing",
|
|
667
|
+
provider: "twilio",
|
|
668
|
+
fromNumber: "+15550001111",
|
|
669
|
+
toNumber: "+15559998888",
|
|
628
670
|
});
|
|
629
671
|
|
|
630
672
|
const res = await fetch(callsUrl(`/${session.id}/instruction`), {
|
|
631
|
-
method:
|
|
632
|
-
headers: {
|
|
673
|
+
method: "POST",
|
|
674
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
633
675
|
body: JSON.stringify({}),
|
|
634
676
|
});
|
|
635
677
|
|
|
636
678
|
expect(res.status).toBe(400);
|
|
637
|
-
const body = await res.json() as {
|
|
638
|
-
|
|
679
|
+
const body = (await res.json()) as {
|
|
680
|
+
error: { message: string; code?: string };
|
|
681
|
+
};
|
|
682
|
+
expect(body.error.message).toContain("instructionText");
|
|
639
683
|
|
|
640
684
|
await stopServer();
|
|
641
685
|
});
|
|
642
686
|
|
|
643
|
-
test(
|
|
687
|
+
test("POST /v1/calls/:id/instruction returns 404 for unknown session", async () => {
|
|
644
688
|
await startServer();
|
|
645
689
|
|
|
646
|
-
const res = await fetch(callsUrl(
|
|
647
|
-
method:
|
|
648
|
-
headers: {
|
|
649
|
-
body: JSON.stringify({ instruction:
|
|
690
|
+
const res = await fetch(callsUrl("/nonexistent-id/instruction"), {
|
|
691
|
+
method: "POST",
|
|
692
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
693
|
+
body: JSON.stringify({ instruction: "Speed things up" }),
|
|
650
694
|
});
|
|
651
695
|
|
|
652
696
|
expect(res.status).toBe(404);
|
|
653
|
-
const body = await res.json() as {
|
|
654
|
-
|
|
697
|
+
const body = (await res.json()) as {
|
|
698
|
+
error: { message: string; code?: string };
|
|
699
|
+
};
|
|
700
|
+
expect(body.error.message).toContain("No call session found");
|
|
655
701
|
|
|
656
702
|
await stopServer();
|
|
657
703
|
});
|
|
658
704
|
|
|
659
|
-
test(
|
|
705
|
+
test("POST /v1/calls/:id/instruction returns 409 for ended call", async () => {
|
|
660
706
|
await startServer();
|
|
661
|
-
ensureConversation(
|
|
707
|
+
ensureConversation("conv-instr-ended");
|
|
662
708
|
|
|
663
709
|
const session = createCallSession({
|
|
664
|
-
conversationId:
|
|
665
|
-
provider:
|
|
666
|
-
fromNumber:
|
|
667
|
-
toNumber:
|
|
710
|
+
conversationId: "conv-instr-ended",
|
|
711
|
+
provider: "twilio",
|
|
712
|
+
fromNumber: "+15550001111",
|
|
713
|
+
toNumber: "+15559998888",
|
|
668
714
|
});
|
|
669
715
|
|
|
670
|
-
updateCallSession(session.id, { status:
|
|
716
|
+
updateCallSession(session.id, { status: "completed", endedAt: Date.now() });
|
|
671
717
|
|
|
672
718
|
const res = await fetch(callsUrl(`/${session.id}/instruction`), {
|
|
673
|
-
method:
|
|
674
|
-
headers: {
|
|
675
|
-
body: JSON.stringify({ instruction:
|
|
719
|
+
method: "POST",
|
|
720
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
721
|
+
body: JSON.stringify({ instruction: "Speed things up" }),
|
|
676
722
|
});
|
|
677
723
|
|
|
678
724
|
expect(res.status).toBe(409);
|
|
679
|
-
const body = await res.json() as {
|
|
680
|
-
|
|
725
|
+
const body = (await res.json()) as {
|
|
726
|
+
error: { message: string; code?: string };
|
|
727
|
+
};
|
|
728
|
+
expect(body.error.message).toContain("not active");
|
|
681
729
|
|
|
682
730
|
await stopServer();
|
|
683
731
|
});
|
|
684
732
|
|
|
685
|
-
test(
|
|
733
|
+
test("POST /v1/calls/:id/instruction returns 409 when no orchestrator", async () => {
|
|
686
734
|
await startServer();
|
|
687
|
-
ensureConversation(
|
|
735
|
+
ensureConversation("conv-instr-no-orch");
|
|
688
736
|
|
|
689
737
|
const session = createCallSession({
|
|
690
|
-
conversationId:
|
|
691
|
-
provider:
|
|
692
|
-
fromNumber:
|
|
693
|
-
toNumber:
|
|
738
|
+
conversationId: "conv-instr-no-orch",
|
|
739
|
+
provider: "twilio",
|
|
740
|
+
fromNumber: "+15550001111",
|
|
741
|
+
toNumber: "+15559998888",
|
|
694
742
|
});
|
|
695
743
|
|
|
696
744
|
const res = await fetch(callsUrl(`/${session.id}/instruction`), {
|
|
697
|
-
method:
|
|
698
|
-
headers: {
|
|
699
|
-
body: JSON.stringify({ instruction:
|
|
745
|
+
method: "POST",
|
|
746
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
|
|
747
|
+
body: JSON.stringify({ instruction: "Speed things up" }),
|
|
700
748
|
});
|
|
701
749
|
|
|
702
750
|
expect(res.status).toBe(409);
|
|
703
|
-
const body = await res.json() as {
|
|
704
|
-
|
|
751
|
+
const body = (await res.json()) as {
|
|
752
|
+
error: { message: string; code?: string };
|
|
753
|
+
};
|
|
754
|
+
expect(body.error.message).toContain("No active controller");
|
|
705
755
|
|
|
706
756
|
await stopServer();
|
|
707
757
|
});
|