@tellescope/sdk 1.255.2 → 1.255.4

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 (43) hide show
  1. package/lib/cjs/tests/api_tests/generate_enduser_summary.test.d.ts +6 -0
  2. package/lib/cjs/tests/api_tests/generate_enduser_summary.test.d.ts.map +1 -0
  3. package/lib/cjs/tests/api_tests/generate_enduser_summary.test.js +186 -0
  4. package/lib/cjs/tests/api_tests/generate_enduser_summary.test.js.map +1 -0
  5. package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  6. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +206 -136
  7. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  8. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +10 -0
  9. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -1
  10. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +184 -41
  11. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -1
  12. package/lib/cjs/tests/api_tests/translations.test.d.ts +6 -0
  13. package/lib/cjs/tests/api_tests/translations.test.d.ts.map +1 -0
  14. package/lib/cjs/tests/api_tests/translations.test.js +148 -0
  15. package/lib/cjs/tests/api_tests/translations.test.js.map +1 -0
  16. package/lib/cjs/tests/tests.d.ts.map +1 -1
  17. package/lib/cjs/tests/tests.js +89 -85
  18. package/lib/cjs/tests/tests.js.map +1 -1
  19. package/lib/esm/tests/api_tests/generate_enduser_summary.test.d.ts +6 -0
  20. package/lib/esm/tests/api_tests/generate_enduser_summary.test.d.ts.map +1 -0
  21. package/lib/esm/tests/api_tests/generate_enduser_summary.test.js +182 -0
  22. package/lib/esm/tests/api_tests/generate_enduser_summary.test.js.map +1 -0
  23. package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  24. package/lib/esm/tests/api_tests/medication_added_trigger.test.js +206 -136
  25. package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  26. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +10 -0
  27. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -1
  28. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +184 -41
  29. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -1
  30. package/lib/esm/tests/api_tests/translations.test.d.ts +6 -0
  31. package/lib/esm/tests/api_tests/translations.test.d.ts.map +1 -0
  32. package/lib/esm/tests/api_tests/translations.test.js +144 -0
  33. package/lib/esm/tests/api_tests/translations.test.js.map +1 -0
  34. package/lib/esm/tests/tests.d.ts.map +1 -1
  35. package/lib/esm/tests/tests.js +89 -85
  36. package/lib/esm/tests/tests.js.map +1 -1
  37. package/lib/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +10 -10
  39. package/src/tests/api_tests/medication_added_trigger.test.ts +59 -1
  40. package/src/tests/api_tests/security/F-0005-ai-conversations-rbac.test.ts +123 -0
  41. package/src/tests/api_tests/translations.test.ts +83 -0
  42. package/src/tests/tests.ts +2 -0
  43. package/test_generated.pdf +0 -0
@@ -182,7 +182,7 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
182
182
  title: "Medication - Beluga Protocol Test"
183
183
  })
184
184
 
