@tellescope/sdk 1.250.1 → 1.250.2

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 (42) hide show
  1. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
  2. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
  3. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
  4. package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  5. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +556 -105
  6. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  7. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
  8. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
  9. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js +436 -0
  10. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
  11. package/lib/cjs/tests/tests.d.ts.map +1 -1
  12. package/lib/cjs/tests/tests.js +149 -141
  13. package/lib/cjs/tests/tests.js.map +1 -1
  14. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
  15. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
  16. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js +114 -0
  17. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
  18. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
  19. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
  20. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
  21. package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  22. package/lib/esm/tests/api_tests/medication_added_trigger.test.js +556 -105
  23. package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  24. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
  25. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
  26. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js +432 -0
  27. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
  28. package/lib/esm/tests/tests.d.ts.map +1 -1
  29. package/lib/esm/tests/tests.js +149 -141
  30. package/lib/esm/tests/tests.js.map +1 -1
  31. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
  32. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
  33. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js +111 -0
  34. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
  35. package/lib/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +10 -10
  37. package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +26 -0
  38. package/src/tests/api_tests/medication_added_trigger.test.ts +345 -4
  39. package/src/tests/api_tests/outbound_chat_sent_trigger.test.ts +339 -0
  40. package/src/tests/tests.ts +5 -1
  41. package/src/tests/unit_tests/conditional_logic_medication.test.ts +133 -0
  42. package/test_generated.pdf +0 -0
