@tellescope/sdk 1.255.19 → 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.
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.js +224 -0
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/custom_dashboards.test.js +99 -8
- package/lib/cjs/tests/api_tests/custom_dashboards.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/default_summary_journey.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js +278 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +212 -33
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +242 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +443 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +231 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +524 -294
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.js +351 -0
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +477 -260
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.js +220 -0
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.js.map +1 -0
- package/lib/esm/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/custom_dashboards.test.js +99 -8
- package/lib/esm/tests/api_tests/custom_dashboards.test.js.map +1 -1
- package/lib/esm/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/default_summary_journey.test.js.map +1 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.js +248 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
- package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js +213 -34
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +238 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +436 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +224 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +524 -294
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -1
- package/lib/esm/tests/api_tests/start_ai_conversation.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/start_ai_conversation.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/start_ai_conversation.test.js +347 -0
- package/lib/esm/tests/api_tests/start_ai_conversation.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +477 -260
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/beluga_trigger_refill.test.ts +152 -0
- package/src/tests/api_tests/custom_dashboards.test.ts +92 -0
- package/src/tests/api_tests/file_download_unicode_names.test.ts +223 -0
- package/src/tests/api_tests/openloop_webhooks.test.ts +138 -0
- package/src/tests/api_tests/phone_calls_cold_transfer.test.ts +54 -0
- package/src/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.ts +215 -0
- package/src/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.ts +292 -0
- package/src/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.ts +179 -0
- package/src/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.ts +215 -0
- package/src/tests/api_tests/start_ai_conversation.test.ts +256 -0
- package/src/tests/tests.ts +220 -1
- 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
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CompoundFilter,
|
|
18
18
|
CreateTicketActionInfo,
|
|
19
19
|
FormResponseValue,
|
|
20
|
+
JourneyContext,
|
|
20
21
|
ModelName,
|
|
21
22
|
RoundRobinAssignmentInfo,
|
|
22
23
|
User,
|
|
@@ -41,6 +42,7 @@ import { user_portal_settings_tests } from "./api_tests/user_portal_settings.tes
|
|
|
41
42
|
import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
|
|
42
43
|
import { healthie_multi_integration_tests } from "./api_tests/healthie_multi_integration.test"
|
|
43
44
|
import { get_some_projection_tests } from "./api_tests/get_some_projection.test"
|
|
45
|
+
import { file_download_unicode_names_tests } from "./api_tests/file_download_unicode_names.test"
|
|
44
46
|
import { mdb_sort_tests } from "./api_tests/mdb_sort.test"
|
|
45
47
|
import { create_user_notifications_trigger_tests } from "./api_tests/create_user_notifications_trigger.test"
|
|
46
48
|
import { inbox_thread_assignment_updates_tests } from "./api_tests/inbox_thread_assignment_updates.test"
|
|
@@ -110,6 +112,7 @@ import { invite_user_enumeration_tests } from "./api_tests/security/F-0007-invit
|
|
|
110
112
|
import { register_business_id_injection_tests } from "./api_tests/security/register_business_id_injection.test";
|
|
111
113
|
import { handle_incoming_communication_cross_tenant_tests } from "./api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test";
|
|
112
114
|
import { sanitize_user_html_xss_tests } from "./api_tests/security/F-0013-sanitize-user-html.test";
|
|
115
|
+
import { sanitize_user_html_with_iframes_tests } from "./api_tests/security/F-0013-sanitize-user-html-with-iframes.test";
|
|
113
116
|
import { prototype_pollution_tests } from "./api_tests/security/F-0016-prototype-pollution.test";
|
|
114
117
|
import { enduser_cross_create_tests } from "./api_tests/security/F-0116-F-0119-enduser-cross-create.test";
|
|
115
118
|
import { globalunique_leak_tests } from "./api_tests/security/F-0145-globalunique-leak.test";
|
|
@@ -117,6 +120,8 @@ import { load_inbox_redaction_tests } from "./api_tests/security/F-0151-load-inb
|
|
|
117
120
|
import { webhook_timeout_tests } from "./api_tests/security/F-0155-webhook-timeout.test";
|
|
118
121
|
import { enduser_ai_summary_trust_gate_tests } from "./api_tests/security/enduser-ai-summary-trust-gate.test";
|
|
119
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";
|
|
120
125
|
import { public_endpoint_auth_field_lock_tests } from "./api_tests/security/public_endpoint_auth_field_lock.test";
|
|
121
126
|
import { formsort_webhook_auth_field_lock_tests } from "./api_tests/security/formsort_webhook_auth_field_lock.test";
|
|
122
127
|
import { bulk_assignment_tests } from "./api_tests/bulk_assignment.test";
|
|
@@ -130,6 +135,7 @@ import { load_team_chat_tests } from "./api_tests/load_team_chat.test";
|
|
|
130
135
|
import { form_started_trigger_tests } from "./api_tests/form_started_trigger.test";
|
|
131
136
|
import { formsort_header_auth_tests } from "./api_tests/formsort_header_auth.test";
|
|
132
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";
|
|
133
139
|
import { dont_sync_to_elation_form_submission_tests } from "./api_tests/dont_sync_to_elation_form_submission.test";
|
|
134
140
|
import { medication_added_trigger_tests } from "./api_tests/medication_added_trigger.test";
|
|
135
141
|
import { conditional_logic_medication_unit_tests } from "./unit_tests/conditional_logic_medication.test";
|
|
@@ -141,6 +147,7 @@ import { openloop_webhooks_tests } from "./api_tests/openloop_webhooks.test";
|
|
|
141
147
|
import { beluga_pharmacy_mappings_tests } from "./api_tests/beluga_pharmacy_mappings.test";
|
|
142
148
|
import { mdi_case_offerings_tests } from "./api_tests/mdi_case_offerings.test";
|
|
143
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";
|
|
144
151
|
import { mdi_webhooks_tests } from "./api_tests/mdi_webhooks.test";
|
|
145
152
|
import { account_switcher_tests } from "./api_tests/account_switcher.test";
|
|
146
153
|
import { totp_mfa_tests } from "./api_tests/totp_mfa.test";
|
|
@@ -7951,9 +7958,49 @@ const merge_enduser_tests = async () => {
|
|
|
7951
7958
|
{ onResult: e => e.attendees.length === 1 && !!e.attendees?.find(a => a.id === destination.id) }
|
|
7952
7959
|
)
|
|
7953
7960
|
|
|
7961
|
+
// merging two endusers that both lack insurance must leave the fields absent, NOT null.
|
|
7962
|
+
// a stored null breaks every later partial update: those get flattened to dotted paths
|
|
7963
|
+
// ('insurance.memberId'), and MongoDB rejects a dotted $set under a null parent
|
|
7964
|
+
const [noInsuranceSource, noInsuranceDestination] = (await sdk.api.endusers.createSome([
|
|
7965
|
+
{ email: 'noins-source@tellescope.com', fname: 'no', lname: 'insurance' },
|
|
7966
|
+
{ email: 'noins-destination@tellescope.com' },
|
|
7967
|
+
])).created
|
|
7968
|
+
|
|
7969
|
+
await sdk.api.endusers.merge({
|
|
7970
|
+
sourceEnduserId: noInsuranceSource.id,
|
|
7971
|
+
destinationEnduserId: noInsuranceDestination.id,
|
|
7972
|
+
})
|
|
7973
|
+
|
|
7974
|
+
await async_test(
|
|
7975
|
+
"Merged endusers have no null insurance",
|
|
7976
|
+
() => sdk.api.endusers.getOne(noInsuranceDestination.id),
|
|
7977
|
+
{ onResult: e => e.insurance === undefined && e.insuranceSecondary === undefined }
|
|
7978
|
+
)
|
|
7979
|
+
|
|
7980
|
+
await async_test(
|
|
7981
|
+
"Partial insurance update applies after merge",
|
|
7982
|
+
async () => {
|
|
7983
|
+
await sdk.api.endusers.updateOne(noInsuranceDestination.id, {
|
|
7984
|
+
insurance: { memberId: 'M1', payerName: 'Test Payer' },
|
|
7985
|
+
}) // no replaceObjectFields, matching the standard update from the UI
|
|
7986
|
+
return await sdk.api.endusers.getOne(noInsuranceDestination.id)
|
|
7987
|
+
},
|
|
7988
|
+
{ onResult: e => e.insurance?.memberId === 'M1' && e.insurance?.payerName === 'Test Payer' }
|
|
7989
|
+
)
|
|
7990
|
+
|
|
7991
|
+
await async_test(
|
|
7992
|
+
"Second partial insurance update merges",
|
|
7993
|
+
async () => {
|
|
7994
|
+
await sdk.api.endusers.updateOne(noInsuranceDestination.id, { insurance: { groupNumber: 'G1' } })
|
|
7995
|
+
return await sdk.api.endusers.getOne(noInsuranceDestination.id)
|
|
7996
|
+
},
|
|
7997
|
+
{ onResult: e => e.insurance?.groupNumber === 'G1' && e.insurance?.memberId === 'M1' }
|
|
7998
|
+
)
|
|
7999
|
+
|
|
7954
8000
|
await Promise.all([
|
|
7955
8001
|
sdk.api.endusers.deleteOne(destination.id),
|
|
7956
8002
|
sdk.api.endusers.deleteOne(otherEnduser.id),
|
|
8003
|
+
sdk.api.endusers.deleteOne(noInsuranceDestination.id),
|
|
7957
8004
|
sdk.api.calendar_events.deleteOne(event.id),
|
|
7958
8005
|
sdk.api.calendar_events.deleteOne(eventToMove.id),
|
|
7959
8006
|
])
|
|
@@ -8587,6 +8634,8 @@ const test_ticket_automation_assignment_and_optimization = async () => {
|
|
|
8587
8634
|
enduser,
|
|
8588
8635
|
closedForReason,
|
|
8589
8636
|
testDelayedChild,
|
|
8637
|
+
calendarEventId,
|
|
8638
|
+
ticketOwner,
|
|
8590
8639
|
} : {
|
|
8591
8640
|
assignedTo: string[]
|
|
8592
8641
|
info: Pick<CreateTicketActionInfo, 'assignmentStrategy' | 'defaultAssignee'>,
|
|
@@ -8594,6 +8643,8 @@ const test_ticket_automation_assignment_and_optimization = async () => {
|
|
|
8594
8643
|
closedForReason?: string,
|
|
8595
8644
|
enduser?: Enduser,
|
|
8596
8645
|
testDelayedChild?: boolean,
|
|
8646
|
+
calendarEventId?: string, // copied into journeyContext by close_ticket
|
|
8647
|
+
ticketOwner?: string, // override the seed ticket's owner, to avoid masking a previous-owner fallback
|
|
8597
8648
|
}) => {
|
|
8598
8649
|
|
|
8599
8650
|
const e = enduser || await sdk.api.endusers.createOne({ assignedTo, journeys: { [journey.id]: '' }, fields: { Tag: "tag3"} })
|
|
@@ -8631,8 +8682,9 @@ const test_ticket_automation_assignment_and_optimization = async () => {
|
|
|
8631
8682
|
enduserId: e.id,
|
|
8632
8683
|
automationStepId: PLACEHOLDER_ID,
|
|
8633
8684
|
journeyId: journey.id,
|
|
8634
|
-
owner: validOwners[0],
|
|
8685
|
+
owner: ticketOwner ?? validOwners[0],
|
|
8635
8686
|
closedForReason,
|
|
8687
|
+
...(calendarEventId ? { calendarEventId } : {}),
|
|
8636
8688
|
})
|
|
8637
8689
|
|
|
8638
8690
|
await async_test(
|
|
@@ -8765,11 +8817,13 @@ const test_ticket_automation_assignment_and_optimization = async () => {
|
|
|
8765
8817
|
info,
|
|
8766
8818
|
validOwners,
|
|
8767
8819
|
enduser,
|
|
8820
|
+
journeyContext,
|
|
8768
8821
|
} : {
|
|
8769
8822
|
assignedTo: string[]
|
|
8770
8823
|
info: Pick<CreateTicketActionInfo, 'assignmentStrategy' | 'defaultAssignee'>,
|
|
8771
8824
|
validOwners: string[],
|
|
8772
8825
|
enduser?: Enduser,
|
|
8826
|
+
journeyContext?: JourneyContext,
|
|
8773
8827
|
}) => {
|
|
8774
8828
|
const e = enduser || await sdk.api.endusers.createOne({ assignedTo, fields: { Tag: 'tag3' } })
|
|
8775
8829
|
await sdk.api.automated_actions.createOne({
|
|
@@ -8780,6 +8834,7 @@ const test_ticket_automation_assignment_and_optimization = async () => {
|
|
|
8780
8834
|
journeyId: journey.id,
|
|
8781
8835
|
status: 'active',
|
|
8782
8836
|
processAfter: Date.now(),
|
|
8837
|
+
...(journeyContext ? { journeyContext } : {}),
|
|
8783
8838
|
})
|
|
8784
8839
|
|
|
8785
8840
|
await async_test(
|
|
@@ -8880,6 +8935,115 @@ const test_ticket_automation_assignment_and_optimization = async () => {
|
|
|
8880
8935
|
validOwners: [sdkNonAdmin.userInfo.id],
|
|
8881
8936
|
})
|
|
8882
8937
|
|
|
8938
|
+
// 'Recently-Booked Appointment Host' assignment
|
|
8939
|
+
// journeyContext.calendarEventId, when it resolves to a host, takes priority over the legacy
|
|
8940
|
+
// "newest event for this enduser" lookup. one enduser per case, to avoid trigger throttling.
|
|
8941
|
+
const HOST_A = sdkNonAdmin.userInfo.id
|
|
8942
|
+
const HOST_B = sdk.userInfo.id
|
|
8943
|
+
const appointmentHostInfo = (defaultAssignee: string) => ({
|
|
8944
|
+
assignmentStrategy: { type: 'Recently-Booked Appointment Host', info: {} } as const,
|
|
8945
|
+
defaultAssignee,
|
|
8946
|
+
})
|
|
8947
|
+
// the legacy filter matches an event having *some* user attendee and *some* attendee with this
|
|
8948
|
+
// enduser's id, so each hosted event carries both
|
|
8949
|
+
const createHostedEvent = (enduserId: string, hostId: string) => sdk.api.calendar_events.createOne({
|
|
8950
|
+
title: 'appointment host test',
|
|
8951
|
+
durationInMinutes: 30,
|
|
8952
|
+
startTimeInMS: Date.now(),
|
|
8953
|
+
attendees: [{ type: 'enduser', id: enduserId }, { type: 'user', id: hostId }],
|
|
8954
|
+
})
|
|
8955
|
+
|
|
8956
|
+
// CORE PROOF: two appointments with different hosts, journey context names the OLDER one.
|
|
8957
|
+
// fails before this fix, which always took the newest-created event (HOST_B).
|
|
8958
|
+
{
|
|
8959
|
+
const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
|
|
8960
|
+
const older = await createHostedEvent(e.id, HOST_A)
|
|
8961
|
+
const newer = await createHostedEvent(e.id, HOST_B) // higher _id, so legacy sort picks this one
|
|
8962
|
+
await testBackgroundTicket({
|
|
8963
|
+
assignedTo: [],
|
|
8964
|
+
enduser: e,
|
|
8965
|
+
info: appointmentHostInfo(HOST_B), // distinct from expected owner, so a fallback would fail
|
|
8966
|
+
validOwners: [HOST_A],
|
|
8967
|
+
journeyContext: { calendarEventId: older.id },
|
|
8968
|
+
})
|
|
8969
|
+
await Promise.all([older, newer].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
|
|
8970
|
+
}
|
|
8971
|
+
|
|
8972
|
+
// no journey context -> legacy "newest event" behavior is unchanged
|
|
8973
|
+
{
|
|
8974
|
+
const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
|
|
8975
|
+
const older = await createHostedEvent(e.id, HOST_A)
|
|
8976
|
+
const newer = await createHostedEvent(e.id, HOST_B)
|
|
8977
|
+
await testBackgroundTicket({
|
|
8978
|
+
assignedTo: [],
|
|
8979
|
+
enduser: e,
|
|
8980
|
+
info: appointmentHostInfo(HOST_A),
|
|
8981
|
+
validOwners: [HOST_B], // newest created event's host
|
|
8982
|
+
})
|
|
8983
|
+
await Promise.all([older, newer].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
|
|
8984
|
+
}
|
|
8985
|
+
|
|
8986
|
+
// context event no longer exists -> falls back to the legacy lookup
|
|
8987
|
+
{
|
|
8988
|
+
const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
|
|
8989
|
+
const deleted = await createHostedEvent(e.id, HOST_B)
|
|
8990
|
+
await sdk.api.calendar_events.deleteOne(deleted.id)
|
|
8991
|
+
const hosted = await createHostedEvent(e.id, HOST_A)
|
|
8992
|
+
await testBackgroundTicket({
|
|
8993
|
+
assignedTo: [],
|
|
8994
|
+
enduser: e,
|
|
8995
|
+
info: appointmentHostInfo(HOST_B),
|
|
8996
|
+
validOwners: [HOST_A],
|
|
8997
|
+
journeyContext: { calendarEventId: deleted.id },
|
|
8998
|
+
})
|
|
8999
|
+
await sdk.api.calendar_events.deleteOne(hosted.id)
|
|
9000
|
+
}
|
|
9001
|
+
|
|
9002
|
+
// context event has no user attendee (so yields no host) -> falls back to the legacy lookup
|
|
9003
|
+
{
|
|
9004
|
+
const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
|
|
9005
|
+
const enduserOnly = await sdk.api.calendar_events.createOne({
|
|
9006
|
+
title: 'appointment host test, no host',
|
|
9007
|
+
durationInMinutes: 30,
|
|
9008
|
+
startTimeInMS: Date.now(),
|
|
9009
|
+
attendees: [{ type: 'enduser', id: e.id }],
|
|
9010
|
+
})
|
|
9011
|
+
const hosted = await createHostedEvent(e.id, HOST_A)
|
|
9012
|
+
await testBackgroundTicket({
|
|
9013
|
+
assignedTo: [],
|
|
9014
|
+
enduser: e,
|
|
9015
|
+
info: appointmentHostInfo(HOST_B),
|
|
9016
|
+
validOwners: [HOST_A],
|
|
9017
|
+
journeyContext: { calendarEventId: enduserOnly.id },
|
|
9018
|
+
})
|
|
9019
|
+
await Promise.all([enduserOnly, hosted].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
|
|
9020
|
+
}
|
|
9021
|
+
|
|
9022
|
+
// no events at all -> no resolvable host, so defaultAssignee wins
|
|
9023
|
+
await testBackgroundTicket({
|
|
9024
|
+
assignedTo: [],
|
|
9025
|
+
info: appointmentHostInfo(HOST_A),
|
|
9026
|
+
validOwners: [HOST_A],
|
|
9027
|
+
})
|
|
9028
|
+
|
|
9029
|
+
// api path: close_ticket copies the original ticket's calendarEventId into journeyContext
|
|
9030
|
+
{
|
|
9031
|
+
const e = await sdk.api.endusers.createOne({
|
|
9032
|
+
journeys: { [journey.id]: '' }, fields: { Tag: 'tag3' },
|
|
9033
|
+
})
|
|
9034
|
+
const older = await createHostedEvent(e.id, HOST_A)
|
|
9035
|
+
const newer = await createHostedEvent(e.id, HOST_B)
|
|
9036
|
+
await testForegroundTicket({
|
|
9037
|
+
assignedTo: [],
|
|
9038
|
+
enduser: e,
|
|
9039
|
+
info: appointmentHostInfo(HOST_B),
|
|
9040
|
+
validOwners: [HOST_A],
|
|
9041
|
+
calendarEventId: older.id,
|
|
9042
|
+
ticketOwner: HOST_B, // so a previous-owner style fallback would also fail
|
|
9043
|
+
})
|
|
9044
|
+
await Promise.all([older, newer].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
|
|
9045
|
+
}
|
|
9046
|
+
|
|
8883
9047
|
return Promise.all([
|
|
8884
9048
|
await sdk.api.journeys.deleteOne(journey.id),
|
|
8885
9049
|
await sdk.api.ticket_queues.deleteOne(queue.id),
|
|
@@ -13764,6 +13928,55 @@ const replace_form_field_template_values_tests = async () => {
|
|
|
13764
13928
|
) === '<p>Locations: NYC<br />SF<br />LA</p>',
|
|
13765
13929
|
'fail CRLF in substituted value', 'CRLF in substituted value'
|
|
13766
13930
|
)
|
|
13931
|
+
|
|
13932
|
+
// escapeHTMLValues: substituted values are patient-supplied (their own intake answers / enduser
|
|
13933
|
+
// record), so on any path rendered by a sanitizer that permits iframes they must be escaped —
|
|
13934
|
+
// otherwise a patient can inject a frame into an admin-authored description.
|
|
13935
|
+
const enduserWithMarkupField = {
|
|
13936
|
+
fname: "Multi",
|
|
13937
|
+
fields: { Injected: '<iframe src="https://evil.example"></iframe>' },
|
|
13938
|
+
} as Partial<Enduser>
|
|
13939
|
+
|
|
13940
|
+
const escaped = replace_form_field_template_values(
|
|
13941
|
+
'<p>{{enduser.Injected}}</p>',
|
|
13942
|
+
{ enduser: enduserWithMarkupField, escapeNewlinesAsHTMLBreaks: true, escapeHTMLValues: true }
|
|
13943
|
+
)
|
|
13944
|
+
assert(
|
|
13945
|
+
escaped.includes('<iframe') && !escaped.includes('<iframe'),
|
|
13946
|
+
'fail escapeHTMLValues escapes markup', 'escapeHTMLValues escapes markup'
|
|
13947
|
+
)
|
|
13948
|
+
|
|
13949
|
+
// Escaping must run BEFORE the newline conversion, or the injected breaks become <br />
|
|
13950
|
+
const enduserWithMarkupAndNewline = {
|
|
13951
|
+
fields: { Mixed: 'a<b\nc&d' },
|
|
13952
|
+
} as Partial<Enduser>
|
|
13953
|
+
assert(
|
|
13954
|
+
replace_form_field_template_values(
|
|
13955
|
+
'{{enduser.Mixed}}',
|
|
13956
|
+
{ enduser: enduserWithMarkupAndNewline, escapeNewlinesAsHTMLBreaks: true, escapeHTMLValues: true }
|
|
13957
|
+
) === 'a<b<br />c&d',
|
|
13958
|
+
'fail escapeHTMLValues ordering vs newline breaks', 'escapeHTMLValues ordering vs newline breaks'
|
|
13959
|
+
)
|
|
13960
|
+
|
|
13961
|
+
// Flag absent => byte-identical to today. Plain-text callers (field titles/descriptions rendered
|
|
13962
|
+
// in a Typography) rely on this: escaping there would corrupt e.g. a name containing '&'.
|
|
13963
|
+
const enduserWithAmpersand = {
|
|
13964
|
+
fields: { Practice: 'Smith & Jones <Cardiology>' },
|
|
13965
|
+
} as Partial<Enduser>
|
|
13966
|
+
assert(
|
|
13967
|
+
replace_form_field_template_values(
|
|
13968
|
+
'Practice: {{enduser.Practice}}',
|
|
13969
|
+
{ enduser: enduserWithAmpersand }
|
|
13970
|
+
) === 'Practice: Smith & Jones <Cardiology>',
|
|
13971
|
+
'fail no escaping without the flag', 'no escaping without the flag'
|
|
13972
|
+
)
|
|
13973
|
+
assert(
|
|
13974
|
+
replace_form_field_template_values(
|
|
13975
|
+
'Practice: {{enduser.Practice}}',
|
|
13976
|
+
{ enduser: enduserWithAmpersand, escapeHTMLValues: true }
|
|
13977
|
+
) === 'Practice: Smith & Jones <Cardiology>',
|
|
13978
|
+
'fail escaping with the flag', 'escaping with the flag'
|
|
13979
|
+
)
|
|
13767
13980
|
}
|
|
13768
13981
|
|
|
13769
13982
|
const inbox_threads_building_tests = async () => {
|
|
@@ -15065,6 +15278,8 @@ const ip_address_form_tests = async () => {
|
|
|
15065
15278
|
await replace_form_field_template_values_tests()
|
|
15066
15279
|
await mfa_tests()
|
|
15067
15280
|
await setup_tests(sdk, sdkNonAdmin)
|
|
15281
|
+
await proxy_image_ssrf_tests({ sdk, sdkNonAdmin })
|
|
15282
|
+
await webhook_outbound_ssrf_tests({ sdk, sdkNonAdmin })
|
|
15068
15283
|
await allowed_paths_confinement_tests({ sdk, sdkNonAdmin })
|
|
15069
15284
|
await phone_calls_conference_hold_tests({ sdk, sdkNonAdmin })
|
|
15070
15285
|
await tickets_bulk_assign_care_team_tests({ sdk, sdkNonAdmin })
|
|
@@ -15073,6 +15288,7 @@ const ip_address_form_tests = async () => {
|
|
|
15073
15288
|
await automation_trigger_tests()
|
|
15074
15289
|
await resource_access_tags_tests({ sdk, sdkNonAdmin })
|
|
15075
15290
|
await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
|
|
15291
|
+
await beluga_trigger_refill_tests({ sdk, sdkNonAdmin })
|
|
15076
15292
|
await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
|
|
15077
15293
|
await mdi_case_offerings_tests({ sdk, sdkNonAdmin })
|
|
15078
15294
|
await enduser_write_restrictions_tests({ sdk, sdkNonAdmin })
|
|
@@ -15095,6 +15311,7 @@ const ip_address_form_tests = async () => {
|
|
|
15095
15311
|
await cascade_role_rename_cross_tenant_tests({ sdk, sdkNonAdmin })
|
|
15096
15312
|
await self_admin_role_assignment_tests({ sdk, sdkNonAdmin })
|
|
15097
15313
|
await sanitize_user_html_xss_tests()
|
|
15314
|
+
await sanitize_user_html_with_iframes_tests()
|
|
15098
15315
|
await prototype_pollution_tests()
|
|
15099
15316
|
await account_switcher_tests({ sdk, sdkNonAdmin })
|
|
15100
15317
|
await totp_mfa_tests({ sdk, sdkNonAdmin })
|
|
@@ -15110,6 +15327,7 @@ const ip_address_form_tests = async () => {
|
|
|
15110
15327
|
await chats_analytics_tests({ sdk, sdkNonAdmin })
|
|
15111
15328
|
await field_redaction_tests({ sdk, sdkNonAdmin })
|
|
15112
15329
|
await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
|
|
15330
|
+
await start_ai_conversation_tests({ sdk, sdkNonAdmin })
|
|
15113
15331
|
await dont_sync_to_elation_form_submission_tests({ sdk, sdkNonAdmin })
|
|
15114
15332
|
await date_string_validation_tests({ sdk, sdkNonAdmin })
|
|
15115
15333
|
await phone_tree_enduser_condition_tests({ sdk, sdkNonAdmin })
|
|
@@ -15129,6 +15347,7 @@ const ip_address_form_tests = async () => {
|
|
|
15129
15347
|
await time_tracks_appointment_duration_tests({ sdk, sdkNonAdmin })
|
|
15130
15348
|
await calendar_event_limits_tests({ sdk, sdkNonAdmin })
|
|
15131
15349
|
await get_some_projection_tests({ sdk, sdkNonAdmin })
|
|
15350
|
+
await file_download_unicode_names_tests({ sdk, sdkNonAdmin })
|
|
15132
15351
|
await elation_user_id_tests({ sdk, sdkNonAdmin })
|
|
15133
15352
|
await concurrent_build_threads_tests({ sdk, sdkNonAdmin })
|
|
15134
15353
|
await custom_aggregation_tests({ sdk, sdkNonAdmin })
|
package/test_generated.pdf
CHANGED
|
Binary file
|