@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.
- package/CLAUDE.md +24 -0
- package/env.example +6 -0
- package/lib/cjs/sdk.d.ts +373 -3
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/programs.test.js +73 -17
- package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
- package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
- package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
- package/lib/cjs/tests/setup_isolated.d.ts +48 -0
- package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
- package/lib/cjs/tests/setup_isolated.js +119 -1
- package/lib/cjs/tests/setup_isolated.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +374 -292
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
- package/lib/cjs/tests/webhooks_tests.js +44 -34
- package/lib/cjs/tests/webhooks_tests.js.map +1 -1
- package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/esm/sdk.d.ts +373 -3
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
- package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -1
- package/lib/esm/tests/api_tests/organization_signup.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
- package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
- package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/programs.test.js +72 -16
- package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +2 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
- package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
- package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
- package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
- package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
- package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
- package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
- package/lib/esm/tests/setup_isolated.d.ts +48 -0
- package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
- package/lib/esm/tests/setup_isolated.js +114 -0
- package/lib/esm/tests/setup_isolated.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +375 -293
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
- package/lib/esm/tests/webhooks_tests.js +44 -34
- package/lib/esm/tests/webhooks_tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -11
- package/src/sdk.ts +3 -2
- package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
- package/src/tests/api_tests/enduser_fields_sync.test.ts +4 -0
- package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
- package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
- package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
- package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
- package/src/tests/api_tests/load_threads_care_team.test.ts +4 -0
- package/src/tests/api_tests/organization_signup.test.ts +4 -0
- package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
- package/src/tests/api_tests/programs.test.ts +351 -0
- package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
- package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +4 -0
- package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +4 -0
- package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
- package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
- package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
- package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
- package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
- package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
- package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
- package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
- package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
- package/src/tests/api_tests/totp_mfa.test.ts +10 -5
- package/src/tests/api_tests/trigger_contains_matching.test.ts +4 -0
- package/src/tests/api_tests/user_logs_restore_deleted.test.ts +4 -0
- package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
- package/src/tests/api_tests/vitl_push.test.ts +323 -0
- package/src/tests/setup_isolated.ts +140 -0
- package/src/tests/tests.ts +124 -30
- package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
- package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
- package/src/tests/webhooks_tests.ts +6 -1
- 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
|
+
}
|