@@ -0,0 +1,339 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session, EnduserSession } from "../../sdk"
4
+ import {
5
+ log_header,
6
+ wait,
7
+ } from "@tellescope/testing"
8
+ import { setup_tests } from "../setup"
9
+
10
+ /**
11
+ * Tests for the "Outbound Chat Sent" automation trigger event.
12
+ *
13
+ * Covered cases (one shared trigger, one shared TAG):
14
+ * 1. Single-enduser room, inbound (enduser session) → no trigger, no recentOutboundChatAt
15
+ * 2. Single-enduser room, outbound (user session) → trigger fires, recentOutboundChatAt set + recent
16
+ * 3. Multi-enduser room, inbound (one enduser's session) → neither enduser tagged or stamped
17
+ * 4. Multi-enduser room, outbound (user session) → BOTH endusers tagged + stamped
18
+ * 5. User session, senderId = enduser.id (backfill case) → treated as inbound: no trigger, no stamp
19
+ *
20
+ * Chats are NOT explicitly deleted — they cascade-delete from chat_rooms.
21
+ */
22
+
23
+ const isRecent = (value: Date | string | undefined, sinceMs: number): boolean => {
24
+ if (!value) return false
25
+ const t = new Date(value).valueOf()
26
+ return t >= sinceMs - 1000 && t <= Date.now() + 1000
27
+ }
28
+
29
+ export const outbound_chat_sent_trigger_tests = async ({ sdk }: { sdk: Session }) => {
30
+ log_header("Outbound Chat Sent Trigger Tests")
31
+
32
+ const host = process.env.API_URL || 'http://localhost:8080'
33
+ const TAG = `outbound-chat-sent-${Date.now()}`
34
+
35
+ let endSolo: { id: string } | undefined
36
+ let endA: { id: string } | undefined
37
+ let endB: { id: string } | undefined
38
+ let endBackfill: { id: string } | undefined
39
+ let endAutoreply: { id: string } | undefined
40
+
41
+ let trigger: { id: string } | undefined
42
+ let roomSolo: { id: string } | undefined
43
+ let roomGroup: { id: string } | undefined
44
+ let roomBackfill: { id: string } | undefined
45
+ let roomAutoreply: { id: string } | undefined
46
+
47
+ let settingsModified = false
48
+ let originalOutOfOfficeHours: any[] = []
49
+ let originalAutoReplyEnabled = false
50
+
51
+ try {
52
+ // ── Endusers ──────────────────────────────────────────────────────
53
+ endSolo = await sdk.api.endusers.createOne({ fname: 'Solo', lname: 'Patient' })
54
+ endA = await sdk.api.endusers.createOne({ fname: 'GroupA', lname: 'Patient' })
55
+ endB = await sdk.api.endusers.createOne({ fname: 'GroupB', lname: 'Patient' })
56
+ endBackfill = await sdk.api.endusers.createOne({ fname: 'Backfill', lname: 'Patient' })
57
+ console.log(`Created endusers: solo=${endSolo.id}, A=${endA.id}, B=${endB.id}, backfill=${endBackfill.id}`)
58
+
59
+ // ── Enduser sessions for inbound sends ────────────────────────────
60
+ const { authToken: tokenSolo } = await sdk.api.endusers.generate_auth_token({ id: endSolo.id })
61
+ const endSoloSDK = new EnduserSession({ host, authToken: tokenSolo, businessId: sdk.userInfo.businessId })
62
+
63
+ const { authToken: tokenA } = await sdk.api.endusers.generate_auth_token({ id: endA.id })
64
+ const endASDK = new EnduserSession({ host, authToken: tokenA, businessId: sdk.userInfo.businessId })
65
+
66
+ // ── Trigger ───────────────────────────────────────────────────────
67
+ trigger = await sdk.api.automation_triggers.createOne({
68
+ title: `Outbound Chat Sent Test ${Date.now()}`,
69
+ status: 'Active',
70
+ event: { type: 'Outbound Chat Sent', info: {} },
71
+ action: { type: 'Add Tags', info: { tags: [TAG] } },
72
+ })
73
+ console.log(`Created trigger: ${trigger.id}`)
74
+
75
+ // ── Rooms ─────────────────────────────────────────────────────────
76
+ roomSolo = await sdk.api.chat_rooms.createOne({
77
+ userIds: [sdk.userInfo.id],
78
+ enduserIds: [endSolo.id],
79
+ })
80
+ roomGroup = await sdk.api.chat_rooms.createOne({
81
+ userIds: [sdk.userInfo.id],
82
+ enduserIds: [endA.id, endB.id],
83
+ })
84
+ roomBackfill = await sdk.api.chat_rooms.createOne({
85
+ userIds: [sdk.userInfo.id],
86
+ enduserIds: [endBackfill.id],
87
+ })
88
+ console.log(`Created rooms: solo=${roomSolo.id}, group=${roomGroup.id}, backfill=${roomBackfill.id}`)
89
+
90
+ // ═════════════════════════════════════════════════════════════════
91
+ // Case 1: Single-enduser inbound (enduser session)
92
+ // ═════════════════════════════════════════════════════════════════
93
+ await endSoloSDK.api.chats.createOne({
94
+ roomId: roomSolo.id,
95
+ message: 'solo inbound from enduser',
96
+ })
97
+ await wait(undefined, 2000)
98
+
99
+ const soloAfterInbound = await sdk.api.endusers.getOne(endSolo.id)
100
+ const c1_notTagged = !soloAfterInbound.tags?.includes(TAG)
101
+ const c1_noStamp = !soloAfterInbound.recentOutboundChatAt
102
+ console.log(c1_notTagged
103
+ ? `✅ [1] Solo inbound did NOT fire trigger`
104
+ : `❌ [1] Solo inbound fired trigger. tags=${JSON.stringify(soloAfterInbound.tags)}`)
105
+ console.log(c1_noStamp
106
+ ? `✅ [1] Solo inbound did NOT set recentOutboundChatAt`
107
+ : `❌ [1] Solo inbound set recentOutboundChatAt=${soloAfterInbound.recentOutboundChatAt}`)
108
+
109
+ // ═════════════════════════════════════════════════════════════════
110
+ // Case 2: Single-enduser outbound (user session)
111
+ // ═════════════════════════════════════════════════════════════════
112
+ const c2_sendStart = Date.now()
113
+ await sdk.api.chats.createOne({
114
+ roomId: roomSolo.id,
115
+ message: 'solo outbound from user',
116
+ senderId: sdk.userInfo.id,
117
+ })
118
+ await wait(undefined, 2000)
119
+
120
+ const soloAfterOutbound = await sdk.api.endusers.getOne(endSolo.id)
121
+ const c2_tagged = !!soloAfterOutbound.tags?.includes(TAG)
122
+ const c2_stamped = isRecent(soloAfterOutbound.recentOutboundChatAt, c2_sendStart)
123
+ console.log(c2_tagged
124
+ ? `✅ [2] Solo outbound fired trigger`
125
+ : `❌ [2] Solo outbound did NOT fire trigger. tags=${JSON.stringify(soloAfterOutbound.tags)}`)
126
+ console.log(c2_stamped
127
+ ? `✅ [2] Solo outbound set recentOutboundChatAt=${soloAfterOutbound.recentOutboundChatAt}`
128
+ : `❌ [2] Solo outbound did not set a recent recentOutboundChatAt. got=${soloAfterOutbound.recentOutboundChatAt}`)
129
+
130
+ // ═════════════════════════════════════════════════════════════════
131
+ // Case 3: Multi-enduser inbound (endA sends in [A, B] room)
132
+ // ═════════════════════════════════════════════════════════════════
133
+ await endASDK.api.chats.createOne({
134
+ roomId: roomGroup.id,
135
+ message: 'group inbound from endA',
136
+ })
137
+ await wait(undefined, 2000)
138
+
139
+ const aAfterInbound = await sdk.api.endusers.getOne(endA.id)
140
+ const bAfterInbound = await sdk.api.endusers.getOne(endB.id)
141
+ const c3_aNotTagged = !aAfterInbound.tags?.includes(TAG)
142
+ const c3_bNotTagged = !bAfterInbound.tags?.includes(TAG)
143
+ const c3_neitherTagged = c3_aNotTagged && c3_bNotTagged
144
+ const c3_neitherStamped = !aAfterInbound.recentOutboundChatAt && !bAfterInbound.recentOutboundChatAt
145
+ console.log(c3_neitherTagged
146
+ ? `✅ [3] Group inbound did NOT fire trigger on either enduser`
147
+ : `❌ [3] Group inbound fired trigger. A tags=${JSON.stringify(aAfterInbound.tags)} B tags=${JSON.stringify(bAfterInbound.tags)}`)
148
+ console.log(c3_neitherStamped
149
+ ? `✅ [3] Group inbound did NOT set recentOutboundChatAt on either enduser`
150
+ : `❌ [3] Group inbound stamped recentOutboundChatAt. A=${aAfterInbound.recentOutboundChatAt} B=${bAfterInbound.recentOutboundChatAt}`)
151
+
152
+ // ═════════════════════════════════════════════════════════════════
153
+ // Case 4: Multi-enduser outbound (user sends in [A, B] room)
154
+ // ═════════════════════════════════════════════════════════════════
155
+ const c4_sendStart = Date.now()
156
+ await sdk.api.chats.createOne({
157
+ roomId: roomGroup.id,
158
+ message: 'group outbound from user',
159
+ senderId: sdk.userInfo.id,
160
+ })
161
+ await wait(undefined, 2000)
162
+
163
+ const aAfterOutbound = await sdk.api.endusers.getOne(endA.id)
164
+ const bAfterOutbound = await sdk.api.endusers.getOne(endB.id)
165
+ const c4_bothTagged = !!aAfterOutbound.tags?.includes(TAG) && !!bAfterOutbound.tags?.includes(TAG)
166
+ const c4_bothStamped = isRecent(aAfterOutbound.recentOutboundChatAt, c4_sendStart)
167
+ && isRecent(bAfterOutbound.recentOutboundChatAt, c4_sendStart)
168
+ console.log(c4_bothTagged
169
+ ? `✅ [4] Group outbound fired trigger on BOTH endusers`
170
+ : `❌ [4] Group outbound did not tag both. A tags=${JSON.stringify(aAfterOutbound.tags)} B tags=${JSON.stringify(bAfterOutbound.tags)}`)
171
+ console.log(c4_bothStamped
172
+ ? `✅ [4] Group outbound set recentOutboundChatAt on BOTH endusers`
173
+ : `❌ [4] Group outbound did not stamp both. A=${aAfterOutbound.recentOutboundChatAt} B=${bAfterOutbound.recentOutboundChatAt}`)
174
+
175
+ // ═════════════════════════════════════════════════════════════════
176
+ // Case 5: User session, senderId = enduser.id (backfill)
177
+ // ═════════════════════════════════════════════════════════════════
178
+ await sdk.api.chats.createOne({
179
+ roomId: roomBackfill.id,
180
+ message: 'backfilled inbound (user session, enduser senderId)',
181
+ senderId: endBackfill.id,
182
+ })
183
+ await wait(undefined, 2000)
184
+
185
+ const backfillAfter = await sdk.api.endusers.getOne(endBackfill.id)
186
+ const c5_notTagged = !backfillAfter.tags?.includes(TAG)
187
+ const c5_noStamp = !backfillAfter.recentOutboundChatAt
188
+ console.log(c5_notTagged
189
+ ? `✅ [5] Backfill (user-session + enduser senderId) did NOT fire trigger`
190
+ : `❌ [5] Backfill fired trigger. tags=${JSON.stringify(backfillAfter.tags)}`)
191
+ console.log(c5_noStamp
192
+ ? `✅ [5] Backfill did NOT set recentOutboundChatAt`
193
+ : `❌ [5] Backfill set recentOutboundChatAt=${backfillAfter.recentOutboundChatAt}`)
194
+
195
+ // ═════════════════════════════════════════════════════════════════
196
+ // Case 6: Autoreply — enduser inbound triggers an org autoreply.
197
+ // The autoreply chat (isAutoreply: true) MUST NOT fire the trigger
198
+ // or stamp recentOutboundChatAt.
199
+ //
200
+ // Setup requires enabling org autoreply with an OOO window covering
201
+ // "now" (is_out_of_office returns false on empty config).
202
+ // ═════════════════════════════════════════════════════════════════
203
+ const originalOrg = await sdk.api.organizations.getOne(sdk.userInfo.businessId)
204
+ originalOutOfOfficeHours = originalOrg.outOfOfficeHours ?? []
205
+ originalAutoReplyEnabled = !!originalOrg.settings?.endusers?.autoReplyEnabled
206
+
207
+ settingsModified = true
208
+ await sdk.api.organizations.updateOne(sdk.userInfo.businessId, {
209
+ outOfOfficeHours: [{
210
+ from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
211
+ to: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
212
+ autoreplyText: 'OOO autoreply (test)',
213
+ }] as any,
214
+ settings: { endusers: { autoReplyEnabled: true } } as any,
215
+ }, { replaceObjectFields: true })
216
+
217
+ endAutoreply = await sdk.api.endusers.createOne({ fname: 'Autoreply', lname: 'Patient' })
218
+ const { authToken: tokenAR } = await sdk.api.endusers.generate_auth_token({ id: endAutoreply.id })
219
+ const endAutoreplySDK = new EnduserSession({ host, authToken: tokenAR, businessId: sdk.userInfo.businessId })
220
+
221
+ roomAutoreply = await sdk.api.chat_rooms.createOne({
222
+ userIds: [sdk.userInfo.id],
223
+ enduserIds: [endAutoreply.id],
224
+ })
225
+
226
+ await endAutoreplySDK.api.chats.createOne({
227
+ roomId: roomAutoreply.id,
228
+ message: 'trigger an autoreply',
229
+ })
230
+
231
+ // Autoreply path involves an extra org/user lookup + insert before
232
+ // handle_chat_create re-runs against the autoreply chat.
233
+ await wait(undefined, 4000)
234
+
235
+ const arAfter = await sdk.api.endusers.getOne(endAutoreply.id)
236
+ const roomChats = await sdk.api.chats.getSome({ filter: { roomId: roomAutoreply.id } })
237
+ const sanity_autoreplySent = roomChats.some(c => (c as any).isAutoreply === true)
238
+
239
+ const c6_notTagged = !arAfter.tags?.includes(TAG)
240
+ const c6_noStamp = !arAfter.recentOutboundChatAt
241
+
242
+ console.log(sanity_autoreplySent
243
+ ? `✅ [6] Autoreply chat was sent (sanity check)`
244
+ : `❌ [6] No autoreply chat found in room — test setup invalid. Cannot validate trigger skip.`)
245
+ console.log(c6_notTagged
246
+ ? `✅ [6] Autoreply did NOT fire trigger`
247
+ : `❌ [6] Autoreply fired trigger. tags=${JSON.stringify(arAfter.tags)}`)
248
+ console.log(c6_noStamp
249
+ ? `✅ [6] Autoreply did NOT set recentOutboundChatAt`
250
+ : `❌ [6] Autoreply set recentOutboundChatAt=${arAfter.recentOutboundChatAt}`)
251
+
252
+ // ── Summary ───────────────────────────────────────────────────────
253
+ console.log(`\n=== Outbound Chat Sent Trigger Test Results ===`)
254
+ console.log(`[1] Solo inbound — no trigger: ${c1_notTagged ? '✅' : '❌'}`)
255
+ console.log(`[1] Solo inbound — no stamp: ${c1_noStamp ? '✅' : '❌'}`)
256
+ console.log(`[2] Solo outbound — trigger: ${c2_tagged ? '✅' : '❌'}`)
257
+ console.log(`[2] Solo outbound — stamp: ${c2_stamped ? '✅' : '❌'}`)
258
+ console.log(`[3] Group inbound — no trigger (both): ${c3_neitherTagged ? '✅' : '❌'}`)
259
+ console.log(`[3] Group inbound — no stamp (both): ${c3_neitherStamped ? '✅' : '❌'}`)
260
+ console.log(`[4] Group outbound — trigger (both): ${c4_bothTagged ? '✅' : '❌'}`)
261
+ console.log(`[4] Group outbound — stamp (both): ${c4_bothStamped ? '✅' : '❌'}`)
262
+ console.log(`[5] Backfill — no trigger: ${c5_notTagged ? '✅' : '❌'}`)
263
+ console.log(`[5] Backfill — no stamp: ${c5_noStamp ? '✅' : '❌'}`)
264
+ console.log(`[6] Autoreply — sanity sent: ${sanity_autoreplySent ? '✅' : '❌'}`)
265
+ console.log(`[6] Autoreply — no trigger: ${c6_notTagged ? '✅' : '❌'}`)
266
+ console.log(`[6] Autoreply — no stamp: ${c6_noStamp ? '✅' : '❌'}`)
267
+
268
+ const allPassed =
269
+ c1_notTagged && c1_noStamp
270
+ && c2_tagged && c2_stamped
271
+ && c3_neitherTagged && c3_neitherStamped
272
+ && c4_bothTagged && c4_bothStamped
273
+ && c5_notTagged && c5_noStamp
274
+ && sanity_autoreplySent && c6_notTagged && c6_noStamp
275
+
276
+ if (!allPassed) {
277
+ throw new Error('Outbound Chat Sent trigger tests failed')
278
+ }
279
+
280
+ return { success: true }
281
+ } finally {
282
+ try {
283
+ // Restore org settings FIRST (sequentially, before deletes), so a
284
+ // failure here doesn't leave autoReplyEnabled=true polluting other tests.
285
+ if (settingsModified) {
286
+ try {
287
+ await sdk.api.organizations.updateOne(sdk.userInfo.businessId, {
288
+ outOfOfficeHours: originalOutOfOfficeHours as any,
289
+ settings: { endusers: { autoReplyEnabled: originalAutoReplyEnabled } } as any,
290
+ }, { replaceObjectFields: true })
291
+ console.log(`Restored org settings (autoReplyEnabled=${originalAutoReplyEnabled}, outOfOfficeHours.length=${originalOutOfOfficeHours.length})`)
292
+ } catch (err) {
293
+ console.error(`❌ Failed to restore org settings — manual cleanup may be required: ${err}`)
294
+ }
295
+ }
296
+
297
+ const cleanups: Promise<any>[] = []
298
+
299
+ if (trigger?.id) cleanups.push(sdk.api.automation_triggers.deleteOne(trigger.id).catch(() => {}))
300
+ if (roomSolo?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomSolo.id).catch(() => {}))
301
+ if (roomGroup?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomGroup.id).catch(() => {}))
302
+ if (roomBackfill?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomBackfill.id).catch(() => {}))
303
+ if (roomAutoreply?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomAutoreply.id).catch(() => {}))
304
+ if (endSolo?.id) cleanups.push(sdk.api.endusers.deleteOne(endSolo.id).catch(() => {}))
305
+ if (endA?.id) cleanups.push(sdk.api.endusers.deleteOne(endA.id).catch(() => {}))
306
+ if (endB?.id) cleanups.push(sdk.api.endusers.deleteOne(endB.id).catch(() => {}))
307
+ if (endBackfill?.id) cleanups.push(sdk.api.endusers.deleteOne(endBackfill.id).catch(() => {}))
308
+ if (endAutoreply?.id) cleanups.push(sdk.api.endusers.deleteOne(endAutoreply.id).catch(() => {}))
309
+
310
+ await Promise.all(cleanups)
311
+ console.log(`Outbound Chat Sent trigger test cleanup completed`)
312
+ } catch (error) {
313
+ console.error(`Cleanup error: ${error}`)
314
+ }
315
+ }
316
+ }
317
+
318
+ // Allow running this test file independently
319
+ if (require.main === module) {
320
+ const host = process.env.API_URL || 'http://localhost:8080'
321
+ console.log(`🌐 Using API URL: ${host}`)
322
+ const sdk = new Session({ host })
323
+ const sdkNonAdmin = new Session({ host })
324
+
325
+ const runTests = async () => {
326
+ await setup_tests(sdk, sdkNonAdmin)
327
+ await outbound_chat_sent_trigger_tests({ sdk })
328
+ }
329
+
330
+ runTests()
331
+ .then(() => {
332
+ console.log("✅ Outbound Chat Sent trigger tests completed successfully")
333
+ process.exit(0)
334
+ })
335
+ .catch((error) => {
336
+ console.error("❌ Outbound Chat Sent trigger tests failed:", error)
337
+ process.exit(1)
338
+ })
339
+ }
@@ -78,6 +78,7 @@ import { eom_procedure_codes_tests } from "./api_tests/eom_procedure_codes.test"
78
78
  import { cross_org_api_key_tests } from "./api_tests/cross_org_api_key.test";
