@salesforce/afv-skills 1.46.0 → 1.48.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 +1 -1
- package/skills/agentforce-observe/SKILL.md +32 -4
- package/skills/agentforce-observe/references/ahm-alerts.md +719 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +275 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/README.md +32 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls +75 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls-meta.xml +5 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/interview-answers.json +13 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +10 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/create.json +20 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/update.json +16 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +273 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +236 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/reference-example-set-comment-value.md +132 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +211 -0
- package/skills/dx-devops-project-manage/SKILL.md +197 -0
- package/skills/dx-devops-project-manage/examples/common-workflows.md +197 -0
- package/skills/dx-devops-project-manage/references/cli-commands.md +295 -0
- package/skills/dx-devops-project-manage/scripts/create-project.sh +48 -0
- package/skills/dx-devops-project-manage/scripts/list-projects.sh +51 -0
- package/skills/dx-devops-project-manage/scripts/update-project.sh +96 -0
- package/skills/education-cloud-academic-calendar-generate/SKILL.md +225 -0
- package/skills/education-cloud-academic-calendar-generate/examples/quarter-calendar.json +47 -0
- package/skills/education-cloud-academic-calendar-generate/examples/sample-output.md +57 -0
- package/skills/education-cloud-academic-calendar-generate/examples/semester-calendar.json +54 -0
- package/skills/education-cloud-academic-calendar-generate/references/calendar-systems.md +127 -0
- package/skills/education-cloud-academic-calendar-generate/references/date-validation.md +222 -0
- package/skills/education-cloud-academic-calendar-generate/references/foundation_prerequisites.md +40 -0
- package/skills/education-cloud-academic-calendar-generate/scripts/validate_calendar_dates.py +143 -0
- package/skills/education-cloud-course-catalog-migrate/SKILL.md +321 -0
- package/skills/education-cloud-course-catalog-migrate/references/gotchas-detail.md +16 -0
- package/skills/education-cloud-course-catalog-migrate/references/gotchas.md +16 -0
- package/skills/education-cloud-course-catalog-migrate/references/large-catalog-handling.md +42 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/batch_courses.py +36 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/detect_linked_courses.py +51 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/detect_modality_variants.py +48 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/resolve_api_version.py +43 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/split_course_code.py +39 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/validate_completeness.py +54 -0
- package/skills/education-cloud-domain-configure/SKILL.md +234 -0
- package/skills/education-cloud-domain-configure/references/dependent-features.md +198 -0
- package/skills/education-cloud-domain-configure/references/domain-features/academic-operations.md +386 -0
- package/skills/education-cloud-domain-configure/references/domain-features/alumni-relations.md +155 -0
- package/skills/education-cloud-domain-configure/references/domain-features/cross-domain-dependencies.md +18 -0
- package/skills/education-cloud-domain-configure/references/domain-features/industriessettings-toggle-reference.md +43 -0
- package/skills/education-cloud-domain-configure/references/domain-features/mentoring.md +198 -0
- package/skills/education-cloud-domain-configure/references/domain-features/recruitment-admissions.md +315 -0
- package/skills/education-cloud-domain-configure/references/domain-features/student-management.md +155 -0
- package/skills/education-cloud-domain-configure/references/domain-features/student-success.md +249 -0
- package/skills/education-cloud-domain-configure/references/domain-features.md +27 -0
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +23 -0
- package/skills/education-cloud-domain-configure/references/toggle-write-path.md +67 -0
- package/skills/education-cloud-domain-configure/scripts/check_license_provisioned.py +62 -0
- package/skills/education-cloud-domain-configure/scripts/check_org_edition.py +44 -0
- package/skills/education-cloud-domain-configure/scripts/check_permission_assigned.py +39 -0
- package/skills/education-cloud-multi-campus-configure/SKILL.md +7 -0
- package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +1 -2
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +177 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/agent-and-subagents.md +151 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/customer-narration.md +34 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/execution-model.md +54 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/flows.md +82 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/grounding.md +199 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/permissions.md +183 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/platform-enablement.md +82 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +158 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/routing.md +141 -0
- package/skills/experience-cms-brand-apply/SKILL.md +5 -5
- package/skills/experience-cms-brand-create/SKILL.md +2 -2
- package/skills/experience-cms-content-generate/SKILL.md +1 -0
- package/skills/experience-cms-content-render/SKILL.md +173 -0
- package/skills/experience-cms-content-render/assets/angular/DetailPage.component.ts +25 -0
- package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +133 -0
- package/skills/experience-cms-content-render/assets/angular/TypeList.component.ts +38 -0
- package/skills/experience-cms-content-render/assets/angular/TypeRenderer.component.ts +90 -0
- package/skills/experience-cms-content-render/assets/angular/cms-content.component.ts +248 -0
- package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +100 -0
- package/skills/experience-cms-content-render/assets/react/DetailPage.tsx +20 -0
- package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +129 -0
- package/skills/experience-cms-content-render/assets/react/TypeList.tsx +40 -0
- package/skills/experience-cms-content-render/assets/react/TypeRenderer.tsx +64 -0
- package/skills/experience-cms-content-render/assets/react/heuristicRenderer.tsx +310 -0
- package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +129 -0
- package/skills/experience-cms-content-render/assets/shared/cmsContentType.ts +49 -0
- package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +96 -0
- package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +55 -0
- package/skills/experience-cms-content-render/references/bulk-loading.md +60 -0
- package/skills/experience-cms-content-render/references/codegen-guardrails.md +111 -0
- package/skills/experience-cms-content-render/references/detail-pages.md +87 -0
- package/skills/experience-cms-content-render/references/embed-recipes.md +127 -0
- package/skills/experience-cms-content-render/references/failure-modes.md +96 -0
- package/skills/experience-cms-content-render/references/heuristic-render-rules.md +131 -0
- package/skills/experience-cms-content-render/references/init-scaffold.md +122 -0
- package/skills/experience-cms-content-render/references/interaction-model.md +173 -0
- package/skills/experience-cms-content-render/references/package-api.md +106 -0
- package/skills/experience-cms-content-render/references/schema-sync.md +114 -0
- package/skills/experience-cms-content-render/references/styling-scopes.md +65 -0
- package/skills/experience-cms-content-render/references/verify.md +49 -0
- package/skills/experience-cms-content-type-generate/SKILL.md +2 -2
- package/skills/experience-content-media-stock-image-search/SKILL.md +5 -4
- package/skills/experience-search-coordinate/SKILL.md +198 -0
- package/skills/experience-search-coordinate/assets/search-payload-template.json +25 -0
- package/skills/experience-search-coordinate/references/content-route.md +313 -0
- package/skills/experience-search-coordinate/references/content-type-discovery.md +57 -0
- package/skills/experience-search-coordinate/references/media-route.md +172 -0
- package/skills/experience-search-coordinate/references/scope-resolution.md +14 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +1 -1
- package/skills/experience-ui-bundle-localize/references/i18n-setup.md +5 -3
- package/skills/experience-ui-bundle-project-generate/SKILL.md +18 -14
- package/skills/experience-ui-bundle-project-generate/references/angular-project-generate.md +22 -0
- package/skills/experience-ui-bundle-project-generate/references/react-project-generate.md +20 -0
- package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +58 -54
- package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +6 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +2 -2
- package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +6 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +2 -1
- package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +6 -0
- package/skills/experience-ui-bundle-site-generate/SKILL.md +59 -8
- package/skills/experience-ui-bundle-site-generate/references/configure-metadata-digital-experience.md +8 -3
- package/skills/experience-ui-bundle-site-generate/references/configure-metadata-language-settings.md +120 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +336 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +143 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +127 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-deploy-commands.md +116 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +111 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-overview.md +312 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +171 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-state-tracking.md +64 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-trigger-handlers.md +122 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-overview.md +335 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-user-provisioning-details.md +140 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +196 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-metadata-cache-generation.md +155 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +307 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +211 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/state-machine-and-changes.md +108 -0
- package/skills/life-sciences-kam-coordinate/SKILL.md +241 -0
- package/skills/life-sciences-kam-coordinate/references/orchestration-flow.md +152 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +79 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-deploy-commands.md +131 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-kam-config-records.md +85 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +112 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-overview.md +202 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +67 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-state-tracking.md +65 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-trigger-handlers.md +123 -0
- package/skills/life-sciences-kam-coordinate/references/stage-4-participant-role-and-sprint.md +89 -0
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +337 -0
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +248 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-ipad-validation-script.md +35 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-metadata-cache-generation.md +155 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-details.md +146 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-overview.md +89 -0
- package/skills/life-sciences-kam-coordinate/references/state-machine-and-changes.md +114 -0
- package/skills/life-sciences-prerequisites-validate/SKILL.md +138 -0
- package/skills/life-sciences-prerequisites-validate/references/checks-org-settings.md +190 -0
- package/skills/life-sciences-prerequisites-validate/references/checks-user-and-package.md +211 -0
- package/skills/life-sciences-territory-configure/SKILL.md +217 -0
- package/skills/life-sciences-territory-configure/references/territory-metadata.md +262 -0
- package/skills/platform-dsar-policy-manage/SKILL.md +272 -0
- package/skills/platform-dsar-policy-manage/references/configure.md +106 -0
- package/skills/platform-dsar-policy-manage/references/export-and-history.md +123 -0
- package/skills/platform-dsar-policy-manage/references/gap-analysis-guide.md +150 -0
- package/skills/platform-dsar-policy-manage/references/gap-scan.md +129 -0
- package/skills/platform-dsar-policy-manage/references/headless-sor.md +59 -0
- package/skills/platform-dsar-policy-manage/references/report-format.md +59 -0
- package/skills/platform-dsar-policy-manage/scripts/tests/__init__.py +0 -0
- package/skills/platform-dsar-policy-manage/scripts/tests/test_validate_policy_tree.py +76 -0
- package/skills/platform-dsar-policy-manage/scripts/validate-policy-tree.py +130 -0
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +359 -0
- package/skills/platform-salesforce-connect-adapter-generate/references/official-examples.md +69 -0
- package/skills/platform-salesforce-connect-adapter-generate/references/scenarios.md +187 -0
- package/skills/service-agentforce-channel-configure/SKILL.md +33 -28
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +205 -0
- package/skills/service-agentforce-human-escalation-configure/assets/escalation-thresholds.instructions.md +37 -0
- package/skills/service-agentforce-human-escalation-configure/assets/force-app/main/default/flows/Human_Escalation_Outbound_Flow.flow-meta.xml +173 -0
- package/skills/service-agentforce-human-escalation-configure/assets/package.xml +8 -0
- package/skills/service-agentforce-human-escalation-configure/assets/sfdx-project.json +5 -0
- package/skills/service-agentforce-human-escalation-configure/references/classifier-contracts.md +103 -0
- package/skills/service-agentforce-human-escalation-configure/references/escalation-mechanism.md +63 -0
- package/skills/service-agentforce-human-escalation-configure/references/failure-policy.md +44 -0
- package/skills/service-agentforce-human-escalation-configure/references/nga-escalation.md +57 -0
- package/skills/service-agentforce-human-escalation-configure/references/runtime-verification.md +72 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/classify-agent-active.mjs +49 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/classify-nga-escalation.mjs +166 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/extract-outbound-route.mjs +67 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs +131 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/__init__.py +0 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +249 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +899 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/verify-and-configure.sh +705 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/verify-escalation-config.mjs +89 -0
- package/skills/service-de-channel-routing-configure/SKILL.md +13 -5
- package/skills/service-de-channel-routing-configure/scripts/tests/test_skill_contract.py +25 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +23 -10
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -48
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +6 -2
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +20 -27
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +47 -27
- package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +13 -9
- package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +104 -45
- package/skills/service-omni-agent-users-create/SKILL.md +96 -0
- package/skills/service-omni-agent-users-create/assets/create-users.apex.template +80 -0
- package/skills/service-omni-agent-users-create/references/apex-patterns.md +135 -0
- package/skills/service-omni-agent-users-create/references/apex-template-notes.md +40 -0
- package/skills/service-omni-agent-users-create/scripts/detect-and-create.sh +206 -0
- package/skills/service-omni-agent-users-create/scripts/detect-existing.sh +122 -0
- package/skills/service-omni-agent-users-create/scripts/run-create.sh +298 -0
- package/skills/service-omni-agent-users-create/scripts/tests/test_user_create_security.py +171 -0
- package/skills/service-omni-agent-work-sharing-configure/SKILL.md +78 -0
- package/skills/service-omni-agent-work-sharing-configure/references/api-notes.md +31 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/configure-and-report.sh +186 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/tests/_bootstrap.py +120 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/tests/test_agent_work_sharing_contracts.py +93 -0
- package/skills/service-omni-base-settings-configure/SKILL.md +77 -0
- package/skills/service-omni-base-settings-configure/assets/force-app/main/default/settings/OmniChannel.settings-meta.xml +8 -0
- package/skills/service-omni-base-settings-configure/assets/package.xml +8 -0
- package/skills/service-omni-base-settings-configure/assets/sfdx-project.json +12 -0
- package/skills/service-omni-base-settings-configure/references/api-notes.md +121 -0
- package/skills/service-omni-base-settings-configure/scripts/configure-and-report.sh +164 -0
- package/skills/service-omni-base-settings-configure/scripts/detect-existing.sh +69 -0
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +168 -0
- package/skills/service-omni-channel-setup-coordinate/examples/output-templates.md +161 -0
- package/skills/service-omni-channel-setup-coordinate/references/gap-catalog.md +99 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +1542 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/__init__.py +0 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/_bootstrap.py +223 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +804 -0
- package/skills/service-omni-command-center-analyze/SKILL.md +84 -0
- package/skills/service-omni-command-center-analyze/references/api-notes.md +54 -0
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +198 -0
- package/skills/service-omni-command-center-analyze/scripts/tests/_bootstrap.py +115 -0
- package/skills/service-omni-command-center-analyze/scripts/tests/test_command_center_analyze_contracts.py +165 -0
- package/skills/service-omni-permission-set-assign/SKILL.md +88 -0
- package/skills/service-omni-permission-set-assign/assets/package.xml +8 -0
- package/skills/service-omni-permission-set-assign/references/api-notes.md +143 -0
- package/skills/service-omni-permission-set-assign/scripts/verify-and-assign.sh +397 -0
- package/skills/service-omni-presence-status-deploy/SKILL.md +87 -0
- package/skills/service-omni-presence-status-deploy/assets/package.xml +12 -0
- package/skills/service-omni-presence-status-deploy/references/api-notes.md +115 -0
- package/skills/service-omni-presence-status-deploy/scripts/deploy-and-report.sh +422 -0
- package/skills/service-omni-presence-user-config-deploy/SKILL.md +80 -0
- package/skills/service-omni-presence-user-config-deploy/references/api-notes.md +60 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh +261 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/tests/_bootstrap.py +106 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/tests/test_presence_user_config_contracts.py +142 -0
- package/skills/service-omni-queue-deploy/SKILL.md +91 -0
- package/skills/service-omni-queue-deploy/references/api-notes.md +140 -0
- package/skills/service-omni-queue-deploy/scripts/verify-and-align.sh +470 -0
- package/skills/service-omni-queue-members-assign/SKILL.md +93 -0
- package/skills/service-omni-queue-members-assign/references/api-notes.md +144 -0
- package/skills/service-omni-queue-members-assign/scripts/tests/_bootstrap.py +67 -0
- package/skills/service-omni-queue-members-assign/scripts/tests/test_queue_members_contracts.py +73 -0
- package/skills/service-omni-queue-members-assign/scripts/verify-and-bind.sh +362 -0
- package/skills/service-omni-queue-routing-config-deploy/SKILL.md +101 -0
- package/skills/service-omni-queue-routing-config-deploy/references/api-notes.md +127 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/tests/_bootstrap.py +90 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/tests/test_queue_routing_config_contracts.py +70 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh +538 -0
- package/skills/service-omni-routing-flow-deploy/SKILL.md +111 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml +94 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.flow-meta.xml +88 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Cases.flow-meta.xml +144 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml +94 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.flow-meta.xml +88 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCalls.flow-meta.xml +144 -0
- package/skills/service-omni-routing-flow-deploy/assets/package.xml +11 -0
- package/skills/service-omni-routing-flow-deploy/assets/sfdx-project.json +11 -0
- package/skills/service-omni-routing-flow-deploy/references/api-notes.md +265 -0
- package/skills/service-omni-routing-flow-deploy/scripts/deploy-and-report.sh +684 -0
- package/skills/service-omni-service-channel-configure/SKILL.md +83 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Cases.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Incidents.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/MessagingSessions.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/VoiceCalls.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/package.xml +11 -0
- package/skills/service-omni-service-channel-configure/assets/sfdx-project.json +12 -0
- package/skills/service-omni-service-channel-configure/references/api-notes.md +84 -0
- package/skills/service-omni-service-channel-configure/scripts/deploy-and-report.sh +383 -0
- package/skills/service-omni-sidebar-configure/SKILL.md +77 -0
- package/skills/service-omni-sidebar-configure/references/api-notes.md +51 -0
- package/skills/service-omni-sidebar-configure/scripts/enable-and-report.sh +315 -0
- package/skills/service-omni-sidebar-configure/scripts/tests/_bootstrap.py +142 -0
- package/skills/service-omni-sidebar-configure/scripts/tests/test_sidebar_contracts.py +139 -0
- package/skills/service-omni-skills-based-routing-configure/SKILL.md +81 -0
- package/skills/service-omni-skills-based-routing-configure/references/api-notes.md +50 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/configure-and-report.sh +270 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/tests/_bootstrap.py +120 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/tests/test_sbr_contracts.py +129 -0
- package/skills/service-omni-supervisor-config-deploy/SKILL.md +97 -0
- package/skills/service-omni-supervisor-config-deploy/assets/force-app/main/default/omniSupervisorConfigs/Omni_Supervisor.omniSupervisorConfig-meta.xml +9 -0
- package/skills/service-omni-supervisor-config-deploy/assets/package.xml +8 -0
- package/skills/service-omni-supervisor-config-deploy/assets/sfdx-project.json +12 -0
- package/skills/service-omni-supervisor-config-deploy/references/api-notes.md +205 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh +586 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/tests/_bootstrap.py +153 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/tests/test_supervisor_config_contracts.py +116 -0
- package/skills/service-omni-supervisor-permset-assign/SKILL.md +88 -0
- package/skills/service-omni-supervisor-permset-assign/references/api-notes.md +48 -0
- package/skills/service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh +316 -0
- package/skills/service-omni-supervisor-surface-deploy/SKILL.md +85 -0
- package/skills/service-omni-supervisor-surface-deploy/references/api-notes.md +47 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh +216 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/tests/_bootstrap.py +95 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/tests/test_supervisor_surface_contracts.py +154 -0
- package/skills/service-omni-supervisor-users-create/SKILL.md +92 -0
- package/skills/service-omni-supervisor-users-create/assets/create-supervisors.apex.template +72 -0
- package/skills/service-omni-supervisor-users-create/references/apex-patterns.md +73 -0
- package/skills/service-omni-supervisor-users-create/references/apex-template-notes.md +41 -0
- package/skills/service-omni-supervisor-users-create/scripts/detect-and-create.sh +197 -0
- package/skills/service-omni-supervisor-users-create/scripts/detect-existing.sh +112 -0
- package/skills/service-omni-supervisor-users-create/scripts/run-create.sh +296 -0
- package/skills/service-omni-work-skill-routing-configure/SKILL.md +80 -0
- package/skills/service-omni-work-skill-routing-configure/references/api-notes.md +67 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/configure-and-report.sh +262 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/tests/_bootstrap.py +121 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/tests/test_work_skill_routing_contracts.py +161 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: service-omni-presence-user-config-deploy
|
|
3
|
+
description: "Use to deploy an Omni PresenceUserConfig (presence configuration) as one whole-record Metadata write with the decline/ACW fields set consistently: Auto-Accept off, Decline on, Decline Reason on (with a PresenceDeclineReason deployed in the same package), and an After-Conversation-Work wrap-up timer. Agents are assigned by username. Idempotent via the Metadata API files[].state signal. Triggers: create presence configuration, enable decline + decline reason + ACW wrap-up timer for agents, assign presence config to users. Do not use for ServicePresenceStatus (separate skill), for permission-set access to statuses, or to set the channel-level ACW timer on a ServiceChannel."
|
|
4
|
+
allowed-tools: Bash Read Write Edit Glob Grep
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0"
|
|
7
|
+
domains: ["Service"]
|
|
8
|
+
minApiVersion: "66.0"
|
|
9
|
+
relatedSkills:
|
|
10
|
+
- "service-omni-agent-users-create"
|
|
11
|
+
- "service-omni-base-settings-configure"
|
|
12
|
+
- "service-omni-channel-setup-coordinate"
|
|
13
|
+
- "service-omni-presence-status-deploy"
|
|
14
|
+
accessCheck:
|
|
15
|
+
- type: license
|
|
16
|
+
value: ServiceCloud
|
|
17
|
+
cliTools:
|
|
18
|
+
- tool: ["jq"]
|
|
19
|
+
semver: ">=1.6"
|
|
20
|
+
- tool: ["python3"]
|
|
21
|
+
semver: ">=3.8"
|
|
22
|
+
- tool: ["sf"]
|
|
23
|
+
semver: ">=2.139.6"
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# service-omni-presence-user-config-deploy
|
|
27
|
+
|
|
28
|
+
Deploy an Omni **PresenceUserConfig** (the "presence configuration" that governs how work is offered to agents) together with the **PresenceDeclineReason** it references, in one atomic Metadata API package. The decline / auto-accept / decline-reason / ACW fields have cross-field validators — enabling Decline requires Auto-Accept off, Decline Reason requires Decline on, and the ACW timer must be paired with its max time — so they can only be set correctly as a **single whole-record write**, not field-by-field. This skill encodes that consistent record and assigns it to the given agents. It runs after `service-omni-agent-users-create` / `service-omni-presence-status-deploy` and is invoked by `service-omni-channel-setup-coordinate` as a rep-experience step.
|
|
29
|
+
|
|
30
|
+
## Inputs
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bash scripts/deploy-and-report.sh <org-alias> [config_developer_name] [agent_usernames_csv]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `org-alias` (required).
|
|
37
|
+
- `config_developer_name` (optional, default `Omni_Demo_Presence_Config`).
|
|
38
|
+
- `agent_usernames_csv` (optional). Comma-separated Usernames (`…@…`) and/or 15/18-char User Ids (`005…`) to assign; Ids are resolved to usernames (metadata assigns by username). May also be set via `AGENT_USERNAMES_CSV`. Empty → the config deploys with no user assignments.
|
|
39
|
+
|
|
40
|
+
Env overrides: `DECLINE_REASON_LABEL` (default `Training`), `DECLINE_REASON_DEVELOPER_NAME` (default derived from the label), `CAPACITY` (default `5`, 1–100), `ACW_SECONDS` (default `60`, 10–3600), `PRESENCE_STATUS_ON_DECLINE` (optional ServicePresenceStatus DeveloperName).
|
|
41
|
+
|
|
42
|
+
## Preconditions and safety
|
|
43
|
+
|
|
44
|
+
- Target org authenticated via `sf` CLI, Service Cloud license, `sf` CLI ≥ 2.139.6.
|
|
45
|
+
- Omni-Channel base settings enabled (`service-omni-base-settings-configure`).
|
|
46
|
+
- Any `PRESENCE_STATUS_ON_DECLINE` must already exist (`service-omni-presence-status-deploy`).
|
|
47
|
+
- The three-way `safe_to_write` production guard applies.
|
|
48
|
+
|
|
49
|
+
## Run
|
|
50
|
+
|
|
51
|
+
`deploy-and-report.sh` materializes two components into a temp DX project and deploys them in one call:
|
|
52
|
+
|
|
53
|
+
1. `PresenceDeclineReason:<reason>` — the decline reason (label only).
|
|
54
|
+
2. `PresenceUserConfig:<config>` — capacity + label, `enableAutoAccept=false`, `enableDecline=true`, `enableDeclineReason=true`, `declineReasons=<reason>`, `hasAfterConvoWorkTimer=true`, `afterConvoWorkMaxTime=<ACW_SECONDS>`, optional `presenceStatusOnDecline`, and `assignments/users` for the resolved agents.
|
|
55
|
+
|
|
56
|
+
Elements are emitted in strict XSD order. Idempotency comes from the Metadata API `files[].state` per component (`Unchanged`→reused, `Changed`→updated, `Created`→created); the deploy runs `--async` and polls to a terminal state.
|
|
57
|
+
|
|
58
|
+
## Behavior
|
|
59
|
+
|
|
60
|
+
**Whole-record + consistent.** The record is always emitted with the validator-safe combination, so a re-deploy is a clean no-op rather than a field diff that could trip a cross-field rule.
|
|
61
|
+
|
|
62
|
+
**Non-destructive.** Only the named config and decline reason are written; other presence configs and decline reasons on the org are never touched. Assignments are declared for the resolved agents; the skill does not remove users it did not add (a redeploy declares the full assignment set for this config).
|
|
63
|
+
|
|
64
|
+
## Output contract
|
|
65
|
+
|
|
66
|
+
A single JSON object: `status` ∈ `created` | `updated` | `reused` | `blocked`, `config` (`{developer_name, label, capacity, acw_seconds, state}`), `decline_reason` (`{developer_name, label, state}`), `assigned_usernames`, `deploy_id`, `manual_actions`, `blocking_issue`.
|
|
67
|
+
|
|
68
|
+
## Limitations
|
|
69
|
+
|
|
70
|
+
- One presence configuration per invocation.
|
|
71
|
+
- Encodes the decline+ACW rep profile from the steel thread; other field combinations require forking the XML template.
|
|
72
|
+
- Does **not** set the channel-level After-Conversation-Work timer on a `ServiceChannel` (a separate concern), deploy `ServicePresenceStatus`, or grant status access via permission set.
|
|
73
|
+
|
|
74
|
+
## References
|
|
75
|
+
|
|
76
|
+
| File | When to read |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `references/api-notes.md` | PresenceUserConfig cross-field validators, XSD element order, decline-reason packaging, and ACW field pairing |
|
|
79
|
+
| `scripts/tests/_bootstrap.py` | Test bootstrap loaded by the contract suite to locate the skill root and run its shell entry point |
|
|
80
|
+
| `scripts/tests/test_presence_user_config_contracts.py` | Run after changing the deployment script to verify guard, whole-record, membership, and output contracts |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# API notes — service-omni-presence-user-config-deploy
|
|
2
|
+
|
|
3
|
+
## Why one whole-record Metadata write
|
|
4
|
+
|
|
5
|
+
`PresenceUserConfig` has **cross-field validators** that make field-by-field editing fragile — the
|
|
6
|
+
correct combination only validates when the whole record is written together:
|
|
7
|
+
|
|
8
|
+
- `enableAutoAccept` is allowed only when `enableDecline=false`, and vice versa. They are mutually
|
|
9
|
+
exclusive; this skill sets Auto-Accept **off** and Decline **on**.
|
|
10
|
+
- `enableDeclineReason=true` is permitted only when decline reasons are enabled (Decline on) and at
|
|
11
|
+
least one `declineReasons` entry is present.
|
|
12
|
+
- `presenceStatusOnDecline` is available only when `enableDecline=true`.
|
|
13
|
+
- The ACW timer is paired: `hasAfterConvoWorkTimer=true` **requires** `afterConvoWorkMaxTime`
|
|
14
|
+
(10–3600 s). Optional extension uses `hasAcwExtensionEnabled` + `acwExtensionDuration` +
|
|
15
|
+
`maxExtensions` (not set by this skill).
|
|
16
|
+
|
|
17
|
+
Deploying the record as a single Metadata component applies all of these atomically.
|
|
18
|
+
|
|
19
|
+
## Required fields + XSD element order
|
|
20
|
+
|
|
21
|
+
`capacity` and `label` are required. Metadata API enforces element order; the emitted document
|
|
22
|
+
follows the schema sequence:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
acwExtensionDuration, afterConvoWorkMaxTime, assignments, capacity, declineReasons,
|
|
26
|
+
enableAutoAccept, enableDecline, enableDeclineReason, enableDisconnectSound, enableRequestSound,
|
|
27
|
+
hasAcwExtensionEnabled, hasAfterConvoWorkTimer, interruptibleCapacity, label, maxExtensions,
|
|
28
|
+
presenceStatusOnDecline, presenceStatusOnPushTimeout, userDisplayName
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This skill emits: `afterConvoWorkMaxTime → assignments → capacity → declineReasons →
|
|
32
|
+
enableAutoAccept → enableDecline → enableDeclineReason → hasAfterConvoWorkTimer → label →
|
|
33
|
+
[presenceStatusOnDecline]` — all ascending in the sequence above.
|
|
34
|
+
|
|
35
|
+
## Decline reason packaging
|
|
36
|
+
|
|
37
|
+
`declineReasons` references a `PresenceDeclineReason` by its DeveloperName (fullName). The two
|
|
38
|
+
components are deployed in the **same package** so the reference resolves in one deploy; a
|
|
39
|
+
standalone config referencing a not-yet-deployed reason would fail. `PresenceDeclineReason` itself
|
|
40
|
+
carries only a `label`.
|
|
41
|
+
|
|
42
|
+
## Assignments
|
|
43
|
+
|
|
44
|
+
`assignments/users/user` takes **usernames**, not User Ids — the skill resolves any `005…` Ids to
|
|
45
|
+
usernames first. A redeploy declares the full assignment set for this config; the skill does not
|
|
46
|
+
remove users it did not add. The `<assignments>` block is omitted entirely when no agents are given
|
|
47
|
+
(a valid, unassigned config).
|
|
48
|
+
|
|
49
|
+
## Idempotency signal
|
|
50
|
+
|
|
51
|
+
Per-component `files[].state` from the Metadata API is authoritative: `Unchanged` → reused,
|
|
52
|
+
`Changed` → updated, `Created` → created. The deploy runs `--async` and polls
|
|
53
|
+
`sf project deploy report` to a terminal state; a job that never reaches one is reported as blocked
|
|
54
|
+
rather than assumed successful.
|
|
55
|
+
|
|
56
|
+
## Out of scope: channel-level ACW
|
|
57
|
+
|
|
58
|
+
The steel thread also mentions an After-Conversation-Work timer on the **Voice `ServiceChannel`**.
|
|
59
|
+
That is a different field on a different object (the channel, not the presence config) and is not set
|
|
60
|
+
here — this skill governs the agent-level presence/wrap-up behavior only.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# deploy-and-report.sh - deploy a PresenceUserConfig (+ referenced PresenceDeclineReason) as one atomic Metadata package with the validator-safe decline/ACW rep profile; idempotent. Args, env, exit codes: SKILL.md.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ $# -lt 1 ]; then
|
|
7
|
+
echo '{"error":"Usage: bash deploy-and-report.sh <org-alias> [config_developer_name] [agent_usernames_csv]"}' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
ORG="$1"
|
|
12
|
+
CONFIG_DN="${2:-Omni_Demo_Presence_Config}"
|
|
13
|
+
AGENTS_CSV="${3:-${AGENT_USERNAMES_CSV:-}}"
|
|
14
|
+
|
|
15
|
+
DECLINE_REASON_LABEL="${DECLINE_REASON_LABEL:-Training}"
|
|
16
|
+
DECLINE_REASON_DN="${DECLINE_REASON_DEVELOPER_NAME:-$(printf '%s' "$DECLINE_REASON_LABEL" | tr ' ' '_' | tr -cd 'A-Za-z0-9_')}"
|
|
17
|
+
CAPACITY="${CAPACITY:-5}"
|
|
18
|
+
ACW_SECONDS="${ACW_SECONDS:-60}"
|
|
19
|
+
PRESENCE_STATUS_ON_DECLINE="${PRESENCE_STATUS_ON_DECLINE:-}"
|
|
20
|
+
CONFIG_LABEL=$(printf '%s' "$CONFIG_DN" | tr '_' ' ')
|
|
21
|
+
|
|
22
|
+
# ---- validation ----
|
|
23
|
+
if ! [[ "$CONFIG_DN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
|
|
24
|
+
echo "{\"error\":\"Invalid config_developer_name '$CONFIG_DN'. Must start with a letter and contain only letters, digits, underscores (max 80).\"}" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
if ! [[ "$DECLINE_REASON_DN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
|
|
28
|
+
echo "{\"error\":\"Invalid DECLINE_REASON_DEVELOPER_NAME '$DECLINE_REASON_DN'.\"}" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
if ! [[ "$CAPACITY" =~ ^[0-9]+$ ]] || [ "$CAPACITY" -lt 1 ] || [ "$CAPACITY" -gt 100 ]; then
|
|
32
|
+
echo "{\"error\":\"Invalid CAPACITY '$CAPACITY' - must be an integer 1..100.\"}" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
if ! [[ "$ACW_SECONDS" =~ ^[0-9]+$ ]] || [ "$ACW_SECONDS" -lt 10 ] || [ "$ACW_SECONDS" -gt 3600 ]; then
|
|
36
|
+
echo "{\"error\":\"Invalid ACW_SECONDS '$ACW_SECONDS' - must be an integer 10..3600 (Metadata API bound).\"}" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
if [ -n "$PRESENCE_STATUS_ON_DECLINE" ] && ! [[ "$PRESENCE_STATUS_ON_DECLINE" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
|
|
40
|
+
echo "{\"error\":\"Invalid PRESENCE_STATUS_ON_DECLINE '$PRESENCE_STATUS_ON_DECLINE'.\"}" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
|
|
45
|
+
echo "{\"error\":\"Org alias '$ORG' is not authenticated. Run: sf org login web -a $ORG -r <my-domain-url>\"}" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Escape the five XML predefined entities so a label/username with & < > " ' cannot break the
|
|
50
|
+
# hand-built metadata XML (or inject elements) when interpolated into the heredocs below.
|
|
51
|
+
xml_escape() {
|
|
52
|
+
local s="$1"
|
|
53
|
+
s="${s//&/&}"; s="${s//</<}"; s="${s//>/>}"; s="${s//\"/"}"; s="${s//\'/'}"
|
|
54
|
+
printf '%s' "$s"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
emit_blocked() {
|
|
58
|
+
local msg="$1"; local target_skill="${2:-}"
|
|
59
|
+
local manual_actions='[]'
|
|
60
|
+
if [ -n "$target_skill" ]; then
|
|
61
|
+
manual_actions=$(jq -n --arg ts "$target_skill" '[{id:"MISSING_PREREQ",title:"Run prerequisite skill",target_skill:$ts}]')
|
|
62
|
+
fi
|
|
63
|
+
jq -n --arg msg "$msg" --arg cdn "$CONFIG_DN" --arg rdn "$DECLINE_REASON_DN" --argjson ma "$manual_actions" \
|
|
64
|
+
'{skill:"service-omni-presence-user-config-deploy",status:"blocked",
|
|
65
|
+
config:{developer_name:$cdn,state:"not_deployed"},
|
|
66
|
+
decline_reason:{developer_name:$rdn,state:"not_deployed"},
|
|
67
|
+
assigned_usernames:[],deploy_id:null,manual_actions:$ma,blocking_issue:$msg}'
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
emit_plan() {
|
|
72
|
+
local detail="$1"
|
|
73
|
+
jq -n --arg detail "$detail" --arg cdn "$CONFIG_DN" \
|
|
74
|
+
'{skill:"service-omni-presence-user-config-deploy",status:"action_needed",plan_mode:true,
|
|
75
|
+
plan_detail:$detail,config:{developer_name:$cdn},manual_actions:[],blocking_issue:null}'
|
|
76
|
+
exit 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Step 1 - Organization + safe_to_write guard
|
|
80
|
+
ORG_JSON=$(sf data query --target-org "$ORG" \
|
|
81
|
+
--query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" --json 2>/dev/null) \
|
|
82
|
+
|| emit_blocked "Failed to query Organization for the safe_to_write guard."
|
|
83
|
+
IS_SANDBOX=$(echo "$ORG_JSON" | jq -r '.result.records[0].IsSandbox')
|
|
84
|
+
TRIAL_EXP=$(echo "$ORG_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
|
|
85
|
+
ORG_TYPE=$(echo "$ORG_JSON" | jq -r '.result.records[0].OrganizationType')
|
|
86
|
+
SAFE_TO_WRITE=false
|
|
87
|
+
if [ "$IS_SANDBOX" = "true" ] || [ "$TRIAL_EXP" != "null" ] \
|
|
88
|
+
|| [ "$ORG_TYPE" = "Developer Edition" ] || [ "$ORG_TYPE" = "Base Edition" ]; then
|
|
89
|
+
SAFE_TO_WRITE=true
|
|
90
|
+
fi
|
|
91
|
+
[ "$SAFE_TO_WRITE" = "true" ] || emit_blocked "Refusing to deploy a presence configuration to a real production customer org (IsSandbox=$IS_SANDBOX, TrialExpirationDate=$TRIAL_EXP, OrganizationType=$ORG_TYPE). Target a sandbox, trial CDO, or Developer Edition org."
|
|
92
|
+
|
|
93
|
+
# Step 2 - Resolve agent usernames (accept usernames and/or 005... Ids)
|
|
94
|
+
USERNAMES=()
|
|
95
|
+
if [ -n "$AGENTS_CSV" ]; then
|
|
96
|
+
ID_IN=""; RAW_USERNAMES=()
|
|
97
|
+
while IFS= read -r tok; do
|
|
98
|
+
[ -z "$tok" ] && continue
|
|
99
|
+
if [[ "$tok" =~ ^005[A-Za-z0-9]{12}([A-Za-z0-9]{3})?$ ]]; then
|
|
100
|
+
ID_IN="${ID_IN:+$ID_IN,}'$tok'"
|
|
101
|
+
elif [[ "$tok" =~ ^[^,\'\"[:space:]]+@[^,\'\"[:space:]]+$ ]]; then
|
|
102
|
+
RAW_USERNAMES+=("$tok")
|
|
103
|
+
else
|
|
104
|
+
emit_blocked "agent_usernames_csv token is neither a Username (...@...) nor a 15/18-char User Id (005...): '$tok'"
|
|
105
|
+
fi
|
|
106
|
+
done <<< "$(printf '%s' "$AGENTS_CSV" | tr ',' '\n' | sed 's/^ *//;s/ *$//')"
|
|
107
|
+
|
|
108
|
+
for u in "${RAW_USERNAMES[@]}"; do USERNAMES+=("$u"); done
|
|
109
|
+
if [ -n "$ID_IN" ]; then
|
|
110
|
+
# Fail-closed: a swallowed query failure would silently drop the Id-resolved users and deploy a
|
|
111
|
+
# config bound to fewer agents than requested. Block on both a non-zero query and an unparseable body.
|
|
112
|
+
IDU_JSON=$(sf data query --target-org "$ORG" --json \
|
|
113
|
+
--query "SELECT Username FROM User WHERE Id IN ($ID_IN) AND IsActive=true" 2>/dev/null) \
|
|
114
|
+
|| emit_blocked "Could not resolve agent User Ids to usernames (query failed: authentication, network, or API access). Refusing to deploy a presence config bound to an unverified user set."
|
|
115
|
+
echo "$IDU_JSON" | jq -e '.result.records' >/dev/null 2>&1 \
|
|
116
|
+
|| emit_blocked "Agent User Id resolution returned no parseable records (inconclusive). Refusing to deploy against an unverified user set."
|
|
117
|
+
while IFS= read -r u; do [ -n "$u" ] && USERNAMES+=("$u"); done \
|
|
118
|
+
< <(echo "$IDU_JSON" | jq -r '.result.records[].Username // empty')
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# PLAN_ONLY - detection is read-only; report intent and stop before any write.
|
|
123
|
+
if [ "${PLAN_ONLY:-}" = "1" ]; then
|
|
124
|
+
emit_plan "Would deploy PresenceDeclineReason '$DECLINE_REASON_DN' and PresenceUserConfig '$CONFIG_DN' (capacity=$CAPACITY, ACW=${ACW_SECONDS}s, decline+decline-reason on, auto-accept off) assigned to ${#USERNAMES[@]} agent(s)."
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Step 3 - Materialize the two components in XSD order
|
|
128
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
129
|
+
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
|
130
|
+
cp "$SCRIPT_DIR/../assets/sfdx-project.json" "$WORK/sfdx-project.json" 2>/dev/null \
|
|
131
|
+
|| echo '{"packageDirectories":[{"path":"force-app","default":true}],"sourceApiVersion":"66.0"}' > "$WORK/sfdx-project.json"
|
|
132
|
+
DR_DIR="$WORK/force-app/main/default/presenceDeclineReasons"
|
|
133
|
+
PUC_DIR="$WORK/force-app/main/default/presenceUserConfigs"
|
|
134
|
+
mkdir -p "$DR_DIR" "$PUC_DIR"
|
|
135
|
+
|
|
136
|
+
# PresenceDeclineReason (label only).
|
|
137
|
+
cat > "$DR_DIR/${DECLINE_REASON_DN}.presenceDeclineReason-meta.xml" <<XML
|
|
138
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
139
|
+
<PresenceDeclineReason xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
140
|
+
<label>$(xml_escape "$DECLINE_REASON_LABEL")</label>
|
|
141
|
+
</PresenceDeclineReason>
|
|
142
|
+
XML
|
|
143
|
+
|
|
144
|
+
# Build the <assignments> block only when we have usernames.
|
|
145
|
+
ASSIGN_BLOCK=""
|
|
146
|
+
if [ ${#USERNAMES[@]} -gt 0 ]; then
|
|
147
|
+
USER_LINES=""
|
|
148
|
+
for u in "${USERNAMES[@]}"; do USER_LINES+=" <user>$(xml_escape "$u")</user>
|
|
149
|
+
"; done
|
|
150
|
+
ASSIGN_BLOCK=" <assignments>
|
|
151
|
+
<users>
|
|
152
|
+
${USER_LINES} </users>
|
|
153
|
+
</assignments>
|
|
154
|
+
"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Optional presenceStatusOnDecline (emitted in XSD order, after label).
|
|
158
|
+
PSOD_LINE=""
|
|
159
|
+
[ -n "$PRESENCE_STATUS_ON_DECLINE" ] && PSOD_LINE=" <presenceStatusOnDecline>${PRESENCE_STATUS_ON_DECLINE}</presenceStatusOnDecline>
|
|
160
|
+
"
|
|
161
|
+
|
|
162
|
+
# PresenceUserConfig - elements in strict XSD order:
|
|
163
|
+
# afterConvoWorkMaxTime, assignments, capacity, declineReasons, enableAutoAccept, enableDecline,
|
|
164
|
+
# enableDeclineReason, hasAfterConvoWorkTimer, label, presenceStatusOnDecline.
|
|
165
|
+
cat > "$PUC_DIR/${CONFIG_DN}.presenceUserConfig-meta.xml" <<XML
|
|
166
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
167
|
+
<PresenceUserConfig xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
168
|
+
<afterConvoWorkMaxTime>${ACW_SECONDS}</afterConvoWorkMaxTime>
|
|
169
|
+
${ASSIGN_BLOCK} <capacity>${CAPACITY}</capacity>
|
|
170
|
+
<declineReasons>${DECLINE_REASON_DN}</declineReasons>
|
|
171
|
+
<enableAutoAccept>false</enableAutoAccept>
|
|
172
|
+
<enableDecline>true</enableDecline>
|
|
173
|
+
<enableDeclineReason>true</enableDeclineReason>
|
|
174
|
+
<hasAfterConvoWorkTimer>true</hasAfterConvoWorkTimer>
|
|
175
|
+
<label>$(xml_escape "$CONFIG_LABEL")</label>
|
|
176
|
+
${PSOD_LINE}</PresenceUserConfig>
|
|
177
|
+
XML
|
|
178
|
+
|
|
179
|
+
# Step 4 - Deploy both components (async + poll to terminal state)
|
|
180
|
+
START_JSON=$(cd "$WORK" && sf project deploy start --target-org "$ORG" \
|
|
181
|
+
--metadata "PresenceDeclineReason:${DECLINE_REASON_DN}" \
|
|
182
|
+
--metadata "PresenceUserConfig:${CONFIG_DN}" \
|
|
183
|
+
--async --json 2>/dev/null || true)
|
|
184
|
+
DEPLOY_ID=$(echo "$START_JSON" | jq -r '.result.id // ""' 2>/dev/null)
|
|
185
|
+
DEPLOY_JSON="$START_JSON"
|
|
186
|
+
|
|
187
|
+
if [ -n "$DEPLOY_ID" ] && [ "$DEPLOY_ID" != "null" ]; then
|
|
188
|
+
POLL_STATUS=""
|
|
189
|
+
for _ in $(seq 1 60); do
|
|
190
|
+
REPORT_JSON=$(cd "$WORK" && sf project deploy report --job-id "$DEPLOY_ID" --target-org "$ORG" --json 2>/dev/null || true)
|
|
191
|
+
CUR_STATUS=$(echo "$REPORT_JSON" | jq -r '.result.status // ""' 2>/dev/null)
|
|
192
|
+
case "$CUR_STATUS" in
|
|
193
|
+
Succeeded|Failed|SucceededPartial|Canceled) DEPLOY_JSON="$REPORT_JSON"; POLL_STATUS="$CUR_STATUS"; break ;;
|
|
194
|
+
esac
|
|
195
|
+
sleep 15
|
|
196
|
+
done
|
|
197
|
+
[ -n "$POLL_STATUS" ] || emit_blocked "Presence-config deploy job $DEPLOY_ID did not reach a terminal state within the poll budget. Not reporting success."
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
DEPLOY_SUCCESS=$(echo "$DEPLOY_JSON" | jq -r '.result.success // false' 2>/dev/null)
|
|
201
|
+
DEPLOY_ID=$(echo "$DEPLOY_JSON" | jq -r '.result.id // ""' 2>/dev/null)
|
|
202
|
+
|
|
203
|
+
if [ "$DEPLOY_SUCCESS" != "true" ]; then
|
|
204
|
+
FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.result.details.componentFailures // [] | map(.problem) | join("; ")' 2>/dev/null)
|
|
205
|
+
[ -n "$FAILURES" ] && [ "$FAILURES" != "null" ] || FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.message // .result.errorMessage // "Unknown deploy error"' 2>/dev/null)
|
|
206
|
+
# The presence-status arm must only fire when PRESENCE_STATUS_ON_DECLINE is set; as a case glob an
|
|
207
|
+
# empty value becomes `**`, which matches ANY failure and misattributes every error. Check it
|
|
208
|
+
# explicitly (literal grep -F) after the base-settings arm to preserve the original priority.
|
|
209
|
+
case "$FAILURES" in
|
|
210
|
+
*INVALID_TYPE*|*"OmniChannel"*) emit_blocked "Presence-config deploy failed ($FAILURES). Omni-Channel base settings may not be enabled." "service-omni-base-settings-configure" ;;
|
|
211
|
+
*)
|
|
212
|
+
if [ -n "$PRESENCE_STATUS_ON_DECLINE" ] && printf '%s' "$FAILURES" | grep -qF -- "$PRESENCE_STATUS_ON_DECLINE"; then
|
|
213
|
+
emit_blocked "Presence-config deploy failed: presenceStatusOnDecline '$PRESENCE_STATUS_ON_DECLINE' does not exist. Deploy it first." "service-omni-presence-status-deploy"
|
|
214
|
+
fi
|
|
215
|
+
emit_blocked "Presence-config deploy failed: $FAILURES"
|
|
216
|
+
;;
|
|
217
|
+
esac
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
get_state() {
|
|
221
|
+
local fn="$1"; local type="$2"
|
|
222
|
+
echo "$DEPLOY_JSON" | jq -r --arg fn "$fn" --arg t "$type" \
|
|
223
|
+
'.result.files[] | select(.fullName==$fn and .type==$t) | .state' 2>/dev/null | head -1
|
|
224
|
+
}
|
|
225
|
+
STATE_DR=$(get_state "$DECLINE_REASON_DN" "PresenceDeclineReason"); STATE_DR="${STATE_DR:-unknown}"
|
|
226
|
+
STATE_PUC=$(get_state "$CONFIG_DN" "PresenceUserConfig"); STATE_PUC="${STATE_PUC:-unknown}"
|
|
227
|
+
|
|
228
|
+
for pair in "DECLINE_REASON:$STATE_DR" "CONFIG:$STATE_PUC"; do
|
|
229
|
+
st="${pair#*:}"
|
|
230
|
+
case "$st" in
|
|
231
|
+
Created|Changed|Unchanged) ;;
|
|
232
|
+
*) emit_blocked "Deploy reported success overall but component ${pair%%:*} has an unrecognized/absent state \"$st\" (expected Created, Changed, or Unchanged). Not reporting success." ;;
|
|
233
|
+
esac
|
|
234
|
+
done
|
|
235
|
+
|
|
236
|
+
# Skill status: created if any Created; else updated if any Changed; else reused.
|
|
237
|
+
STATUS="reused"
|
|
238
|
+
if [ "$STATE_DR" = "Created" ] || [ "$STATE_PUC" = "Created" ]; then
|
|
239
|
+
STATUS="created"
|
|
240
|
+
elif [ "$STATE_DR" = "Changed" ] || [ "$STATE_PUC" = "Changed" ]; then
|
|
241
|
+
STATUS="updated"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
ASSIGNED_JSON=$(printf '%s\n' "${USERNAMES[@]:-}" | jq -R . | jq -s 'map(select(length>0))')
|
|
245
|
+
|
|
246
|
+
jq -n \
|
|
247
|
+
--arg status "$STATUS" \
|
|
248
|
+
--arg cdn "$CONFIG_DN" --arg clbl "$CONFIG_LABEL" --argjson cap "$CAPACITY" --argjson acw "$ACW_SECONDS" --arg cstate "$STATE_PUC" \
|
|
249
|
+
--arg rdn "$DECLINE_REASON_DN" --arg rlbl "$DECLINE_REASON_LABEL" --arg rstate "$STATE_DR" \
|
|
250
|
+
--argjson assigned "$ASSIGNED_JSON" \
|
|
251
|
+
--arg deploy_id "$DEPLOY_ID" \
|
|
252
|
+
'{
|
|
253
|
+
skill: "service-omni-presence-user-config-deploy",
|
|
254
|
+
status: $status,
|
|
255
|
+
config: {developer_name:$cdn, label:$clbl, capacity:$cap, acw_seconds:$acw, state:$cstate},
|
|
256
|
+
decline_reason: {developer_name:$rdn, label:$rlbl, state:$rstate},
|
|
257
|
+
assigned_usernames: $assigned,
|
|
258
|
+
deploy_id: (if $deploy_id=="" then null else $deploy_id end),
|
|
259
|
+
manual_actions: [],
|
|
260
|
+
blocking_issue: null
|
|
261
|
+
}'
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Shared paths + fake-`sf` harness for the presence-user-config contract suite.
|
|
2
|
+
|
|
3
|
+
Drives ``deploy-and-report.sh`` via ``subprocess`` and asserts on exit codes + emitted JSON.
|
|
4
|
+
Org-free and network-free. ``jq`` comes from the real PATH.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SKILLS_ROOT = Path(__file__).resolve().parents[3]
|
|
14
|
+
SKILL_DIR = "service-omni-presence-user-config-deploy"
|
|
15
|
+
SCRIPT = f"{SKILL_DIR}/scripts/deploy-and-report.sh"
|
|
16
|
+
|
|
17
|
+
# PRODUCTION org - safe_to_write must refuse before any deploy.
|
|
18
|
+
_FAKE_SF_PROD = r"""#!/usr/bin/env bash
|
|
19
|
+
sub="${1:-} ${2:-}"; args="$*"
|
|
20
|
+
case "$sub" in
|
|
21
|
+
"org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
|
|
22
|
+
"data query")
|
|
23
|
+
if printf '%s' "$args" | grep -q "FROM Organization"; then
|
|
24
|
+
echo '{"status":0,"result":{"records":[{"IsSandbox":false,"TrialExpirationDate":null,"OrganizationType":"Enterprise Edition"}]}}'
|
|
25
|
+
else
|
|
26
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
27
|
+
fi
|
|
28
|
+
exit 0;;
|
|
29
|
+
"project deploy") echo '{"status":1,"result":{"success":false},"message":"FAKE_SF_DEPLOY_SHOULD_NOT_BE_REACHED"}'; exit 1;;
|
|
30
|
+
*) echo '{"status":0,"result":{}}'; exit 0;;
|
|
31
|
+
esac
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# SANDBOX org. Tunables:
|
|
35
|
+
# FAKE_STATE_DR / FAKE_STATE_PUC → Created|Changed|Unchanged (default Created).
|
|
36
|
+
# FAKE_DEPLOY_FAIL=1 → deploy fails with an OmniChannel-shaped problem.
|
|
37
|
+
# FAKE_CONFIG_DN / FAKE_DR_DN → fullNames echoed in the report (default the script defaults).
|
|
38
|
+
_FAKE_SF_SANDBOX = r"""#!/usr/bin/env bash
|
|
39
|
+
sub="${1:-} ${2:-}"; args="$*"
|
|
40
|
+
STATE_DR="${FAKE_STATE_DR:-Created}"
|
|
41
|
+
STATE_PUC="${FAKE_STATE_PUC:-Created}"
|
|
42
|
+
CONFIG_DN="${FAKE_CONFIG_DN:-Omni_Demo_Presence_Config}"
|
|
43
|
+
DR_DN="${FAKE_DR_DN:-Training}"
|
|
44
|
+
if [ -n "${FAKE_SF_CALLLOG:-}" ]; then printf '%s\n' "$args" >> "$FAKE_SF_CALLLOG"; fi
|
|
45
|
+
case "$sub" in
|
|
46
|
+
"org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
|
|
47
|
+
"data query")
|
|
48
|
+
if printf '%s' "$args" | grep -q "FROM Organization"; then
|
|
49
|
+
echo '{"status":0,"result":{"records":[{"IsSandbox":true,"TrialExpirationDate":null,"OrganizationType":"Developer Edition"}]}}'
|
|
50
|
+
elif printf '%s' "$args" | grep -q "FROM User"; then
|
|
51
|
+
echo '{"status":0,"result":{"records":[{"Username":"resolved.byid@example.com"}],"totalSize":1}}'
|
|
52
|
+
else
|
|
53
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
54
|
+
fi
|
|
55
|
+
exit 0;;
|
|
56
|
+
"project deploy")
|
|
57
|
+
if printf '%s' "$args" | grep -q "report"; then
|
|
58
|
+
if [ "${FAKE_DEPLOY_FAIL:-0}" = "1" ]; then
|
|
59
|
+
PROB="${FAKE_DEPLOY_PROBLEM:-INVALID_TYPE: OmniChannel not enabled}"
|
|
60
|
+
printf '{"status":0,"result":{"success":false,"id":"0AfXX0000000001","status":"Failed","details":{"componentFailures":[{"problem":"%s"}]}}}' "$PROB"
|
|
61
|
+
else
|
|
62
|
+
printf '{"status":0,"result":{"success":true,"id":"0AfXX0000000001","status":"Succeeded","files":[{"fullName":"%s","type":"PresenceDeclineReason","state":"%s"},{"fullName":"%s","type":"PresenceUserConfig","state":"%s"}]}}' "$DR_DN" "$STATE_DR" "$CONFIG_DN" "$STATE_PUC"
|
|
63
|
+
fi
|
|
64
|
+
else
|
|
65
|
+
# async start → return a job id so the script polls report.
|
|
66
|
+
echo '{"status":0,"result":{"id":"0AfXX0000000001","status":"Queued"}}'
|
|
67
|
+
fi
|
|
68
|
+
exit 0;;
|
|
69
|
+
*) echo '{"status":0,"result":{}}'; exit 0;;
|
|
70
|
+
esac
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
|
|
75
|
+
env = dict(os.environ)
|
|
76
|
+
if scrub_sf:
|
|
77
|
+
kept = [d for d in env.get("PATH", "").split(os.pathsep)
|
|
78
|
+
if d and not os.path.exists(os.path.join(d, "sf"))]
|
|
79
|
+
env["PATH"] = os.pathsep.join(kept)
|
|
80
|
+
if path_prefix:
|
|
81
|
+
env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
|
|
82
|
+
if extra_env:
|
|
83
|
+
env.update({k: str(v) for k, v in extra_env.items()})
|
|
84
|
+
proc = subprocess.run(
|
|
85
|
+
["bash", str(SKILLS_ROOT / SCRIPT), *args],
|
|
86
|
+
capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
|
|
87
|
+
)
|
|
88
|
+
return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FakeSf:
|
|
92
|
+
def __init__(self, body=_FAKE_SF_PROD):
|
|
93
|
+
self._body = body
|
|
94
|
+
self._tmp = None
|
|
95
|
+
|
|
96
|
+
def __enter__(self):
|
|
97
|
+
self._tmp = tempfile.mkdtemp(prefix="puc-fake-sf-")
|
|
98
|
+
sf = Path(self._tmp) / "sf"
|
|
99
|
+
sf.write_text(self._body)
|
|
100
|
+
sf.chmod(0o755)
|
|
101
|
+
return self._tmp
|
|
102
|
+
|
|
103
|
+
def __exit__(self, *exc):
|
|
104
|
+
import shutil
|
|
105
|
+
if self._tmp:
|
|
106
|
+
shutil.rmtree(self._tmp, ignore_errors=True)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Contract tests for service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
11
|
+
from _bootstrap import ( # noqa: E402
|
|
12
|
+
FakeSf,
|
|
13
|
+
_FAKE_SF_PROD,
|
|
14
|
+
_FAKE_SF_SANDBOX,
|
|
15
|
+
run,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _last_json(out: str) -> dict:
|
|
20
|
+
dec = json.JSONDecoder()
|
|
21
|
+
obj, i, n = None, 0, len(out)
|
|
22
|
+
while i < n:
|
|
23
|
+
if out[i] in "{[":
|
|
24
|
+
try:
|
|
25
|
+
cand, end = dec.raw_decode(out, i)
|
|
26
|
+
if isinstance(cand, dict):
|
|
27
|
+
obj = cand
|
|
28
|
+
i = end
|
|
29
|
+
continue
|
|
30
|
+
except json.JSONDecodeError:
|
|
31
|
+
pass
|
|
32
|
+
i += 1
|
|
33
|
+
assert obj is not None, f"no JSON object found in output:\n{out}"
|
|
34
|
+
return obj
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InputContractTests(unittest.TestCase):
|
|
38
|
+
def test_missing_args_rejected(self):
|
|
39
|
+
rc, out = run([], scrub_sf=True)
|
|
40
|
+
self.assertEqual(rc, 1, out)
|
|
41
|
+
self.assertIn("Usage", out)
|
|
42
|
+
|
|
43
|
+
def test_injection_shaped_config_name_rejected(self):
|
|
44
|
+
rc, out = run(["org", "Bad Name; DROP"], scrub_sf=True)
|
|
45
|
+
self.assertEqual(rc, 1, out)
|
|
46
|
+
self.assertIn("Invalid config_developer_name", out)
|
|
47
|
+
|
|
48
|
+
def test_capacity_out_of_range_rejected(self):
|
|
49
|
+
rc, out = run(["org", "Omni_Demo_Presence_Config"], scrub_sf=True,
|
|
50
|
+
extra_env={"CAPACITY": "0"})
|
|
51
|
+
self.assertEqual(rc, 1, out)
|
|
52
|
+
self.assertIn("Invalid CAPACITY", out)
|
|
53
|
+
|
|
54
|
+
def test_acw_out_of_range_rejected(self):
|
|
55
|
+
rc, out = run(["org", "Omni_Demo_Presence_Config"], scrub_sf=True,
|
|
56
|
+
extra_env={"ACW_SECONDS": "5"})
|
|
57
|
+
self.assertEqual(rc, 1, out)
|
|
58
|
+
self.assertIn("Invalid ACW_SECONDS", out)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProductionGuardTests(unittest.TestCase):
|
|
62
|
+
def test_refuses_production_org(self):
|
|
63
|
+
with FakeSf(_FAKE_SF_PROD) as p:
|
|
64
|
+
rc, out = run(["prodorg"], path_prefix=p)
|
|
65
|
+
self.assertEqual(rc, 1, out)
|
|
66
|
+
v = _last_json(out)
|
|
67
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
68
|
+
self.assertIn("production", v["blocking_issue"].lower())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DeployTests(unittest.TestCase):
|
|
72
|
+
def test_created_reports_states_and_assignments(self):
|
|
73
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
74
|
+
rc, out = run(["sandbox", "Omni_Demo_Presence_Config", "agent1@example.com,agent2@example.com"],
|
|
75
|
+
path_prefix=p, extra_env={"FAKE_STATE_DR": "Created", "FAKE_STATE_PUC": "Created"})
|
|
76
|
+
self.assertEqual(rc, 0, out)
|
|
77
|
+
v = _last_json(out)
|
|
78
|
+
self.assertEqual(v["status"], "created", out)
|
|
79
|
+
self.assertEqual(v["config"]["state"], "Created", out)
|
|
80
|
+
self.assertEqual(v["decline_reason"]["state"], "Created", out)
|
|
81
|
+
self.assertEqual(v["config"]["acw_seconds"], 60, out)
|
|
82
|
+
self.assertIn("agent1@example.com", v["assigned_usernames"], out)
|
|
83
|
+
self.assertIn("agent2@example.com", v["assigned_usernames"], out)
|
|
84
|
+
|
|
85
|
+
def test_reused_when_both_unchanged(self):
|
|
86
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
87
|
+
rc, out = run(["sandbox"], path_prefix=p,
|
|
88
|
+
extra_env={"FAKE_STATE_DR": "Unchanged", "FAKE_STATE_PUC": "Unchanged"})
|
|
89
|
+
self.assertEqual(rc, 0, out)
|
|
90
|
+
v = _last_json(out)
|
|
91
|
+
self.assertEqual(v["status"], "reused", out)
|
|
92
|
+
|
|
93
|
+
def test_updated_when_config_changed(self):
|
|
94
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
95
|
+
rc, out = run(["sandbox"], path_prefix=p,
|
|
96
|
+
extra_env={"FAKE_STATE_DR": "Unchanged", "FAKE_STATE_PUC": "Changed"})
|
|
97
|
+
self.assertEqual(rc, 0, out)
|
|
98
|
+
v = _last_json(out)
|
|
99
|
+
self.assertEqual(v["status"], "updated", out)
|
|
100
|
+
|
|
101
|
+
def test_deploy_failure_points_at_base_settings(self):
|
|
102
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
103
|
+
rc, out = run(["sandbox"], path_prefix=p, extra_env={"FAKE_DEPLOY_FAIL": "1"})
|
|
104
|
+
self.assertEqual(rc, 1, out)
|
|
105
|
+
v = _last_json(out)
|
|
106
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
107
|
+
self.assertTrue(any(a.get("target_skill") == "service-omni-base-settings-configure"
|
|
108
|
+
for a in v["manual_actions"]), out)
|
|
109
|
+
|
|
110
|
+
def test_empty_status_on_decline_does_not_swallow_unrelated_failure(self):
|
|
111
|
+
# Regression (blocker 3): with PRESENCE_STATUS_ON_DECLINE="" the presence-status case arm was
|
|
112
|
+
# effectively `*""*`, matching EVERY failure and misreporting an unrelated deploy error as a
|
|
113
|
+
# missing presence status. An unrelated failure must now surface as the generic message.
|
|
114
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
115
|
+
rc, out = run(["sandbox"], path_prefix=p, extra_env={
|
|
116
|
+
"FAKE_DEPLOY_FAIL": "1",
|
|
117
|
+
"FAKE_DEPLOY_PROBLEM": "FIELD_CUSTOM_VALIDATION_EXCEPTION: unrelated failure",
|
|
118
|
+
"PRESENCE_STATUS_ON_DECLINE": "",
|
|
119
|
+
})
|
|
120
|
+
self.assertEqual(rc, 1, out)
|
|
121
|
+
v = _last_json(out)
|
|
122
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
123
|
+
self.assertIn("FIELD_CUSTOM_VALIDATION_EXCEPTION", v["blocking_issue"], out)
|
|
124
|
+
self.assertNotIn("presenceStatusOnDecline", v["blocking_issue"], out)
|
|
125
|
+
self.assertFalse(any(a.get("target_skill") == "service-omni-presence-status-deploy"
|
|
126
|
+
for a in v.get("manual_actions", [])), out)
|
|
127
|
+
|
|
128
|
+
def test_plan_only_reports_without_deploying(self):
|
|
129
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
130
|
+
calllog = str(Path(p) / "calls.log")
|
|
131
|
+
rc, out = run(["sandbox", "Omni_Demo_Presence_Config", "agent1@example.com"],
|
|
132
|
+
path_prefix=p, extra_env={"PLAN_ONLY": "1", "FAKE_SF_CALLLOG": calllog})
|
|
133
|
+
log = Path(calllog).read_text() if Path(calllog).exists() else ""
|
|
134
|
+
self.assertEqual(rc, 0, out)
|
|
135
|
+
v = _last_json(out)
|
|
136
|
+
self.assertTrue(v.get("plan_mode"), out)
|
|
137
|
+
self.assertEqual(v["status"], "action_needed", out)
|
|
138
|
+
self.assertNotIn("project deploy", log, "plan mode must not deploy")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
unittest.main(verbosity=2)
|