185
- // Simulate the Beluga RX_WRITTEN webhook with two meds covering category present (typical) and "N/A"
185
+ // Simulate the Beluga RX_WRITTEN webhook with two meds covering category/pharmacyNotes present (typical) and "N/A"/absent
186
186
  const webhookResponse = await fetch(`${host}/v1/webhooks/beluga`, {
187
187
  method: 'POST',
188
188
  headers: { 'Content-Type': 'application/json' },
@@ -198,6 +198,7 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
198
198
  medId: 'test-ndc-123',
199
199
  rxId: 'test-rx-456',
200
200
  category: 'weightloss1',
201
+ pharmacyNotes: 'Dispense with syringes',
201
202
  }, {
202
203
  name: 'Metformin',
203
204
  strength: '500mg',
@@ -245,6 +246,16 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
245
246
  async () => belugaMedMetformin,
246
247
  { onResult: (m: EnduserMedication) => m?.category === 'N/A' }
247
248
  )
249
+ await async_test(
250
+ "Beluga RX_WRITTEN - Medication has notes from webhook pharmacyNotes",
251
+ async () => belugaMedSemaglutide,
252
+ { onResult: (m: EnduserMedication) => m?.notes === 'Dispense with syringes' }
253
+ )
254
+ await async_test(
255
+ "Beluga RX_WRITTEN - Medication omits notes when pharmacyNotes not provided on med",
256
+ async () => belugaMedMetformin,
257
+ { onResult: (m: EnduserMedication) => m?.notes === undefined || m?.notes === null }
258
+ )
248
259
 
249
260
  // Backwards-compatibility: a webhook with no category on any med should result in undefined category
250
261
  const belugaFormNoCategory = await sdk.api.forms.createOne({
@@ -294,6 +305,11 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
294
305
  async () => belugaMedNoCategory,
295
306
  { onResult: (m: EnduserMedication) => m?.category === undefined || m?.category === null }
296
307
  )
308
+ await async_test(
309
+ "Beluga RX_WRITTEN - Medication omits notes when pharmacyNotes not provided",
310
+ async () => belugaMedNoCategory,
311
+ { onResult: (m: EnduserMedication) => m?.notes === undefined || m?.notes === null }
312
+ )
297
313
 
298
314
  try {
299
315
  // Clean up Beluga test data
@@ -389,14 +405,56 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
389
405
  { onResult: (e: Enduser) => e.fields?.['Medication Category'] === '' }
390
406
  )
391
407
 
408
+ // ---- Set Fields with {{medication.notes}} Test ----
409
+ log_header("Medication Added - Set Fields with {{medication.notes}}")
410
+
411
+ const setFieldsPharmacyNotesTrigger = await sdk.api.automation_triggers.createOne({
412
+ event: { type: 'Medication Added', info: { titles: [], protocols: [] } },
413
+ action: { type: 'Set Fields', info: { fields: [{ name: 'Medication Notes', type: 'Custom Value' as const, value: '{{medication.notes}}' }] }},
414
+ status: 'Active',
415
+ title: "Medication - Set Fields medication.notes"
416
+ })
417
+
418
+ const setFieldsPharmacyNotesEnduser = await sdk.api.endusers.createOne({})
419
+
420
+ const setFieldsPharmacyNotesMed = await sdk.api.enduser_medications.createOne({
421
+ enduserId: setFieldsPharmacyNotesEnduser.id,
422
+ title: 'Ozempic 1mg',
423
+ notes: 'Dispense with syringes',
424
+ })
425
+ await wait(undefined, 500)
426
+
427
+ await async_test(
428
+ "Medication Added - Set Fields copies medication.notes to enduser field",
429
+ () => sdk.api.endusers.getOne(setFieldsPharmacyNotesEnduser.id),
430
+ { onResult: (e: Enduser) => e.fields?.['Medication Notes'] === 'Dispense with syringes' }
431
+ )
432
+
433
+ // notes absent on medication → placeholder resolves to empty string
434
+ const setFieldsPharmacyNotesEnduserEmpty = await sdk.api.endusers.createOne({})
435
+ const setFieldsPharmacyNotesMedEmpty = await sdk.api.enduser_medications.createOne({
436
+ enduserId: setFieldsPharmacyNotesEnduserEmpty.id,
437
+ title: 'Lisinopril 40mg',
438
+ })
439
+ await wait(undefined, 500)
440
+
441
+ await async_test(
442
+ "Medication Added - Set Fields with no notes resolves to empty string",
443
+ () => sdk.api.endusers.getOne(setFieldsPharmacyNotesEnduserEmpty.id),
444
+ { onResult: (e: Enduser) => e.fields?.['Medication Notes'] === '' }
445
+ )
446
+
392
447
  try {
393
448
  await sdk.api.automation_triggers.deleteOne(setFieldsTrigger.id)
394
449
  await sdk.api.automation_triggers.deleteOne(setFieldsTriggerFiltered.id)
395
450
  await sdk.api.automation_triggers.deleteOne(setFieldsCategoryTrigger.id)
451
+ await sdk.api.automation_triggers.deleteOne(setFieldsPharmacyNotesTrigger.id)
396
452
  await sdk.api.endusers.deleteOne(setFieldsEnduser.id)
397
453
  await sdk.api.endusers.deleteOne(setFieldsEnduser2.id)
398
454
  await sdk.api.endusers.deleteOne(setFieldsCategoryEnduser.id)
399
455
  await sdk.api.endusers.deleteOne(setFieldsCategoryEnduserEmpty.id)
456
+ await sdk.api.endusers.deleteOne(setFieldsPharmacyNotesEnduser.id)
457
+ await sdk.api.endusers.deleteOne(setFieldsPharmacyNotesEnduserEmpty.id)
400
458
  } finally {}
401
459
 
402
460
  // ---- titleCondition (compound conditional logic on title) ----
@@ -28,6 +28,16 @@ const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.
28
28
  * 2. Assigns the role to the non-admin user.
29
29
  * 3. Calls each endpoint as the non-admin.
30
30
  * 4. Asserts each endpoint returns a 403-equivalent error (not 200).
31
+ * 5. Positive case: a role granting ai_conversations create (read/update denied) must pass
32
+ * the send_message gate — generating a NEW conversation requires only create access.
33
+ * 6. But the same role must still 403 when passing a conversationId: continuing an existing
34
+ * conversation reads its stored history (returned in the response) and appends to it,
35
+ * so it requires read + update access in addition to create.
36
+ * 7. Default-provider-permissions role (ai_conversations = Assigned): send_message must not
37
+ * 500. Regression: the post-generation $push previously ran through the caller's
38
+ * access-scoped DB, where "Assigned" filters can't match the just-created conversation,
39
+ * returning null and crashing the handler (Cannot read properties of null '_id') after
40
+ * credits were consumed. bedrock.ts now persists via tenant-scoped org-wide queries.
31
41
  *
32
42
  * Pre-fix:
33
43
  * - send_message: 200 (or some downstream error from Bedrock) — NOT 403. Test fails.
@@ -39,7 +49,11 @@ export const ai_conversations_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk:
39
49
  log_header("F-0005: ai_conversations RBAC bypass regression")
40
50
 
41
51
  const roleName = `f0005-ai-conversations-no-access-${Date.now()}`
52
+ const grantRoleName = `f0005-ai-conversations-create-granted-${Date.now()}`
53
+ const assignedRoleName = `f0005-ai-conversations-assigned-default-${Date.now()}`
42
54
  let rbapId: string | undefined
55
+ let grantRbapId: string | undefined
56
+ let assignedRbapId: string | undefined
43
57
  const originalRoles = sdkNonAdmin.userInfo.roles
44
58
 
45
59
  try {
@@ -109,6 +123,109 @@ export const ai_conversations_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk:
109
123
  },
110
124
  },
111
125
  )
