@tellescope/sdk 2.0.0-beta.0 → 2.0.0-beta.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 (303) hide show
  1. package/CLAUDE.md +24 -0
  2. package/env.example +6 -0
  3. package/lib/cjs/sdk.d.ts +376 -3
  4. package/lib/cjs/sdk.d.ts.map +1 -1
  5. package/lib/cjs/sdk.js +1 -0
  6. package/lib/cjs/sdk.js.map +1 -1
  7. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  8. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  9. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
  10. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  11. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  12. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  13. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
  14. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  15. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  16. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  17. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
  18. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  19. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  20. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  21. package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
  22. package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
  23. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  24. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  25. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
  26. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  27. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  28. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  29. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
  30. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  31. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  32. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  33. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
  34. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  35. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
  36. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js +139 -9
  37. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
  38. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +3 -1
  39. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  40. package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
  41. package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
  42. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  43. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  44. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
  45. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  46. package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
  47. package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
  48. package/lib/cjs/tests/api_tests/programs.test.js +73 -17
  49. package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
  50. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  51. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  52. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
  53. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  54. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  55. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  56. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +58 -48
  57. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  58. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  59. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  60. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
  61. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  62. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  63. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  64. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
  65. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  66. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  67. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  68. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
  69. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  70. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  71. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  72. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  73. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  74. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  75. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  76. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  77. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
  78. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  79. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  80. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  81. package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
  82. package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
  83. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  84. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
  85. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  86. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  87. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  88. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
  89. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  90. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  91. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  92. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
  93. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  94. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  95. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  96. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
  97. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  98. package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  99. package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
  100. package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
  101. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  102. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  103. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
  104. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  105. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  106. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  107. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
  108. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  109. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  110. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  111. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
  112. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  113. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
  114. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  115. package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
  116. package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
  117. package/lib/cjs/tests/setup_isolated.d.ts +48 -0
  118. package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
  119. package/lib/cjs/tests/setup_isolated.js +119 -1
  120. package/lib/cjs/tests/setup_isolated.js.map +1 -1
  121. package/lib/cjs/tests/tests.d.ts.map +1 -1
  122. package/lib/cjs/tests/tests.js +378 -282
  123. package/lib/cjs/tests/tests.js.map +1 -1
  124. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  125. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  126. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
  127. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  128. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  129. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  130. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
  131. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  132. package/lib/cjs/tests/webhooks_tests.js +44 -34
  133. package/lib/cjs/tests/webhooks_tests.js.map +1 -1
  134. package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  135. package/lib/esm/sdk.d.ts +376 -3
  136. package/lib/esm/sdk.d.ts.map +1 -1
  137. package/lib/esm/sdk.js +1 -0
  138. package/lib/esm/sdk.js.map +1 -1
  139. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  140. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  141. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
  142. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  143. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  144. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  145. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
  146. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  147. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  148. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  149. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
  150. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  151. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  152. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  153. package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
  154. package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
  155. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  156. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  157. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
  158. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  159. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  160. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  161. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
  162. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  163. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  164. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  165. package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
  166. package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  167. package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
  168. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js +139 -9
  169. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
  170. package/lib/esm/tests/api_tests/organization_signup.test.d.ts +3 -1
  171. package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  172. package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
  173. package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
  174. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  175. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  176. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
  177. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  178. package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
  179. package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
  180. package/lib/esm/tests/api_tests/programs.test.js +72 -16
  181. package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
  182. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  183. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  184. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
  185. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  186. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  187. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  188. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +57 -47
  189. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  190. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  191. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  192. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
  193. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  194. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  195. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  196. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
  197. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  198. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  199. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  200. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
  201. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  202. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  203. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  204. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  205. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  206. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  207. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  208. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  209. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
  210. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  211. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  212. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  213. package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
  214. package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
  215. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  216. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
  217. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  218. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  219. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  220. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
  221. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  222. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  223. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  224. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
  225. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  226. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  227. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  228. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
  229. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  230. package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  231. package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
  232. package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
  233. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  234. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  235. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
  236. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  237. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  238. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  239. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
  240. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  241. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  242. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  243. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
  244. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  245. package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
  246. package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  247. package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
  248. package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
  249. package/lib/esm/tests/setup_isolated.d.ts +48 -0
  250. package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
  251. package/lib/esm/tests/setup_isolated.js +114 -0
  252. package/lib/esm/tests/setup_isolated.js.map +1 -1
  253. package/lib/esm/tests/tests.d.ts.map +1 -1
  254. package/lib/esm/tests/tests.js +379 -283
  255. package/lib/esm/tests/tests.js.map +1 -1
  256. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  257. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  258. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
  259. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  260. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  261. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  262. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
  263. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  264. package/lib/esm/tests/webhooks_tests.js +44 -34
  265. package/lib/esm/tests/webhooks_tests.js.map +1 -1
  266. package/lib/tsconfig.tsbuildinfo +1 -1
  267. package/package.json +12 -11
  268. package/src/sdk.ts +9 -2
  269. package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
  270. package/src/tests/api_tests/enduser_fields_sync.test.ts +154 -0
  271. package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
  272. package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
  273. package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
  274. package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
  275. package/src/tests/api_tests/load_threads_care_team.test.ts +214 -0
  276. package/src/tests/api_tests/mdi_case_offerings.test.ts +138 -0
  277. package/src/tests/api_tests/organization_signup.test.ts +4 -0
  278. package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
  279. package/src/tests/api_tests/programs.test.ts +351 -0
  280. package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
  281. package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +37 -26
  282. package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +436 -0
  283. package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
  284. package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
  285. package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
  286. package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
  287. package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
  288. package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
  289. package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
  290. package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
  291. package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
  292. package/src/tests/api_tests/totp_mfa.test.ts +10 -5
  293. package/src/tests/api_tests/trigger_contains_matching.test.ts +349 -0
  294. package/src/tests/api_tests/user_logs_restore_deleted.test.ts +419 -0
  295. package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
  296. package/src/tests/api_tests/vitl_push.test.ts +323 -0
  297. package/src/tests/setup_isolated.ts +140 -0
  298. package/src/tests/tests.ts +126 -20
  299. package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
  300. package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
  301. package/src/tests/unit_tests/trigger_title_partials.test.ts +174 -0
  302. package/src/tests/webhooks_tests.ts +6 -1
  303. package/test_generated.pdf +0 -0