79
79
  import { custom_dashboards_tests } from "./api_tests/custom_dashboards.test";
80
80
  import { message_assignment_trigger_tests } from "./api_tests/message_assignment_trigger.test";
81
+ import { outbound_chat_sent_trigger_tests } from "./api_tests/outbound_chat_sent_trigger.test";
81
82
  import { time_tracks_tests, time_tracks_historical_tests, time_tracks_correction_tests, time_tracks_review_tests, time_tracks_lock_tests, time_tracks_edge_case_tests } from "./api_tests/time_tracks.test";
82
83
  import { monthly_availability_restrictions_tests } from "./api_tests/monthly_availability_restrictions.test";
83
84
  import { calendar_event_limits_tests } from "./api_tests/calendar_event_limits.test";
@@ -95,6 +96,7 @@ import { load_team_chat_tests } from "./api_tests/load_team_chat.test";
95
96
  import { form_started_trigger_tests } from "./api_tests/form_started_trigger.test";
96
97
  import { form_submitted_trigger_tests } from "./api_tests/form_submitted_trigger.test";
97
98
  import { medication_added_trigger_tests } from "./api_tests/medication_added_trigger.test";
99
+ import { conditional_logic_medication_unit_tests } from "./unit_tests/conditional_logic_medication.test";
98
100
  import { elation_user_id_tests } from "./api_tests/elation_user_id.test";
