@tellescope/sdk 1.256.0 → 1.256.1

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.
Files changed (36) hide show
  1. package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.d.ts +6 -0
  2. package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.d.ts.map +1 -0
  3. package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.js +224 -0
  4. package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.js.map +1 -0
  5. package/lib/cjs/tests/api_tests/start_ai_conversation.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/start_ai_conversation.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/start_ai_conversation.test.js +351 -0
  8. package/lib/cjs/tests/api_tests/start_ai_conversation.test.js.map +1 -0
  9. package/lib/cjs/tests/tests.d.ts.map +1 -1
  10. package/lib/cjs/tests/tests.js +201 -185
  11. package/lib/cjs/tests/tests.js.map +1 -1
  12. package/lib/esm/enduser.d.ts +1 -0
  13. package/lib/esm/enduser.d.ts.map +1 -1
  14. package/lib/esm/sdk.d.ts +3 -2
  15. package/lib/esm/sdk.d.ts.map +1 -1
  16. package/lib/esm/session.d.ts +1 -0
  17. package/lib/esm/session.d.ts.map +1 -1
  18. package/lib/esm/tests/api_tests/beluga_trigger_refill.test.d.ts +6 -0
  19. package/lib/esm/tests/api_tests/beluga_trigger_refill.test.d.ts.map +1 -0
  20. package/lib/esm/tests/api_tests/beluga_trigger_refill.test.js +220 -0
  21. package/lib/esm/tests/api_tests/beluga_trigger_refill.test.js.map +1 -0
  22. package/lib/esm/tests/api_tests/start_ai_conversation.test.d.ts +6 -0
  23. package/lib/esm/tests/api_tests/start_ai_conversation.test.d.ts.map +1 -0
  24. package/lib/esm/tests/api_tests/start_ai_conversation.test.js +347 -0
  25. package/lib/esm/tests/api_tests/start_ai_conversation.test.js.map +1 -0
  26. package/lib/esm/tests/tests.d.ts.map +1 -1
  27. package/lib/esm/tests/tests.js +201 -185
  28. package/lib/esm/tests/tests.js.map +1 -1
  29. package/lib/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +8 -8
  31. package/src/tests/api_tests/beluga_trigger_refill.test.ts +152 -0
  32. package/src/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.ts +292 -0
  33. package/src/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.ts +179 -0
  34. package/src/tests/api_tests/start_ai_conversation.test.ts +256 -0
  35. package/src/tests/tests.ts +8 -0
  36. package/test_generated.pdf +0 -0