@@ -0,0 +1,237 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ IsolatedSuite,
8
+ } from "../setup_isolated"
9
+
10
+ const RAND = () => Math.random().toString(36).slice(2, 10)
11
+
12
+ /**
13
+ * Coverage for the caching added to GET /v1/journeys/statistics.
14
+ *
15
+ * The aggregation behind this endpoint runs past the proxy's 30s socket timeout on large tenants,
16
+ * so the handler now stores each complete result in the `cache` collection and can serve it back
17
+ * without aggregating (`cachedOnly: true`). The editor paints the stored blob on mount and decides
18
+ * for itself when to rebuild.
19
+ *
20
+ * `setup_isolated` rather than the shared fixture because these checks assert on the COMPLETE
21
+ * per-step counts of a journey — on the shared org, other journey suites leave automated_actions
22
+ * behind and every "exactly these counts" assertion would be flaky. It also gives each run its own
23
+ * businessId, which is what the cache key is scoped by.
24
+ *
25
+ * automated_actions are created directly rather than by driving a real journey: the counts are what
26
+ * is under test, and seeding them makes the expected numbers exact and the test fast.
27
+ *
28
+ * The >30s path itself is not automatable here — it needs a tenant whose aggregation genuinely
29
+ * exceeds the proxy timeout. What IS covered is the contract the fix rests on: a non-cachedOnly
30
+ * call stores a blob, and a later cachedOnly call returns that same blob without recomputing.
31
+ */
32
+ export const journey_statistics_cache_checks = async (): Promise<Check[]> => {
33
+ const checks: Check[] = []
34
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
35
+
36
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
37
+
38
+ try {
39
+ tenant = await setup_isolated_tenant({
40
+ name: `JourneyStatisticsCache ${RAND()}`,
41
+ users: [{ isAdmin: true }, { roles: ['Non-Admin'] }],
42
+ apiKeys: 0,
43
+ })
44
+ const { sdk, sdkNonAdmin } = tenant
45
+
46
+ /* ================= fixtures ================= */
47
+
48
+ const [journey, uncachedJourney] = await Promise.all([
49
+ sdk.api.journeys.createOne({ title: `Stats Cache ${RAND()}` }),
50
+ sdk.api.journeys.createOne({ title: `Never Computed ${RAND()}` }),
51
+ ])
52
+
53
+ const step = await sdk.api.automation_steps.createOne({
54
+ journeyId: journey.id,
55
+ action: { type: 'setEnduserStatus', info: { status: 'Cached Stats' } },
56
+ events: [{ type: 'onJourneyStart', info: {} }],
57
+ })
58
+
59
+ const enduser = await sdk.api.endusers.createOne({
60
+ fname: 'Stats', lname: 'Cache', email: `dontsend-stats-cache-${RAND()}@tellescope.com`,
61
+ })
62
+
63
+ // 2 finished, 1 active, 1 cancelled => count 4. processAfter is far future so the worker
64
+ // cannot process (and so mutate) these out from under the assertions.
65
+ const seed = (status: 'finished' | 'active' | 'cancelled') => sdk.api.automated_actions.createOne({
66
+ journeyId: journey.id,
67
+ automationStepId: step.id,
68
+ enduserId: enduser.id,
69
+ processAfter: Date.now() + 1000000,
70
+ status,
71
+ event: { type: 'onJourneyStart', info: {} },
72
+ action: { type: 'setEnduserStatus', info: { status: 'Cached Stats' } },
73
+ })
74
+ await Promise.all([seed('finished'), seed('finished'), seed('active'), seed('cancelled')])
75
+
76
+ /* ====== 1. cachedOnly on a journey that was never computed ====== */
77
+
78
+ const neverComputed = await sdk.api.journeys.get_journey_statistics({
79
+ journeyId: uncachedJourney.id, cachedOnly: true,
80
+ })
81
+ record(
82
+ '1a. cachedOnly on an uncached journey returns empty steps, not an error',
83
+ !!neverComputed.statistics && Object.keys(neverComputed.statistics.steps ?? {}).length === 0,
84
+ `statistics=${JSON.stringify(neverComputed.statistics)}`,
85
+ )
86
+ // the absence of cachedAt is the whole signal the editor uses to decide to rebuild. If this
87
+ // ever starts returning a timestamp for a journey that was never computed, the editor would
88
+ // paint zeroes and never recompute them.
89
+ record(
90
+ '1b. cachedOnly on an uncached journey returns NO cachedAt',
91
+ neverComputed.cachedAt === undefined || neverComputed.cachedAt === null,
92
+ `cachedAt=${JSON.stringify(neverComputed.cachedAt)}`,
93
+ )
94
+
95
+ /* ====== 2. a plain call computes fresh numbers and now carries cachedAt ====== */
96
+
97
+ const fresh = await sdk.api.journeys.get_journey_statistics({ journeyId: journey.id })
98
+ const freshForStep = fresh.statistics?.steps?.[step.id]
99
+
100
+ record(
101
+ '2a. a plain call (no new parameter) still returns the per-step counts',
102
+ !!freshForStep
103
+ && freshForStep.count === 4
104
+ && freshForStep.finishedCount === 2
105
+ && freshForStep.activeCount === 1
106
+ && freshForStep.cancelledCount === 1
107
+ && freshForStep.errorCount === 0,
108
+ `steps[${step.id}]=${JSON.stringify(freshForStep)}`,
109
+ )
110
+ record(
111
+ '2b. a plain call returns a cachedAt for the result it just stored',
112
+ !!fresh.cachedAt && !isNaN(new Date(fresh.cachedAt).getTime()),
113
+ `cachedAt=${JSON.stringify(fresh.cachedAt)}`,
114
+ )
115
+ // engagement ran (there is a step id), so clicked must be a real 0 rather than absent —
116
+ // absent is what drives the editor's "Opens and link clicks are unavailable" line
117
+ record(
118
+ '2c. engagement ran, so clicked is 0 rather than absent',
119
+ freshForStep?.clicked === 0,
120
+ `clicked=${JSON.stringify(freshForStep?.clicked)}`,
121
+ )
122
+
123
+ /* ====== 3. store-then-cachedOnly-read returns the SAME blob ====== */
124
+
125
+ const fromCache = await sdk.api.journeys.get_journey_statistics({
126
+ journeyId: journey.id, cachedOnly: true,
127
+ })
128
+
129
+ record(
130
+ '3a. cachedOnly returns the stored statistics verbatim',
131
+ JSON.stringify(fromCache.statistics) === JSON.stringify(fresh.statistics),
132
+ `cached=${JSON.stringify(fromCache.statistics)} fresh=${JSON.stringify(fresh.statistics)}`,
133
+ )
134
+ // the same timestamp is the proof that cachedOnly SERVED the blob rather than recomputing it
135
+ record(
136
+ '3b. cachedOnly returns the stored cachedAt, proving it did not recompute',
137
+ !!fromCache.cachedAt && new Date(fromCache.cachedAt).getTime() === new Date(fresh.cachedAt!).getTime(),
138
+ `cached=${JSON.stringify(fromCache.cachedAt)} fresh=${JSON.stringify(fresh.cachedAt)}`,
139
+ )
140
+
141
+ /* ====== 4. the blob is per journey, not shared across journeys ====== */
142
+
143
+ const stillUncached = await sdk.api.journeys.get_journey_statistics({
144
+ journeyId: uncachedJourney.id, cachedOnly: true,
145
+ })
146
+ record(
147
+ '4. computing one journey does not populate another journey\'s blob',
148
+ (stillUncached.cachedAt === undefined || stillUncached.cachedAt === null)
149
+ && Object.keys(stillUncached.statistics?.steps ?? {}).length === 0,
150
+ `cachedAt=${JSON.stringify(stillUncached.cachedAt)} steps=${JSON.stringify(stillUncached.statistics?.steps)}`,
151
+ )
152
+
153
+ /* ====== 5. a rebuild refreshes the stored blob ====== */
154
+
155
+ await seed('finished') // a 5th action, so a rebuild must produce different numbers
156
+
157
+ const rebuilt = await sdk.api.journeys.get_journey_statistics({ journeyId: journey.id })
158
+ record(
159
+ '5a. a rebuild picks up records added since the blob was stored',
160
+ rebuilt.statistics?.steps?.[step.id]?.count === 5
161
+ && rebuilt.statistics?.steps?.[step.id]?.finishedCount === 3,
162
+ `steps[${step.id}]=${JSON.stringify(rebuilt.statistics?.steps?.[step.id])}`,
163
+ )
164
+ record(
165
+ '5b. the rebuild advanced cachedAt',
166
+ !!rebuilt.cachedAt && new Date(rebuilt.cachedAt).getTime() >= new Date(fresh.cachedAt!).getTime(),
167
+ `rebuilt=${JSON.stringify(rebuilt.cachedAt)} previous=${JSON.stringify(fresh.cachedAt)}`,
168
+ )
169
+
170
+ const afterRebuild = await sdk.api.journeys.get_journey_statistics({
171
+ journeyId: journey.id, cachedOnly: true,
172
+ })
173
+ record(
174
+ '5c. the refreshed blob is what a later cachedOnly read returns',
175
+ afterRebuild.statistics?.steps?.[step.id]?.count === 5,
176
+ `steps[${step.id}]=${JSON.stringify(afterRebuild.statistics?.steps?.[step.id])}`,
177
+ )
178
+
179
+ /* ====== 6. metric: 'steps' is unchanged for external consumers ====== */
180
+
181
+ // the parameter documents itself as per-step counts only; it must not start returning
182
+ // engagement just because a blob now exists for this journey
183
+ const stepsOnly = await sdk.api.journeys.get_journey_statistics({
184
+ journeyId: journey.id, metric: 'steps',
185
+ })
186
+ const stepsOnlyForStep = stepsOnly.statistics?.steps?.[step.id] as any
187
+ record(
188
+ '6a. metric: \'steps\' still returns the counts',
189
+ stepsOnlyForStep?.count === 5 && stepsOnlyForStep?.finishedCount === 3,
190
+ `steps[${step.id}]=${JSON.stringify(stepsOnlyForStep)}`,
191
+ )
192
+ record(
193
+ '6b. metric: \'steps\' does not return opens/clicked',
194
+ stepsOnlyForStep?.opens === undefined && stepsOnlyForStep?.clicked === undefined,
195
+ `opens=${JSON.stringify(stepsOnlyForStep?.opens)} clicked=${JSON.stringify(stepsOnlyForStep?.clicked)}`,
196
+ )
197
+
198
+ /* ====== 7. the endpoint is not admin-only ====== */
199
+
200
+ const asNonAdmin = await sdkNonAdmin.api.journeys.get_journey_statistics({
201
+ journeyId: journey.id, cachedOnly: true,
202
+ })
203
+ record(
204
+ '7. a non-admin staff user can still read journey statistics',
205
+ asNonAdmin.statistics?.steps?.[step.id]?.count === 5,
206
+ `steps[${step.id}]=${JSON.stringify(asNonAdmin.statistics?.steps?.[step.id])}`,
207
+ )
208
+ } catch (err: any) {
209
+ record('unexpected error', false, err?.message ?? JSON.stringify(err))
210
+ } finally {
211
+ await tenant?.teardown()
212
+ }
213
+
214
+ return checks
215
+ }
216
+
217
+ export const journey_statistics_cache_tests = async () => {
218
+ assert_checks('Journey statistics cache', await journey_statistics_cache_checks())
219
+ }
220
+
221
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
222
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Journey statistics cache', run: journey_statistics_cache_checks }
223
+
224
+ // Allow running this test file independently
225
+ if (require.main === module) {
226
+ const host = process.env.API_URL || 'http://localhost:8080'
227
+ console.log(`🌐 Using API URL: ${host}`)
228
+ journey_statistics_cache_tests()
229
+ .then(() => {
230
+ console.log("✅ Journey statistics cache test suite completed successfully")
231
+ process.exit(0)
232
+ })
233
+ .catch((error) => {
234
+ console.error("❌ Journey statistics cache test suite failed:", error)
235
+ process.exit(1)
236
+ })
237
+ }
@@ -0,0 +1,220 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ IsolatedSuite,
8
+ } from "../setup_isolated"
9
+
10
+ const RAND = () => Math.random().toString(36).slice(2, 10)
11
+
12
+ /**
13
+ * The backend contract behind the Journey editor's manual per-step count.
14
+ *
15
+ * When the journey statistics aggregation cannot produce numbers, a step node offers a "Get count"
16
+ * link that counts that one step through the ORDINARY automated_actions read — no new endpoint:
17
+ *
18
+ * automated_actions.getSome({ returnCount: true, filter: { automationStepId } })
19
+ * automated_actions.getSome({ returnCount: true, filter: { automationStepId, status: 'finished' } })
20
+ *
21
+ * Two properties the UI depends on and that nothing else covers:
22
+ *
23
+ * 1. `returnCount` resolves to `{ count }` (not a list), and `filter` is actually applied to the
24
+ * count — `queries.count` takes a different code path from `findPage`, so "the filter is
25
+ * honoured" is a real assertion, not a tautology. A count that ignored `filter` would silently
26
+ * report the whole tenant's automated_actions on every step.
27
+ * 2. The count is session-scoped the same way the list read is, so it equals the number of rows
28
+ * the step drawer lists for that same session. This is what makes a lower number for a
29
+ * restricted user correct rather than a bug — and it is asserted by comparing the count with
30
+ * the rows, for an admin and for a non-admin, rather than by hardcoding either access level.
31
+ *
32
+ * `setup_isolated` because every assertion here is an exact count over a whole businessId
33
+ * partition; on the shared org, other journey suites leave automated_actions behind.
34
+ *
35
+ * automated_actions are seeded directly rather than by driving a real journey: the counts are what
36
+ * is under test, and seeding makes the expected numbers exact and the run fast. `processAfter` is
37
+ * far in the future so the worker cannot process (and so mutate) them mid-assertion.
38
+ */
39
+ export const journey_step_manual_count_checks = async (): Promise<Check[]> => {
40
+ const checks: Check[] = []
41
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
42
+
43
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
44
+
45
+ try {
46
+ tenant = await setup_isolated_tenant({
47
+ name: `JourneyStepManualCount ${RAND()}`,
48
+ users: [{ isAdmin: true }, { roles: ['Non-Admin'] }],
49
+ apiKeys: 0,
50
+ })
51
+ const { sdk, sdkNonAdmin } = tenant
52
+
53
+ /* ================= fixtures ================= */
54
+
55
+ const journey = await sdk.api.journeys.createOne({ title: `Manual Count ${RAND()}` })
56
+
57
+ const makeStep = (status: string) => sdk.api.automation_steps.createOne({
58
+ journeyId: journey.id,
59
+ action: { type: 'setEnduserStatus', info: { status } },
60
+ events: [{ type: 'onJourneyStart', info: {} }],
61
+ })
62
+ // counted, a second step to prove the filter discriminates, and one that is never entered
63
+ const [countedStep, otherStep, emptyStep] = await Promise.all([
64
+ makeStep('Counted'), makeStep('Other'), makeStep('Empty'),
65
+ ])
66
+
67
+ const enduser = await sdk.api.endusers.createOne({
68
+ fname: 'Manual', lname: 'Count', email: `dontsend-manual-count-${RAND()}@tellescope.com`,
69
+ })
70
+
71
+ const seed = (automationStepId: string, status: 'finished' | 'active' | 'cancelled' | 'error') => (
72
+ sdk.api.automated_actions.createOne({
73
+ journeyId: journey.id,
74
+ automationStepId,
75
+ enduserId: enduser.id,
76
+ processAfter: Date.now() + 1000000,
77
+ status,
78
+ event: { type: 'onJourneyStart', info: {} },
79
+ action: { type: 'setEnduserStatus', info: { status: 'Counted' } },
80
+ })
81
+ )
82
+
83
+ // countedStep: 3 finished, 2 active, 1 cancelled, 1 error => total 7, finished 3.
84
+ // The non-finished statuses matter: the UI shows Total and Finished only, so a count that
85
+ // quietly excluded cancelled/error rows would still look plausible with a simpler fixture.
86
+ await Promise.all([
87
+ seed(countedStep.id, 'finished'), seed(countedStep.id, 'finished'), seed(countedStep.id, 'finished'),
88
+ seed(countedStep.id, 'active'), seed(countedStep.id, 'active'),
89
+ seed(countedStep.id, 'cancelled'),
90
+ seed(countedStep.id, 'error'),
91
+ // a second step, so a filter that was dropped would report 9 rather than 7
92
+ seed(otherStep.id, 'finished'), seed(otherStep.id, 'active'),
93
+ ])
94
+
95
+ /* ================= helpers ================= */
96
+
97
+ // getSome is typed Promise<T[]>, but a returnCount read resolves to { count }
98
+ const count_for = async (session: typeof sdk, automationStepId: string, status?: string) => (
99
+ await session.api.automated_actions.getSome({
100
+ returnCount: true,
101
+ filter: { automationStepId, ...(status ? { status } : { }) } as any,
102
+ }) as unknown as { count?: number }
103
+ )
104
+
105
+ const rows_for = (session: typeof sdk, automationStepId: string, status?: string) => (
106
+ session.api.automated_actions.getSome({
107
+ limit: 100, // the step drawer's page size
108
+ filter: { automationStepId, ...(status ? { status } : { }) } as any,
109
+ })
110
+ )
111
+
112
+ /* ====== 1. returnCount + filter: the shape and the arithmetic ====== */
113
+
114
+ const total = await count_for(sdk, countedStep.id)
115
+ record(
116
+ '1a. returnCount resolves to { count }, not a list of records',
117
+ !Array.isArray(total) && typeof total?.count === 'number',
118
+ `response=${JSON.stringify(total)}`,
119
+ )
120
+ record(
121
+ '1b. the count is filtered to the step, counting every status',
122
+ total?.count === 7,
123
+ `count=${JSON.stringify(total?.count)}, expected 7 (3 finished + 2 active + 1 cancelled + 1 error)`,
124
+ )
125
+
126
+ const finished = await count_for(sdk, countedStep.id, 'finished')
127
+ record(
128
+ '1c. adding status: finished narrows the same count',
129
+ finished?.count === 3,
130
+ `count=${JSON.stringify(finished?.count)}, expected 3`,
131
+ )
132
+
133
+ // if `filter` were dropped on the count path, both steps would report the tenant-wide 9
134
+ const otherTotal = await count_for(sdk, otherStep.id)
135
+ record(
136
+ '1d. a different step counts separately, so the filter is genuinely applied',
137
+ otherTotal?.count === 2,
138
+ `count=${JSON.stringify(otherTotal?.count)}, expected 2 (and NOT 9, the tenant-wide total)`,
139
+ )
140
+
141
+ // QA step 13: a step nobody has entered is 0, not a failure
142
+ const emptyTotal = await count_for(sdk, emptyStep.id)
143
+ record(
144
+ '1e. a step with no automated actions counts 0 rather than erroring',
145
+ emptyTotal?.count === 0,
146
+ `count=${JSON.stringify(emptyTotal?.count)}`,
147
+ )
148
+
149
+ /* ====== 2. the count equals the rows the step drawer lists ====== */
150
+
151
+ const [adminRows, adminFinishedRows] = await Promise.all([
152
+ rows_for(sdk, countedStep.id),
153
+ rows_for(sdk, countedStep.id, 'finished'),
154
+ ])
155
+ record(
156
+ '2a. the admin count equals the rows the same session can list',
157
+ total?.count === adminRows.length && finished?.count === adminFinishedRows.length,
158
+ `count=${total?.count} vs rows=${adminRows.length};`
159
+ + ` finished count=${finished?.count} vs rows=${adminFinishedRows.length}`,
160
+ )
161
+
162
+ /* ====== 3. the count is session-scoped, the same way the list read is ====== */
163
+
164
+ // The point is NOT that a non-admin sees a particular number — that depends on the tenant's
165
+ // role-based access for automated_actions. It is that whatever they can READ is what they
166
+ // COUNT, so the number on the node always matches the drawer they open from it.
167
+ const [nonAdminTotal, nonAdminFinished] = await Promise.all([
168
+ count_for(sdkNonAdmin, countedStep.id),
169
+ count_for(sdkNonAdmin, countedStep.id, 'finished'),
170
+ ])
171
+ const [nonAdminRows, nonAdminFinishedRows] = await Promise.all([
172
+ rows_for(sdkNonAdmin, countedStep.id),
173
+ rows_for(sdkNonAdmin, countedStep.id, 'finished'),
174
+ ])
175
+
176
+ record(
177
+ '3a. a non-admin count equals the rows that non-admin can list',
178
+ nonAdminTotal?.count === nonAdminRows.length
179
+ && nonAdminFinished?.count === nonAdminFinishedRows.length,
180
+ `count=${nonAdminTotal?.count} vs rows=${nonAdminRows.length};`
181
+ + ` finished count=${nonAdminFinished?.count} vs rows=${nonAdminFinishedRows.length}`,
182
+ )
183
+ // a restricted session may legitimately count lower, but never higher than the admin
184
+ record(
185
+ '3b. a non-admin never counts more than the admin',
186
+ (nonAdminTotal?.count ?? 0) <= (total?.count ?? 0)
187
+ && (nonAdminFinished?.count ?? 0) <= (finished?.count ?? 0),
188
+ `non-admin total=${nonAdminTotal?.count} vs admin ${total?.count};`
189
+ + ` non-admin finished=${nonAdminFinished?.count} vs admin ${finished?.count}`,
190
+ )
191
+ record(
192
+ '3c. finished never exceeds the total, for either session',
193
+ (finished?.count ?? 0) <= (total?.count ?? 0)
194
+ && (nonAdminFinished?.count ?? 0) <= (nonAdminTotal?.count ?? 0),
195
+ `admin ${finished?.count}/${total?.count}, non-admin ${nonAdminFinished?.count}/${nonAdminTotal?.count}`,
196
+ )
197
+
198
+ return checks
199
+ } finally {
200
+ await tenant?.teardown()
201
+ }
202
+ }
203
+
204
+ /** Asserting wrapper, for standalone runs. */
205
+ export const journey_step_manual_count_tests = async () => {
206
+ assert_checks('Journey step manual count', await journey_step_manual_count_checks())
207
+ }
208
+
209
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
210
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Journey step manual count', run: journey_step_manual_count_checks }
211
+
212
+ // Allow running this test file independently
213
+ if (require.main === module) {
214
+ const host = process.env.API_URL || 'http://localhost:8080'
215
+ console.log(`🌐 Using API URL: ${host}`)
216
+
217
+ journey_step_manual_count_tests()
218
+ .then(() => { console.log("✅ Journey step manual count tests passed"); process.exit(0) })
219
+ .catch(error => { console.error("❌ Journey step manual count tests failed:", error); process.exit(1) })
220
+ }
@@ -0,0 +1,214 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ IsolatedSuite,
8
+ } from "../setup_isolated"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ const RAND = () => Math.random().toString(36).slice(2, 10)
13
+
14
+ /**
15
+ * Coverage for the `careTeamUserIds` parameter on `load_threads` (GET /v1/inbox-threads/load).
16
+ *
17
+ * The caller used to resolve "every patient these users are on the care team for" client-side and
18
+ * push the resulting enduser ids into `mdbFilter`. For a care team of hundreds of patients that
19
+ * overflows the GET query string (Node's 16KB header limit) before any Tellescope code runs. The
20
+ * parameter moves the join to the server: the API resolves the user ids to endusers via
21
+ * `assignedTo`, and ANDs `{ enduserIds: { $in: [...] } }` onto the thread filter.
22
+ *
23
+ * `setup_isolated` rather than `setup_tests` because several checks assert on the COMPLETE thread
24
+ * set of the tenant. On the shared fixture org, `build_threads` and the other inbox suites leave
25
+ * threads behind and every "returns exactly these" assertion would be flaky.
26
+ *
27
+ * The threads are created directly with `inbox_threads.createOne` (the model exposes create for
28
+ * exactly this reason) rather than via `build_threads`, so the tenant contains exactly three.
29
+ *
30
+ * Check 4 is the one that catches a wrong implementation: a care team with no patients resolves to
31
+ * an empty enduser list, and the clause must still be applied (returning nothing) rather than
32
+ * dropped (returning everything). Check 5 is its counterweight — it rules out an implementation
33
+ * that returns nothing for every call, which would satisfy 3, 4 and 7c vacuously.
34
+ *
35
+ * Check 7 is the permission boundary. A provisioned non-admin gets
36
+ * `sms_messages: ASSIGNED_AND_DEFAULT_ACCESS` (read: 'Assigned'), and the fixture threads carry
37
+ * `userIds: []`, so that session sees a thread only through the assigned-enduser branch. 7c fails
38
+ * if the care-team clause is ever pushed into the access `$or` instead of `filter.$and`, i.e. if
39
+ * the parameter could be used to WIDEN what a session can see.
40
+ */
41
+ export const load_threads_care_team_checks = async (): Promise<Check[]> => {
42
+ const checks: Check[] = []
43
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
44
+
45
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
46
+
47
+ try {
48
+ /* ================= fixtures ================= */
49
+
50
+ tenant = await setup_isolated_tenant({
51
+ name: `LoadThreadsCareTeam ${RAND()}`,
52
+ users: [{ isAdmin: true }, { roles: ['Non-Admin'] }],
53
+ apiKeys: 0,
54
+ })
55
+
56
+ const { sdk, sdkNonAdmin } = tenant
57
+ const NA = tenant.users[1].id // the non-admin's own user id
58
+
59
+ // care-team users: they never log in, they exist only to appear in endusers.assignedTo
60
+ const [U1, U2, U3] = await Promise.all([1, 2, 3].map(n => sdk.api.users.createOne({
61
+ email: `dontsend-care-team-${n}-${RAND()}@tellescope.com`,
62
+ fname: 'CareTeam', lname: `User${n}`,
63
+ verifiedEmail: true,
64
+ notificationEmailsDisabled: true,
65
+ })))
66
+
67
+ // E1 is also assigned to the non-admin, so check 7a has a non-empty baseline to measure against
68
+ const E1 = await sdk.api.endusers.createOne({ fname: 'CareTeam', lname: 'PatientOne', assignedTo: [U1.id, NA] })
69
+ const E2 = await sdk.api.endusers.createOne({ fname: 'CareTeam', lname: 'PatientTwo', assignedTo: [U2.id] })
70
+ const E3 = await sdk.api.endusers.createOne({ fname: 'CareTeam', lname: 'PatientThree', assignedTo: [] })
71
+
72
+ const now = Date.now()
73
+ const MINUTE = 60 * 1000
74
+ const defaultThreadFields = {
75
+ type: 'SMS' as const,
76
+ assignedTo: [],
77
+ userIds: [],
78
+ inboxStatus: 'New',
79
+ preview: 'care team test',
80
+ title: 'Care team test',
81
+ }
82
+
83
+ const T1 = await sdk.api.inbox_threads.createOne({
84
+ ...defaultThreadFields,
85
+ enduserIds: [E1.id], threadId: `care-team-t1-${RAND()}`, timestamp: new Date(now - 3 * MINUTE),
86
+ })
87
+ const T2 = await sdk.api.inbox_threads.createOne({
88
+ ...defaultThreadFields,
89
+ enduserIds: [E2.id], threadId: `care-team-t2-${RAND()}`, timestamp: new Date(now - 2 * MINUTE),
90
+ })
91
+ const T3 = await sdk.api.inbox_threads.createOne({
92
+ ...defaultThreadFields,
93
+ enduserIds: [E3.id], threadId: `care-team-t3-${RAND()}`, timestamp: new Date(now - 1 * MINUTE),
94
+ })
95
+
96
+ const label_for: { [id: string]: string } = { [T1.id]: 'T1', [T2.id]: 'T2', [T3.id]: 'T3' }
97
+ const describe = (ids: string[]) => `[${ids.map(id => label_for[id] || id).sort().join(', ')}]`
98
+ const load = async (args: any, session = sdk) => (
99
+ (await session.api.inbox_threads.load_threads(args)).threads.map(t => t.id)
100
+ )
101
+ const same_set = (a: string[], b: string[]) => (
102
+ a.length === b.length && [...a].sort().join() === [...b].sort().join()
103
+ )
104
+
105
+ /* ===== 1: a single care-team user ===== */
106
+
107
+ const one = await load({ careTeamUserIds: [U1.id] })
108
+ record(
109
+ '1: careTeamUserIds=[U1] returns exactly the thread for U1\'s patient',
110
+ same_set(one, [T1.id]),
111
+ `got ${describe(one)} (want [T1])`,
112
+ )
113
+
114
+ /* ===== 2: multiple care-team users union their patients ===== */
115
+
116
+ const two = await load({ careTeamUserIds: [U1.id, U2.id] })
117
+ record(
118
+ '2: careTeamUserIds=[U1, U2] returns both their threads and not the unassigned one',
119
+ same_set(two, [T1.id, T2.id]),
120
+ `got ${describe(two)} (want [T1, T2] — T3\'s enduser has an empty care team)`,
121
+ )
122
+
123
+ /* ===== 3: ANDs with enduserIds rather than ORing ===== */
124
+
125
+ const intersect = await load({ careTeamUserIds: [U2.id], enduserIds: [E1.id] })
126
+ record(
127
+ '3: careTeamUserIds AND enduserIds narrows to the intersection',
128
+ intersect.length === 0,
129
+ `got ${describe(intersect)} (want [] — E1 is not on U2\'s care team; a non-empty result means the`
130
+ + ` two filters OR instead of AND)`,
131
+ )
132
+
133
+ /* ===== 4: an empty resolution must not degrade to "no filter" ===== */
134
+
135
+ const emptyCareTeam = await load({ careTeamUserIds: [U3.id] })
136
+ record(
137
+ '4: a care-team user with no patients returns no threads, not every thread',
138
+ emptyCareTeam.length === 0,
139
+ `got ${describe(emptyCareTeam)} (want [] — returning all three means the clause was dropped when`
140
+ + ` the enduser lookup came back empty)`,
141
+ )
142
+
143
+ /* ===== 5: the feature-off path is unchanged ===== */
144
+
145
+ const baseline = await load({})
146
+ const emptyParam = await load({ careTeamUserIds: [] })
147
+ record(
148
+ '5: omitting careTeamUserIds, and passing [], both return exactly the three fixture threads',
149
+ same_set(baseline, [T1.id, T2.id, T3.id]) && same_set(emptyParam, baseline),
150
+ `omitted=${describe(baseline)} empty=${describe(emptyParam)} (want [T1, T2, T3] for both)`,
151
+ )
152
+
153
+ /* ===== 6: the count path shares the filter ===== */
154
+
155
+ const { count } = await sdk.api.inbox_threads.load_threads({ careTeamUserIds: [U1.id], returnCount: true })
156
+ record(
157
+ '6: returnCount applies the care-team filter too',
158
+ count === 1,
159
+ `count=${count} (want 1 — 3 means the count path skipped the filter)`,
160
+ )
161
+
162
+ /* ===== 7: permission boundary — the parameter can only narrow, never widen ===== */
163
+
164
+ const naBaseline = await load({}, sdkNonAdmin)
165
+ record(
166
+ '7a: the non-admin\'s unfiltered inbox is exactly the thread for the patient assigned to them',
167
+ same_set(naBaseline, [T1.id]),
168
+ `got ${describe(naBaseline)} (want [T1] — the baseline the next two checks are measured against;`
169
+ + ` if this is empty, 7b and 7c pass vacuously)`,
170
+ )
171
+
172
+ const naOwnCareTeam = await load({ careTeamUserIds: [U1.id] }, sdkNonAdmin)
173
+ record(
174
+ '7b: the parameter does not remove a thread the non-admin legitimately sees',
175
+ same_set(naOwnCareTeam, [T1.id]),
176
+ `got ${describe(naOwnCareTeam)} (want [T1])`,
177
+ )
178
+
179
+ const naForeignCareTeam = await load({ careTeamUserIds: [U2.id] }, sdkNonAdmin)
180
+ record(
181
+ '7c: the parameter cannot reach a thread outside the non-admin\'s access',
182
+ naForeignCareTeam.length === 0,
183
+ `got ${describe(naForeignCareTeam)} (want [] — T2 matches U2\'s care team but is outside this`
184
+ + ` session's access; a non-empty result means the clause was pushed into the access $or)`,
185
+ )
186
+ } finally {
187
+ // Teardown BEFORE anything asserts — assert calls process.exit, which would strand the tenant.
188
+ if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
189
+ }
190
+
191
+ return checks
192
+ }
193
+
194
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
195
+ export const load_threads_care_team_tests = async () => {
196
+ assert_checks('Load threads care team', await load_threads_care_team_checks())
197
+ }
198
+
199
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
200
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Load threads care team', run: load_threads_care_team_checks }
201
+
202
+ // Allow running this test file independently
203
+ if (require.main === module) {
204
+ console.log(`🌐 Using API URL: ${host}`)
205
+ load_threads_care_team_tests()
206
+ .then(() => {
207
+ console.log("✅ Load threads care team test suite completed successfully")
208
+ process.exit(0)
209
+ })
210
+ .catch((error) => {
211
+ console.error("❌ Load threads care team test suite failed:", error)
212
+ process.exit(1)
213
+ })
214
+ }