@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.
Files changed (288) hide show
  1. package/package.json +2 -6
  2. package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +3 -0
  3. package/skills/automation-sandbox-post-copy-configure/SKILL.md +4 -0
  4. package/skills/design-systems-slds-validate/SKILL.md +2 -2
  5. package/skills/dx-app-analytics-query/SKILL.md +1 -3
  6. package/skills/dx-devops-conflict-resolve/SKILL.md +205 -0
  7. package/skills/dx-devops-conflict-resolve/examples/conflict-workflows.md +165 -0
  8. package/skills/dx-devops-conflict-resolve/references/deploy-failure-resolution.md +75 -0
  9. package/skills/dx-devops-conflict-resolve/references/git-conflict-resolution.md +124 -0
  10. package/skills/dx-devops-conflict-resolve/scripts/detect-conflicts.sh +91 -0
  11. package/skills/dx-devops-conflict-resolve/scripts/diagnose-deploy-failure.sh +156 -0
  12. package/skills/dx-devops-request-status/SKILL.md +160 -0
  13. package/skills/dx-devops-request-status/examples/polling-workflows.md +101 -0
  14. package/skills/dx-devops-request-status/references/cli-commands.md +176 -0
  15. package/skills/dx-devops-request-status/scripts/poll-status.sh +134 -0
  16. package/skills/dx-devops-test-failures-analyze/SKILL.md +6 -6
  17. package/skills/dx-devops-test-pipeline-configure/SKILL.md +7 -7
  18. package/skills/dx-devops-test-suite-assignments-configure/SKILL.md +6 -6
  19. package/skills/dx-devops-test-suite-run/SKILL.md +6 -6
  20. package/skills/dx-devops-work-item-manage/SKILL.md +2 -0
  21. package/skills/dx-org-manage/references/creating-scratch-org.md +5 -2
  22. package/skills/dx-org-manage/references/creating-snapshot.md +1 -0
  23. package/skills/dx-org-shape-manage/SKILL.md +152 -0
  24. package/skills/dx-org-shape-manage/examples/create_error_output.json +9 -0
  25. package/skills/dx-org-shape-manage/examples/create_success_output.json +9 -0
  26. package/skills/dx-org-shape-manage/examples/delete_output.json +11 -0
  27. package/skills/dx-org-shape-manage/examples/list_inactive_output.json +32 -0
  28. package/skills/dx-org-shape-manage/examples/list_output.json +24 -0
  29. package/skills/dx-org-shape-manage/references/cli_flags.md +135 -0
  30. package/skills/dx-org-switch/SKILL.md +2 -2
  31. package/skills/dx-org-trial-expiration-check/SKILL.md +2 -0
  32. package/skills/dx-pkg-post-install-configure/SKILL.md +3 -0
  33. package/skills/education-cloud-multi-campus-configure/SKILL.md +211 -0
  34. package/skills/education-cloud-multi-campus-configure/examples/hierarchy_visualization.md +174 -0
  35. package/skills/education-cloud-multi-campus-configure/examples/output_examples.md +54 -0
  36. package/skills/education-cloud-multi-campus-configure/examples/sample_hierarchy_input.csv +29 -0
  37. package/skills/education-cloud-multi-campus-configure/examples/sample_hierarchy_input_edgecases.csv +16 -0
  38. package/skills/education-cloud-multi-campus-configure/references/account_recordtype_prerequisite.md +44 -0
  39. package/skills/education-cloud-multi-campus-configure/references/delta_computation.md +33 -0
  40. package/skills/education-cloud-multi-campus-configure/references/error_handling.md +198 -0
  41. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +54 -0
  42. package/skills/education-cloud-multi-campus-configure/references/gotchas.md +16 -0
  43. package/skills/education-cloud-multi-campus-configure/references/hierarchy_parsing_rules.md +133 -0
  44. package/skills/education-cloud-multi-campus-configure/references/mcp-invocation.md +75 -0
  45. package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/SKILL.md +1 -1
  46. package/skills/experience-cms-brand-apply/SKILL.md +4 -1
  47. package/skills/experience-cms-brand-create/SKILL.md +197 -0
  48. package/skills/experience-cms-brand-create/assets/brand-template.json +329 -0
  49. package/skills/experience-cms-brand-create/references/brand-anatomy.md +120 -0
  50. package/skills/experience-cms-brand-create/references/disk-contract.md +84 -0
  51. package/skills/experience-cms-content-generate/SKILL.md +190 -0
  52. package/skills/experience-cms-content-generate/assets/display-formats.md +84 -0
  53. package/skills/experience-cms-content-generate/assets/payloads/create-content-bulk.json +11 -0
  54. package/skills/experience-cms-content-generate/assets/payloads/create-content-single.json +11 -0
  55. package/skills/experience-cms-content-generate/assets/payloads/publish-content.json +4 -0
  56. package/skills/experience-cms-content-generate/assets/payloads/update-content.json +9 -0
  57. package/skills/experience-cms-content-generate/assets/questions.md +241 -0
  58. package/skills/experience-cms-content-generate/examples/create-content-call.md +81 -0
  59. package/skills/experience-cms-content-generate/references/bulk-batching.md +56 -0
  60. package/skills/experience-cms-content-generate/references/content-type-classification.md +88 -0
  61. package/skills/experience-cms-content-generate/references/content-write-tool.md +95 -0
  62. package/skills/experience-cms-content-generate/references/delegation-protocol.md +37 -0
  63. package/skills/experience-cms-content-generate/references/edit-publish-workflow.md +63 -0
  64. package/skills/experience-cms-content-generate/references/error-recovery.md +72 -0
  65. package/skills/experience-cms-content-generate/references/identifier-resolution.md +26 -0
  66. package/skills/experience-cms-content-generate/references/intent-routing.md +45 -0
  67. package/skills/experience-cms-content-generate/references/principles.md +19 -0
  68. package/skills/experience-cms-content-generate/references/ux-rules.md +68 -0
  69. package/skills/experience-cms-content-generate/references/workspace-resolution.md +33 -0
  70. package/skills/experience-cms-content-type-generate/SKILL.md +449 -0
  71. package/skills/experience-cms-content-type-generate/assets/discovery-prompts.md +79 -0
  72. package/skills/experience-cms-content-type-generate/assets/schema-example.json +70 -0
  73. package/skills/experience-cms-content-type-generate/references/agent-checklist.md +50 -0
  74. package/skills/experience-cms-content-type-generate/references/anti-patterns.md +39 -0
  75. package/skills/experience-cms-content-type-generate/references/deployment-errors.md +27 -0
  76. package/skills/experience-cms-content-type-generate/references/discovery-details.md +106 -0
  77. package/skills/experience-cms-content-type-generate/references/discovery-query-rules.md +85 -0
  78. package/skills/experience-cms-content-type-generate/references/edit-fields-loop.md +48 -0
  79. package/skills/experience-cms-content-type-generate/references/pre-deploy-checklist.md +21 -0
  80. package/skills/experience-cms-content-type-generate/references/retrieve-and-reconcile.md +105 -0
  81. package/skills/experience-cms-content-type-generate/references/schema-rules.md +51 -0
  82. package/skills/experience-cms-content-type-generate/references/schema-summary-format.md +60 -0
  83. package/skills/experience-lds-graphql-generate/SKILL.md +4 -0
  84. package/skills/experience-lwc-accessibility-jest-run/SKILL.md +79 -0
  85. package/skills/experience-lwc-accessibility-jest-run/references/running-sa11y-jest-tests.md +205 -0
  86. package/skills/experience-lwc-design-generate/SKILL.md +5 -4
  87. package/skills/experience-lwc-generate/SKILL.md +10 -9
  88. package/skills/experience-ui-bundle-deploy/references/dev-preview.md +21 -0
  89. package/skills/experience-ui-bundle-deploy/references/social-login.md +1 -0
  90. package/skills/experience-ui-bundle-file-upload-generate/SKILL.md +6 -6
  91. package/skills/experience-ui-bundle-localize/SKILL.md +30 -37
  92. package/skills/experience-ui-bundle-localize/references/gotchas.md +23 -7
  93. package/skills/experience-ui-bundle-localize/references/i18n-setup.md +48 -4
  94. package/skills/experience-ui-bundle-localize/references/label-xml.md +22 -8
  95. package/skills/experience-ui-bundle-localize/references/verifying.md +39 -11
  96. package/skills/experience-ui-bundle-localize/scripts/detect-bundle-type.sh +118 -15
  97. package/skills/experience-ui-bundle-localize/scripts/tests/test-detect-bundle-type.sh +187 -0
  98. package/skills/experience-ui-bundle-mfa-configure/SKILL.md +13 -2
  99. package/skills/experience-ui-bundle-mfa-configure/references/social-login.md +1 -0
  100. package/skills/experience-ui-bundle-project-generate/SKILL.md +2 -2
  101. package/skills/integration-connectivity-generate/SKILL.md +11 -10
  102. package/skills/integration-eventing-cdc-configure/SKILL.md +6 -6
  103. package/skills/integration-eventing-subscription-configure/SKILL.md +6 -5
  104. package/skills/mobile-platform-native-capabilities-integrate/SKILL.md +2 -2
  105. package/skills/mobile-platform-offline-validate/scripts/package.json +1 -1
  106. package/skills/platform-agentexchange-partner-offers-configure/SKILL.md +8 -8
  107. package/skills/platform-custom-application-generate/SKILL.md +2 -0
  108. package/skills/platform-custom-field-generate/SKILL.md +8 -6
  109. package/skills/platform-custom-metadata-type-generate/SKILL.md +460 -0
  110. package/skills/platform-custom-metadata-type-generate/references/cmdt-records.md +251 -0
  111. package/skills/platform-custom-metadata-type-generate/scripts/sanitize-developer-name.sh +46 -0
  112. package/skills/platform-custom-setting-generate/SKILL.md +419 -0
  113. package/skills/platform-dataspace-access-configure/SKILL.md +3 -3
  114. package/skills/platform-sharing-owd-configure/SKILL.md +3 -0
  115. package/skills/platform-value-set-generate/SKILL.md +2 -0
  116. package/skills/service-agentforce-channel-configure/SKILL.md +29 -20
  117. package/skills/service-agentforce-channel-configure/assets/BotEmailDefinition.botEmailDefinition-meta.xml +36 -0
  118. package/skills/service-agentforce-channel-configure/assets/email/unfiled$public/AgentforceForServiceEmailTemplate.email +17 -0
  119. package/skills/service-agentforce-channel-configure/assets/email/unfiled$public/AgentforceForServiceEmailTemplate.email-meta.xml +29 -0
  120. package/skills/service-agentforce-channel-configure/assets/mdapi-package.xml +25 -0
  121. package/skills/service-agentforce-channel-configure/assets/settings-mdapi-package.xml +30 -0
  122. package/skills/service-agentforce-channel-configure/references/agent-wiring.md +20 -0
  123. package/skills/service-agentforce-channel-configure/references/botemaildefinition.md +107 -0
  124. package/skills/service-agentforce-channel-configure/references/channel-branch-email.md +203 -50
  125. package/skills/service-agentforce-channel-configure/references/channel-types.md +30 -3
  126. package/skills/service-agentforce-channel-configure/references/queue-resolution.md +17 -8
  127. package/skills/service-agentforce-channel-configure/references/routing-flow.md +14 -3
  128. package/skills/service-agentforce-channel-configure/scripts/validate-botemaildefinition.py +126 -0
  129. package/skills/service-agentforce-channel-configure/scripts/validate-emailtemplate.py +133 -0
  130. package/skills/service-de-channel-activate/SKILL.md +271 -0
  131. package/skills/service-de-channel-activate/references/gotchas.md +31 -0
  132. package/skills/service-de-channel-activate/references/phone-verification.md +96 -0
  133. package/skills/service-de-channel-activate/references/worked-examples.md +14 -0
  134. package/skills/service-de-channel-consent-configure/SKILL.md +230 -0
  135. package/skills/service-de-channel-consent-configure/references/gotchas.md +41 -0
  136. package/skills/service-de-channel-consent-configure/references/language-keywords.md +84 -0
  137. package/skills/service-de-channel-consent-configure/references/worked-examples.md +134 -0
  138. package/skills/service-de-channel-create/SKILL.md +369 -0
  139. package/skills/service-de-channel-create/references/apple.md +59 -0
  140. package/skills/service-de-channel-create/references/connect-insert.md +84 -0
  141. package/skills/service-de-channel-create/references/facebook.md +154 -0
  142. package/skills/service-de-channel-create/references/line.md +66 -0
  143. package/skills/service-de-channel-create/references/sms.md +98 -0
  144. package/skills/service-de-channel-create/references/whatsapp.md +98 -0
  145. package/skills/service-de-channel-create/references/worked-examples.md +70 -0
  146. package/skills/service-de-channel-routing-configure/SKILL.md +345 -0
  147. package/skills/service-de-channel-routing-configure/references/asa-routing.md +90 -0
  148. package/skills/service-de-channel-routing-configure/references/gotchas.md +27 -0
  149. package/skills/service-de-channel-routing-configure/references/queue-creation.md +123 -0
  150. package/skills/service-de-channel-routing-configure/references/target-locate.md +98 -0
  151. package/skills/service-de-channel-routing-configure/references/worked-examples.md +174 -0
  152. package/skills/service-de-headless-channel-configure/SKILL.md +305 -0
  153. package/skills/service-de-headless-channel-configure/references/gotchas.md +27 -0
  154. package/skills/service-de-headless-channel-configure/references/inputs.md +49 -0
  155. package/skills/service-de-headless-channel-configure/references/output-envelopes.md +53 -0
  156. package/skills/service-de-headless-channel-configure/references/partial-success.md +24 -0
  157. package/skills/service-de-headless-channel-configure/references/terms-and-conditions.md +54 -0
  158. package/skills/service-de-headless-channel-configure/references/worked-examples.md +108 -0
  159. package/skills/service-de-waba-integrate/SKILL.md +227 -0
  160. package/skills/service-digital-engagement-channel-configure/scripts/check-api-version.sh +29 -0
  161. package/skills/service-digital-engagement-messaging-site-integrate/SKILL.md +10 -9
  162. package/skills/service-digital-engagement-messaging-site-integrate/references/lwr_patch.md +39 -21
  163. package/skills/service-digital-engagement-messaging-site-integrate/scripts/patch_lwr_bundle.sh +157 -96
  164. package/skills/service-email-to-case-configure/SKILL.md +190 -0
  165. package/skills/service-email-to-case-configure/assets/CaseSettings.settings-meta.xml +45 -0
  166. package/skills/service-email-to-case-configure/examples/CaseSettings-two-addresses.settings-meta.xml +36 -0
  167. package/skills/service-email-to-case-configure/references/apply-mechanics.md +73 -0
  168. package/skills/service-email-to-case-configure/references/routing_address_reference.md +73 -0
  169. package/skills/service-email-to-case-configure/references/troubleshooting.md +25 -0
  170. package/skills/service-email-to-case-configure/scripts/apply-casesettings.py +1315 -0
  171. package/skills/service-email-to-case-configure/scripts/check-agent-email-capability.sh +26 -0
  172. package/skills/service-email-to-case-configure/scripts/tests/__init__.py +0 -0
  173. package/skills/service-email-to-case-configure/scripts/tests/_bootstrap.py +45 -0
  174. package/skills/service-email-to-case-configure/scripts/tests/_fakeorg.py +366 -0
  175. package/skills/service-email-to-case-configure/scripts/tests/_run.py +84 -0
  176. package/skills/service-email-to-case-configure/scripts/tests/test_apply_org_scenarios.py +655 -0
  177. package/skills/service-email-to-case-configure/scripts/tests/test_get_session.py +134 -0
  178. package/skills/service-email-to-case-configure/scripts/tests/test_new_capabilities.py +193 -0
  179. package/skills/service-email-to-case-configure/scripts/tests/test_validate_casesettings.py +170 -0
  180. package/skills/service-email-to-case-configure/scripts/validate-casesettings.py +229 -0
  181. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +33 -15
  182. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -47
  183. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -12
  184. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md +5 -3
  185. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-enable-plan.mjs +11 -3
  186. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +8 -6
  187. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/references/cli-invocation.md +3 -3
  188. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/scripts/classify-readiness.mjs +11 -3
  189. package/skills/service-itsm-agentic-setup-configure/SKILL.md +5 -5
  190. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +5 -5
  191. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +2 -2
  192. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +11 -7
  193. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +20 -7
  194. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +10 -6
  195. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/action-availability.md +7 -7
  196. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +11 -7
  197. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/build-create-body.mjs +24 -17
  198. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-action-availability.mjs +98 -58
  199. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +20 -7
  200. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/strip-release-management.mjs +176 -0
  201. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +19 -19
  202. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/cli-invocation.md +3 -4
  203. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/helper-contracts.md +6 -6
  204. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/permset-topology.md +3 -10
  205. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-permset-availability.mjs +7 -9
  206. package/skills/service-itsm-incident-mgmt-configure/SKILL.md +2 -0
  207. package/skills/service-itsm-incident-priority-configure/SKILL.md +15 -12
  208. package/skills/service-itsm-incident-priority-configure/examples/matrix-operations.md +27 -1
  209. package/skills/service-itsm-incident-priority-configure/references/sf-cli-invocation.md +37 -6
  210. package/skills/service-native-voice-recording-transcription-configure/SKILL.md +146 -0
  211. package/skills/service-native-voice-recording-transcription-configure/assets/confirmation-output.json +16 -0
  212. package/skills/service-native-voice-recording-transcription-configure/assets/package.xml +22 -0
  213. package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +101 -0
  214. package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +181 -0
  215. package/skills/data360-activate/README.md +0 -38
  216. package/skills/data360-activate/SKILL.md +0 -127
  217. package/skills/data360-connect/README.md +0 -57
  218. package/skills/data360-connect/SKILL.md +0 -163
  219. package/skills/data360-connect/examples/connections/heroku-postgres.json +0 -15
  220. package/skills/data360-connect/examples/connections/ingest-api-connection.json +0 -5
  221. package/skills/data360-connect/examples/connections/ingest-api-schema.json +0 -31
  222. package/skills/data360-connect/examples/connections/redshift.json +0 -16
  223. package/skills/data360-connect/examples/connections/sharepoint-unstructured.json +0 -20
  224. package/skills/data360-connect/examples/connections/snowflake-connection.json +0 -42
  225. package/skills/data360-harmonize/README.md +0 -31
  226. package/skills/data360-harmonize/SKILL.md +0 -126
  227. package/skills/data360-orchestrate/README.md +0 -120
  228. package/skills/data360-orchestrate/SKILL.md +0 -263
  229. package/skills/data360-orchestrate/assets/definitions/activation-target.template.json +0 -5
  230. package/skills/data360-orchestrate/assets/definitions/activation.template.json +0 -7
  231. package/skills/data360-orchestrate/assets/definitions/calculated-insight.template.json +0 -7
  232. package/skills/data360-orchestrate/assets/definitions/data-action-target.template.json +0 -5
  233. package/skills/data360-orchestrate/assets/definitions/data-action.template.json +0 -5
  234. package/skills/data360-orchestrate/assets/definitions/data-graph.template.json +0 -21
  235. package/skills/data360-orchestrate/assets/definitions/data-stream.template.json +0 -55
  236. package/skills/data360-orchestrate/assets/definitions/dmo.template.json +0 -17
  237. package/skills/data360-orchestrate/assets/definitions/identity-resolution.template.json +0 -30
  238. package/skills/data360-orchestrate/assets/definitions/mapping.template.json +0 -14
  239. package/skills/data360-orchestrate/assets/definitions/relationship.template.json +0 -12
  240. package/skills/data360-orchestrate/assets/definitions/search-index.template.json +0 -9
  241. package/skills/data360-orchestrate/assets/definitions/segment.template.json +0 -16
  242. package/skills/data360-orchestrate/references/feature-readiness.md +0 -157
  243. package/skills/data360-orchestrate/references/plugin-setup.md +0 -138
  244. package/skills/data360-orchestrate/scripts/bootstrap-plugin.sh +0 -53
  245. package/skills/data360-orchestrate/scripts/diagnose-org.mjs +0 -511
  246. package/skills/data360-orchestrate/scripts/generate-manifest.mjs +0 -68
  247. package/skills/data360-orchestrate/scripts/verify-plugin.sh +0 -58
  248. package/skills/data360-prepare/README.md +0 -50
  249. package/skills/data360-prepare/SKILL.md +0 -203
  250. package/skills/data360-prepare/examples/ingestion-api/.env.example +0 -8
  251. package/skills/data360-prepare/examples/ingestion-api/README.md +0 -48
  252. package/skills/data360-prepare/examples/ingestion-api/send-data.py +0 -144
  253. package/skills/data360-query/README.md +0 -43
  254. package/skills/data360-query/SKILL.md +0 -128
  255. package/skills/data360-query/examples/search-indexes/hybrid-structured.json +0 -44
  256. package/skills/data360-query/examples/search-indexes/vector-knowledge.json +0 -43
  257. package/skills/data360-segment/README.md +0 -35
  258. package/skills/data360-segment/SKILL.md +0 -123
  259. package/skills/service-helpagent-coordinate/references/channel-help-portal.md +0 -22
  260. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-1-1-non-text-content.md +0 -0
  261. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-i-lists.md +0 -0
  262. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-ii-tables.md +0 -0
  263. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-iii-form-labels.md +0 -0
  264. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-iv-regions.md +0 -0
  265. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-1-v-groups.md +0 -0
  266. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-3-5-identify-input.md +0 -0
  267. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-1-4-3-contrast.md +0 -0
  268. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-1-1-keyboard.md +0 -0
  269. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-4-4-link-purpose.md +0 -0
  270. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-4-6-headings-labels.md +0 -0
  271. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-1-pointer-gestures.md +0 -0
  272. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-2-pointer-cancellation.md +0 -0
  273. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-3-label-in-name.md +0 -0
  274. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-2-5-7-dragging-movement.md +0 -0
  275. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-2-1-on-focus.md +0 -0
  276. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-2-2-on-input.md +0 -0
  277. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-3-1-error-identification.md +0 -0
  278. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-3-2-labels-instructions.md +0 -0
  279. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-3-3-3-error-suggestion.md +0 -0
  280. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-4-1-2-i-name.md +0 -0
  281. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-4-1-2-ii-role.md +0 -0
  282. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/reviewers/sc-4-1-2-iii-value.md +0 -0
  283. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-1-1-non-text-content.md +0 -0
  284. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-1-use-of-color.md +0 -0
  285. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-10-resize-reflow.md +0 -0
  286. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-11-non-text-contrast.md +0 -0
  287. /package/skills/{experience-lwc-accessibility-validate → experience-accessibility-validate}/references/vision/sc-1-4-3-contrast.md +0 -0
  288. /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()