@@ -0,0 +1,256 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import { log_header, wait, async_test, assert } from "@tellescope/testing"
5
+ import { setup_tests } from "../setup"
6
+
7
+ const host = process.env.API_URL || "http://localhost:8080"
8
+
9
+ const TEST_MARKER = '__TELLESCOPE_TEST_INCOMING_SMS_AUTOMATION__'
10
+ const TEST_PHONE = '+15555555555' // send_sms short-circuits real sends for this number
11
+
12
+ // SMS AI conversations (startAIConversation journey action).
13
+ // Runtime flows (start/supersede/takeover/inbound absorption) require the organization to have an
14
+ // SMS number configured — when it doesn't (typical dev org), those sections assert the
15
+ // configuration error path and are otherwise skipped; config validation runs everywhere.
16
+ export const start_ai_conversation_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
17
+ log_header("Start AI Conversation (SMS Agent) Tests")
18
+
19
+ const users = await sdk.api.users.getSome({})
20
+ const senderId = users[0].id
21
+
22
+ const journey = await sdk.api.journeys.createOne({ title: 'SMS Agent Test Journey' })
23
+ const createdEnduserIds: string[] = []
24
+
25
+ try {
26
+ /*************** Config validation round-trip ***************/
27
+ const validInfo = {
28
+ channel: 'SMS' as const,
29
+ senderId,
30
+ initialMessage: 'Hello {{enduser.fname}}! Quick question for you.',
31
+ prompt: 'You are texting on behalf of Test Clinic. Answer briefly.',
32
+ outcomes: [{ value: 'resolved', description: 'The question was answered' }],
33
+ }
34
+
35
+ const step = await sdk.api.automation_steps.createOne({
36
+ journeyId: journey.id,
37
+ events: [{ type: 'onJourneyStart', info: {} }],
38
+ action: { type: 'startAIConversation', info: validInfo },
39
+ })
40
+ assert(
41
+ step.action.type === 'startAIConversation' && step.action.info.initialMessage === validInfo.initialMessage,
42
+ 'startAIConversation step round-trip failed',
43
+ 'startAIConversation step round-trips',
44
+ )
45
+
46
+ const outcomeChild = await sdk.api.automation_steps.createOne({
47
+ journeyId: journey.id,
48
+ events: [{ type: 'onAIConversationOutcome', info: { automationStepId: step.id, outcome: 'resolved' } }],
49
+ action: { type: 'addEnduserTags', info: { tags: ['sms-agent-resolved'] } },
50
+ })
51
+ assert(
52
+ outcomeChild.events[0].type === 'onAIConversationOutcome',
53
+ 'onAIConversationOutcome event round-trip failed',
54
+ 'onAIConversationOutcome event round-trips',
55
+ )
56
+
57
+ await async_test('startAIConversation without prompt is rejected',
58
+ () => sdk.api.automation_steps.createOne({
59
+ journeyId: journey.id,
60
+ events: [{ type: 'onJourneyStart', info: {} }],
61
+ action: { type: 'startAIConversation', info: { ...validInfo, prompt: undefined } as any },
62
+ }),
63
+ { shouldError: true, onError: () => true },
64
+ )
65
+ await async_test('startAIConversation without senderId is rejected',
66
+ () => sdk.api.automation_steps.createOne({
67
+ journeyId: journey.id,
68
+ events: [{ type: 'onJourneyStart', info: {} }],
69
+ action: { type: 'startAIConversation', info: { ...validInfo, senderId: undefined } as any },
70
+ }),
71
+ { shouldError: true, onError: () => true },
72
+ )
73
+ await async_test('startAIConversation without initialMessage is rejected',
74
+ () => sdk.api.automation_steps.createOne({
75
+ journeyId: journey.id,
76
+ events: [{ type: 'onJourneyStart', info: {} }],
77
+ action: { type: 'startAIConversation', info: { ...validInfo, initialMessage: undefined } as any },
78
+ }),
79
+ { shouldError: true, onError: () => true },
80
+ )
81
+ await async_test('startAIConversation with invalid tool is rejected',
82
+ () => sdk.api.automation_steps.createOne({
83
+ journeyId: journey.id,
84
+ events: [{ type: 'onJourneyStart', info: {} }],
85
+ action: { type: 'startAIConversation', info: { ...validInfo, tools: [{ type: 'Submit Form', info: { formId: 'not-an-id' } }] } as any },
86
+ }),
87
+ { shouldError: true, onError: () => true },
88
+ )
89
+
90
+ /*************** Runtime: the process endpoint ***************/
91
+ const processFor = async (enduserId: string) => {
92
+ const automatedAction = await sdk.api.automated_actions.createOne({
93
+ enduserId,
94
+ journeyId: journey.id,
95
+ automationStepId: step.id,
96
+ event: { type: 'onJourneyStart', info: {} },
97
+ action: { type: 'startAIConversation', info: validInfo },
98
+ status: 'active',
99
+ processAfter: Date.now(),
100
+ })
101
+ return sdk.api.automated_actions.process({ automatedActionId: automatedAction.id })
102
+ }
103
+
104
+ // no-phone error path works regardless of org SMS configuration
105
+ const phonelessEnduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-nophone' })
106
+ createdEnduserIds.push(phonelessEnduser.id)
107
+ const noPhoneResult = await processFor(phonelessEnduser.id)
108
+ assert(
109
+ !noPhoneResult.success && !!noPhoneResult.error,
110
+ 'phoneless start should fail with an error',
111
+ 'start action errors for a contact without a phone',
112
+ )
113
+
114
+ // resolve the same organization the action does (last organizationId, else businessId) —
115
+ // getSome({})[0] is an arbitrary sub-organization and can be a different record entirely
116
+ const session = sdk.userInfo as { businessId?: string, organizationIds?: string[] }
117
+ const resolvedOrgId = [...(session.organizationIds ?? [])].pop() || session.businessId
118
+ const organization = (await sdk.api.organizations.getSome({})).find(o => o.id === resolvedOrgId)
119
+ const orgHasNumber = !!(organization as any)?.twilioNumber || !!((organization as any)?.twilioNumbers?.length)
120
+
121
+ if (!orgHasNumber) {
122
+ // still assert the wiring: the action processes and reports a configuration error rather than
123
+ // crashing. AI enablement is checked before the SMS number and short-circuits first, and
124
+ // bedrockAIAllowed/creditCount are super-admin-only (unreadable here), so accept either.
125
+ const enduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-nonumber', phone: TEST_PHONE })
126
+ createdEnduserIds.push(enduser.id)
127
+ const result = await processFor(enduser.id)
128
+ assert(
129
+ !result.success && /no sms number configured|ai is not enabled/i.test(result.error || ''),
130
+ `expected a configuration error, got: ${result.error}`,
131
+ 'start action reports a configuration error (org has no SMS number — runtime flows skipped)',
132
+ )
133
+ return
134
+ }
135
+
136
+ /*************** Runtime flows (org has an SMS number) ***************/
137
+ const enduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-runtime', phone: TEST_PHONE })
138
+ createdEnduserIds.push(enduser.id)
139
+
140
+ const startResult = await processFor(enduser.id)
141
+ assert(startResult.success === true, `start failed: ${startResult.error}`, 'start action succeeds')
142
+ await wait(undefined, 1000)
143
+
144
+ let refreshed = await sdk.api.endusers.getOne(enduser.id)
145
+ const entry = (refreshed.aiConversations ?? []).find(c => c.active)
146
+ assert(!!entry, 'no active aiConversations entry on enduser', 'enduser has an active conversation entry')
147
+ assert(entry?.destination === TEST_PHONE, 'wrong destination on entry', 'conversation entry pairs the contact number')
148
+
149
+ const conversation = await sdk.api.ai_conversations.getOne(entry!.aiConversationId)
150
+ assert(conversation.type === 'sms_agent', 'wrong ai_conversations type', 'ai_conversations doc has type sms_agent')
151
+
152
+ const initialMessages = await sdk.api.sms_messages.getSome({ filter: { enduserId: enduser.id } })
153
+ const outbound = initialMessages.find(m => !m.inbound && m.automationStepId === step.id)
154
+ // exact equality on a merge-field-containing opener guards two regressions: templating must
155
+ // happen in send_sms (raw text on the record), and the agent-reply {{ escaping must NOT be
156
+ // applied to the org-authored initial message (preserveMergeFields)
157
+ assert(
158
+ !!outbound && outbound.message === validInfo.initialMessage,
159
+ 'initial SMS record missing or not raw text',
160
+ 'initial SMS logged with raw (untemplated) text + automationStepId',
161
+ )
162
+
163
+ // supersede: second start on the same pairing replaces the first
164
+ const secondStart = await processFor(enduser.id)
165
+ assert(secondStart.success === true, `second start failed: ${secondStart.error}`, 'duplicate start succeeds (replace)')
166
+ await wait(undefined, 1000)
167
+ refreshed = await sdk.api.endusers.getOne(enduser.id)
168
+ const activeEntries = (refreshed.aiConversations ?? []).filter(c => c.active)
169
+ assert(activeEntries.length === 1, 'expected exactly one active conversation', 'supersede leaves exactly one active conversation')
170
+ assert(
171
+ activeEntries[0].aiConversationId !== entry!.aiConversationId,
172
+ 'active conversation was not replaced',
173
+ 'supersede started a fresh conversation',
174
+ )
175
+
176
+ // inbound absorption via the testing hook: STOP ends the conversation
177
+ await sdk.api.sms_messages.createOne({
178
+ enduserId: enduser.id,
179
+ message: `stop${TEST_MARKER}`, // marker is stripped before the AI branch sees the text
180
+ inbound: true,
181
+ logOnly: true,
182
+ phoneNumber: activeEntries[0].source,
183
+ enduserPhoneNumber: TEST_PHONE,
184
+ } as any)
185
+ await wait(undefined, 2000)
186
+ refreshed = await sdk.api.endusers.getOne(enduser.id)
187
+ const stopped = (refreshed.aiConversations ?? []).find(c => c.aiConversationId === activeEntries[0].aiConversationId)
188
+ assert(
189
+ stopped?.active === false && stopped?.endedReason === 'unsubscribed',
190
+ `STOP did not end the conversation (${JSON.stringify(stopped)})`,
191
+ 'STOP reply ends the conversation',
192
+ )
193
+
194
+ // staff takeover: start again, then a manual (user-session) SMS on the thread ends it
195
+ const takeoverStart = await processFor(enduser.id)
196
+ assert(takeoverStart.success === true, `takeover-start failed: ${takeoverStart.error}`, 'start (for takeover test) succeeds')
197
+ await wait(undefined, 1000)
198
+ await sdk.api.sms_messages.createOne({
199
+ enduserId: enduser.id,
200
+ message: 'Hi, this is a real person taking over.',
201
+ inbound: false,
202
+ phoneNumber: (await sdk.api.endusers.getOne(enduser.id)).aiConversations?.find(c => c.active)?.source,
203
+ enduserPhoneNumber: TEST_PHONE,
204
+ } as any)
205
+ await wait(undefined, 2000)
206
+ refreshed = await sdk.api.endusers.getOne(enduser.id)
207
+ const takenOver = (refreshed.aiConversations ?? []).find(c => c.endedReason === 'staff-takeover')
208
+ assert(!!takenOver, 'no staff-takeover ended entry found', 'manual staff SMS ends the conversation (takeover)')
209
+
210
+ // no-conversation baseline: marker inbound for a fresh enduser produces zero AI artifacts
211
+ const baselineEnduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-baseline', phone: '+15555555554' })
212
+ createdEnduserIds.push(baselineEnduser.id)
213
+ await sdk.api.sms_messages.createOne({
214
+ enduserId: baselineEnduser.id,
215
+ message: `hello there${TEST_MARKER}`,
216
+ inbound: true,
217
+ logOnly: true,
218
+ phoneNumber: activeEntries[0].source,
219
+ enduserPhoneNumber: '+15555555554',
220
+ } as any)
221
+ await wait(undefined, 2000)
222
+ const baselineRefreshed = await sdk.api.endusers.getOne(baselineEnduser.id)
223
+ const baselineMessages = await sdk.api.sms_messages.getSome({ filter: { enduserId: baselineEnduser.id } })
224
+ assert(
225
+ !(baselineRefreshed.aiConversations ?? []).length && !baselineMessages.find(m => !m.inbound),
226
+ 'AI artifacts appeared for an enduser with no conversation',
227
+ 'no-conversation inbound produces zero AI artifacts (regression baseline)',
228
+ )
229
+ } finally {
230
+ await sdk.api.journeys.deleteOne(journey.id).catch(() => {})
231
+ for (const id of createdEnduserIds) {
232
+ await sdk.api.endusers.deleteOne(id).catch(() => {})
233
+ }
234
+ }
235
+ }
236
+
237
+ // Allow running this test file independently
238
+ if (require.main === module) {
239
+ const sdk = new Session({ host })
240
+ const sdkNonAdmin = new Session({ host })
241
+
242
+ const runTests = async () => {
243
+ await setup_tests(sdk, sdkNonAdmin)
244
+ await start_ai_conversation_tests({ sdk, sdkNonAdmin })
245
+ }
246
+
247
+ runTests()
248
+ .then(() => {
249
+ console.log("✅ Start AI Conversation test suite completed successfully")
250
+ process.exit(0)
251
+ })
252
+ .catch((error) => {
253
+ console.error("❌ Start AI Conversation test suite failed:", error)
254
+ process.exit(1)
255
+ })
256
+ }
@@ -120,6 +120,8 @@ import { load_inbox_redaction_tests } from "./api_tests/security/F-0151-load-inb
120
120
  import { webhook_timeout_tests } from "./api_tests/security/F-0155-webhook-timeout.test";
