@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,351 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { EnduserSession } from "../../enduser"
|
|
4
|
+
import { EnduserProgram } from "@tellescope/types-models"
|
|
5
|
+
import {
|
|
6
|
+
assert_checks,
|
|
7
|
+
Check,
|
|
8
|
+
setup_isolated_tenant,
|
|
9
|
+
IsolatedSuite,
|
|
10
|
+
} from "../setup_isolated"
|
|
11
|
+
|
|
12
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
+
|
|
14
|
+
const RAND = () => Math.random().toString(36).slice(2, 10)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Coverage for the `programs` field on Enduser / EnduserObservation / EnduserOrder, and for the
|
|
18
|
+
* `programs` filter on the Order Status Equals and Vital Update automation triggers.
|
|
19
|
+
*
|
|
20
|
+
* `EnduserOrder.programs` and `EnduserObservation.programs` are lists of `{ name }` objects; the
|
|
21
|
+
* trigger's `info.programs` is a list of names. A trigger matches when the two overlap, and — the part
|
|
22
|
+
* that matters most — a trigger with NO programs filter must keep matching every record, since that is
|
|
23
|
+
* every trigger that exists today.
|
|
24
|
+
*
|
|
25
|
+
* Both handlers apply the filter in JS after loading candidate triggers:
|
|
26
|
+
* - handle_order_status_equals, api/modules/automation_triggers.ts
|
|
27
|
+
* - handle_vital_create, api/v1/event_handlers_v2/vitals.ts
|
|
28
|
+
*
|
|
29
|
+
* This suite provisions its own throwaway tenant. That is not optional here: `vital_configurations`
|
|
30
|
+
* are business-wide (handle_vital_create reads them with `enduserId: { $exists: false }` across the
|
|
31
|
+
* businessId), so running on the shared fixture would classify other concurrently running suites'
|
|
32
|
+
* readings and theirs would bleed into these checks. It never calls `sdk.reset_db()`, which clears
|
|
33
|
+
* global throttle namespaces shared with other suites.
|
|
34
|
+
*
|
|
35
|
+
* Every check gets its own enduser: triggers are throttled to one fire per trigger per enduser per
|
|
36
|
+
* minute, so reusing a patient across checks would silently swallow the second fire.
|
|
37
|
+
*
|
|
38
|
+
* Triggers fire asynchronously. Each check polls for the unfiltered trigger's tag and uses that as the
|
|
39
|
+
* synchronization barrier, then asserts on the filtered trigger's tag. Without the barrier a negative
|
|
40
|
+
* check would pass merely by reading too early.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const poll_for = async <T,>(get: () => Promise<T>, done: (v: T) => boolean, attempts=20): Promise<T> => {
|
|
44
|
+
let value = await get()
|
|
45
|
+
for (let i = 0; i < attempts && !done(value); i++) {
|
|
46
|
+
await new Promise(resolve => setTimeout(resolve, 250))
|
|
47
|
+
value = await get()
|
|
48
|
+
}
|
|
49
|
+
return value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const programs_checks = async (): Promise<Check[]> => {
|
|
53
|
+
const checks: Check[] = []
|
|
54
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
55
|
+
|
|
56
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
tenant = await setup_isolated_tenant({
|
|
60
|
+
name: `ProgramFilters ${RAND()}`,
|
|
61
|
+
users: [{ isAdmin: true }],
|
|
62
|
+
apiKeys: 0,
|
|
63
|
+
})
|
|
64
|
+
const { sdk } = tenant
|
|
65
|
+
|
|
66
|
+
/* ===================== Enduser.programs ===================== */
|
|
67
|
+
|
|
68
|
+
const programsEnduser = await sdk.api.endusers.createOne({
|
|
69
|
+
fname: 'Programs',
|
|
70
|
+
lname: 'Field',
|
|
71
|
+
email: `programs-field-${RAND()}@tellescope.example`,
|
|
72
|
+
devices: [{ title: 'Cuff', id: `dev-a-${RAND()}` }, { title: 'Scale', id: `dev-b-${RAND()}` }],
|
|
73
|
+
programs: [
|
|
74
|
+
{ name: 'CHF', deviceIds: ['dev-a'], status: 'Active' },
|
|
75
|
+
{ name: 'Diabetes' }, // deviceIds and status are optional
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const createdPrograms = (await sdk.api.endusers.getOne(programsEnduser.id)).programs
|
|
80
|
+
record(
|
|
81
|
+
'F: Enduser.programs round-trips on create, with and without deviceIds / status',
|
|
82
|
+
createdPrograms?.length === 2
|
|
83
|
+
&& createdPrograms[0].name === 'CHF'
|
|
84
|
+
&& JSON.stringify(createdPrograms[0].deviceIds) === JSON.stringify(['dev-a'])
|
|
85
|
+
&& createdPrograms[0].status === 'Active'
|
|
86
|
+
&& createdPrograms[1].name === 'Diabetes'
|
|
87
|
+
&& createdPrograms[1].deviceIds === undefined
|
|
88
|
+
&& createdPrograms[1].status === undefined,
|
|
89
|
+
`programs=${JSON.stringify(createdPrograms)}`,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
await sdk.api.endusers.updateOne(
|
|
93
|
+
programsEnduser.id,
|
|
94
|
+
{ programs: [{ name: 'CHF', deviceIds: ['dev-a', 'dev-b'] }] },
|
|
95
|
+
{ replaceObjectFields: true },
|
|
96
|
+
)
|
|
97
|
+
const replacedPrograms = (await sdk.api.endusers.getOne(programsEnduser.id)).programs
|
|
98
|
+
record(
|
|
99
|
+
'G: updating programs with replaceObjectFields replaces the whole list',
|
|
100
|
+
replacedPrograms?.length === 1
|
|
101
|
+
&& replacedPrograms[0].name === 'CHF'
|
|
102
|
+
&& JSON.stringify(replacedPrograms[0].deviceIds) === JSON.stringify(['dev-a', 'dev-b']),
|
|
103
|
+
`programs=${JSON.stringify(replacedPrograms)}`,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// name carries a non-optional validator inside the object validator, so it is required per item
|
|
107
|
+
let missingNameError = ''
|
|
108
|
+
try {
|
|
109
|
+
await sdk.api.endusers.updateOne(programsEnduser.id, { programs: [{ deviceIds: ['dev-a'] } as any] })
|
|
110
|
+
} catch (err: any) { missingNameError = err?.message ?? String(err) }
|
|
111
|
+
record(
|
|
112
|
+
'H: a program entry without a name is rejected',
|
|
113
|
+
missingNameError.includes('name'),
|
|
114
|
+
`error=${JSON.stringify(missingNameError) || '(none — the write was ACCEPTED)'}`,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
/* --- programs is staff-only: redacted from enduser sessions, and not enduser-updatable --- */
|
|
118
|
+
|
|
119
|
+
const { authToken } = await sdk.api.endusers.generate_auth_token({ id: programsEnduser.id })
|
|
120
|
+
const enduserSession = new EnduserSession({ host, authToken, businessId: tenant.businessId })
|
|
121
|
+
await enduserSession.refresh_session()
|
|
122
|
+
|
|
123
|
+
const selfRead = await enduserSession.api.endusers.getOne(programsEnduser.id)
|
|
124
|
+
record(
|
|
125
|
+
'I: Enduser.programs is redacted from the enduser\'s own read of their record',
|
|
126
|
+
selfRead.programs === undefined,
|
|
127
|
+
`enduser-session programs=${JSON.stringify(selfRead.programs)}`,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
let selfUpdateError = ''
|
|
131
|
+
try {
|
|
132
|
+
await enduserSession.api.endusers.updateOne(programsEnduser.id, { programs: [{ name: 'Self Assigned' }] })
|
|
133
|
+
} catch (err: any) { selfUpdateError = err?.message ?? String(err) }
|
|
134
|
+
const afterSelfUpdate = (await sdk.api.endusers.getOne(programsEnduser.id)).programs
|
|
135
|
+
record(
|
|
136
|
+
'J: an enduser cannot update their own programs',
|
|
137
|
+
selfUpdateError.includes('cannot be updated by endusers')
|
|
138
|
+
&& afterSelfUpdate?.length === 1 && afterSelfUpdate[0].name === 'CHF',
|
|
139
|
+
`error=${JSON.stringify(selfUpdateError) || '(none — the write was ACCEPTED)'}`
|
|
140
|
+
+ ` programs=${JSON.stringify(afterSelfUpdate)}`,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
/* ===================== Order Status Equals ===================== */
|
|
144
|
+
|
|
145
|
+
const ORDER_SOURCE = `ProgramFilterSource${RAND()}`
|
|
146
|
+
|
|
147
|
+
await sdk.api.automation_triggers.createOne({
|
|
148
|
+
event: { type: 'Order Status Equals', info: { source: ORDER_SOURCE, status: 'shipped', programs: ['CHF'] } },
|
|
149
|
+
action: { type: 'Add Tags', info: { tags: ['order-chf'] } },
|
|
150
|
+
status: 'Active',
|
|
151
|
+
title: `Order programs=[CHF] ${RAND()}`,
|
|
152
|
+
})
|
|
153
|
+
// no programs filter — the shape of every trigger that exists today; must match regardless
|
|
154
|
+
await sdk.api.automation_triggers.createOne({
|
|
155
|
+
event: { type: 'Order Status Equals', info: { source: ORDER_SOURCE, status: 'shipped' } },
|
|
156
|
+
action: { type: 'Add Tags', info: { tags: ['order-any'] } },
|
|
157
|
+
status: 'Active',
|
|
158
|
+
title: `Order programs unset ${RAND()}`,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const create_order = async (enduserId: string, programs?: { name: string }[]) => (
|
|
162
|
+
sdk.api.enduser_orders.createOne({
|
|
163
|
+
enduserId,
|
|
164
|
+
source: ORDER_SOURCE,
|
|
165
|
+
externalId: `ext-${RAND()}`,
|
|
166
|
+
title: 'Program filter order',
|
|
167
|
+
status: 'shipped',
|
|
168
|
+
...(programs ? { programs } : {}),
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const tags_after_order = async (programs: { name: string }[] | undefined) => {
|
|
173
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'Programs', lname: 'Order' })
|
|
174
|
+
await create_order(enduser.id, programs)
|
|
175
|
+
// the unfiltered trigger always fires, so its tag is the barrier that makes an absence meaningful
|
|
176
|
+
const after = await poll_for(
|
|
177
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
178
|
+
e => !!e.tags?.includes('order-any'),
|
|
179
|
+
)
|
|
180
|
+
return after.tags ?? []
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const orderMatching = await tags_after_order([{ name: 'CHF' }])
|
|
184
|
+
record(
|
|
185
|
+
'A: Order Status Equals with programs=[CHF] fires for an order in program CHF',
|
|
186
|
+
orderMatching.includes('order-chf') && orderMatching.includes('order-any'),
|
|
187
|
+
`tags=${JSON.stringify(orderMatching)}`,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const orderNonMatching = await tags_after_order([{ name: 'Diabetes' }])
|
|
191
|
+
record(
|
|
192
|
+
'B: Order Status Equals with programs=[CHF] does NOT fire for an order in program Diabetes',
|
|
193
|
+
!orderNonMatching.includes('order-chf') && orderNonMatching.includes('order-any'),
|
|
194
|
+
`tags=${JSON.stringify(orderNonMatching)}`,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const orderNoPrograms = await tags_after_order(undefined)
|
|
198
|
+
record(
|
|
199
|
+
'C: Order Status Equals with programs=[CHF] does NOT fire for an order with no programs,'
|
|
200
|
+
+ ' while the unfiltered trigger still does (back-compat)',
|
|
201
|
+
!orderNoPrograms.includes('order-chf') && orderNoPrograms.includes('order-any'),
|
|
202
|
+
`tags=${JSON.stringify(orderNoPrograms)}`,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
/* ===================== Vital Update ===================== */
|
|
206
|
+
|
|
207
|
+
const configuration = await sdk.api.vital_configurations.createOne({
|
|
208
|
+
title: `Program filter SBP ${RAND()}`,
|
|
209
|
+
unit: 'SBP',
|
|
210
|
+
ranges: [
|
|
211
|
+
{
|
|
212
|
+
classification: 'High',
|
|
213
|
+
comparison: { type: 'Greater Than', value: 200 },
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await sdk.api.automation_triggers.createOne({
|
|
219
|
+
event: {
|
|
220
|
+
type: 'Vital Update',
|
|
221
|
+
info: { configurationIds: [configuration.id], classifications: ['High'], programs: ['CHF'] },
|
|
222
|
+
},
|
|
223
|
+
action: { type: 'Add Tags', info: { tags: ['vital-chf'] } },
|
|
224
|
+
status: 'Active',
|
|
225
|
+
title: `Vital programs=[CHF] ${RAND()}`,
|
|
226
|
+
})
|
|
227
|
+
await sdk.api.automation_triggers.createOne({
|
|
228
|
+
event: {
|
|
229
|
+
type: 'Vital Update',
|
|
230
|
+
info: { configurationIds: [configuration.id], classifications: ['High'] },
|
|
231
|
+
},
|
|
232
|
+
action: { type: 'Add Tags', info: { tags: ['vital-any'] } },
|
|
233
|
+
status: 'Active',
|
|
234
|
+
title: `Vital programs unset ${RAND()}`,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const tags_after_reading = async (programs: { name: string }[] | undefined) => {
|
|
238
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'Programs', lname: 'Vital' })
|
|
239
|
+
await sdk.api.enduser_observations.createOne({
|
|
240
|
+
category: 'vital-signs',
|
|
241
|
+
status: 'final',
|
|
242
|
+
enduserId: enduser.id,
|
|
243
|
+
measurement: { unit: 'SBP', value: 250 }, // High
|
|
244
|
+
timestamp: new Date(),
|
|
245
|
+
...(programs ? { programs } : {}),
|
|
246
|
+
})
|
|
247
|
+
const after = await poll_for(
|
|
248
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
249
|
+
e => !!e.tags?.includes('vital-any'),
|
|
250
|
+
)
|
|
251
|
+
return after.tags ?? []
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const vitalMatching = await tags_after_reading([{ name: 'CHF' }])
|
|
255
|
+
record(
|
|
256
|
+
'D: Vital Update with programs=[CHF] fires for a reading in program CHF',
|
|
257
|
+
vitalMatching.includes('vital-chf') && vitalMatching.includes('vital-any'),
|
|
258
|
+
`tags=${JSON.stringify(vitalMatching)}`,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const vitalNonMatching = await tags_after_reading([{ name: 'Diabetes' }])
|
|
262
|
+
record(
|
|
263
|
+
'E: Vital Update with programs=[CHF] does NOT fire for a reading in program Diabetes,'
|
|
264
|
+
+ ' while the unfiltered trigger still does (back-compat)',
|
|
265
|
+
!vitalNonMatching.includes('vital-chf') && vitalNonMatching.includes('vital-any'),
|
|
266
|
+
`tags=${JSON.stringify(vitalNonMatching)}`,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
/* =============== Programs as a trigger enduserCondition (conditional logic) =============== */
|
|
270
|
+
|
|
271
|
+
// The `Programs` key is evaluated by evaluate_conditional_logic_for_enduser_fields, which is what
|
|
272
|
+
// handle_actions_for_triggers runs every trigger's enduserCondition through. These two checks are the
|
|
273
|
+
// end-to-end proof that a condition built in the UI actually gates a real trigger fire.
|
|
274
|
+
const CONDITION_FIELD = `ProgramConditionKey${RAND()}`
|
|
275
|
+
|
|
276
|
+
await sdk.api.automation_triggers.createOne({
|
|
277
|
+
event: { type: 'Field Equals', info: { field: CONDITION_FIELD, value: 'go' } },
|
|
278
|
+
action: { type: 'Add Tags', info: { tags: ['condition-chf'] } },
|
|
279
|
+
enduserCondition: { $and: [{ condition: { Programs: { $in: 'CHF' } } }] },
|
|
280
|
+
status: 'Active',
|
|
281
|
+
title: `Programs condition Is In CHF ${RAND()}`,
|
|
282
|
+
})
|
|
283
|
+
// the same event with no enduserCondition — the barrier that makes an absence above meaningful
|
|
284
|
+
await sdk.api.automation_triggers.createOne({
|
|
285
|
+
event: { type: 'Field Equals', info: { field: CONDITION_FIELD, value: 'go' } },
|
|
286
|
+
action: { type: 'Add Tags', info: { tags: ['condition-any'] } },
|
|
287
|
+
status: 'Active',
|
|
288
|
+
title: `Programs condition unset ${RAND()}`,
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// `waitFor` is the tag that makes the read meaningful. For the positive check that is the
|
|
292
|
+
// conditioned trigger's own tag — the two triggers are dispatched by separate awaited calls, so the
|
|
293
|
+
// unconditioned trigger's tag can land first and polling on it alone would read too early. For the
|
|
294
|
+
// negative check, where there is no tag to wait for, the unconditioned trigger is the barrier.
|
|
295
|
+
const tags_after_create = async (programs: EnduserProgram[] | undefined, waitFor: string) => {
|
|
296
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
297
|
+
fname: 'Programs',
|
|
298
|
+
lname: 'Condition',
|
|
299
|
+
fields: { [CONDITION_FIELD]: 'go' },
|
|
300
|
+
...(programs ? { programs } : {}),
|
|
301
|
+
})
|
|
302
|
+
const after = await poll_for(
|
|
303
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
304
|
+
e => !!e.tags?.includes(waitFor),
|
|
305
|
+
)
|
|
306
|
+
return after.tags ?? []
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const conditionMatching = await tags_after_create([{ name: 'CHF', status: 'active' }], 'condition-chf')
|
|
310
|
+
record(
|
|
311
|
+
'K: a trigger with a Programs Is In CHF enduserCondition fires for a patient in program CHF',
|
|
312
|
+
conditionMatching.includes('condition-chf') && conditionMatching.includes('condition-any'),
|
|
313
|
+
`tags=${JSON.stringify(conditionMatching)}`,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const conditionNonMatching = await tags_after_create(undefined, 'condition-any')
|
|
317
|
+
record(
|
|
318
|
+
'L: the same trigger does NOT fire for a patient with no programs,'
|
|
319
|
+
+ ' while the unconditioned trigger still does',
|
|
320
|
+
!conditionNonMatching.includes('condition-chf') && conditionNonMatching.includes('condition-any'),
|
|
321
|
+
`tags=${JSON.stringify(conditionNonMatching)}`,
|
|
322
|
+
)
|
|
323
|
+
} finally {
|
|
324
|
+
// Teardown BEFORE anything asserts — assert calls process.exit, which would strand this tenant.
|
|
325
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return checks
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
332
|
+
export const programs_tests = async () => {
|
|
333
|
+
assert_checks('Programs', await programs_checks())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
|
|
337
|
+
export const ISOLATED_SUITE: IsolatedSuite = { name: 'Programs', run: programs_checks }
|
|
338
|
+
|
|
339
|
+
// Allow running this test file independently
|
|
340
|
+
if (require.main === module) {
|
|
341
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
342
|
+
programs_tests()
|
|
343
|
+
.then(() => {
|
|
344
|
+
console.log("✅ Programs test suite completed successfully")
|
|
345
|
+
process.exit(0)
|
|
346
|
+
})
|
|
347
|
+
.catch((error) => {
|
|
348
|
+
console.error("❌ Programs test suite failed:", error)
|
|
349
|
+
process.exit(1)
|
|
350
|
+
})
|
|
351
|
+
}
|