126
+
127
+ // 4. Positive case: granting only ai_conversations create must pass the RBAC gate.
128
+ // send_message may still fail downstream (e.g. 400 "Organization has not set up credits"),
129
+ // but it must NOT be an access-denial error.
130
+ const grantRbap = await sdk.api.role_based_access_permissions.createOne({
131
+ role: grantRoleName,
132
+ permissions: {
133
+ ...PROVIDER_PERMISSIONS,
134
+ ai_conversations: { create: 'All', read: null, update: null, delete: null },
135
+ },
136
+ })
137
+ grantRbapId = grantRbap.id
138
+
139
+ await sdk.api.users.updateOne(
140
+ sdkNonAdmin.userInfo.id,
141
+ { roles: [grantRoleName] },
142
+ { replaceObjectFields: true },
143
+ )
144
+ await wait(undefined, 1500)
145
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
146
+
147
+ await async_test(
148
+ "F-0005: ai_conversations.send_message must NOT be access-denied when role grants create",
149
+ () => sdkNonAdmin.api.ai_conversations.send_message({
150
+ message: 'F-0005 positive-case test',
151
+ type: 'Test',
152
+ maxTokens: 1,
153
+ } as any)
154
+ .then(() => 'allowed' as const)
155
+ .catch((e: any) => {
156
+ const msg = (e?.message ?? '').toLowerCase()
157
+ const status = e?.status ?? e?.code
158
+ if (status === 403 || status === 401
159
+ || msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')) {
160
+ throw e // access denial — the gate incorrectly blocked a create-granted role
161
+ }
162
+ return 'allowed' as const // downstream (e.g. credits) error is fine — the gate passed
163
+ }),
164
+ { onResult: r => r === 'allowed' },
165
+ )
166
+
167
+ // 5. The create-granted (read/update-denied) role must still be blocked from CONTINUING an
168
+ // existing conversation — send_message with conversationId returns the full stored history
169
+ // and appends to it. Fake id is fine: the access check must fire before any lookup.
170
+ await async_test(
171
+ "F-0005: ai_conversations.send_message with conversationId must 403 when role denies read/update",
172
+ () => sdkNonAdmin.api.ai_conversations.send_message({
173
+ message: 'F-0005 conversationId bypass test',
174
+ type: 'Test',
175
+ maxTokens: 1,
176
+ conversationId: new ObjectId().toHexString(),
177
+ } as any),
178
+ {
179
+ shouldError: true,
180
+ onError: (e: any) => {
181
+ const msg = (e?.message ?? '').toLowerCase()
182
+ const status = e?.status ?? e?.code
183
+ return status === 403 || status === 401
184
+ || msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')
185
+ },
186
+ },
187
+ )
188
+
189
+ // 6. Regression: a role with default provider permissions (ai_conversations = Assigned)
190
+ // must be able to generate a new conversation without a 500. Previously the
191
+ // post-generation $push ran through the access-scoped DB, matched nothing under
192
+ // "Assigned", and crashed the handler with "Cannot read properties of null ('_id')".
193
+ const assignedRbap = await sdk.api.role_based_access_permissions.createOne({
194
+ role: assignedRoleName,
195
+ permissions: { ...PROVIDER_PERMISSIONS },
196
+ })
197
+ assignedRbapId = assignedRbap.id
198
+
199
+ await sdk.api.users.updateOne(
200
+ sdkNonAdmin.userInfo.id,
201
+ { roles: [assignedRoleName] },
202
+ { replaceObjectFields: true },
203
+ )
204
+ await wait(undefined, 1500)
205
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
206
+
207
+ await async_test(
208
+ "F-0005: ai_conversations.send_message must not 500 for Assigned-access (default provider) role",
209
+ () => sdkNonAdmin.api.ai_conversations.send_message({
210
+ message: 'F-0005 assigned-access regression test',
211
+ type: 'Test',
212
+ maxTokens: 1,
213
+ } as any)
214
+ .then(() => 'ok' as const)
215
+ .catch((e: any) => {
216
+ const msg = (e?.message ?? '').toLowerCase()
217
+ const status = e?.status ?? e?.code
218
+ if (status === 500 || msg.includes('internal error') || msg.includes('cannot read properties')) {
219
+ throw e // the null-update crash — regression
220
+ }
221
+ if (status === 403 || status === 401
222
+ || msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')) {
223
+ throw e // Assigned access must pass the RBAC gate
224
+ }
225
+ return 'ok' as const // downstream (e.g. credits) error is fine
226
+ }),
227
+ { onResult: r => r === 'ok' },
228
+ )
112
229
  } finally {
113
230
  // Cleanup: restore original roles, delete the test role.
114
231
  try {
@@ -121,6 +238,12 @@ export const ai_conversations_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk:
121
238
  if (rbapId) {
122
239
  try { await sdk.api.role_based_access_permissions.deleteOne(rbapId) } catch {}
123
240
  }
241
+ if (grantRbapId) {
242
+ try { await sdk.api.role_based_access_permissions.deleteOne(grantRbapId) } catch {}
243
+ }
244
+ if (assignedRbapId) {
245
+ try { await sdk.api.role_based_access_permissions.deleteOne(assignedRbapId) } catch {}
246
+ }
124
247
  // Re-authenticate the non-admin to drop the no-access role from their JWT
125
248
  // before subsequent tests run.
126
249
  // Role restore above re-triggers deauthenticate_user; wait > 1s so the freshly minted
@@ -0,0 +1,83 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import {
5
+ assert,
6
+ log_header,
7
+ } from "@tellescope/testing"
8
+ import { setup_tests } from "../setup"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ // Modular test for the translations framework.
13
+ // Validates that a model's `translations` field (nested { field: { language: text } }) is accepted by
14
+ // schema validation, persists, and accumulates multiple languages (uses phone_calls as the MVP model).
15
+ //
16
+ // NOTE: this test deliberately does NOT invoke the AI translation endpoint (ai_conversations.send_message).
17
+ // That path consumes AI credits and belongs in an eval, not a regularly-run test. The endpoint itself is
18
+ // already covered by the AI Summary feature, and the translation-specific logic (the system-prompt
19
+ // builder in @tellescope/utilities) is a pure function verified at compile time.
20
+ export const translations_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
21
+ log_header("Translations Framework Tests")
22
+
23
+ const enduser = await sdk.api.endusers.createOne({ fname: 'Translation', lname: 'Tester' })
24
+ let callId: string | undefined
25
+
26
+ try {
27
+ const call = await sdk.api.phone_calls.createOne({
28
+ enduserId: enduser.id,
29
+ inbound: true,
30
+ userId: sdk.userInfo.id,
31
+ transcription: 'Hola, por favor devuélvame la llamada cuando pueda.',
32
+ })
33
+ callId = call.id
34
+
35
+ // --- 1. translations field accepted + persisted ---
36
+ const translations = { transcription: { en: 'Hi, please call me back when you can.' } }
37
+ await sdk.api.phone_calls.updateOne(call.id, { translations }, { replaceObjectFields: true })
38
+
39
+ const updated = await sdk.api.phone_calls.getOne(call.id)
40
+ assert(
41
+ updated.translations?.transcription?.en === translations.transcription.en,
42
+ `translations not persisted: ${JSON.stringify(updated.translations)}`,
43
+ 'translations field persisted correctly',
44
+ )
45
+
46
+ // add a second language and confirm accumulation (full merged object written by convention)
47
+ const merged = {
48
+ ...updated.translations,
49
+ transcription: { ...updated.translations?.transcription, es: 'Hola de nuevo.' },
50
+ }
51
+ await sdk.api.phone_calls.updateOne(call.id, { translations: merged }, { replaceObjectFields: true })
52
+ const updated2 = await sdk.api.phone_calls.getOne(call.id)
53
+ assert(
54
+ !!updated2.translations?.transcription?.en && !!updated2.translations?.transcription?.es,
55
+ `translations did not accumulate: ${JSON.stringify(updated2.translations)}`,
56
+ 'multiple language translations accumulate under one field',
57
+ )
58
+ } finally {
59
+ if (callId) await sdk.api.phone_calls.deleteOne(callId).catch(() => {})
60
+ await sdk.api.endusers.deleteOne(enduser.id).catch(() => {})
61
+ }
62
+ }
63
+
64
+ // Allow running this test file independently
65
+ if (require.main === module) {
66
+ const sdk = new Session({ host })
67
+ const sdkNonAdmin = new Session({ host })
68
+
69
+ const runTests = async () => {
70
+ await setup_tests(sdk, sdkNonAdmin)
71
+ await translations_tests({ sdk, sdkNonAdmin })
72
+ }
73
+
74
+ runTests()
75
+ .then(() => {
76
+ console.log("✅ Translations test suite completed successfully")
77
+ process.exit(0)
78
+ })
79
+ .catch((error) => {
80
+ console.error("❌ Translations test suite failed:", error)
81
+ process.exit(1)
82
+ })
83
+ }
@@ -36,6 +36,7 @@ import {
36
36
 
37
37
  import { Session, APIQuery, EnduserSession } from "../sdk"
38
38
  import { enduser_observations_acknowledge_tests } from "./api_tests/enduser_observations_acknowledge.test"
39
+ import { translations_tests } from "./api_tests/translations.test"
39
40
  import { user_portal_settings_tests } from "./api_tests/user_portal_settings.test"
40
41
  import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
41
42
  import { get_some_projection_tests } from "./api_tests/get_some_projection.test"
@@ -15113,6 +15114,7 @@ const ip_address_form_tests = async () => {
15113
15114
  await inbox_threads_loading_tests()
15114
15115
  await load_inbox_data_tests({ sdk, sdkNonAdmin })
15115
15116
  await enduser_observations_acknowledge_tests({ sdk, sdkNonAdmin })
15117
+ await translations_tests({ sdk, sdkNonAdmin })
15116
15118
  await create_user_notifications_trigger_tests({ sdk })
15117
15119
  await group_mms_active_tests()
15118
15120
  await auto_reply_tests()
Binary file