121
121
  import { enduser_ai_summary_trust_gate_tests } from "./api_tests/security/enduser-ai-summary-trust-gate.test";
122
122
  import { allowed_paths_confinement_tests } from "./api_tests/security/F-0166-allowed-paths-wildcard-bypass.test";
123
+ import { proxy_image_ssrf_tests } from "./api_tests/security/F-0025-proxy-image-unauth-ssrf.test";
124
+ import { webhook_outbound_ssrf_tests } from "./api_tests/security/F-0015-webhook-outbound-ssrf.test";
123
125
  import { public_endpoint_auth_field_lock_tests } from "./api_tests/security/public_endpoint_auth_field_lock.test";
124
126
  import { formsort_webhook_auth_field_lock_tests } from "./api_tests/security/formsort_webhook_auth_field_lock.test";
125
127
  import { bulk_assignment_tests } from "./api_tests/bulk_assignment.test";
@@ -133,6 +135,7 @@ import { load_team_chat_tests } from "./api_tests/load_team_chat.test";
133
135
  import { form_started_trigger_tests } from "./api_tests/form_started_trigger.test";
134
136
  import { formsort_header_auth_tests } from "./api_tests/formsort_header_auth.test";
135
137
  import { form_submitted_trigger_tests } from "./api_tests/form_submitted_trigger.test";