99
101
  import { organization_settings_duplicates_tests } from "./api_tests/organization_settings_duplicates.test";
100
102
  import { calendar_events_bulk_update_tests } from "./api_tests/calendar_events_bulk_update.test";
@@ -14303,10 +14305,13 @@ const ip_address_form_tests = async () => {
14303
14305
 
14304
14306
 
14305
14307
  await enduser_conditional_logic_tests()
14308
+ await conditional_logic_medication_unit_tests()
14306
14309
  await replace_enduser_template_values_tests()
14307
14310
  await replace_form_field_template_values_tests()
14308
14311
  await mfa_tests()
14309
14312
  await setup_tests(sdk, sdkNonAdmin)
14313
+ await outbound_chat_sent_trigger_tests({ sdk })
14314
+ await automation_trigger_tests()
14310
14315
  await enduser_cross_access_isolation_tests({ sdk, sdkNonAdmin })
14311
14316
  await eom_procedure_codes_tests({ sdk, sdkNonAdmin })
14312
14317
  await cross_org_api_key_tests({ sdk, sdkNonAdmin })
@@ -14317,7 +14322,6 @@ const ip_address_form_tests = async () => {
14317
14322
  await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
14318
14323
  await date_string_validation_tests({ sdk, sdkNonAdmin })
14319
14324
  await openloop_webhooks_tests({ sdk, sdkNonAdmin })
14320
- await automation_trigger_tests()
14321
14325
  await integrations_redacted_tests({ sdk, sdkNonAdmin })
14322
14326
  await mdb_sort_tests({ sdk, sdkNonAdmin })
14323
14327
  await search_tests()
@@ -0,0 +1,133 @@
1
+ import {
2
+ evaluate_conditional_logic_for_medication_title,
3
+ evaluate_string_field_comparison,
4
+ } from "@tellescope/utilities"
5
+ import { CompoundFilter } from "@tellescope/types-models"
6
+
7
+ let failures = 0
8
+ let passed = 0
9
+
10
+ const assert = (name: string, actual: boolean, expected: boolean) => {
11
+ if (actual === expected) {
12
+ passed++
13
+ console.log(` ✓ ${name}`)
14
+ } else {
15
+ failures++
16
+ console.error(` ✗ ${name} — expected ${expected}, got ${actual}`)
17
+ }
18
+ }
19
+
20
+ const run_string_comparison_tests = () => {
21
+ console.log("\n[evaluate_string_field_comparison]")
22
+
23
+ // Plain-string equals (implicit operator)
24
+ assert("equals plain string match", evaluate_string_field_comparison('Aspirin', 'Aspirin'), true)
25
+ assert("equals plain string mismatch", evaluate_string_field_comparison('Aspirin', 'Lisinopril'), false)
26
+ assert("equals empty string against empty title", evaluate_string_field_comparison('', ''), true)
27
+ assert("equals empty string against undefined title", evaluate_string_field_comparison(undefined, ''), true)
28
+
29
+ // $ne
30
+ assert("$ne matches when different", evaluate_string_field_comparison('Aspirin', { $ne: 'Lisinopril' }), true)
31
+ assert("$ne fails when equal", evaluate_string_field_comparison('Aspirin', { $ne: 'Aspirin' }), false)
32
+
33
+ // $contains (case sensitive — consistent with enduser/form-response evaluators)
34
+ assert("$contains substring match", evaluate_string_field_comparison('Semaglutide GLP-1', { $contains: 'GLP' }), true)
35
+ assert("$contains case sensitive miss", evaluate_string_field_comparison('Lisinopril 10MG', { $contains: 'mg' }), false)
36
+ assert("$contains case sensitive match", evaluate_string_field_comparison('Lisinopril 10mg', { $contains: 'mg' }), true)
37
+ assert("$contains no match", evaluate_string_field_comparison('Aspirin', { $contains: 'GLP' }), false)
38
+ assert("$contains empty string is always true", evaluate_string_field_comparison('Aspirin', { $contains: '' }), true)
39
+
40
+ // $doesNotContain
41
+ assert("$doesNotContain mismatch true", evaluate_string_field_comparison('Aspirin', { $doesNotContain: 'GLP' }), true)
42
+ assert("$doesNotContain match false", evaluate_string_field_comparison('Semaglutide', { $doesNotContain: 'Sema' }), false)
43
+ assert("$doesNotContain case sensitive (different case = no match)", evaluate_string_field_comparison('Placebo', { $doesNotContain: 'PLACEBO' }), true)
44
+
45
+ // $exists
46
+ assert("$exists:true on present", evaluate_string_field_comparison('Aspirin', { $exists: true }), true)
47
+ assert("$exists:true on empty", evaluate_string_field_comparison('', { $exists: true }), false)
48
+ assert("$exists:true on undefined", evaluate_string_field_comparison(undefined, { $exists: true }), false)
49
+ assert("$exists:false on present", evaluate_string_field_comparison('Aspirin', { $exists: false }), false)
50
+ assert("$exists:false on undefined", evaluate_string_field_comparison(undefined, { $exists: false }), true)
51
+
52
+ // null operator → treated as "no value set"
53
+ assert("null operator on undefined title", evaluate_string_field_comparison(undefined, null), true)
54
+ assert("null operator on present title", evaluate_string_field_comparison('Aspirin', null), false)
55
+
56
+ // Unknown operator → returns true (don't suppress triggers on bad data)
57
+ assert("unknown operator returns true", evaluate_string_field_comparison('Aspirin', { $weirdOp: 'x' } as any), true)
58
+ }
59
+
60
+ const run_conditional_logic_tests = () => {
61
+ console.log("\n[evaluate_conditional_logic_for_medication_title]")
62
+
63
+ // Single condition (case-sensitive)
64
+ const c1: CompoundFilter<'title'> = { condition: { title: { $contains: 'GLP' } } }
65
+ assert("single $contains true", evaluate_conditional_logic_for_medication_title('Semaglutide GLP-1', c1), true)
66
+ assert("single $contains false", evaluate_conditional_logic_for_medication_title('Aspirin', c1), false)
67
+ assert("single $contains case-sensitive miss", evaluate_conditional_logic_for_medication_title('Semaglutide glp-1', c1), false)
68
+
69
+ // Plain string default-equals
70
+ const c2: CompoundFilter<'title'> = { condition: { title: 'Aspirin' } }
71
+ assert("plain equals true", evaluate_conditional_logic_for_medication_title('Aspirin', c2), true)
72
+ assert("plain equals false", evaluate_conditional_logic_for_medication_title('Lisinopril', c2), false)
73
+
74
+ // $and (both true)
75
+ const cAnd: CompoundFilter<'title'> = {
76
+ $and: [
77
+ { condition: { title: { $contains: 'mg' } } },
78
+ { condition: { title: { $doesNotContain: 'Placebo' } } },
79
+ ],
80
+ }
81
+ assert("$and both pass", evaluate_conditional_logic_for_medication_title('Lisinopril 10mg', cAnd), true)
82
+ assert("$and first fails", evaluate_conditional_logic_for_medication_title('Lisinopril', cAnd), false)
83
+ assert("$and second fails", evaluate_conditional_logic_for_medication_title('Placebo 5mg', cAnd), false)
84
+
85
+ // $or
86
+ const cOr: CompoundFilter<'title'> = {
87
+ $or: [
88
+ { condition: { title: 'Aspirin' } },
89
+ { condition: { title: { $contains: 'pril' } } },
90
+ ],
91
+ }
92
+ assert("$or first matches", evaluate_conditional_logic_for_medication_title('Aspirin', cOr), true)
93
+ assert("$or second matches", evaluate_conditional_logic_for_medication_title('Lisinopril', cOr), true)
94
+ assert("$or neither matches", evaluate_conditional_logic_for_medication_title('Metformin', cOr), false)
95
+
96
+ // Mixed compound: $and containing $or — the canonical "compound" case
97
+ const cMixed: CompoundFilter<'title'> = {
98
+ $and: [
99
+ { condition: { title: { $contains: 'mg' } } },
100
+ {
101
+ $or: [
102
+ { condition: { title: { $contains: 'Lisin' } } },
103
+ { condition: { title: { $contains: 'Metform' } } },
104
+ ],
105
+ },
106
+ ],
107
+ }
108
+ assert("$and+$or first or-branch matches", evaluate_conditional_logic_for_medication_title('Lisinopril 10mg', cMixed), true)
109
+ assert("$and+$or second or-branch matches", evaluate_conditional_logic_for_medication_title('Metformin 500mg', cMixed), true)
110
+ assert("$and+$or and-branch fails", evaluate_conditional_logic_for_medication_title('Lisinopril', cMixed), false)
111
+ assert("$and+$or or-branch fails", evaluate_conditional_logic_for_medication_title('Aspirin 81mg', cMixed), false)
112
+
113
+ // Empty / no-op
114
+ assert("empty conditions returns true", evaluate_conditional_logic_for_medication_title('Aspirin', {}), true)
115
+
116
+ // Unknown operator inside condition → returns true (safe)
117
+ const cUnknown: CompoundFilter<'title'> = { condition: { title: { $regex: 'foo' } as any } }
118
+ assert("unknown operator is permissive", evaluate_conditional_logic_for_medication_title('Aspirin', cUnknown), true)
119
+ }
120
+
121
+ const run_all = () => {
122
+ console.log("Running conditional_logic_medication unit tests")
123
+ run_string_comparison_tests()
124
+ run_conditional_logic_tests()
125
+ console.log(`\nResults: ${passed} passed, ${failures} failed`)
126
+ if (failures > 0) process.exit(1)
127
+ }
128
+
129
+ if (require.main === module) {
130
+ run_all()
131
+ }
132
+
133
+ export { run_all as conditional_logic_medication_unit_tests }
Binary file