@salesforce/afv-skills 1.41.0 → 1.43.0
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/package.json +2 -6
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +3 -0
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +4 -0
- package/skills/design-systems-slds-validate/SKILL.md +2 -2
- package/skills/dx-app-analytics-query/SKILL.md +1 -3
- package/skills/dx-devops-conflict-resolve/SKILL.md +205 -0
- package/skills/dx-devops-conflict-resolve/examples/conflict-workflows.md +165 -0
- package/skills/dx-devops-conflict-resolve/references/deploy-failure-resolution.md +75 -0
- package/skills/dx-devops-conflict-resolve/references/git-conflict-resolution.md +124 -0
- package/skills/dx-devops-conflict-resolve/scripts/detect-conflicts.sh +91 -0
- package/skills/dx-devops-conflict-resolve/scripts/diagnose-deploy-failure.sh +156 -0
- package/skills/dx-devops-request-status/SKILL.md +160 -0
- package/skills/dx-devops-request-status/examples/polling-workflows.md +101 -0
- package/skills/dx-devops-request-status/references/cli-commands.md +176 -0
- package/skills/dx-devops-request-status/scripts/poll-status.sh +134 -0
- package/skills/dx-devops-test-failures-analyze/SKILL.md +6 -6
- package/skills/dx-devops-test-pipeline-configure/SKILL.md +7 -7
- package/skills/dx-devops-test-suite-assignments-configure/SKILL.md +6 -6
- package/skills/dx-devops-test-suite-run/SKILL.md +6 -6
- package/skills/dx-devops-work-item-manage/SKILL.md +2 -0
- package/skills/dx-org-manage/references/creating-scratch-org.md +5 -2
- package/skills/dx-org-manage/references/creating-snapshot.md +1 -0
- package/skills/dx-org-shape-manage/SKILL.md +152 -0
- package/skills/dx-org-shape-manage/examples/create_error_output.json +9 -0
- package/skills/dx-org-shape-manage/examples/create_success_output.json +9 -0
- package/skills/dx-org-shape-manage/examples/delete_output.json +11 -0
- package/skills/dx-org-shape-manage/examples/list_inactive_output.json +32 -0
- package/skills/dx-org-shape-manage/examples/list_output.json +24 -0
- package/skills/dx-org-shape-manage/references/cli_flags.md +135 -0
- package/skills/dx-org-switch/SKILL.md +2 -2
- package/skills/dx-org-trial-expiration-check/SKILL.md +2 -0
- package/skills/dx-pkg-post-install-configure/SKILL.md +3 -0
- package/skills/education-cloud-multi-campus-configure/SKILL.md +211 -0
- package/skills/education-cloud-multi-campus-configure/examples/hierarchy_visualization.md +174 -0
- package/skills/education-cloud-multi-campus-configure/examples/output_examples.md +54 -0
- package/skills/education-cloud-multi-campus-configure/examples/sample_hierarchy_input.csv +29 -0
- package/skills/education-cloud-multi-campus-configure/examples/sample_hierarchy_input_edgecases.csv +16 -0
- package/skills/education-cloud-multi-campus-configure/references/account_recordtype_prerequisite.md +44 -0
- package/skills/education-cloud-multi-campus-configure/references/delta_computation.md +33 -0
- package/skills/education-cloud-multi-campus-configure/references/error_handling.md +198 -0
- package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +54 -0
- package/skills/education-cloud-multi-campus-configure/references/gotchas.md +16 -0
- package/skills/education-cloud-multi-campus-configure/references/hierarchy_parsing_rules.md +133 -0
- package/skills/education-cloud-multi-campus-configure/references/mcp-invocation.md +75 -0
- package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/SKILL.md +1 -1
- package/skills/experience-cms-brand-apply/SKILL.md +4 -1
- package/skills/experience-cms-brand-create/SKILL.md +197 -0
- package/skills/experience-cms-brand-create/assets/brand-template.json +329 -0
- package/skills/experience-cms-brand-create/references/brand-anatomy.md +120 -0
- package/skills/experience-cms-brand-create/references/disk-contract.md +84 -0
- package/skills/experience-cms-content-generate/SKILL.md +190 -0
- package/skills/experience-cms-content-generate/assets/display-formats.md +84 -0
- package/skills/experience-cms-content-generate/assets/payloads/create-content-bulk.json +11 -0
- package/skills/experience-cms-content-generate/assets/payloads/create-content-single.json +11 -0
- package/skills/experience-cms-content-generate/assets/payloads/publish-content.json +4 -0
- package/skills/experience-cms-content-generate/assets/payloads/update-content.json +9 -0
- package/skills/experience-cms-content-generate/assets/questions.md +241 -0
- package/skills/experience-cms-content-generate/examples/create-content-call.md +81 -0
- package/skills/experience-cms-content-generate/references/bulk-batching.md +56 -0
- package/skills/experience-cms-content-generate/references/content-type-classification.md +88 -0
- package/skills/experience-cms-content-generate/references/content-write-tool.md +95 -0
- package/skills/experience-cms-content-generate/references/delegation-protocol.md +37 -0
- package/skills/experience-cms-content-generate/references/edit-publish-workflow.md +63 -0
- package/skills/experience-cms-content-generate/references/error-recovery.md +72 -0
- package/skills/experience-cms-content-generate/references/identifier-resolution.md +26 -0
- package/skills/experience-cms-content-generate/references/intent-routing.md +45 -0
- package/skills/experience-cms-content-generate/references/principles.md +19 -0
- package/skills/experience-cms-content-generate/references/ux-rules.md +68 -0
- package/skills/experience-cms-content-generate/references/workspace-resolution.md +33 -0
- package/skills/experience-cms-content-type-generate/SKILL.md +449 -0
- package/skills/experience-cms-content-type-generate/assets/discovery-prompts.md +79 -0
- package/skills/experience-cms-content-type-generate/assets/schema-example.json +70 -0
- package/skills/experience-cms-content-type-generate/references/agent-checklist.md +50 -0
- package/skills/experience-cms-content-type-generate/references/anti-patterns.md +39 -0
- package/skills/experience-cms-content-type-generate/references/deployment-errors.md +27 -0
- package/skills/experience-cms-content-type-generate/references/discovery-details.md +106 -0
- package/skills/experience-cms-content-type-generate/references/discovery-query-rules.md +85 -0
- package/skills/experience-cms-content-type-generate/references/edit-fields-loop.md +48 -0
- package/skills/experience-cms-content-type-generate/references/pre-deploy-checklist.md +21 -0
- package/skills/experience-cms-content-type-generate/references/retrieve-and-reconcile.md +105 -0
- package/skills/experience-cms-content-type-generate/references/schema-rules.md +51 -0
- package/skills/experience-cms-content-type-generate/references/schema-summary-format.md +60 -0
- package/skills/experience-lds-graphql-generate/SKILL.md +4 -0
- package/skills/experience-lwc-accessibility-jest-run/SKILL.md +79 -0
- package/skills/experience-lwc-accessibility-jest-run/references/running-sa11y-jest-tests.md +205 -0
- package/skills/experience-lwc-design-generate/SKILL.md +5 -4
- package/skills/experience-lwc-generate/SKILL.md +10 -9
- package/skills/experience-ui-bundle-deploy/references/dev-preview.md +21 -0
- package/skills/experience-ui-bundle-deploy/references/social-login.md +1 -0
- package/skills/experience-ui-bundle-file-upload-generate/SKILL.md +6 -6
- package/skills/experience-ui-bundle-localize/SKILL.md +30 -37
- package/skills/experience-ui-bundle-localize/references/gotchas.md +23 -7
- package/skills/experience-ui-bundle-localize/references/i18n-setup.md +48 -4
- package/skills/experience-ui-bundle-localize/references/label-xml.md +22 -8
- package/skills/experience-ui-bundle-localize/references/verifying.md +39 -11
- package/skills/experience-ui-bundle-localize/scripts/detect-bundle-type.sh +118 -15
- package/skills/experience-ui-bundle-localize/scripts/tests/test-detect-bundle-type.sh +187 -0
- package/skills/experience-ui-bundle-mfa-configure/SKILL.md +13 -2
- package/skills/experience-ui-bundle-mfa-configure/references/social-login.md +1 -0
- package/skills/experience-ui-bundle-project-generate/SKILL.md +2 -2
- package/skills/integration-connectivity-generate/SKILL.md +11 -10
- package/skills/integration-eventing-cdc-configure/SKILL.md +6 -6
- package/skills/integration-eventing-subscription-configure/SKILL.md +6 -5
- package/skills/mobile-platform-native-capabilities-integrate/SKILL.md +2 -2
- package/skills/mobile-platform-offline-validate/scripts/package.json +1 -1
- package/skills/platform-agentexchange-partner-offers-configure/SKILL.md +8 -8
- package/skills/platform-custom-application-generate/SKILL.md +2 -0
- package/skills/platform-custom-field-generate/SKILL.md +8 -6
- package/skills/platform-custom-metadata-type-generate/SKILL.md +460 -0
- package/skills/platform-custom-metadata-type-generate/references/cmdt-records.md +251 -0
- package/skills/platform-custom-metadata-type-generate/scripts/sanitize-developer-name.sh +46 -0
- package/skills/platform-custom-setting-generate/SKILL.md +419 -0
- package/skills/platform-dataspace-access-configure/SKILL.md +3 -3
- package/skills/platform-sharing-owd-configure/SKILL.md +3 -0
- package/skills/platform-value-set-generate/SKILL.md +2 -0
- package/skills/service-agentforce-channel-configure/SKILL.md +29 -20
- package/skills/service-agentforce-channel-configure/assets/BotEmailDefinition.botEmailDefinition-meta.xml +36 -0
- package/skills/service-agentforce-channel-configure/assets/email/unfiled$public/AgentforceForServiceEmailTemplate.email +17 -0
- package/skills/service-agentforce-channel-configure/assets/email/unfiled$public/AgentforceForServiceEmailTemplate.email-meta.xml +29 -0
- package/skills/service-agentforce-channel-configure/assets/mdapi-package.xml +25 -0
- package/skills/service-agentforce-channel-configure/assets/settings-mdapi-package.xml +30 -0
- package/skills/service-agentforce-channel-configure/references/agent-wiring.md +20 -0
- package/skills/service-agentforce-channel-configure/references/botemaildefinition.md +107 -0
- package/skills/service-agentforce-channel-configure/references/channel-branch-email.md +203 -50
- package/skills/service-agentforce-channel-configure/references/channel-types.md +30 -3
- package/skills/service-agentforce-channel-configure/references/queue-resolution.md +17 -8
- package/skills/service-agentforce-channel-configure/references/routing-flow.md +14 -3
- package/skills/service-agentforce-channel-configure/scripts/validate-botemaildefinition.py +126 -0
- package/skills/service-agentforce-channel-configure/scripts/validate-emailtemplate.py +133 -0
- package/skills/service-de-channel-activate/SKILL.md +271 -0
- package/skills/service-de-channel-activate/references/gotchas.md +31 -0
- package/skills/service-de-channel-activate/references/phone-verification.md +96 -0
- package/skills/service-de-channel-activate/references/worked-examples.md +14 -0
- package/skills/service-de-channel-consent-configure/SKILL.md +230 -0
- package/skills/service-de-channel-consent-configure/references/gotchas.md +41 -0
- package/skills/service-de-channel-consent-configure/references/language-keywords.md +84 -0
- package/skills/service-de-channel-consent-configure/references/worked-examples.md +134 -0
- package/skills/service-de-channel-create/SKILL.md +369 -0
- package/skills/service-de-channel-create/references/apple.md +59 -0
- package/skills/service-de-channel-create/references/connect-insert.md +84 -0
- package/skills/service-de-channel-create/references/facebook.md +154 -0
- package/skills/service-de-channel-create/references/line.md +66 -0
- package/skills/service-de-channel-create/references/sms.md +98 -0
- package/skills/service-de-channel-create/references/whatsapp.md +98 -0
- package/skills/service-de-channel-create/references/worked-examples.md +70 -0
- package/skills/service-de-channel-routing-configure/SKILL.md +345 -0
- package/skills/service-de-channel-routing-configure/references/asa-routing.md +90 -0
- package/skills/service-de-channel-routing-configure/references/gotchas.md +27 -0
- package/skills/service-de-channel-routing-configure/references/queue-creation.md +123 -0
- package/skills/service-de-channel-routing-configure/references/target-locate.md +98 -0
- package/skills/service-de-channel-routing-configure/references/worked-examples.md +174 -0
- package/skills/service-de-headless-channel-configure/SKILL.md +305 -0
- package/skills/service-de-headless-channel-configure/references/gotchas.md +27 -0
- package/skills/service-de-headless-channel-configure/references/inputs.md +49 -0
- package/skills/service-de-headless-channel-configure/references/output-envelopes.md +53 -0
- package/skills/service-de-headless-channel-configure/references/partial-success.md +24 -0
- package/skills/service-de-headless-channel-configure/references/terms-and-conditions.md +54 -0
- package/skills/service-de-headless-channel-configure/references/worked-examples.md +108 -0
- package/skills/service-de-waba-integrate/SKILL.md +227 -0
- package/skills/service-digital-engagement-channel-configure/scripts/check-api-version.sh +29 -0
- package/skills/service-digital-engagement-messaging-site-integrate/SKILL.md +10 -9
- package/skills/service-digital-engagement-messaging-site-integrate/references/lwr_patch.md +39 -21
- package/skills/service-digital-engagement-messaging-site-integrate/scripts/patch_lwr_bundle.sh +157 -96
- package/skills/service-email-to-case-configure/SKILL.md +190 -0
- package/skills/service-email-to-case-configure/assets/CaseSettings.settings-meta.xml +45 -0
- package/skills/service-email-to-case-configure/examples/CaseSettings-two-addresses.settings-meta.xml +36 -0
- package/skills/service-email-to-case-configure/references/apply-mechanics.md +73 -0
- package/skills/service-email-to-case-configure/references/routing_address_reference.md +73 -0
- package/skills/service-email-to-case-configure/references/troubleshooting.md +25 -0
- package/skills/service-email-to-case-configure/scripts/apply-casesettings.py +1315 -0
- package/skills/service-email-to-case-configure/scripts/check-agent-email-capability.sh +26 -0
- package/skills/service-email-to-case-configure/scripts/tests/__init__.py +0 -0
- package/skills/service-email-to-case-configure/scripts/tests/_bootstrap.py +45 -0
- package/skills/service-email-to-case-configure/scripts/tests/_fakeorg.py +366 -0
- package/skills/service-email-to-case-configure/scripts/tests/_run.py +84 -0
- package/skills/service-email-to-case-configure/scripts/tests/test_apply_org_scenarios.py +655 -0
- package/skills/service-email-to-case-configure/scripts/tests/test_get_session.py +134 -0
- package/skills/service-email-to-case-configure/scripts/tests/test_new_capabilities.py +193 -0
- package/skills/service-email-to-case-configure/scripts/tests/test_validate_casesettings.py +170 -0
- package/skills/service-email-to-case-configure/scripts/validate-casesettings.py +229 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +33 -15
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -47
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -12
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md +5 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-enable-plan.mjs +11 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +8 -6
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/references/cli-invocation.md +3 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/scripts/classify-readiness.mjs +11 -3
- package/skills/service-itsm-agentic-setup-configure/SKILL.md +5 -5
- package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +5 -5
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +2 -2
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +11 -7
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +20 -7
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +10 -6
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/action-availability.md +7 -7
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +11 -7
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/build-create-body.mjs +24 -17
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-action-availability.mjs +98 -58
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +20 -7
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/strip-release-management.mjs +176 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +19 -19
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/cli-invocation.md +3 -4
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/helper-contracts.md +6 -6
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/permset-topology.md +3 -10
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-permset-availability.mjs +7 -9
- package/skills/service-itsm-incident-mgmt-configure/SKILL.md +2 -0
- package/skills/service-itsm-incident-priority-configure/SKILL.md +15 -12
- package/skills/service-itsm-incident-priority-configure/examples/matrix-operations.md +27 -1
- package/skills/service-itsm-incident-priority-configure/references/sf-cli-invocation.md +37 -6
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +146 -0
- package/skills/service-native-voice-recording-transcription-configure/assets/confirmation-output.json +16 -0
- package/skills/service-native-voice-recording-transcription-configure/assets/package.xml +22 -0
- package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +101 -0
- package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +181 -0
- package/skills/data360-activate/README.md +0 -38
- package/skills/data360-activate/SKILL.md +0 -127
- package/skills/data360-connect/README.md +0 -57
- package/skills/data360-connect/SKILL.md +0 -163
- package/skills/data360-connect/examples/connections/heroku-postgres.json +0 -15
- package/skills/data360-connect/examples/connections/ingest-api-connection.json +0 -5
- package/skills/data360-connect/examples/connections/ingest-api-schema.json +0 -31
- package/skills/data360-connect/examples/connections/redshift.json +0 -16
- package/skills/data360-connect/examples/connections/sharepoint-unstructured.json +0 -20
- package/skills/data360-connect/examples/connections/snowflake-connection.json +0 -42
- package/skills/data360-harmonize/README.md +0 -31
- package/skills/data360-harmonize/SKILL.md +0 -126
- package/skills/data360-orchestrate/README.md +0 -120
- package/skills/data360-orchestrate/SKILL.md +0 -263
- package/skills/data360-orchestrate/assets/definitions/activation-target.template.json +0 -5
- package/skills/data360-orchestrate/assets/definitions/activation.template.json +0 -7
- package/skills/data360-orchestrate/assets/definitions/calculated-insight.template.json +0 -7
- package/skills/data360-orchestrate/assets/definitions/data-action-target.template.json +0 -5
- package/skills/data360-orchestrate/assets/definitions/data-action.template.json +0 -5
- package/skills/data360-orchestrate/assets/definitions/data-graph.template.json +0 -21
- package/skills/data360-orchestrate/assets/definitions/data-stream.template.json +0 -55
- package/skills/data360-orchestrate/assets/definitions/dmo.template.json +0 -17
- package/skills/data360-orchestrate/assets/definitions/identity-resolution.template.json +0 -30
- package/skills/data360-orchestrate/assets/definitions/mapping.template.json +0 -14
- package/skills/data360-orchestrate/assets/definitions/relationship.template.json +0 -12
- package/skills/data360-orchestrate/assets/definitions/search-index.template.json +0 -9
- package/skills/data360-orchestrate/assets/definitions/segment.template.json +0 -16
- package/skills/data360-orchestrate/references/feature-readiness.md +0 -157
- package/skills/data360-orchestrate/references/plugin-setup.md +0 -138
- package/skills/data360-orchestrate/scripts/bootstrap-plugin.sh +0 -53
- package/skills/data360-orchestrate/scripts/diagnose-org.mjs +0 -511
- package/skills/data360-orchestrate/scripts/generate-manifest.mjs +0 -68
- package/skills/data360-orchestrate/scripts/verify-plugin.sh +0 -58
- package/skills/data360-prepare/README.md +0 -50
- package/skills/data360-prepare/SKILL.md +0 -203
- package/skills/data360-prepare/examples/ingestion-api/.env.example +0 -8
- package/skills/data360-prepare/examples/ingestion-api/README.md +0 -48
- package/skills/data360-prepare/examples/ingestion-api/send-data.py +0 -144
- package/skills/data360-query/README.md +0 -43
- package/skills/data360-query/SKILL.md +0 -128
- package/skills/data360-query/examples/search-indexes/hybrid-structured.json +0 -44
- package/skills/data360-query/examples/search-indexes/vector-knowledge.json +0 -43
- package/skills/data360-segment/README.md +0 -35
- package/skills/data360-segment/SKILL.md +0 -123
- package/skills/service-helpagent-coordinate/references/channel-help-portal.md +0 -22
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-1-1-non-text-content.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-i-lists.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-ii-tables.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-iii-form-labels.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-iv-regions.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-v-groups.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-5-identify-input.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-4-3-contrast.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-1-1-keyboard.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-4-4-link-purpose.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-4-6-headings-labels.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-1-pointer-gestures.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-2-pointer-cancellation.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-3-label-in-name.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-7-dragging-movement.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-2-1-on-focus.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-2-2-on-input.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-3-1-error-identification.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-3-2-labels-instructions.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-3-3-error-suggestion.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-4-1-2-i-name.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-4-1-2-ii-role.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-4-1-2-iii-value.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-1-1-non-text-content.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-1-use-of-color.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-10-resize-reflow.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-11-non-text-contrast.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-3-contrast.md +0 -0
- /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/scripts/contrast-ratio.py +0 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"""End-to-end scenario tests for apply-casesettings.py against a FakeOrg.
|
|
2
|
+
|
|
3
|
+
Covers the org-state matrix requested for this skill:
|
|
4
|
+
1. Org with NO support settings and NO email-to-case toggles.
|
|
5
|
+
2. Org with support settings already set but NO email-to-case toggles.
|
|
6
|
+
3. Org with support settings + enableEmailToCase, but NO on-demand toggle.
|
|
7
|
+
4. Org with all required toggles but NO routing address.
|
|
8
|
+
5. Org with all required toggles + an existing routing address — a NEW address
|
|
9
|
+
is CREATED, the existing one is preserved (not edited).
|
|
10
|
+
6. Support-settings resolution with User, Queue, and System.
|
|
11
|
+
7. Per-address caseOwner supplied vs not; User- and Queue-type owners.
|
|
12
|
+
|
|
13
|
+
Plus the optional-toggle guarantees:
|
|
14
|
+
* optional toggles ARE written to the org when present in the source file;
|
|
15
|
+
* when the user opts OUT of a toggle (it is absent from the source file), it
|
|
16
|
+
is NOT written and the org keeps its current value.
|
|
17
|
+
|
|
18
|
+
All org I/O is mocked via FakeOrg — no live org required.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import tempfile
|
|
23
|
+
import unittest
|
|
24
|
+
|
|
25
|
+
from . import _bootstrap
|
|
26
|
+
from ._fakeorg import FakeOrg, last_e2c_write, top_level_keys_written
|
|
27
|
+
from ._run import Input, run_main, write_input
|
|
28
|
+
|
|
29
|
+
apply_mod = _bootstrap.load_apply()
|
|
30
|
+
|
|
31
|
+
# Fields the skill defaults ON (opt-out model); used to assert opt-out behavior.
|
|
32
|
+
DEFAULT_E2C_TOGGLES = ["enableHtmlEmail", "notifyOwnerOnNewCaseEmail",
|
|
33
|
+
"enableE2CDeduplicateAttachments",
|
|
34
|
+
"showWordCountInComposer"]
|
|
35
|
+
|
|
36
|
+
ADMIN = "admin@acme.com"
|
|
37
|
+
AUTH = "authuser@fake.example.com" # matches FakeOrg.auth_username
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fresh_org(**kw):
|
|
41
|
+
"""Org with nothing configured: E2C off, no addresses, no support settings."""
|
|
42
|
+
kw.setdefault("case_settings",
|
|
43
|
+
{"fullName": "Case",
|
|
44
|
+
"emailToCase": {"enableEmailToCase": False}})
|
|
45
|
+
return FakeOrg(**kw)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# The three mutually-exclusive automated-user fields. The platform merges
|
|
49
|
+
# top-level fields, so the ONLY layer the script can guarantee they are
|
|
50
|
+
# non-contradictory on a type switch is the payload it sends — hence tests
|
|
51
|
+
# assert on the write payload, not the FakeOrg's post-merge state.
|
|
52
|
+
AUTOMATED_USER_FIELDS = ("defaultCaseUser", "useSystemUserAsDefaultCaseUser",
|
|
53
|
+
"systemUserEmail")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def automated_user_payload(org):
|
|
57
|
+
"""The automated-user fields carried by the Phase A updateMetadata write."""
|
|
58
|
+
for w in org.writes:
|
|
59
|
+
if any(f in w for f in AUTOMATED_USER_FIELDS):
|
|
60
|
+
return {f: w[f] for f in AUTOMATED_USER_FIELDS if f in w}
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Scenario1_NoSupportNoToggles(unittest.TestCase):
|
|
65
|
+
"""No support settings, no toggles: skill must set support settings from
|
|
66
|
+
supplied values and enable the toggles + address."""
|
|
67
|
+
|
|
68
|
+
def test_sets_owner_toggles_and_address(self):
|
|
69
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
70
|
+
src = (Input().with_default_toggles()
|
|
71
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
72
|
+
caseOrigin="Email", casePriority="Medium",
|
|
73
|
+
saveEmailHeaders="true").build())
|
|
74
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
75
|
+
path = write_input(d, src)
|
|
76
|
+
summary = run_main(apply_mod, [
|
|
77
|
+
"--target-org", "x", "--input", path,
|
|
78
|
+
"--routing-email", "support@acme.com",
|
|
79
|
+
"--owner-type", "User", "--owner-value", ADMIN,
|
|
80
|
+
"--automated-type", "User", "--automated-value", ADMIN,
|
|
81
|
+
])
|
|
82
|
+
# support settings set from input
|
|
83
|
+
self.assertEqual(org.state["defaultCaseOwner"], ADMIN)
|
|
84
|
+
self.assertEqual(org.state["defaultCaseOwnerType"], "User")
|
|
85
|
+
self.assertEqual(org.state["defaultCaseUser"], ADMIN)
|
|
86
|
+
# toggles + on-demand live
|
|
87
|
+
e2c = org.state["emailToCase"]
|
|
88
|
+
self.assertTrue(e2c["enableEmailToCase"])
|
|
89
|
+
self.assertTrue(e2c["enableOnDemandEmailToCase"])
|
|
90
|
+
# address created
|
|
91
|
+
self.assertEqual(summary["phaseB"][0]["status"], "created")
|
|
92
|
+
self.assertEqual(summary["verified"]["routingAddressCount"], 1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Scenario2_SupportSetNoToggles(unittest.TestCase):
|
|
96
|
+
"""Support settings already configured: they are PRESERVED (no flags), and
|
|
97
|
+
the toggles are enabled."""
|
|
98
|
+
|
|
99
|
+
def test_preserves_existing_support_settings(self):
|
|
100
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
101
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
102
|
+
"defaultCaseOwnerType": "User",
|
|
103
|
+
"defaultCaseUser": ADMIN})
|
|
104
|
+
src = (Input().with_default_toggles()
|
|
105
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
106
|
+
caseOrigin="Email", casePriority="Medium").build())
|
|
107
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
108
|
+
path = write_input(d, src)
|
|
109
|
+
summary = run_main(apply_mod, [
|
|
110
|
+
"--target-org", "x", "--input", path,
|
|
111
|
+
"--routing-email", "support@acme.com",
|
|
112
|
+
])
|
|
113
|
+
self.assertEqual(summary["supportSettings"]["action"],
|
|
114
|
+
"preserved-existing")
|
|
115
|
+
# unchanged
|
|
116
|
+
self.assertEqual(org.state["defaultCaseOwner"], ADMIN)
|
|
117
|
+
self.assertTrue(org.state["emailToCase"]["enableOnDemandEmailToCase"])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Scenario2b_PartialSupportSettings(unittest.TestCase):
|
|
121
|
+
"""Default Case Owner and Automated Case User are independent — an org can
|
|
122
|
+
have one configured and the other not. Each is preserved or set on its own:
|
|
123
|
+
supplying only the missing field succeeds; the configured field is left
|
|
124
|
+
untouched; overwriting a configured field still requires the flag."""
|
|
125
|
+
|
|
126
|
+
ADMIN2 = "admin2@acme.com"
|
|
127
|
+
|
|
128
|
+
def _run(self, org, extra):
|
|
129
|
+
src = (Input().with_default_toggles()
|
|
130
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
131
|
+
caseOrigin="Email", casePriority="Medium").build())
|
|
132
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
133
|
+
path = write_input(d, src)
|
|
134
|
+
return run_main(apply_mod, [
|
|
135
|
+
"--target-org", "x", "--input", path,
|
|
136
|
+
"--routing-email", "support@acme.com", *extra])
|
|
137
|
+
|
|
138
|
+
def test_owner_set_supply_only_automated_user(self):
|
|
139
|
+
# Owner already configured, automated user NOT. Supplying only the
|
|
140
|
+
# automated user must succeed and preserve the existing owner untouched.
|
|
141
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
142
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
143
|
+
"defaultCaseOwnerType": "User"})
|
|
144
|
+
summary = self._run(org, ["--automated-type", "User",
|
|
145
|
+
"--automated-value", ADMIN])
|
|
146
|
+
ss = summary["supportSettings"]
|
|
147
|
+
self.assertEqual(ss["ownerAction"], "preserved")
|
|
148
|
+
self.assertEqual(ss["automatedUserAction"], "set-from-input")
|
|
149
|
+
# owner preserved, automated user set
|
|
150
|
+
self.assertEqual(org.state["defaultCaseOwner"], ADMIN)
|
|
151
|
+
self.assertEqual(org.state["defaultCaseUser"], ADMIN)
|
|
152
|
+
|
|
153
|
+
def test_automated_user_set_supply_only_owner(self):
|
|
154
|
+
# Automated user already configured, owner NOT. Supplying only the owner
|
|
155
|
+
# must succeed and preserve the existing automated user untouched.
|
|
156
|
+
org = fresh_org(active_users=[ADMIN, self.ADMIN2, AUTH])
|
|
157
|
+
org.state.update({"defaultCaseUser": ADMIN,
|
|
158
|
+
"useSystemUserAsDefaultCaseUser": False})
|
|
159
|
+
summary = self._run(org, ["--owner-type", "User",
|
|
160
|
+
"--owner-value", self.ADMIN2])
|
|
161
|
+
ss = summary["supportSettings"]
|
|
162
|
+
self.assertEqual(ss["ownerAction"], "set-from-input")
|
|
163
|
+
self.assertEqual(ss["automatedUserAction"], "preserved")
|
|
164
|
+
self.assertEqual(org.state["defaultCaseOwner"], self.ADMIN2)
|
|
165
|
+
self.assertEqual(org.state["defaultCaseUser"], ADMIN) # preserved
|
|
166
|
+
|
|
167
|
+
def test_owner_set_supplying_owner_does_not_overwrite_without_flag(self):
|
|
168
|
+
# Owner already configured; caller supplies a DIFFERENT owner but no
|
|
169
|
+
# --overwrite-support-settings. The existing owner must be preserved
|
|
170
|
+
# (the supplied value is ignored, not written).
|
|
171
|
+
org = fresh_org(active_users=[ADMIN, self.ADMIN2, AUTH])
|
|
172
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
173
|
+
"defaultCaseOwnerType": "User"})
|
|
174
|
+
summary = self._run(org, ["--owner-type", "User",
|
|
175
|
+
"--owner-value", self.ADMIN2,
|
|
176
|
+
"--automated-type", "User",
|
|
177
|
+
"--automated-value", ADMIN])
|
|
178
|
+
ss = summary["supportSettings"]
|
|
179
|
+
self.assertEqual(ss["ownerAction"], "preserved")
|
|
180
|
+
self.assertEqual(org.state["defaultCaseOwner"], ADMIN) # NOT ADMIN2
|
|
181
|
+
|
|
182
|
+
def test_overwrite_flag_replaces_configured_owner(self):
|
|
183
|
+
# With --overwrite-support-settings, a configured owner IS replaced.
|
|
184
|
+
org = fresh_org(active_users=[ADMIN, self.ADMIN2, AUTH])
|
|
185
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
186
|
+
"defaultCaseOwnerType": "User"})
|
|
187
|
+
summary = self._run(org, ["--owner-type", "User",
|
|
188
|
+
"--owner-value", self.ADMIN2,
|
|
189
|
+
"--automated-type", "User",
|
|
190
|
+
"--automated-value", ADMIN,
|
|
191
|
+
"--overwrite-support-settings"])
|
|
192
|
+
ss = summary["supportSettings"]
|
|
193
|
+
self.assertEqual(ss["ownerAction"], "overwrote-existing")
|
|
194
|
+
self.assertEqual(ss["action"], "overwrote-existing")
|
|
195
|
+
self.assertEqual(org.state["defaultCaseOwner"], self.ADMIN2)
|
|
196
|
+
|
|
197
|
+
# --- --overwrite-support-settings is scoped PER FIELD ---
|
|
198
|
+
# A single global overwrite flag must NOT force a rewrite of the field the
|
|
199
|
+
# caller did not supply input for. Overwriting only the owner leaves a
|
|
200
|
+
# configured automated user untouched, and vice versa.
|
|
201
|
+
|
|
202
|
+
def test_overwrite_owner_only_preserves_configured_automated_user(self):
|
|
203
|
+
# BOTH configured. Caller overwrites ONLY the owner (supplies only owner
|
|
204
|
+
# input) with the flag set. The automated user must be PRESERVED — not
|
|
205
|
+
# rewritten and not demanding input.
|
|
206
|
+
org = fresh_org(active_users=[ADMIN, self.ADMIN2, AUTH])
|
|
207
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
208
|
+
"defaultCaseOwnerType": "User",
|
|
209
|
+
"defaultCaseUser": ADMIN,
|
|
210
|
+
"useSystemUserAsDefaultCaseUser": False})
|
|
211
|
+
summary = self._run(org, ["--owner-type", "User",
|
|
212
|
+
"--owner-value", self.ADMIN2,
|
|
213
|
+
"--overwrite-support-settings"])
|
|
214
|
+
ss = summary["supportSettings"]
|
|
215
|
+
self.assertEqual(ss["ownerAction"], "overwrote-existing")
|
|
216
|
+
self.assertEqual(ss["automatedUserAction"], "preserved")
|
|
217
|
+
self.assertEqual(org.state["defaultCaseOwner"], self.ADMIN2)
|
|
218
|
+
self.assertEqual(org.state["defaultCaseUser"], ADMIN) # untouched
|
|
219
|
+
# Preserved means UNCHANGED: the automated user is never resolved from
|
|
220
|
+
# fresh input or flipped to another type — its existing value is carried
|
|
221
|
+
# through as-is (the named user stays; the System flag is not set).
|
|
222
|
+
self.assertEqual(automated_user_payload(org),
|
|
223
|
+
{"defaultCaseUser": ADMIN,
|
|
224
|
+
"useSystemUserAsDefaultCaseUser": False})
|
|
225
|
+
|
|
226
|
+
def test_overwrite_automated_only_preserves_configured_owner(self):
|
|
227
|
+
# BOTH configured. Caller overwrites ONLY the automated user (supplies
|
|
228
|
+
# only automated input) with the flag set. The owner must be PRESERVED.
|
|
229
|
+
org = fresh_org(active_users=[ADMIN, self.ADMIN2, AUTH])
|
|
230
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
231
|
+
"defaultCaseOwnerType": "User",
|
|
232
|
+
"defaultCaseUser": ADMIN,
|
|
233
|
+
"useSystemUserAsDefaultCaseUser": False})
|
|
234
|
+
summary = self._run(org, ["--automated-type", "User",
|
|
235
|
+
"--automated-value", self.ADMIN2,
|
|
236
|
+
"--overwrite-support-settings"])
|
|
237
|
+
ss = summary["supportSettings"]
|
|
238
|
+
self.assertEqual(ss["ownerAction"], "preserved")
|
|
239
|
+
self.assertEqual(ss["automatedUserAction"], "overwrote-existing")
|
|
240
|
+
self.assertEqual(org.state["defaultCaseOwner"], ADMIN) # untouched
|
|
241
|
+
self.assertEqual(automated_user_payload(org),
|
|
242
|
+
{"defaultCaseUser": self.ADMIN2,
|
|
243
|
+
"useSystemUserAsDefaultCaseUser": False})
|
|
244
|
+
|
|
245
|
+
# --- Automated-user TYPE switches clear the mutually-exclusive field ---
|
|
246
|
+
# System and a named User are mutually exclusive at the platform layer, so
|
|
247
|
+
# on an overwrite the payload must carry ONLY the new type's fields and must
|
|
248
|
+
# NOT re-send the old type's field (leaving both populated would produce a
|
|
249
|
+
# contradictory payload). Asserted on the write payload
|
|
250
|
+
# because the platform preserves any OMITTED top-level field via merge.
|
|
251
|
+
|
|
252
|
+
def test_overwrite_named_user_with_system_clears_named_user(self):
|
|
253
|
+
org = fresh_org(active_users=[ADMIN, AUTH], queues=["Support_Queue"])
|
|
254
|
+
org.state.update({"defaultCaseOwner": "Support_Queue",
|
|
255
|
+
"defaultCaseOwnerType": "Queue",
|
|
256
|
+
"defaultCaseUser": ADMIN,
|
|
257
|
+
"useSystemUserAsDefaultCaseUser": False})
|
|
258
|
+
summary = self._run(org, ["--automated-type", "System",
|
|
259
|
+
"--system-user-email", "ops@acme.com",
|
|
260
|
+
"--overwrite-support-settings"])
|
|
261
|
+
self.assertEqual(summary["supportSettings"]["automatedUserAction"],
|
|
262
|
+
"overwrote-existing")
|
|
263
|
+
payload = automated_user_payload(org)
|
|
264
|
+
self.assertEqual(payload.get("useSystemUserAsDefaultCaseUser"), True)
|
|
265
|
+
self.assertEqual(payload.get("systemUserEmail"), "ops@acme.com")
|
|
266
|
+
# The stale named user must NOT be re-sent alongside System.
|
|
267
|
+
self.assertNotIn("defaultCaseUser", payload)
|
|
268
|
+
|
|
269
|
+
def test_overwrite_system_with_named_user_clears_system_flag(self):
|
|
270
|
+
org = fresh_org(active_users=[ADMIN, AUTH], queues=["Support_Queue"])
|
|
271
|
+
org.state.update({"defaultCaseOwner": "Support_Queue",
|
|
272
|
+
"defaultCaseOwnerType": "Queue",
|
|
273
|
+
"useSystemUserAsDefaultCaseUser": True,
|
|
274
|
+
"systemUserEmail": "ops@acme.com"})
|
|
275
|
+
summary = self._run(org, ["--automated-type", "User",
|
|
276
|
+
"--automated-value", ADMIN,
|
|
277
|
+
"--overwrite-support-settings"])
|
|
278
|
+
self.assertEqual(summary["supportSettings"]["automatedUserAction"],
|
|
279
|
+
"overwrote-existing")
|
|
280
|
+
payload = automated_user_payload(org)
|
|
281
|
+
self.assertEqual(payload.get("defaultCaseUser"), ADMIN)
|
|
282
|
+
self.assertEqual(payload.get("useSystemUserAsDefaultCaseUser"), False)
|
|
283
|
+
# The stale System-user email must NOT be re-sent alongside a named User.
|
|
284
|
+
self.assertNotIn("systemUserEmail", payload)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class Scenario3_ToggleOnNoOnDemand(unittest.TestCase):
|
|
288
|
+
"""Org has enableEmailToCase already true but on-demand off: the apply must
|
|
289
|
+
turn on-demand on (so addresses can bind)."""
|
|
290
|
+
|
|
291
|
+
def test_enables_on_demand(self):
|
|
292
|
+
org = FakeOrg(
|
|
293
|
+
case_settings={"fullName": "Case",
|
|
294
|
+
"defaultCaseOwner": ADMIN,
|
|
295
|
+
"defaultCaseOwnerType": "User",
|
|
296
|
+
"defaultCaseUser": ADMIN,
|
|
297
|
+
"emailToCase": {"enableEmailToCase": True,
|
|
298
|
+
"enableOnDemandEmailToCase": False}},
|
|
299
|
+
active_users=[ADMIN, AUTH])
|
|
300
|
+
src = (Input().with_default_toggles()
|
|
301
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
302
|
+
caseOrigin="Email", casePriority="Medium").build())
|
|
303
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
304
|
+
path = write_input(d, src)
|
|
305
|
+
summary = run_main(apply_mod, [
|
|
306
|
+
"--target-org", "x", "--input", path,
|
|
307
|
+
"--routing-email", "support@acme.com",
|
|
308
|
+
])
|
|
309
|
+
self.assertTrue(org.state["emailToCase"]["enableOnDemandEmailToCase"])
|
|
310
|
+
self.assertEqual(summary["verified"]["enableOnDemandEmailToCase"], True)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class Scenario4_AllTogglesNoAddress(unittest.TestCase):
|
|
314
|
+
"""All required toggles present, input declares no routing address: Phase A
|
|
315
|
+
runs, Phase B is a no-op, count stays 0."""
|
|
316
|
+
|
|
317
|
+
def test_no_address_declared(self):
|
|
318
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
319
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
320
|
+
"defaultCaseOwnerType": "User",
|
|
321
|
+
"defaultCaseUser": ADMIN})
|
|
322
|
+
src = Input().with_default_toggles().build() # no .address()
|
|
323
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
324
|
+
path = write_input(d, src)
|
|
325
|
+
summary = run_main(apply_mod, ["--target-org", "x", "--input", path])
|
|
326
|
+
self.assertEqual(summary["phaseB"], [])
|
|
327
|
+
self.assertEqual(summary["verified"]["routingAddressCount"], 0)
|
|
328
|
+
self.assertTrue(org.state["emailToCase"]["enableEmailToCase"])
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class Scenario5_NewAddressNotEdit(unittest.TestCase):
|
|
332
|
+
"""An org with an existing address gets a NEW one appended; the existing is
|
|
333
|
+
preserved (create, not edit)."""
|
|
334
|
+
|
|
335
|
+
def test_appends_new_preserves_existing(self):
|
|
336
|
+
org = FakeOrg(
|
|
337
|
+
case_settings={"fullName": "Case",
|
|
338
|
+
"defaultCaseOwner": ADMIN,
|
|
339
|
+
"defaultCaseOwnerType": "User",
|
|
340
|
+
"defaultCaseUser": ADMIN,
|
|
341
|
+
"emailToCase": {
|
|
342
|
+
"enableEmailToCase": True,
|
|
343
|
+
"enableOnDemandEmailToCase": True,
|
|
344
|
+
"routingAddresses": {
|
|
345
|
+
"addressType": "EmailToCase",
|
|
346
|
+
"routingName": "Existing Support",
|
|
347
|
+
"caseOrigin": "Email",
|
|
348
|
+
"casePriority": "Medium",
|
|
349
|
+
"emailAddress": "old@acme.com",
|
|
350
|
+
# A real provisioned address always carries
|
|
351
|
+
# these platform-managed read-only fields on
|
|
352
|
+
# read; seed them so the fake mirrors a live
|
|
353
|
+
# org. Preservation is driven by document
|
|
354
|
+
# ORDER (new address before existing), not by
|
|
355
|
+
# these fields — see MultiAddressOrdering...
|
|
356
|
+
"emailServicesAddress": "old@svc.example.com",
|
|
357
|
+
"isVerified": True}}},
|
|
358
|
+
active_users=[ADMIN, AUTH])
|
|
359
|
+
src = (Input().with_default_toggles()
|
|
360
|
+
.address(addressType="EmailToCase", routingName="Billing",
|
|
361
|
+
caseOrigin="Email", casePriority="High").build())
|
|
362
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
363
|
+
path = write_input(d, src)
|
|
364
|
+
summary = run_main(apply_mod, [
|
|
365
|
+
"--target-org", "x", "--input", path,
|
|
366
|
+
"--routing-email", "billing@acme.com",
|
|
367
|
+
])
|
|
368
|
+
self.assertEqual(summary["verified"]["routingAddressCount"], 2)
|
|
369
|
+
names = {a["routingName"]
|
|
370
|
+
for a in org.state["emailToCase"]["routingAddresses"]}
|
|
371
|
+
self.assertIn("Existing Support", names) # preserved
|
|
372
|
+
self.assertIn("Billing", names) # created
|
|
373
|
+
self.assertEqual(summary["phaseB"][0]["status"], "created")
|
|
374
|
+
|
|
375
|
+
def test_duplicate_address_reported_not_duplicated(self):
|
|
376
|
+
org = FakeOrg(
|
|
377
|
+
case_settings={"fullName": "Case",
|
|
378
|
+
"defaultCaseOwner": ADMIN,
|
|
379
|
+
"defaultCaseOwnerType": "User",
|
|
380
|
+
"defaultCaseUser": ADMIN,
|
|
381
|
+
"emailToCase": {
|
|
382
|
+
"enableEmailToCase": True,
|
|
383
|
+
"enableOnDemandEmailToCase": True,
|
|
384
|
+
"routingAddresses": {
|
|
385
|
+
"addressType": "EmailToCase",
|
|
386
|
+
"routingName": "Support",
|
|
387
|
+
"caseOrigin": "Email",
|
|
388
|
+
"casePriority": "Medium",
|
|
389
|
+
"emailAddress": "support@acme.com"}}},
|
|
390
|
+
active_users=[ADMIN, AUTH])
|
|
391
|
+
src = (Input().with_default_toggles()
|
|
392
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
393
|
+
caseOrigin="Email", casePriority="Medium").build())
|
|
394
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
395
|
+
path = write_input(d, src)
|
|
396
|
+
summary = run_main(apply_mod, [
|
|
397
|
+
"--target-org", "x", "--input", path,
|
|
398
|
+
"--routing-email", "support@acme.com",
|
|
399
|
+
])
|
|
400
|
+
self.assertEqual(summary["phaseB"][0]["status"], "already_exists")
|
|
401
|
+
self.assertEqual(summary["verified"]["routingAddressCount"], 1)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class MultiAddressOrderingPreservation(unittest.TestCase):
|
|
405
|
+
"""Verifies multi-address ordering preserves existing addresses. The
|
|
406
|
+
wholesale routingAddresses replace drops an existing, already-provisioned
|
|
407
|
+
address if a brand-new address follows it in the payload, so Phase B emits
|
|
408
|
+
new addresses BEFORE existing ones. The platform-managed read-only fields
|
|
409
|
+
(emailServicesAddress/isVerified) are not the deciding factor: preservation
|
|
410
|
+
depends on order, not those fields.
|
|
411
|
+
|
|
412
|
+
Covers:
|
|
413
|
+
* a single append preserves the existing address, and a VERIFIED existing
|
|
414
|
+
address stays verified;
|
|
415
|
+
* the emitted payload actually orders new addresses before existing ones
|
|
416
|
+
(asserting the mechanism, not just the outcome);
|
|
417
|
+
* a multi-address append (two new at once) preserves everything;
|
|
418
|
+
* if the platform drops an address anyway (fault-injected), the runtime
|
|
419
|
+
preservation guard fails LOUDLY rather than reporting a bogus success.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
def _org_with_provisioned_address(self, verified=True,
|
|
423
|
+
drop_one_existing_on_write=False):
|
|
424
|
+
return FakeOrg(
|
|
425
|
+
case_settings={"fullName": "Case",
|
|
426
|
+
"defaultCaseOwner": ADMIN,
|
|
427
|
+
"defaultCaseOwnerType": "User",
|
|
428
|
+
"defaultCaseUser": ADMIN,
|
|
429
|
+
"emailToCase": {
|
|
430
|
+
"enableEmailToCase": True,
|
|
431
|
+
"enableOnDemandEmailToCase": True,
|
|
432
|
+
"routingAddresses": {
|
|
433
|
+
"addressType": "EmailToCase",
|
|
434
|
+
"routingName": "Support Inbox",
|
|
435
|
+
"caseOrigin": "Email",
|
|
436
|
+
"casePriority": "Medium",
|
|
437
|
+
"emailAddress": "alias1@acme.com",
|
|
438
|
+
"emailServicesAddress":
|
|
439
|
+
"alias1@svc.example.com",
|
|
440
|
+
"isVerified": verified}}},
|
|
441
|
+
active_users=[ADMIN, AUTH],
|
|
442
|
+
drop_one_existing_on_write=drop_one_existing_on_write)
|
|
443
|
+
|
|
444
|
+
def _append(self, org, addrs_and_emails):
|
|
445
|
+
builder = Input().with_default_toggles()
|
|
446
|
+
emails = []
|
|
447
|
+
for routing_name, email in addrs_and_emails:
|
|
448
|
+
builder.address(addressType="EmailToCase", routingName=routing_name,
|
|
449
|
+
caseOrigin="Email", casePriority="Medium")
|
|
450
|
+
emails.append(email)
|
|
451
|
+
src = builder.build()
|
|
452
|
+
args = ["--target-org", "x"]
|
|
453
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
454
|
+
path = write_input(d, src)
|
|
455
|
+
args += ["--input", path]
|
|
456
|
+
for email in emails:
|
|
457
|
+
args += ["--routing-email", email]
|
|
458
|
+
return run_main(apply_mod, args)
|
|
459
|
+
|
|
460
|
+
def _addr_list(self, org):
|
|
461
|
+
addrs = org.state["emailToCase"]["routingAddresses"]
|
|
462
|
+
return addrs if isinstance(addrs, list) else [addrs]
|
|
463
|
+
|
|
464
|
+
def test_append_preserves_verified_existing(self):
|
|
465
|
+
org = self._org_with_provisioned_address(verified=True)
|
|
466
|
+
summary = self._append(org, [("Billing Inbox", "alias2@acme.com")])
|
|
467
|
+
self.assertEqual(summary["verified"]["routingAddressCount"], 2)
|
|
468
|
+
by_name = {a["routingName"]: a for a in self._addr_list(org)}
|
|
469
|
+
self.assertEqual(set(by_name), {"Support Inbox", "Billing Inbox"})
|
|
470
|
+
# the existing address survived AND kept its verified state
|
|
471
|
+
self.assertTrue(by_name["Support Inbox"]["isVerified"])
|
|
472
|
+
|
|
473
|
+
def test_payload_orders_new_before_existing(self):
|
|
474
|
+
# Assert the actual fix mechanism: the emitted routingAddresses payload
|
|
475
|
+
# lists the new address ahead of the carried-over existing one.
|
|
476
|
+
org = self._org_with_provisioned_address()
|
|
477
|
+
self._append(org, [("Billing Inbox", "alias2@acme.com")])
|
|
478
|
+
e2c = last_e2c_write(org)
|
|
479
|
+
addrs = e2c["routingAddresses"]
|
|
480
|
+
self.assertIsInstance(addrs, list)
|
|
481
|
+
names = [a["routingName"] for a in addrs]
|
|
482
|
+
self.assertEqual(names.index("Billing Inbox"),
|
|
483
|
+
min(range(len(names))),
|
|
484
|
+
"new address must be emitted before the existing one")
|
|
485
|
+
self.assertLess(names.index("Billing Inbox"),
|
|
486
|
+
names.index("Support Inbox"))
|
|
487
|
+
|
|
488
|
+
def test_multi_add_preserves_all(self):
|
|
489
|
+
org = self._org_with_provisioned_address(verified=True)
|
|
490
|
+
summary = self._append(org, [("Inbox A", "a@acme.com"),
|
|
491
|
+
("Inbox B", "b@acme.com")])
|
|
492
|
+
self.assertEqual(summary["verified"]["routingAddressCount"], 3)
|
|
493
|
+
by_name = {a["routingName"]: a for a in self._addr_list(org)}
|
|
494
|
+
self.assertEqual(set(by_name),
|
|
495
|
+
{"Support Inbox", "Inbox A", "Inbox B"})
|
|
496
|
+
self.assertTrue(by_name["Support Inbox"]["isVerified"])
|
|
497
|
+
|
|
498
|
+
def test_guard_fires_on_platform_drop(self):
|
|
499
|
+
# Fault injection: the org drops an existing address despite correct
|
|
500
|
+
# ordering. The runtime preservation guard must fail loudly.
|
|
501
|
+
org = self._org_with_provisioned_address(
|
|
502
|
+
drop_one_existing_on_write=True)
|
|
503
|
+
with self.assertRaises(SystemExit):
|
|
504
|
+
self._append(org, [("Billing Inbox", "alias2@acme.com")])
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class Scenario6_SupportSettingsTypes(unittest.TestCase):
|
|
508
|
+
"""Owner + automated-user resolution across User, Queue, and System."""
|
|
509
|
+
|
|
510
|
+
def _run(self, org, extra):
|
|
511
|
+
src = Input().with_default_toggles().build()
|
|
512
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
513
|
+
path = write_input(d, src)
|
|
514
|
+
return run_main(apply_mod,
|
|
515
|
+
["--target-org", "x", "--input", path, *extra])
|
|
516
|
+
|
|
517
|
+
def test_user_owner_and_user_automated(self):
|
|
518
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
519
|
+
self._run(org, ["--owner-type", "User", "--owner-value", ADMIN,
|
|
520
|
+
"--automated-type", "User", "--automated-value", ADMIN])
|
|
521
|
+
self.assertEqual(org.state["defaultCaseOwnerType"], "User")
|
|
522
|
+
self.assertEqual(org.state["defaultCaseUser"], ADMIN)
|
|
523
|
+
|
|
524
|
+
def test_queue_owner(self):
|
|
525
|
+
org = fresh_org(active_users=[ADMIN, AUTH], queues=["Support_Queue"])
|
|
526
|
+
self._run(org, ["--owner-type", "Queue", "--owner-value", "Support_Queue",
|
|
527
|
+
"--automated-type", "User", "--automated-value", ADMIN])
|
|
528
|
+
self.assertEqual(org.state["defaultCaseOwnerType"], "Queue")
|
|
529
|
+
self.assertEqual(org.state["defaultCaseOwner"], "Support_Queue")
|
|
530
|
+
|
|
531
|
+
def test_system_automated_user(self):
|
|
532
|
+
org = fresh_org(active_users=[ADMIN, AUTH], queues=["Support_Queue"])
|
|
533
|
+
self._run(org, ["--owner-type", "Queue", "--owner-value", "Support_Queue",
|
|
534
|
+
"--automated-type", "System",
|
|
535
|
+
"--system-user-email", "ops@acme.com"])
|
|
536
|
+
self.assertTrue(org.state["useSystemUserAsDefaultCaseUser"])
|
|
537
|
+
# System and named defaultCaseUser are mutually exclusive
|
|
538
|
+
self.assertNotIn("defaultCaseUser", org.state)
|
|
539
|
+
|
|
540
|
+
def test_invalid_owner_fails_closed(self):
|
|
541
|
+
org = fresh_org(active_users=[AUTH]) # ADMIN is NOT active
|
|
542
|
+
with self.assertRaises(SystemExit):
|
|
543
|
+
self._run(org, ["--owner-type", "User", "--owner-value", ADMIN,
|
|
544
|
+
"--automated-type", "User",
|
|
545
|
+
"--automated-value", ADMIN])
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class Scenario7_PerAddressCaseOwner(unittest.TestCase):
|
|
549
|
+
"""Per-address caseOwner: absent (default), User type, Queue type, and
|
|
550
|
+
fail-closed on an owner that doesn't exist."""
|
|
551
|
+
|
|
552
|
+
def _apply(self, org, addr_kw, email="a@acme.com"):
|
|
553
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
554
|
+
"defaultCaseOwnerType": "User",
|
|
555
|
+
"defaultCaseUser": ADMIN})
|
|
556
|
+
src = (Input().with_default_toggles()
|
|
557
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
558
|
+
caseOrigin="Email", casePriority="Medium",
|
|
559
|
+
**addr_kw).build())
|
|
560
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
561
|
+
path = write_input(d, src)
|
|
562
|
+
return run_main(apply_mod, ["--target-org", "x", "--input", path,
|
|
563
|
+
"--routing-email", email])
|
|
564
|
+
|
|
565
|
+
def test_no_case_owner(self):
|
|
566
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
567
|
+
self._apply(org, {})
|
|
568
|
+
addr = org.state["emailToCase"]["routingAddresses"]
|
|
569
|
+
addr = addr[0] if isinstance(addr, list) else addr
|
|
570
|
+
self.assertNotIn("caseOwner", addr)
|
|
571
|
+
|
|
572
|
+
def test_user_case_owner(self):
|
|
573
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
574
|
+
self._apply(org, {"caseOwner": ADMIN, "caseOwnerType": "User"})
|
|
575
|
+
addr = org.state["emailToCase"]["routingAddresses"]
|
|
576
|
+
addr = addr[0] if isinstance(addr, list) else addr
|
|
577
|
+
self.assertEqual(addr["caseOwner"], ADMIN)
|
|
578
|
+
self.assertEqual(addr["caseOwnerType"], "User")
|
|
579
|
+
|
|
580
|
+
def test_queue_case_owner(self):
|
|
581
|
+
org = fresh_org(active_users=[ADMIN, AUTH], queues=["Billing_Queue"])
|
|
582
|
+
self._apply(org, {"caseOwner": "Billing_Queue", "caseOwnerType": "Queue"})
|
|
583
|
+
addr = org.state["emailToCase"]["routingAddresses"]
|
|
584
|
+
addr = addr[0] if isinstance(addr, list) else addr
|
|
585
|
+
self.assertEqual(addr["caseOwner"], "Billing_Queue")
|
|
586
|
+
self.assertEqual(addr["caseOwnerType"], "Queue")
|
|
587
|
+
|
|
588
|
+
def test_nonexistent_case_owner_fails_closed(self):
|
|
589
|
+
org = fresh_org(active_users=[ADMIN, AUTH]) # no such queue
|
|
590
|
+
with self.assertRaises(SystemExit):
|
|
591
|
+
self._apply(org, {"caseOwner": "Ghost_Queue", "caseOwnerType": "Queue"})
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class OptionalToggleBehavior(unittest.TestCase):
|
|
595
|
+
"""Optional toggles are written when present; opted-out toggles are NOT
|
|
596
|
+
written and the org keeps its current value."""
|
|
597
|
+
|
|
598
|
+
def test_optional_toggles_written_when_present(self):
|
|
599
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
600
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
601
|
+
"defaultCaseOwnerType": "User",
|
|
602
|
+
"defaultCaseUser": ADMIN})
|
|
603
|
+
src = (Input().with_default_toggles()
|
|
604
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
605
|
+
caseOrigin="Email", casePriority="Medium").build())
|
|
606
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
607
|
+
path = write_input(d, src)
|
|
608
|
+
summary = run_main(apply_mod, [
|
|
609
|
+
"--target-org", "x", "--input", path,
|
|
610
|
+
"--routing-email", "support@acme.com"])
|
|
611
|
+
e2c = org.state["emailToCase"]
|
|
612
|
+
for t in DEFAULT_E2C_TOGGLES:
|
|
613
|
+
self.assertTrue(e2c[t], f"{t} should be enabled")
|
|
614
|
+
self.assertTrue(org.state["enableDraftEmails"])
|
|
615
|
+
for t in DEFAULT_E2C_TOGGLES:
|
|
616
|
+
self.assertEqual(summary["verified"][f"emailToCase.{t}"], True)
|
|
617
|
+
|
|
618
|
+
def test_opted_out_toggle_is_not_enabled(self):
|
|
619
|
+
# User opted OUT of showWordCountInComposer and enableDraftEmails: they
|
|
620
|
+
# are absent from the source file. The provisioning-safe mechanism does
|
|
621
|
+
# a read-modify-write of the whole emailToCase block, so an opted-out
|
|
622
|
+
# field DOES ride along on the wire at its CURRENT org value (echoing an
|
|
623
|
+
# unchanged value is a field-level-merge no-op). The real opt-out
|
|
624
|
+
# guarantee is therefore behavioral:
|
|
625
|
+
# (a) the skill does not report it in phaseA as a field it SET, and
|
|
626
|
+
# (b) the org value is unchanged — opt-out never turns it ON.
|
|
627
|
+
org = fresh_org(active_users=[ADMIN, AUTH])
|
|
628
|
+
org.state.update({"defaultCaseOwner": ADMIN,
|
|
629
|
+
"defaultCaseOwnerType": "User",
|
|
630
|
+
"defaultCaseUser": ADMIN})
|
|
631
|
+
# org currently has these OFF; opting out must leave them OFF (not on).
|
|
632
|
+
org.state["emailToCase"]["showWordCountInComposer"] = False
|
|
633
|
+
org.state["enableDraftEmails"] = False
|
|
634
|
+
src = (Input()
|
|
635
|
+
.e2c(enableHtmlEmail="true", notifyOwnerOnNewCaseEmail="true",
|
|
636
|
+
enableE2CDeduplicateAttachments="true") # NO showWordCount
|
|
637
|
+
.address(addressType="EmailToCase", routingName="Support",
|
|
638
|
+
caseOrigin="Email", casePriority="Medium").build())
|
|
639
|
+
with tempfile.TemporaryDirectory() as d, org.patch(apply_mod):
|
|
640
|
+
path = write_input(d, src)
|
|
641
|
+
summary = run_main(apply_mod, ["--target-org", "x", "--input", path,
|
|
642
|
+
"--routing-email", "support@acme.com"])
|
|
643
|
+
# (a) the skill did not claim to set the opted-out fields
|
|
644
|
+
self.assertNotIn("emailToCase.showWordCountInComposer",
|
|
645
|
+
summary["phaseA"])
|
|
646
|
+
self.assertNotIn("enableDraftEmails", summary["phaseA"])
|
|
647
|
+
# (b) org values unchanged (still off) — opt-out did NOT enable them
|
|
648
|
+
self.assertFalse(org.state["emailToCase"]["showWordCountInComposer"])
|
|
649
|
+
self.assertFalse(org.state["enableDraftEmails"])
|
|
650
|
+
# the ones the user DID keep are on
|
|
651
|
+
self.assertTrue(org.state["emailToCase"]["enableHtmlEmail"])
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
if __name__ == "__main__":
|
|
655
|
+
unittest.main()
|