138
+ import { start_ai_conversation_tests } from "./api_tests/start_ai_conversation.test";
136
139
  import { dont_sync_to_elation_form_submission_tests } from "./api_tests/dont_sync_to_elation_form_submission.test";
137
140
  import { medication_added_trigger_tests } from "./api_tests/medication_added_trigger.test";
138
141
  import { conditional_logic_medication_unit_tests } from "./unit_tests/conditional_logic_medication.test";
@@ -144,6 +147,7 @@ import { openloop_webhooks_tests } from "./api_tests/openloop_webhooks.test";
144
147
  import { beluga_pharmacy_mappings_tests } from "./api_tests/beluga_pharmacy_mappings.test";
145
148
  import { mdi_case_offerings_tests } from "./api_tests/mdi_case_offerings.test";
146
149
  import { beluga_manual_sync_tests } from "./api_tests/beluga_manual_sync.test";
150
+ import { beluga_trigger_refill_tests } from "./api_tests/beluga_trigger_refill.test";
147
151
  import { mdi_webhooks_tests } from "./api_tests/mdi_webhooks.test";
148
152
  import { account_switcher_tests } from "./api_tests/account_switcher.test";
149
153
  import { totp_mfa_tests } from "./api_tests/totp_mfa.test";
@@ -15274,6 +15278,8 @@ const ip_address_form_tests = async () => {
15274
15278
  await replace_form_field_template_values_tests()
15275
15279
  await mfa_tests()
15276
15280
  await setup_tests(sdk, sdkNonAdmin)
15281
+ await proxy_image_ssrf_tests({ sdk, sdkNonAdmin })
15282
+ await webhook_outbound_ssrf_tests({ sdk, sdkNonAdmin })
15277
15283
  await allowed_paths_confinement_tests({ sdk, sdkNonAdmin })
15278
15284
  await phone_calls_conference_hold_tests({ sdk, sdkNonAdmin })
15279
15285
  await tickets_bulk_assign_care_team_tests({ sdk, sdkNonAdmin })
@@ -15282,6 +15288,7 @@ const ip_address_form_tests = async () => {
15282
15288
  await automation_trigger_tests()
15283
15289
  await resource_access_tags_tests({ sdk, sdkNonAdmin })
15284
15290
  await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
15291
+ await beluga_trigger_refill_tests({ sdk, sdkNonAdmin })
15285
15292
  await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
15286
15293
  await mdi_case_offerings_tests({ sdk, sdkNonAdmin })
15287
15294
  await enduser_write_restrictions_tests({ sdk, sdkNonAdmin })
@@ -15320,6 +15327,7 @@ const ip_address_form_tests = async () => {
15320
15327
  await chats_analytics_tests({ sdk, sdkNonAdmin })
15321
15328
  await field_redaction_tests({ sdk, sdkNonAdmin })
15322
15329
  await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
15330
+ await start_ai_conversation_tests({ sdk, sdkNonAdmin })
15323
15331
  await dont_sync_to_elation_form_submission_tests({ sdk, sdkNonAdmin })
15324
15332
  await date_string_validation_tests({ sdk, sdkNonAdmin })
15325
15333
  await phone_tree_enduser_condition_tests({ sdk, sdkNonAdmin })
Binary file