@tellescope/sdk 2.0.0-beta.1 → 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 (293) hide show
  1. package/CLAUDE.md +24 -0
  2. package/env.example +6 -0
  3. package/lib/cjs/sdk.d.ts +373 -3
  4. package/lib/cjs/sdk.d.ts.map +1 -1
  5. package/lib/cjs/sdk.js.map +1 -1
  6. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  7. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  8. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
  9. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  10. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  11. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  12. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
  13. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  14. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  15. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  16. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
  17. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  18. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  19. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  20. package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
  21. package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
  22. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  23. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  24. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
  25. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  26. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  27. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  28. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
  29. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  30. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  31. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  32. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
  33. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  34. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +3 -1
  35. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  36. package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
  37. package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
  38. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  39. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  40. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
  41. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  42. package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
  43. package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
  44. package/lib/cjs/tests/api_tests/programs.test.js +73 -17
  45. package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
  46. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  47. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  48. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
  49. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  50. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  51. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  52. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +3 -1
  53. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  54. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  55. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  56. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
  57. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  58. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  59. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  60. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
  61. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  62. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  63. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  64. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
  65. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  66. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  67. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  68. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  69. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  70. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  71. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  72. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  73. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
  74. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  75. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  76. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  77. package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
  78. package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
  79. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  80. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
  81. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  82. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  83. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  84. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
  85. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  86. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  87. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  88. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
  89. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  90. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  91. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  92. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
  93. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  94. package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  95. package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
  96. package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
  97. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  98. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  99. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
  100. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  101. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  102. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  103. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
  104. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  105. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  106. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  107. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
  108. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  109. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
  110. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  111. package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
  112. package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
  113. package/lib/cjs/tests/setup_isolated.d.ts +48 -0
  114. package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
  115. package/lib/cjs/tests/setup_isolated.js +119 -1
  116. package/lib/cjs/tests/setup_isolated.js.map +1 -1
  117. package/lib/cjs/tests/tests.d.ts.map +1 -1
  118. package/lib/cjs/tests/tests.js +374 -292
  119. package/lib/cjs/tests/tests.js.map +1 -1
  120. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  121. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  122. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
  123. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  124. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  125. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  126. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
  127. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  128. package/lib/cjs/tests/webhooks_tests.js +44 -34
  129. package/lib/cjs/tests/webhooks_tests.js.map +1 -1
  130. package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  131. package/lib/esm/sdk.d.ts +373 -3
  132. package/lib/esm/sdk.d.ts.map +1 -1
  133. package/lib/esm/sdk.js.map +1 -1
  134. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  135. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  136. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
  137. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  138. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  139. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  140. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
  141. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  142. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  143. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  144. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
  145. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  146. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  147. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  148. package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
  149. package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
  150. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  151. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  152. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
  153. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  154. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  155. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  156. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
  157. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  158. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  159. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  160. package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
  161. package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  162. package/lib/esm/tests/api_tests/organization_signup.test.d.ts +3 -1
  163. package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  164. package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
  165. package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
  166. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  167. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  168. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
  169. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  170. package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
  171. package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
  172. package/lib/esm/tests/api_tests/programs.test.js +72 -16
  173. package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
  174. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  175. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  176. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
  177. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  178. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  179. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  180. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +2 -0
  181. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  182. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  183. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  184. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
  185. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  186. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  187. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  188. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
  189. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  190. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  191. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  192. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
  193. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  194. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  195. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  196. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  197. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  198. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  199. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  200. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  201. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
  202. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  203. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  204. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  205. package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
  206. package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
  207. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  208. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
  209. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  210. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  211. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  212. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
  213. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  214. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  215. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  216. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
  217. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  218. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  219. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  220. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
  221. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  222. package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  223. package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
  224. package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
  225. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  226. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  227. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
  228. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  229. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  230. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  231. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
  232. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  233. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  234. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  235. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
  236. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  237. package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
  238. package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  239. package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
  240. package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
  241. package/lib/esm/tests/setup_isolated.d.ts +48 -0
  242. package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
  243. package/lib/esm/tests/setup_isolated.js +114 -0
  244. package/lib/esm/tests/setup_isolated.js.map +1 -1
  245. package/lib/esm/tests/tests.d.ts.map +1 -1
  246. package/lib/esm/tests/tests.js +375 -293
  247. package/lib/esm/tests/tests.js.map +1 -1
  248. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  249. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  250. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
  251. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  252. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  253. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  254. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
  255. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  256. package/lib/esm/tests/webhooks_tests.js +44 -34
  257. package/lib/esm/tests/webhooks_tests.js.map +1 -1
  258. package/lib/tsconfig.tsbuildinfo +1 -1
  259. package/package.json +12 -11
  260. package/src/sdk.ts +3 -2
  261. package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
  262. package/src/tests/api_tests/enduser_fields_sync.test.ts +4 -0
  263. package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
  264. package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
  265. package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
  266. package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
  267. package/src/tests/api_tests/load_threads_care_team.test.ts +4 -0
  268. package/src/tests/api_tests/organization_signup.test.ts +4 -0
  269. package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
  270. package/src/tests/api_tests/programs.test.ts +351 -0
  271. package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
  272. package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +4 -0
  273. package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +4 -0
  274. package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
  275. package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
  276. package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
  277. package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
  278. package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
  279. package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
  280. package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
  281. package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
  282. package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
  283. package/src/tests/api_tests/totp_mfa.test.ts +10 -5
  284. package/src/tests/api_tests/trigger_contains_matching.test.ts +4 -0
  285. package/src/tests/api_tests/user_logs_restore_deleted.test.ts +4 -0
  286. package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
  287. package/src/tests/api_tests/vitl_push.test.ts +323 -0
  288. package/src/tests/setup_isolated.ts +140 -0
  289. package/src/tests/tests.ts +124 -30
  290. package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
  291. package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
  292. package/src/tests/webhooks_tests.ts +6 -1
  293. package/test_generated.pdf +0 -0
