@salesforce/afv-skills 1.47.0 → 1.49.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/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 +2 -0
- package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +4 -3
- 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-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,262 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# configure-and-report.sh - author a WorkSkillRouting rule (field-value -> required Omni Skill) for a related entity via the Metadata API; detect-before-write, idempotent, graceful-degrade when the feature is unavailable. Args, env, exit codes: SKILL.md.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ $# -lt 5 ]; then
|
|
7
|
+
echo '{"error":"Usage: bash configure-and-report.sh <org-alias> <related_entity> <field> <skill> <value> [rule_master_label]"}' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
ORG="$1"
|
|
12
|
+
RELATED_ENTITY="$2"
|
|
13
|
+
FIELD="$3"
|
|
14
|
+
SKILL="$4"
|
|
15
|
+
VALUE="$5"
|
|
16
|
+
MASTER_LABEL_EXPLICIT=false
|
|
17
|
+
{ [ -n "${6:-}" ] || [ -n "${RULE_MASTER_LABEL:-}" ]; } && MASTER_LABEL_EXPLICIT=true
|
|
18
|
+
IS_ACTIVE_EXPLICIT=false
|
|
19
|
+
[ -n "${IS_ACTIVE:-}" ] && IS_ACTIVE_EXPLICIT=true
|
|
20
|
+
RULE_MASTER_LABEL="${6:-${RULE_MASTER_LABEL:-$RELATED_ENTITY Skill Routing}}"
|
|
21
|
+
|
|
22
|
+
RULE_DN="${RULE_DEVELOPER_NAME:-$RELATED_ENTITY}"
|
|
23
|
+
IS_ACTIVE="${IS_ACTIVE:-true}"
|
|
24
|
+
SKILL_LEVEL="${SKILL_LEVEL:-}"
|
|
25
|
+
SKILL_PRIORITY="${SKILL_PRIORITY:-}"
|
|
26
|
+
IS_ADDITIONAL_SKILL="${IS_ADDITIONAL_SKILL:-}"
|
|
27
|
+
|
|
28
|
+
# ---- validation (reject before any sf call) ----
|
|
29
|
+
if ! [[ "$RELATED_ENTITY" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
|
|
30
|
+
echo "{\"error\":\"Invalid related_entity '$RELATED_ENTITY'. Must be an sObject API name (letters, digits, underscores; max 80).\"}" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
if ! [[ "$FIELD" =~ ^[A-Za-z][A-Za-z0-9_.]{0,120}$ ]]; then
|
|
34
|
+
echo "{\"error\":\"Invalid field '$FIELD'. Use a field API name, optionally Entity.Field (letters, digits, underscores, dots).\"}" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
if ! [[ "$SKILL" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
|
|
38
|
+
echo "{\"error\":\"Invalid skill '$SKILL'. Must be a Skill DeveloperName (letters, digits, underscores; max 80).\"}" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
if ! [[ "$VALUE" =~ ^[A-Za-z0-9][A-Za-z0-9\ _./-]{0,254}$ ]]; then
|
|
42
|
+
echo "{\"error\":\"Invalid value '$VALUE'. Provide a plain field value (letters, digits, space, . _ / -); XML-reserved characters are not supported.\"}" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
if ! [[ "$RULE_DN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
|
|
46
|
+
echo "{\"error\":\"Invalid RULE_DEVELOPER_NAME '$RULE_DN'.\"}" >&2
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
if ! [[ "$RULE_MASTER_LABEL" =~ ^[A-Za-z0-9][A-Za-z0-9\ _-]{0,79}$ ]]; then
|
|
50
|
+
echo "{\"error\":\"Invalid rule_master_label '$RULE_MASTER_LABEL'.\"}" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
if [ "$IS_ACTIVE" != "true" ] && [ "$IS_ACTIVE" != "false" ]; then
|
|
54
|
+
echo "{\"error\":\"Invalid IS_ACTIVE '$IS_ACTIVE' - must be true or false.\"}" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
if [ -n "$SKILL_LEVEL" ] && { ! [[ "$SKILL_LEVEL" =~ ^[0-9]+$ ]] || [ "$SKILL_LEVEL" -gt 10 ]; }; then
|
|
58
|
+
echo "{\"error\":\"Invalid SKILL_LEVEL '$SKILL_LEVEL' - must be an integer 0..10.\"}" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
if [ -n "$SKILL_PRIORITY" ] && { ! [[ "$SKILL_PRIORITY" =~ ^[0-9]+$ ]] || [ "$SKILL_PRIORITY" -gt 10 ]; }; then
|
|
62
|
+
echo "{\"error\":\"Invalid SKILL_PRIORITY '$SKILL_PRIORITY' - must be an integer 0..10.\"}" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
if [ -n "$IS_ADDITIONAL_SKILL" ] && [ "$IS_ADDITIONAL_SKILL" != "true" ] && [ "$IS_ADDITIONAL_SKILL" != "false" ]; then
|
|
66
|
+
echo "{\"error\":\"Invalid IS_ADDITIONAL_SKILL '$IS_ADDITIONAL_SKILL' - must be true or false.\"}" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# The Metadata API requires WorkSkillRoutingAttribute.field to be entity-qualified.
|
|
71
|
+
# Bare standard fields are otherwise resolved as CustomField metadata and fail to deploy.
|
|
72
|
+
if [[ "$FIELD" != *.* ]]; then
|
|
73
|
+
FIELD="${RELATED_ENTITY}.${FIELD}"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
|
|
77
|
+
echo "{\"error\":\"Org alias '$ORG' is not authenticated. Run: sf org login web -a $ORG -r <my-domain-url>\"}" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
emit_blocked() {
|
|
82
|
+
local msg="$1"; local target_skill="${2:-}"
|
|
83
|
+
local manual_actions='[]'
|
|
84
|
+
if [ -n "$target_skill" ]; then
|
|
85
|
+
manual_actions=$(jq -n --arg ts "$target_skill" '[{id:"MISSING_PREREQ",title:"Run prerequisite skill",target_skill:$ts}]')
|
|
86
|
+
fi
|
|
87
|
+
jq -n --arg msg "$msg" --arg rdn "$RULE_DN" --arg ent "$RELATED_ENTITY" --argjson ma "$manual_actions" \
|
|
88
|
+
'{skill:"service-omni-work-skill-routing-configure",status:"blocked",
|
|
89
|
+
rule:{developer_name:$rdn,related_entity:$ent,state:"not_deployed"},
|
|
90
|
+
attribute:null,deploy_id:null,manual_actions:$ma,blocking_issue:$msg}'
|
|
91
|
+
exit 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
emit_plan() {
|
|
95
|
+
local detail="$1"
|
|
96
|
+
jq -n --arg detail "$detail" --arg rdn "$RULE_DN" --arg ent "$RELATED_ENTITY" \
|
|
97
|
+
'{skill:"service-omni-work-skill-routing-configure",status:"action_needed",plan_mode:true,
|
|
98
|
+
plan_detail:$detail,rule:{developer_name:$rdn,related_entity:$ent},manual_actions:[],blocking_issue:null}'
|
|
99
|
+
exit 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Step 1 - Organization + safe_to_write guard
|
|
103
|
+
ORG_JSON=$(sf data query --target-org "$ORG" \
|
|
104
|
+
--query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" --json 2>/dev/null) \
|
|
105
|
+
|| emit_blocked "Failed to query Organization for the safe_to_write guard."
|
|
106
|
+
IS_SANDBOX=$(echo "$ORG_JSON" | jq -r '.result.records[0].IsSandbox')
|
|
107
|
+
TRIAL_EXP=$(echo "$ORG_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
|
|
108
|
+
ORG_TYPE=$(echo "$ORG_JSON" | jq -r '.result.records[0].OrganizationType')
|
|
109
|
+
SAFE_TO_WRITE=false
|
|
110
|
+
if [ "$IS_SANDBOX" = "true" ] || [ "$TRIAL_EXP" != "null" ] \
|
|
111
|
+
|| [ "$ORG_TYPE" = "Developer Edition" ] || [ "$ORG_TYPE" = "Base Edition" ]; then
|
|
112
|
+
SAFE_TO_WRITE=true
|
|
113
|
+
fi
|
|
114
|
+
[ "$SAFE_TO_WRITE" = "true" ] || emit_blocked "Refusing to author a routing rule on a real production customer org (IsSandbox=$IS_SANDBOX, TrialExpirationDate=$TRIAL_EXP, OrganizationType=$ORG_TYPE). Target a sandbox, trial CDO, or Developer Edition org."
|
|
115
|
+
|
|
116
|
+
# Step 2 - Referenced Skill must exist (a WorkSkillRoutingAttribute.skill points at a Skill DeveloperName)
|
|
117
|
+
SKILL_JSON=$(sf data query --target-org "$ORG" --json \
|
|
118
|
+
--query "SELECT Id FROM Skill WHERE DeveloperName='$SKILL'" 2>/dev/null || echo '{}')
|
|
119
|
+
SKILL_COUNT=$(echo "$SKILL_JSON" | jq -r '.result.records | length' 2>/dev/null || echo 0)
|
|
120
|
+
[ "${SKILL_COUNT:-0}" -ge 1 ] || emit_blocked "Referenced Skill '$SKILL' does not exist; deploy it first so the rule can bind to it." "service-omni-skills-based-routing-configure"
|
|
121
|
+
|
|
122
|
+
# A metadata deploy replaces the full one-per-entity rule. Read and merge the
|
|
123
|
+
# current attribute array so adding Origin does not erase an existing Priority rule.
|
|
124
|
+
MERGE_MODE=true
|
|
125
|
+
[ "${WSR_REPLACE:-}" = "1" ] && MERGE_MODE=false
|
|
126
|
+
EXISTING_ATTRS='[]'
|
|
127
|
+
MERGED_FROM_EXISTING=false
|
|
128
|
+
if [ "$MERGE_MODE" = "true" ]; then
|
|
129
|
+
WSR_META_JSON=$(sf data query --target-org "$ORG" --use-tooling-api --json \
|
|
130
|
+
--query "SELECT Metadata FROM WorkSkillRouting WHERE DeveloperName='$RULE_DN'" 2>/dev/null || true)
|
|
131
|
+
if echo "$WSR_META_JSON" | jq -e '.result.totalSize == 1' >/dev/null 2>&1; then
|
|
132
|
+
MERGED_FROM_EXISTING=true
|
|
133
|
+
EXISTING_ATTRS=$(echo "$WSR_META_JSON" | jq -c '.result.records[0].Metadata.workSkillRoutingAttributes // []')
|
|
134
|
+
PRESERVED_LABEL=$(echo "$WSR_META_JSON" | jq -r '.result.records[0].Metadata.masterLabel // ""')
|
|
135
|
+
PRESERVED_ACTIVE=$(echo "$WSR_META_JSON" | jq -r '.result.records[0].Metadata.isActive // empty')
|
|
136
|
+
[ "$MASTER_LABEL_EXPLICIT" = "true" ] || [ -z "$PRESERVED_LABEL" ] || RULE_MASTER_LABEL="$PRESERVED_LABEL"
|
|
137
|
+
[ "$IS_ACTIVE_EXPLICIT" = "true" ] || [ -z "$PRESERVED_ACTIVE" ] || IS_ACTIVE="$PRESERVED_ACTIVE"
|
|
138
|
+
elif echo "$WSR_META_JSON" | jq -e '.result.totalSize == 0' >/dev/null 2>&1; then
|
|
139
|
+
:
|
|
140
|
+
elif ! echo "$WSR_META_JSON" | grep -qiE 'INVALID_TYPE|not available|no such|Cannot find'; then
|
|
141
|
+
emit_blocked "Could not read existing WorkSkillRouting '$RULE_DN' for a merge. Refusing a destructive whole-rule replace; retry the read or set WSR_REPLACE=1 to overwrite intentionally."
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
NEW_ATTR=$(jq -n \
|
|
146
|
+
--arg field "$FIELD" --arg skill "$SKILL" --arg value "$VALUE" \
|
|
147
|
+
--arg lvl "$SKILL_LEVEL" --arg pri "$SKILL_PRIORITY" --arg add "$IS_ADDITIONAL_SKILL" \
|
|
148
|
+
'{field:$field,skill:$skill,value:$value}
|
|
149
|
+
+ (if $add=="" then {} else {isAdditionalSkill:($add=="true")} end)
|
|
150
|
+
+ (if $lvl=="" then {} else {skillLevel:($lvl|tonumber)} end)
|
|
151
|
+
+ (if $pri=="" then {} else {skillPriority:($pri|tonumber)} end)')
|
|
152
|
+
MERGED_ATTRS=$(jq -c -n --argjson existing "$EXISTING_ATTRS" --argjson new "$NEW_ATTR" \
|
|
153
|
+
'($existing | map(select((.field==$new.field and .value==$new.value) | not))) + [$new]')
|
|
154
|
+
ATTR_COUNT=$(echo "$MERGED_ATTRS" | jq -r 'length')
|
|
155
|
+
PRESERVED_COUNT=$((ATTR_COUNT - 1))
|
|
156
|
+
|
|
157
|
+
# PLAN_ONLY - read-only; report intent and stop before any write.
|
|
158
|
+
if [ "${PLAN_ONLY:-}" = "1" ]; then
|
|
159
|
+
emit_plan "Would deploy WorkSkillRouting '$RULE_DN' on $RELATED_ENTITY (isActive=$IS_ACTIVE) mapping $FIELD='$VALUE' -> Skill '$SKILL'."
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Step 3 - Materialize the WorkSkillRouting in XSD (alphabetical) order
|
|
163
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
164
|
+
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
|
165
|
+
cp "$SCRIPT_DIR/../assets/sfdx-project.json" "$WORK/sfdx-project.json" 2>/dev/null \
|
|
166
|
+
|| echo '{"packageDirectories":[{"path":"force-app","default":true}],"sourceApiVersion":"66.0"}' > "$WORK/sfdx-project.json"
|
|
167
|
+
WSR_DIR="$WORK/force-app/main/default/workSkillRoutings"
|
|
168
|
+
mkdir -p "$WSR_DIR"
|
|
169
|
+
|
|
170
|
+
ATTR_XML=$(echo "$MERGED_ATTRS" | jq -r '
|
|
171
|
+
def xe: tostring | gsub("&";"&") | gsub("<";"<") | gsub(">";">");
|
|
172
|
+
.[] |
|
|
173
|
+
" <workSkillRoutingAttributes>",
|
|
174
|
+
" <field>\(.field|xe)</field>",
|
|
175
|
+
(if has("isAdditionalSkill") then " <isAdditionalSkill>\(.isAdditionalSkill)</isAdditionalSkill>" else empty end),
|
|
176
|
+
" <skill>\(.skill|xe)</skill>",
|
|
177
|
+
(if .skillLevel != null then " <skillLevel>\(.skillLevel)</skillLevel>" else empty end),
|
|
178
|
+
(if .skillPriority != null then " <skillPriority>\(.skillPriority)</skillPriority>" else empty end),
|
|
179
|
+
" <value>\(.value|xe)</value>",
|
|
180
|
+
" </workSkillRoutingAttributes>"
|
|
181
|
+
')
|
|
182
|
+
|
|
183
|
+
cat > "$WSR_DIR/${RULE_DN}.workSkillRouting-meta.xml" <<XML
|
|
184
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
185
|
+
<WorkSkillRouting xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
186
|
+
<isActive>${IS_ACTIVE}</isActive>
|
|
187
|
+
<masterLabel>${RULE_MASTER_LABEL}</masterLabel>
|
|
188
|
+
<relatedEntity>${RELATED_ENTITY}</relatedEntity>
|
|
189
|
+
${ATTR_XML}
|
|
190
|
+
</WorkSkillRouting>
|
|
191
|
+
XML
|
|
192
|
+
|
|
193
|
+
# Step 4 - Deploy (async + poll to terminal state)
|
|
194
|
+
START_JSON=$(cd "$WORK" && sf project deploy start --target-org "$ORG" \
|
|
195
|
+
--metadata "WorkSkillRouting:${RULE_DN}" --async --json 2>/dev/null || true)
|
|
196
|
+
DEPLOY_ID=$(echo "$START_JSON" | jq -r '.result.id // ""' 2>/dev/null)
|
|
197
|
+
DEPLOY_JSON="$START_JSON"
|
|
198
|
+
|
|
199
|
+
if [ -n "$DEPLOY_ID" ] && [ "$DEPLOY_ID" != "null" ]; then
|
|
200
|
+
POLL_STATUS=""
|
|
201
|
+
for _ in $(seq 1 60); do
|
|
202
|
+
REPORT_JSON=$(cd "$WORK" && sf project deploy report --job-id "$DEPLOY_ID" --target-org "$ORG" --json 2>/dev/null || true)
|
|
203
|
+
CUR_STATUS=$(echo "$REPORT_JSON" | jq -r '.result.status // ""' 2>/dev/null)
|
|
204
|
+
case "$CUR_STATUS" in
|
|
205
|
+
Succeeded|Failed|SucceededPartial|Canceled) DEPLOY_JSON="$REPORT_JSON"; POLL_STATUS="$CUR_STATUS"; break ;;
|
|
206
|
+
esac
|
|
207
|
+
sleep 15
|
|
208
|
+
done
|
|
209
|
+
[ -n "$POLL_STATUS" ] || emit_blocked "WorkSkillRouting deploy job $DEPLOY_ID did not reach a terminal state within the poll budget. Not reporting success."
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
DEPLOY_SUCCESS=$(echo "$DEPLOY_JSON" | jq -r '.result.success // false' 2>/dev/null)
|
|
213
|
+
DEPLOY_ID=$(echo "$DEPLOY_JSON" | jq -r '.result.id // ""' 2>/dev/null)
|
|
214
|
+
|
|
215
|
+
if [ "$DEPLOY_SUCCESS" != "true" ]; then
|
|
216
|
+
FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.result.details.componentFailures // [] | map(.problem) | join("; ")' 2>/dev/null)
|
|
217
|
+
[ -n "$FAILURES" ] && [ "$FAILURES" != "null" ] || FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.message // .result.errorMessage // "Unknown deploy error"' 2>/dev/null)
|
|
218
|
+
case "$FAILURES" in
|
|
219
|
+
*INVALID_TYPE*|*"Cannot find"*|*"not available"*|*"WorkSkillRouting"*)
|
|
220
|
+
emit_blocked "WorkSkillRouting is not available on this org (skills-based routing may be off, or the feature is not provisioned on this trial/CDO): $FAILURES" "service-omni-base-settings-configure" ;;
|
|
221
|
+
*"$SKILL"*)
|
|
222
|
+
emit_blocked "Deploy failed: Skill '$SKILL' could not be resolved by the rule. $FAILURES" "service-omni-skills-based-routing-configure" ;;
|
|
223
|
+
*) emit_blocked "WorkSkillRouting deploy failed: $FAILURES" ;;
|
|
224
|
+
esac
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
STATE_WSR=$(echo "$DEPLOY_JSON" | jq -r --arg fn "$RULE_DN" \
|
|
228
|
+
'.result.files[] | select(.fullName==$fn and .type=="WorkSkillRouting") | .state' 2>/dev/null | head -1)
|
|
229
|
+
STATE_WSR="${STATE_WSR:-unknown}"
|
|
230
|
+
case "$STATE_WSR" in
|
|
231
|
+
Created|Changed|Unchanged) ;;
|
|
232
|
+
*) emit_blocked "Deploy reported success overall but WorkSkillRouting '$RULE_DN' has an unrecognized/absent state \"$STATE_WSR\" (expected Created, Changed, or Unchanged). Not reporting success." ;;
|
|
233
|
+
esac
|
|
234
|
+
|
|
235
|
+
STATUS="reused"
|
|
236
|
+
[ "$STATE_WSR" = "Created" ] && STATUS="created"
|
|
237
|
+
[ "$STATE_WSR" = "Changed" ] && STATUS="updated"
|
|
238
|
+
|
|
239
|
+
jq -n \
|
|
240
|
+
--arg status "$STATUS" \
|
|
241
|
+
--arg rdn "$RULE_DN" --arg lbl "$RULE_MASTER_LABEL" --arg ent "$RELATED_ENTITY" --arg act "$IS_ACTIVE" --arg state "$STATE_WSR" \
|
|
242
|
+
--arg field "$FIELD" --arg skill "$SKILL" --arg value "$VALUE" \
|
|
243
|
+
--arg lvl "$SKILL_LEVEL" --arg pri "$SKILL_PRIORITY" --arg add "$IS_ADDITIONAL_SKILL" \
|
|
244
|
+
--arg deploy_id "$DEPLOY_ID" \
|
|
245
|
+
--argjson merged_from_existing "$MERGED_FROM_EXISTING" \
|
|
246
|
+
--argjson attribute_count "$ATTR_COUNT" \
|
|
247
|
+
--argjson preserved_count "$PRESERVED_COUNT" \
|
|
248
|
+
'{
|
|
249
|
+
skill: "service-omni-work-skill-routing-configure",
|
|
250
|
+
status: $status,
|
|
251
|
+
rule: {developer_name:$rdn, master_label:$lbl, related_entity:$ent, is_active:($act=="true"), state:$state},
|
|
252
|
+
merge: {merged_from_existing:$merged_from_existing, attribute_count:$attribute_count, preserved_count:$preserved_count},
|
|
253
|
+
attribute: {
|
|
254
|
+
field:$field, skill:$skill, value:$value,
|
|
255
|
+
skill_level:(if $lvl=="" then null else ($lvl|tonumber) end),
|
|
256
|
+
skill_priority:(if $pri=="" then null else ($pri|tonumber) end),
|
|
257
|
+
is_additional_skill:(if $add=="" then null else ($add=="true") end)
|
|
258
|
+
},
|
|
259
|
+
deploy_id: (if $deploy_id=="" then null else $deploy_id end),
|
|
260
|
+
manual_actions: [],
|
|
261
|
+
blocking_issue: null
|
|
262
|
+
}'
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Shared paths + fake-`sf` harness for the work-skill-routing contract suite.
|
|
2
|
+
|
|
3
|
+
Drives ``configure-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-work-skill-routing-configure"
|
|
15
|
+
SCRIPT = f"{SKILL_DIR}/scripts/configure-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_WSR -> Created|Changed|Unchanged (default Created).
|
|
36
|
+
# FAKE_SKILL_PRESENT -> 1|0 (default 1); 0 blocks on the missing-Skill prereq.
|
|
37
|
+
# FAKE_DEPLOY_FAIL=1 -> deploy fails with a feature-unavailable (INVALID_TYPE) problem.
|
|
38
|
+
# FAKE_RULE_DN -> fullName echoed in the report (default Case).
|
|
39
|
+
_FAKE_SF_SANDBOX = r"""#!/usr/bin/env bash
|
|
40
|
+
sub="${1:-} ${2:-}"; args="$*"
|
|
41
|
+
STATE_WSR="${FAKE_STATE_WSR:-Created}"
|
|
42
|
+
RULE_DN="${FAKE_RULE_DN:-Case}"
|
|
43
|
+
if [ -n "${FAKE_SF_CALLLOG:-}" ]; then printf '%s\n' "$args" >> "$FAKE_SF_CALLLOG"; fi
|
|
44
|
+
case "$sub" in
|
|
45
|
+
"org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
|
|
46
|
+
"data query")
|
|
47
|
+
if printf '%s' "$args" | grep -q "FROM Organization"; then
|
|
48
|
+
echo '{"status":0,"result":{"records":[{"IsSandbox":true,"TrialExpirationDate":null,"OrganizationType":"Developer Edition"}]}}'
|
|
49
|
+
elif printf '%s' "$args" | grep -q "FROM WorkSkillRouting"; then
|
|
50
|
+
if [ "${FAKE_WSR_INCONCLUSIVE:-0}" = "1" ]; then
|
|
51
|
+
echo '{"status":1,"message":"Transient failure reading WorkSkillRouting metadata"}'
|
|
52
|
+
elif [ "${FAKE_WSR_EXISTING:-0}" = "1" ]; then
|
|
53
|
+
if [ "${FAKE_WSR_NULL_OPTIONALS:-0}" = "1" ]; then
|
|
54
|
+
echo '{"status":0,"result":{"totalSize":1,"records":[{"Metadata":{"masterLabel":"Existing Label","isActive":true,"relatedEntity":"Case","workSkillRoutingAttributes":[{"field":"Case.Priority","skill":"Priority_Support","value":"High","skillLevel":null,"skillPriority":null}]}}]}}'
|
|
55
|
+
else
|
|
56
|
+
echo '{"status":0,"result":{"totalSize":1,"records":[{"Metadata":{"masterLabel":"Existing Label","isActive":true,"relatedEntity":"Case","workSkillRoutingAttributes":[{"field":"Case.Priority","skill":"Priority_Support","value":"High"}]}}]}}'
|
|
57
|
+
fi
|
|
58
|
+
else
|
|
59
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
60
|
+
fi
|
|
61
|
+
elif printf '%s' "$args" | grep -q "FROM Skill"; then
|
|
62
|
+
if [ "${FAKE_SKILL_PRESENT:-1}" = "1" ]; then
|
|
63
|
+
echo '{"status":0,"result":{"records":[{"Id":"0C9xx0000000001"}],"totalSize":1}}'
|
|
64
|
+
else
|
|
65
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
66
|
+
fi
|
|
67
|
+
else
|
|
68
|
+
echo '{"status":0,"result":{"records":[],"totalSize":0}}'
|
|
69
|
+
fi
|
|
70
|
+
exit 0;;
|
|
71
|
+
"project deploy")
|
|
72
|
+
if printf '%s' "$args" | grep -q "report"; then
|
|
73
|
+
if grep -R -q '>null<' force-app/main/default/workSkillRoutings 2>/dev/null; then
|
|
74
|
+
echo '{"status":0,"result":{"success":false,"id":"0AfXX0000000001","status":"Failed","details":{"componentFailures":[{"problem":"null is not a valid value for the type xsd:int"}]}}}'
|
|
75
|
+
elif [ "${FAKE_DEPLOY_FAIL:-0}" = "1" ]; then
|
|
76
|
+
echo '{"status":0,"result":{"success":false,"id":"0AfXX0000000001","status":"Failed","details":{"componentFailures":[{"problem":"INVALID_TYPE: WorkSkillRouting not available"}]}}}'
|
|
77
|
+
else
|
|
78
|
+
printf '{"status":0,"result":{"success":true,"id":"0AfXX0000000001","status":"Succeeded","files":[{"fullName":"%s","type":"WorkSkillRouting","state":"%s"}]}}' "$RULE_DN" "$STATE_WSR"
|
|
79
|
+
fi
|
|
80
|
+
else
|
|
81
|
+
echo '{"status":0,"result":{"id":"0AfXX0000000001","status":"Queued"}}'
|
|
82
|
+
fi
|
|
83
|
+
exit 0;;
|
|
84
|
+
*) echo '{"status":0,"result":{}}'; exit 0;;
|
|
85
|
+
esac
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
|
|
90
|
+
env = dict(os.environ)
|
|
91
|
+
if scrub_sf:
|
|
92
|
+
kept = [d for d in env.get("PATH", "").split(os.pathsep)
|
|
93
|
+
if d and not os.path.exists(os.path.join(d, "sf"))]
|
|
94
|
+
env["PATH"] = os.pathsep.join(kept)
|
|
95
|
+
if path_prefix:
|
|
96
|
+
env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
|
|
97
|
+
if extra_env:
|
|
98
|
+
env.update({k: str(v) for k, v in extra_env.items()})
|
|
99
|
+
proc = subprocess.run(
|
|
100
|
+
["bash", str(SKILLS_ROOT / SCRIPT), *args],
|
|
101
|
+
capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
|
|
102
|
+
)
|
|
103
|
+
return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FakeSf:
|
|
107
|
+
def __init__(self, body=_FAKE_SF_PROD):
|
|
108
|
+
self._body = body
|
|
109
|
+
self._tmp = None
|
|
110
|
+
|
|
111
|
+
def __enter__(self):
|
|
112
|
+
self._tmp = tempfile.mkdtemp(prefix="wsr-fake-sf-")
|
|
113
|
+
sf = Path(self._tmp) / "sf"
|
|
114
|
+
sf.write_text(self._body)
|
|
115
|
+
sf.chmod(0o755)
|
|
116
|
+
return self._tmp
|
|
117
|
+
|
|
118
|
+
def __exit__(self, *exc):
|
|
119
|
+
import shutil
|
|
120
|
+
if self._tmp:
|
|
121
|
+
shutil.rmtree(self._tmp, ignore_errors=True)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Contract tests for service-omni-work-skill-routing-configure/scripts/configure-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(["myorg", "Case", "Origin"], scrub_sf=True)
|
|
40
|
+
self.assertEqual(rc, 1, out)
|
|
41
|
+
self.assertIn("Usage", out)
|
|
42
|
+
|
|
43
|
+
def test_injection_shaped_skill_rejected(self):
|
|
44
|
+
rc, out = run(["myorg", "Case", "Origin", "Bad; DROP", "Phone"], scrub_sf=True)
|
|
45
|
+
self.assertEqual(rc, 1, out)
|
|
46
|
+
self.assertIn("Invalid skill", out)
|
|
47
|
+
|
|
48
|
+
def test_xml_injection_value_rejected(self):
|
|
49
|
+
rc, out = run(["myorg", "Case", "Origin", "Voice", "<x>&bad"], scrub_sf=True)
|
|
50
|
+
self.assertEqual(rc, 1, out)
|
|
51
|
+
self.assertIn("Invalid value", out)
|
|
52
|
+
|
|
53
|
+
def test_bad_related_entity_rejected(self):
|
|
54
|
+
rc, out = run(["myorg", "Case Origin", "Origin", "Voice", "Phone"], scrub_sf=True)
|
|
55
|
+
self.assertEqual(rc, 1, out)
|
|
56
|
+
self.assertIn("Invalid related_entity", out)
|
|
57
|
+
|
|
58
|
+
def test_out_of_range_skill_level_rejected(self):
|
|
59
|
+
rc, out = run(["myorg", "Case", "Origin", "Voice", "Phone"], scrub_sf=True,
|
|
60
|
+
extra_env={"SKILL_LEVEL": "11"})
|
|
61
|
+
self.assertEqual(rc, 1, out)
|
|
62
|
+
self.assertIn("Invalid SKILL_LEVEL", out)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ProductionGuardTests(unittest.TestCase):
|
|
66
|
+
def test_refuses_production_org(self):
|
|
67
|
+
with FakeSf(_FAKE_SF_PROD) as p:
|
|
68
|
+
rc, out = run(["prodorg", "Case", "Origin", "Voice", "Phone"], path_prefix=p)
|
|
69
|
+
self.assertEqual(rc, 1, out)
|
|
70
|
+
v = _last_json(out)
|
|
71
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
72
|
+
self.assertIn("production", v["blocking_issue"].lower())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConfigureTests(unittest.TestCase):
|
|
76
|
+
def test_create_deploys_rule(self):
|
|
77
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
78
|
+
rc, out = run(["sandbox", "Case", "Case.Origin", "Voice", "Phone"], path_prefix=p,
|
|
79
|
+
extra_env={"FAKE_STATE_WSR": "Created"})
|
|
80
|
+
self.assertEqual(rc, 0, out)
|
|
81
|
+
v = _last_json(out)
|
|
82
|
+
self.assertEqual(v["status"], "created", out)
|
|
83
|
+
self.assertEqual(v["rule"]["related_entity"], "Case", out)
|
|
84
|
+
self.assertEqual(v["attribute"]["skill"], "Voice", out)
|
|
85
|
+
self.assertEqual(v["attribute"]["value"], "Phone", out)
|
|
86
|
+
|
|
87
|
+
def test_bare_standard_field_is_entity_qualified(self):
|
|
88
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
89
|
+
rc, out = run(["sandbox", "Case", "Priority", "Voice", "High"], path_prefix=p,
|
|
90
|
+
extra_env={"FAKE_STATE_WSR": "Created"})
|
|
91
|
+
self.assertEqual(rc, 0, out)
|
|
92
|
+
self.assertEqual(_last_json(out)["attribute"]["field"], "Case.Priority", out)
|
|
93
|
+
|
|
94
|
+
def test_reused_when_unchanged(self):
|
|
95
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
96
|
+
rc, out = run(["sandbox", "Case", "Origin", "Voice", "Phone"], path_prefix=p,
|
|
97
|
+
extra_env={"FAKE_STATE_WSR": "Unchanged"})
|
|
98
|
+
self.assertEqual(rc, 0, out)
|
|
99
|
+
v = _last_json(out)
|
|
100
|
+
self.assertEqual(v["status"], "reused", out)
|
|
101
|
+
|
|
102
|
+
def test_blocks_when_referenced_skill_missing(self):
|
|
103
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
104
|
+
rc, out = run(["sandbox", "Case", "Origin", "Voice", "Phone"], path_prefix=p,
|
|
105
|
+
extra_env={"FAKE_SKILL_PRESENT": "0"})
|
|
106
|
+
self.assertEqual(rc, 1, out)
|
|
107
|
+
v = _last_json(out)
|
|
108
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
109
|
+
self.assertTrue(any(a.get("target_skill") == "service-omni-skills-based-routing-configure"
|
|
110
|
+
for a in v["manual_actions"]), out)
|
|
111
|
+
|
|
112
|
+
def test_graceful_degrade_when_feature_unavailable(self):
|
|
113
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
114
|
+
rc, out = run(["sandbox", "Case", "Origin", "Voice", "Phone"], path_prefix=p,
|
|
115
|
+
extra_env={"FAKE_DEPLOY_FAIL": "1"})
|
|
116
|
+
self.assertEqual(rc, 1, out)
|
|
117
|
+
v = _last_json(out)
|
|
118
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
119
|
+
self.assertIn("not available", v["blocking_issue"].lower())
|
|
120
|
+
|
|
121
|
+
def test_plan_only_reports_without_writing(self):
|
|
122
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
123
|
+
calllog = str(Path(p) / "calls.log")
|
|
124
|
+
rc, out = run(["sandbox", "Case", "Origin", "Voice", "Phone"], path_prefix=p,
|
|
125
|
+
extra_env={"PLAN_ONLY": "1", "FAKE_SF_CALLLOG": calllog})
|
|
126
|
+
log = Path(calllog).read_text() if Path(calllog).exists() else ""
|
|
127
|
+
self.assertEqual(rc, 0, out)
|
|
128
|
+
v = _last_json(out)
|
|
129
|
+
self.assertTrue(v.get("plan_mode"), out)
|
|
130
|
+
self.assertEqual(v["status"], "action_needed", out)
|
|
131
|
+
self.assertNotIn("project deploy", log, "plan mode must not deploy")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MergeSafeTests(unittest.TestCase):
|
|
135
|
+
def test_preserves_existing_mapping(self):
|
|
136
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
137
|
+
rc, out = run(["sandbox", "Case", "Origin", "Voice_Support", "Phone"], path_prefix=p,
|
|
138
|
+
extra_env={"FAKE_WSR_EXISTING": "1", "FAKE_STATE_WSR": "Changed"})
|
|
139
|
+
self.assertEqual(rc, 0, out)
|
|
140
|
+
merge = _last_json(out)["merge"]
|
|
141
|
+
self.assertEqual(merge["attribute_count"], 2, out)
|
|
142
|
+
self.assertEqual(merge["preserved_count"], 1, out)
|
|
143
|
+
|
|
144
|
+
def test_omits_null_optional_values_from_preserved_mapping(self):
|
|
145
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
146
|
+
rc, out = run(["sandbox", "Case", "Origin", "Voice_Support", "Phone"], path_prefix=p,
|
|
147
|
+
extra_env={"FAKE_WSR_EXISTING": "1", "FAKE_WSR_NULL_OPTIONALS": "1",
|
|
148
|
+
"FAKE_STATE_WSR": "Changed"})
|
|
149
|
+
self.assertEqual(rc, 0, out)
|
|
150
|
+
self.assertEqual(_last_json(out)["merge"]["preserved_count"], 1, out)
|
|
151
|
+
|
|
152
|
+
def test_inconclusive_read_blocks_destructive_replace(self):
|
|
153
|
+
with FakeSf(_FAKE_SF_SANDBOX) as p:
|
|
154
|
+
rc, out = run(["sandbox", "Case", "Origin", "Voice_Support", "Phone"], path_prefix=p,
|
|
155
|
+
extra_env={"FAKE_WSR_INCONCLUSIVE": "1"})
|
|
156
|
+
self.assertEqual(rc, 1, out)
|
|
157
|
+
self.assertIn("WSR_REPLACE", _last_json(out)["blocking_issue"])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
unittest.main(verbosity=2)
|