@@ -0,0 +1,393 @@
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
+ const FORM_VITAL_READING_SOURCE = 'tellescope-form'
15
+
16
+ /**
17
+ * Coverage for the per-question opt-in that turns a numeric form answer into an
18
+ * `enduser_observations` vital reading on submission — `form_fields.options.vitalUnit`, consumed by
19
+ * `create_vital_readings_for_form_response` in api/modules/forms.ts.
20
+ *
21
+ * The point of the feature is that weight (and the other VITAL_UNITS) collected in a form charts
22
+ * beside device, portal and manually entered readings. The point of the opt-in being per FIELD is
23
+ * that nothing changes for an existing org or form until an admin configures a question — which is
24
+ * why the "no opt-in configured" check below is as load-bearing as the positive ones.
25
+ *
26
+ * Provisions its own throwaway tenant: readings are read back by enduser, and `handle_vital_create`
27
+ * reads business-wide vital_configurations, so a shared tenant would let concurrently running suites
28
+ * see each other's observations. Never calls `sdk.reset_db()`.
29
+ *
30
+ * Readings are created as an asynchronous side effect of the submission (fire-and-forget, so that a
31
+ * reading can never delay or fail a patient's submit), so every read polls — and every NEGATIVE
32
+ * check polls too, with a barrier that proves the submission's side effects have run.
33
+ */
34
+
35
+ const poll_for = async <T,>(get: () => Promise<T>, done: (v: T) => boolean, attempts=20): Promise<T> => {
36
+ let value = await get()
37
+ for (let i = 0; i < attempts && !done(value); i++) {
38
+ await new Promise(resolve => setTimeout(resolve, 250))
39
+ value = await get()
40
+ }
41
+ return value
42
+ }
43
+
44
+ export const form_vital_reading_checks = async (): Promise<Check[]> => {
45
+ const checks: Check[] = []
46
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
47
+
48
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
49
+
50
+ try {
51
+ tenant = await setup_isolated_tenant({
52
+ name: `FormVitals ${RAND()}`,
53
+ users: [{ isAdmin: true }],
54
+ apiKeys: 0,
55
+ })
56
+ const { sdk } = tenant
57
+
58
+ const observations_for = (enduserId: string) => (
59
+ sdk.api.enduser_observations.getSome({ filter: { enduserId } })
60
+ )
61
+
62
+ /* ===== A: a configured Number question creates a reading on submission ===== */
63
+
64
+ const form = await sdk.api.forms.createOne({ title: `Vitals intake ${RAND()}` })
65
+ const weightField = await sdk.api.form_fields.createOne({
66
+ formId: form.id,
67
+ title: 'Weight (lbs)',
68
+ type: 'number',
69
+ options: { vitalUnit: 'LB' },
70
+ previousFields: [{ type: 'root', info: {} }],
71
+ })
72
+ // a second question with NO vitalUnit, to prove the opt-in is per field and not per form
73
+ const plainField = await sdk.api.form_fields.createOne({
74
+ formId: form.id,
75
+ title: 'How many glasses of water?',
76
+ type: 'number',
77
+ previousFields: [{ type: 'after', info: { fieldId: weightField.id } }],
78
+ })
79
+
80
+ const enduser = await sdk.api.endusers.createOne({ fname: 'Form', lname: 'Vitals' })
81
+
82
+ const prepared = await sdk.api.form_responses.prepare_form_response({
83
+ formId: form.id, enduserId: enduser.id,
84
+ })
85
+ const submittedAt = new Date()
86
+ await sdk.api.form_responses.submit_form_response({
87
+ accessCode: prepared.accessCode,
88
+ responses: [
89
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 185 } },
90
+ { fieldId: plainField.id, fieldTitle: plainField.title, answer: { type: 'number', value: 8 } },
91
+ ],
92
+ })
93
+
94
+ const readings = await poll_for(() => observations_for(enduser.id), o => o.length >= 1)
95
+
96
+ record(
97
+ 'A1: exactly one reading is created — the opt-in is per field, not per form',
98
+ readings.length === 1,
99
+ `created ${readings.length}: ${JSON.stringify(readings.map(r => r.measurement))}`
100
+ + ` — the second Number question has no vitalUnit and must contribute nothing`,
101
+ )
102
+
103
+ const reading = readings[0]
104
+ record(
105
+ 'A2: the reading carries the answer value in the configured unit',
106
+ reading?.measurement?.value === 185 && reading?.measurement?.unit === 'LB',
107
+ `measurement=${JSON.stringify(reading?.measurement)} (want { value: 185, unit: 'LB' })`,
108
+ )
109
+ record(
110
+ 'A3: source marks it as form-collected',
111
+ reading?.source === FORM_VITAL_READING_SOURCE,
112
+ `source=${JSON.stringify(reading?.source)} (want ${JSON.stringify(FORM_VITAL_READING_SOURCE)})`
113
+ + ` — the webapp Vitals table renders an unlisted source as "Source: <source>"`,
114
+ )
115
+ record(
116
+ 'A4: it is a final vital-signs observation, like any other charted reading',
117
+ reading?.category === 'vital-signs' && reading?.status === 'final',
118
+ `category=${JSON.stringify(reading?.category)} status=${JSON.stringify(reading?.status)}`,
119
+ )
120
+ record(
121
+ 'A5: timestamp and recordedAt are the submission time',
122
+ (
123
+ !!reading?.timestamp && !!reading?.recordedAt
124
+ && new Date(reading.recordedAt).getTime() === new Date(reading.timestamp).getTime()
125
+ && Math.abs(new Date(reading.timestamp).getTime() - submittedAt.getTime()) < 1000 * 60 * 5
126
+ ),
127
+ `timestamp=${JSON.stringify(reading?.timestamp)} recordedAt=${JSON.stringify(reading?.recordedAt)}`
128
+ + ` (want both ~= the submission at ${submittedAt.toISOString()})`,
129
+ )
130
+ record(
131
+ 'A6: externalId links the reading back to <formResponseId>:<fieldId>',
132
+ reading?.externalId === `${prepared.response.id}:${weightField.id}`,
133
+ `externalId=${JSON.stringify(reading?.externalId)}`
134
+ + ` (want ${JSON.stringify(`${prepared.response.id}:${weightField.id}`)})`,
135
+ )
136
+ record(
137
+ 'A7: vital triggers are left enabled for an ordinary enduser',
138
+ reading?.dontTrigger !== true,
139
+ `dontTrigger=${JSON.stringify(reading?.dontTrigger)} (want not true, so Vital Update triggers fire)`,
140
+ )
141
+
142
+ /* ===== C: a form with no opted-in question creates nothing ===== */
143
+
144
+ const plainForm = await sdk.api.forms.createOne({ title: `No vitals ${RAND()}` })
145
+ const plainOnlyField = await sdk.api.form_fields.createOne({
146
+ formId: plainForm.id,
147
+ title: 'Current weight',
148
+ type: 'number',
149
+ previousFields: [{ type: 'root', info: {} }],
150
+ })
151
+ const plainEnduser = await sdk.api.endusers.createOne({ fname: 'NoOptIn', lname: 'Patient' })
152
+ const plainPrepared = await sdk.api.form_responses.prepare_form_response({
153
+ formId: plainForm.id, enduserId: plainEnduser.id,
154
+ })
155
+ await sdk.api.form_responses.submit_form_response({
156
+ accessCode: plainPrepared.accessCode,
157
+ responses: [
158
+ { fieldId: plainOnlyField.id, fieldTitle: plainOnlyField.title, answer: { type: 'number', value: 200 } },
159
+ ],
160
+ })
161
+ // barrier: the submission completed, so its side effects have had their chance
162
+ await poll_for(
163
+ () => sdk.api.form_responses.getOne(plainPrepared.response.id),
164
+ r => !!r.submittedAt,
165
+ )
166
+ const plainReadings = await poll_for(() => observations_for(plainEnduser.id), o => o.length > 0, 8)
167
+ record(
168
+ 'C1: a Number question with no vitalUnit creates no reading',
169
+ plainReadings.length === 0,
170
+ `created ${plainReadings.length}: ${JSON.stringify(plainReadings.map(r => r.measurement))}`
171
+ + ` — this is what keeps every existing org and form unchanged`,
172
+ )
173
+
174
+ /* ===== D: blank and non-numeric answers are skipped, and never fail the submission ===== */
175
+
176
+ const skipEnduser = await sdk.api.endusers.createOne({ fname: 'Skipped', lname: 'Answers' })
177
+ const skipPrepared = await sdk.api.form_responses.prepare_form_response({
178
+ formId: form.id, enduserId: skipEnduser.id,
179
+ })
180
+
181
+ let submitError: any = undefined
182
+ try {
183
+ await sdk.api.form_responses.submit_form_response({
184
+ accessCode: skipPrepared.accessCode,
185
+ responses: [
186
+ // a blank answer for the opted-in question
187
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: '' as any } },
188
+ { fieldId: plainField.id, fieldTitle: plainField.title, answer: { type: 'number', value: 8 } },
189
+ ],
190
+ })
191
+ } catch (err) { submitError = err }
192
+
193
+ record(
194
+ 'D1: a blank answer does not fail the submission',
195
+ submitError === undefined,
196
+ `error=${submitError ? (submitError.message || JSON.stringify(submitError)) : 'none'}`
197
+ + ` — a patient must never see an error because of a vital reading`,
198
+ )
199
+ if (submitError === undefined) {
200
+ await poll_for(
201
+ () => sdk.api.form_responses.getOne(skipPrepared.response.id),
202
+ r => !!r.submittedAt,
203
+ )
204
+ const skipReadings = await poll_for(() => observations_for(skipEnduser.id), o => o.length > 0, 8)
205
+ record(
206
+ 'D2: a blank answer creates no reading',
207
+ skipReadings.length === 0,
208
+ `created ${skipReadings.length}: ${JSON.stringify(skipReadings.map(r => r.measurement))}`,
209
+ )
210
+ }
211
+
212
+ // A non-numeric answer never reaches the reading code over HTTP: the submit endpoint validates
213
+ // a 'number' answer with numberValidator ("Response must be a number") and rejects the whole
214
+ // submission. Pinned here because QA step 16 is written as if the field type allowed it.
215
+ // The parseFloat/NaN guard in create_vital_readings_for_form_response is therefore defence in
216
+ // depth for the non-HTTP path (submit_form_response_internal, whose responses are persisted
217
+ // incrementally rather than through this validator).
218
+ const badEnduser = await sdk.api.endusers.createOne({ fname: 'NonNumeric', lname: 'Answer' })
219
+ const badPrepared = await sdk.api.form_responses.prepare_form_response({
220
+ formId: form.id, enduserId: badEnduser.id,
221
+ })
222
+ let badError: any = undefined
223
+ try {
224
+ await sdk.api.form_responses.submit_form_response({
225
+ accessCode: badPrepared.accessCode,
226
+ responses: [
227
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 'one hundred' as any } },
228
+ ],
229
+ })
230
+ } catch (err) { badError = err }
231
+ const badReadings = await poll_for(() => observations_for(badEnduser.id), o => o.length > 0, 8)
232
+ record(
233
+ 'D3: a non-numeric answer on a Number question is rejected and creates no reading',
234
+ // assert the rejection too, so this check starts failing if numberValidator ever stops
235
+ // rejecting and the answer begins reaching create_vital_readings_for_form_response for real
236
+ !!badError && badReadings.length === 0,
237
+ `submit ${badError ? `rejected (${badError.message || JSON.stringify(badError)})` : 'ACCEPTED'};`
238
+ + ` ${badReadings.length} reading(s) — rejection happens in numberValidator, before this feature`,
239
+ )
240
+
241
+ /* ===== E: one unit per question — SBP and DBP are two questions ===== */
242
+
243
+ const bpForm = await sdk.api.forms.createOne({ title: `Blood pressure ${RAND()}` })
244
+ const sbpField = await sdk.api.form_fields.createOne({
245
+ formId: bpForm.id, title: 'Systolic', type: 'number',
246
+ options: { vitalUnit: 'SBP' },
247
+ previousFields: [{ type: 'root', info: {} }],
248
+ })
249
+ const dbpField = await sdk.api.form_fields.createOne({
250
+ formId: bpForm.id, title: 'Diastolic', type: 'number',
251
+ options: { vitalUnit: 'DBP' },
252
+ previousFields: [{ type: 'after', info: { fieldId: sbpField.id } }],
253
+ })
254
+ const bpEnduser = await sdk.api.endusers.createOne({ fname: 'Blood', lname: 'Pressure' })
255
+ const bpPrepared = await sdk.api.form_responses.prepare_form_response({
256
+ formId: bpForm.id, enduserId: bpEnduser.id,
257
+ })
258
+ await sdk.api.form_responses.submit_form_response({
259
+ accessCode: bpPrepared.accessCode,
260
+ responses: [
261
+ { fieldId: sbpField.id, fieldTitle: sbpField.title, answer: { type: 'number', value: 128 } },
262
+ { fieldId: dbpField.id, fieldTitle: dbpField.title, answer: { type: 'number', value: 82 } },
263
+ ],
264
+ })
265
+ const bpReadings = await poll_for(() => observations_for(bpEnduser.id), o => o.length >= 2)
266
+ const sbp = bpReadings.find(r => r.measurement?.unit === 'SBP')
267
+ const dbp = bpReadings.find(r => r.measurement?.unit === 'DBP')
268
+ record(
269
+ 'E1: two opted-in questions produce one reading each, in their own units',
270
+ bpReadings.length === 2 && sbp?.measurement?.value === 128 && dbp?.measurement?.value === 82,
271
+ `readings=${JSON.stringify(bpReadings.map(r => r.measurement))} (want SBP 128 and DBP 82)`,
272
+ )
273
+
274
+ /* ===== F: an enduser with vital triggers disabled still gets the reading ===== */
275
+
276
+ const quietEnduser = await sdk.api.endusers.createOne({
277
+ fname: 'Quiet', lname: 'Triggers', vitalTriggersDisabled: true,
278
+ })
279
+ const quietPrepared = await sdk.api.form_responses.prepare_form_response({
280
+ formId: form.id, enduserId: quietEnduser.id,
281
+ })
282
+ await sdk.api.form_responses.submit_form_response({
283
+ accessCode: quietPrepared.accessCode,
284
+ responses: [
285
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 210 } },
286
+ ],
287
+ })
288
+ const quietReadings = await poll_for(() => observations_for(quietEnduser.id), o => o.length >= 1)
289
+ record(
290
+ 'F1: the reading is still created and charted for a triggers-disabled enduser',
291
+ quietReadings.length === 1 && quietReadings[0]?.measurement?.value === 210,
292
+ `readings=${JSON.stringify(quietReadings.map(r => r.measurement))} (want one, 210 LB)`,
293
+ )
294
+ record(
295
+ 'F2: but it is marked dontTrigger, so no Vital Update automation fires',
296
+ quietReadings[0]?.dontTrigger === true,
297
+ `dontTrigger=${JSON.stringify(quietReadings[0]?.dontTrigger)} (want true, as smartmeter.ts does)`,
298
+ )
299
+
300
+ /* ===== G: a partial save creates nothing — final submission only ===== */
301
+
302
+ const partialEnduser = await sdk.api.endusers.createOne({ fname: 'Partial', lname: 'Save' })
303
+ const partialPrepared = await sdk.api.form_responses.prepare_form_response({
304
+ formId: form.id, enduserId: partialEnduser.id,
305
+ })
306
+ await sdk.api.form_responses.save_field_response({
307
+ accessCode: partialPrepared.accessCode,
308
+ response: { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 175 } },
309
+ })
310
+ // barrier: the draft answer is persisted, so anything the save path triggers has had its chance
311
+ await poll_for(
312
+ () => sdk.api.form_responses.getOne(partialPrepared.response.id),
313
+ r => !!(r.responses || []).length,
314
+ )
315
+ const partialReadings = await poll_for(() => observations_for(partialEnduser.id), o => o.length > 0, 8)
316
+ record(
317
+ 'G1: a partial save creates no reading before the response is submitted',
318
+ partialReadings.length === 0,
319
+ `created ${partialReadings.length}: ${JSON.stringify(partialReadings.map(r => r.measurement))}`
320
+ + ` — readings are a final-submission side effect only`,
321
+ )
322
+
323
+
324
+ /* ===== H: clearing the select (vitalUnit '') is the documented off switch ===== */
325
+
326
+ // The editor writes '' rather than removing the key, so '' must read as off just like an absent
327
+ // vitalUnit. This is the path QA step 9 takes to turn a configured question back off.
328
+ const clearedForm = await sdk.api.forms.createOne({ title: `Cleared unit ${RAND()}` })
329
+ const clearedField = await sdk.api.form_fields.createOne({
330
+ formId: clearedForm.id, title: 'Weight (lbs)', type: 'number',
331
+ options: { vitalUnit: 'LB' },
332
+ previousFields: [{ type: 'root', info: {} }],
333
+ })
334
+ await sdk.api.form_fields.updateOne(clearedField.id, { options: { vitalUnit: '' } })
335
+ const storedCleared = await sdk.api.form_fields.getOne(clearedField.id)
336
+ record(
337
+ "H1: clearing the unit persists as '', it is not dropped back to the old value",
338
+ storedCleared.options?.vitalUnit === '',
339
+ `stored vitalUnit=${JSON.stringify(storedCleared.options?.vitalUnit)} (want '')`
340
+ + ` — stringValidatorOptionalEmptyOkay is what lets the empty round-trip`,
341
+ )
342
+
343
+ const clearedEnduser = await sdk.api.endusers.createOne({ fname: 'Cleared', lname: 'OptIn' })
344
+ const clearedPrepared = await sdk.api.form_responses.prepare_form_response({
345
+ formId: clearedForm.id, enduserId: clearedEnduser.id,
346
+ })
347
+ await sdk.api.form_responses.submit_form_response({
348
+ accessCode: clearedPrepared.accessCode,
349
+ responses: [
350
+ { fieldId: clearedField.id, fieldTitle: clearedField.title, answer: { type: 'number', value: 199 } },
351
+ ],
352
+ })
353
+ await poll_for(
354
+ () => sdk.api.form_responses.getOne(clearedPrepared.response.id),
355
+ r => !!r.submittedAt,
356
+ )
357
+ const clearedReadings = await poll_for(() => observations_for(clearedEnduser.id), o => o.length > 0, 8)
358
+ record(
359
+ 'H2: a question whose unit was cleared creates no reading',
360
+ clearedReadings.length === 0,
361
+ `created ${clearedReadings.length}: ${JSON.stringify(clearedReadings.map(r => r.measurement))}`
362
+ + ` — the field query treats '' as off, same as an absent vitalUnit`,
363
+ )
364
+
365
+ } finally {
366
+ // Teardown BEFORE anything asserts — assert calls process.exit, which would strand this tenant.
367
+ if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
368
+ }
369
+
370
+ return checks
371
+ }
372
+
373
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
374
+ export const form_vital_reading_tests = async () => {
375
+ assert_checks('Vitals from form submissions', await form_vital_reading_checks())
376
+ }
377
+
378
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
379
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Vitals from form submissions', run: form_vital_reading_checks }
380
+
381
+ // Allow running this test file independently
382
+ if (require.main === module) {
383
+ console.log(`🌐 Using API URL: ${host}`)
384
+ form_vital_reading_tests()
385
+ .then(() => {
386
+ console.log("✅ Vitals from form submissions test suite completed successfully")
387
+ process.exit(0)
388
+ })
389
+ .catch((error) => {
390
+ console.error("❌ Vitals from form submissions test suite failed:", error)
391
+ process.exit(1)
392
+ })
393
+ }
@@ -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
+ }