@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,216 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# deploy-and-report.sh - populate the Omni Supervisor action + tab surface on an existing OmniSupervisorConfig by inserting OmniSupervisorConfigAction / OmniSupervisorConfigTab rows via the Data API. Idempotent (inserts only the missing types). Args, env, exit codes: SKILL.md; type vocabulary + reference-only exclusions: references/api-notes.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]"}' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
ORG="$1"
|
|
12
|
+
CONFIG_DN="${2:-}"
|
|
13
|
+
|
|
14
|
+
# Reference-free standard types (safe to insert as bare companion rows).
|
|
15
|
+
DEFAULT_ACTIONS="AllAgents.ChangeQueues,AllAgents.ChangeSkills,AllAgents.ChangeGroups,AllAgents.AssignLearning,QueuesBacklog.ManageQueues"
|
|
16
|
+
DEFAULT_TABS="Wallboard,Agents,QueuesBacklog,AssignedWork,SkillsBacklog"
|
|
17
|
+
SAFE_ACTIONS="AllAgents.ChangeQueues AllAgents.ChangeSkills AllAgents.ChangeGroups AllAgents.AssignLearning QueuesBacklog.ManageQueues"
|
|
18
|
+
SAFE_TABS="Wallboard Agents QueuesBacklog AssignedWork SkillsBacklog Reports Alerts"
|
|
19
|
+
|
|
20
|
+
ACTIONS_CSV="${SUPERVISOR_ACTIONS:-$DEFAULT_ACTIONS}"
|
|
21
|
+
TABS_CSV="${SUPERVISOR_TABS:-$DEFAULT_TABS}"
|
|
22
|
+
|
|
23
|
+
if [ -n "$CONFIG_DN" ] && ! [[ "$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
|
+
|
|
28
|
+
if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
|
|
29
|
+
echo "{\"error\":\"Org alias '$ORG' is not authenticated. Run: sf org login web -a $ORG -r <my-domain-url>\"}" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
emit_blocked() {
|
|
34
|
+
local msg="$1"; local target_skill="${2:-}"
|
|
35
|
+
local manual_actions='[]'
|
|
36
|
+
if [ -n "$target_skill" ]; then
|
|
37
|
+
manual_actions=$(jq -n --arg ts "$target_skill" '[{id:"MISSING_PREREQ",title:"Run prerequisite skill",target_skill:$ts}]')
|
|
38
|
+
fi
|
|
39
|
+
EMITTED=1
|
|
40
|
+
jq -n --arg msg "$msg" --arg cdn "${CONFIG_DN:-}" --argjson ma "$manual_actions" \
|
|
41
|
+
'{skill:"service-omni-supervisor-surface-deploy",status:"blocked",
|
|
42
|
+
config:{developer_name:(if $cdn=="" then null else $cdn end),id:null},
|
|
43
|
+
actions:{requested:[],created:[],reused:[],count:0},
|
|
44
|
+
tabs:{requested:[],created:[],reused:[],count:0},
|
|
45
|
+
manual_actions:$ma,blocking_issue:$msg}'
|
|
46
|
+
exit 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Fail-safe: under `set -euo pipefail` an unguarded jq/sf failure could otherwise kill the
|
|
50
|
+
# script with empty stdout. Emit a structured blocked JSON on any exit before a real result so
|
|
51
|
+
# the coordinator never parses empty output. EMITTED flips to 1 before each intentional emit.
|
|
52
|
+
EMITTED=0
|
|
53
|
+
on_exit() {
|
|
54
|
+
if [ "$EMITTED" != "1" ]; then
|
|
55
|
+
jq -n --arg cdn "${CONFIG_DN:-}" \
|
|
56
|
+
'{skill:"service-omni-supervisor-surface-deploy",status:"blocked",
|
|
57
|
+
config:{developer_name:(if $cdn=="" then null else $cdn end),id:null},
|
|
58
|
+
actions:{requested:[],created:[],reused:[],count:0},
|
|
59
|
+
tabs:{requested:[],created:[],reused:[],count:0},
|
|
60
|
+
manual_actions:[],
|
|
61
|
+
blocking_issue:"Supervisor surface deploy aborted before completing (unexpected error; see stderr). Re-run."}'
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
trap on_exit EXIT
|
|
65
|
+
|
|
66
|
+
# Split a CSV into a space list, trimming blanks.
|
|
67
|
+
csv_to_items() { printf '%s' "$1" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$' || true; }
|
|
68
|
+
|
|
69
|
+
# Reject types that need an external reference (custom action, FlexiPage, AWS, AI) - a bare
|
|
70
|
+
# companion row cannot point at them (only OmniSupervisorConfigId/Type/DisplayOrder are createable).
|
|
71
|
+
requires_reference() {
|
|
72
|
+
case "$1" in
|
|
73
|
+
*CustomAction|*AWSDashboard|FlexipageType|AIAgents|AgentforceSDR) return 0 ;;
|
|
74
|
+
*) return 1 ;;
|
|
75
|
+
esac
|
|
76
|
+
}
|
|
77
|
+
in_list() { local needle="$1"; shift; local x; for x in $*; do [ "$x" = "$needle" ] && return 0; done; return 1; }
|
|
78
|
+
|
|
79
|
+
# ---- validate requested types up front (reject before any write) ----
|
|
80
|
+
REQ_ACTIONS=(); while IFS= read -r a; do REQ_ACTIONS+=("$a"); done < <(csv_to_items "$ACTIONS_CSV")
|
|
81
|
+
REQ_TABS=(); while IFS= read -r t; do REQ_TABS+=("$t"); done < <(csv_to_items "$TABS_CSV")
|
|
82
|
+
|
|
83
|
+
for a in "${REQ_ACTIONS[@]:-}"; do
|
|
84
|
+
[ -z "$a" ] && continue
|
|
85
|
+
if requires_reference "$a"; then
|
|
86
|
+
emit_blocked "Action type '$a' needs an external reference (custom action / AWS dashboard) that a bare OmniSupervisorConfigAction row cannot carry; configure it in Setup. Supported reference-free types: $SAFE_ACTIONS."
|
|
87
|
+
fi
|
|
88
|
+
in_list "$a" $SAFE_ACTIONS || emit_blocked "Unsupported action type '$a'. Supported reference-free types: $SAFE_ACTIONS."
|
|
89
|
+
done
|
|
90
|
+
for t in "${REQ_TABS[@]:-}"; do
|
|
91
|
+
[ -z "$t" ] && continue
|
|
92
|
+
if requires_reference "$t"; then
|
|
93
|
+
emit_blocked "Tab type '$t' needs an external reference (FlexiPage / AI agent) that a bare OmniSupervisorConfigTab row cannot carry; configure it in Setup. Supported reference-free types: $SAFE_TABS."
|
|
94
|
+
fi
|
|
95
|
+
in_list "$t" $SAFE_TABS || emit_blocked "Unsupported tab type '$t'. Supported reference-free types: $SAFE_TABS."
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
# Step 1 - safe_to_write guard
|
|
99
|
+
ORG_JSON=$(sf data query --target-org "$ORG" \
|
|
100
|
+
--query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" --json 2>/dev/null) \
|
|
101
|
+
|| emit_blocked "Failed to query Organization for the safe_to_write guard."
|
|
102
|
+
IS_SANDBOX=$(echo "$ORG_JSON" | jq -r '.result.records[0].IsSandbox')
|
|
103
|
+
TRIAL_EXP=$(echo "$ORG_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
|
|
104
|
+
ORG_TYPE=$(echo "$ORG_JSON" | jq -r '.result.records[0].OrganizationType')
|
|
105
|
+
SAFE_TO_WRITE=false
|
|
106
|
+
if [ "$IS_SANDBOX" = "true" ] || [ "$TRIAL_EXP" != "null" ] \
|
|
107
|
+
|| [ "$ORG_TYPE" = "Developer Edition" ] || [ "$ORG_TYPE" = "Base Edition" ]; then
|
|
108
|
+
SAFE_TO_WRITE=true
|
|
109
|
+
fi
|
|
110
|
+
[ "$SAFE_TO_WRITE" = "true" ] || emit_blocked "Refusing to write the supervisor surface to a real production customer org (IsSandbox=$IS_SANDBOX, TrialExpirationDate=$TRIAL_EXP, OrganizationType=$ORG_TYPE). Target a sandbox, trial CDO, or Developer Edition org."
|
|
111
|
+
|
|
112
|
+
# Step 2 - resolve the OmniSupervisorConfig (by DeveloperName, else the org's sole config)
|
|
113
|
+
if [ -n "$CONFIG_DN" ]; then
|
|
114
|
+
CFG_JSON=$(sf data query --target-org "$ORG" --json \
|
|
115
|
+
--query "SELECT Id, DeveloperName FROM OmniSupervisorConfig WHERE DeveloperName='$CONFIG_DN'" 2>/dev/null || echo '{}')
|
|
116
|
+
else
|
|
117
|
+
CFG_JSON=$(sf data query --target-org "$ORG" --json \
|
|
118
|
+
--query "SELECT Id, DeveloperName FROM OmniSupervisorConfig ORDER BY CreatedDate LIMIT 2" 2>/dev/null || echo '{}')
|
|
119
|
+
fi
|
|
120
|
+
CFG_COUNT=$(echo "$CFG_JSON" | jq -r '.result.records | length' 2>/dev/null || echo 0)
|
|
121
|
+
if [ "${CFG_COUNT:-0}" -eq 0 ]; then
|
|
122
|
+
emit_blocked "No OmniSupervisorConfig found${CONFIG_DN:+ named '$CONFIG_DN'} on this org. Deploy it first." "service-omni-supervisor-config-deploy"
|
|
123
|
+
fi
|
|
124
|
+
if [ -z "$CONFIG_DN" ] && [ "$CFG_COUNT" -gt 1 ]; then
|
|
125
|
+
NAMES=$(echo "$CFG_JSON" | jq -r '[.result.records[].DeveloperName] | join(", ")')
|
|
126
|
+
emit_blocked "Multiple OmniSupervisorConfigs exist ($NAMES); pass one as the config_developer_name argument."
|
|
127
|
+
fi
|
|
128
|
+
CONFIG_ID=$(echo "$CFG_JSON" | jq -r '.result.records[0].Id')
|
|
129
|
+
CONFIG_DN=$(echo "$CFG_JSON" | jq -r '.result.records[0].DeveloperName')
|
|
130
|
+
|
|
131
|
+
# Step 3 - read existing surface rows (idempotency baseline). A failed/inconclusive read must
|
|
132
|
+
# NOT fall through to an empty baseline, or we would re-insert rows that already exist. Require a
|
|
133
|
+
# definitive .result.records array; otherwise block.
|
|
134
|
+
EX_ACTIONS_JSON=$(sf data query --target-org "$ORG" --json \
|
|
135
|
+
--query "SELECT OmniSupervisorActionType, DisplayOrder FROM OmniSupervisorConfigAction WHERE OmniSupervisorConfigId='$CONFIG_ID'" 2>/dev/null) \
|
|
136
|
+
|| emit_blocked "Failed to read existing OmniSupervisorConfigAction rows (inconclusive; refusing to insert to avoid duplicates)."
|
|
137
|
+
echo "$EX_ACTIONS_JSON" | jq -e '.result.records' >/dev/null 2>&1 \
|
|
138
|
+
|| emit_blocked "OmniSupervisorConfigAction baseline read returned no records array (inconclusive; refusing to insert to avoid duplicates)."
|
|
139
|
+
EX_TABS_JSON=$(sf data query --target-org "$ORG" --json \
|
|
140
|
+
--query "SELECT OmniSupervisorTabType, DisplayOrder FROM OmniSupervisorConfigTab WHERE OmniSupervisorConfigId='$CONFIG_ID'" 2>/dev/null) \
|
|
141
|
+
|| emit_blocked "Failed to read existing OmniSupervisorConfigTab rows (inconclusive; refusing to insert to avoid duplicates)."
|
|
142
|
+
echo "$EX_TABS_JSON" | jq -e '.result.records' >/dev/null 2>&1 \
|
|
143
|
+
|| emit_blocked "OmniSupervisorConfigTab baseline read returned no records array (inconclusive; refusing to insert to avoid duplicates)."
|
|
144
|
+
EXIST_ACTIONS=$(echo "$EX_ACTIONS_JSON" | jq -r '[.result.records[]?.OmniSupervisorActionType] | @json' 2>/dev/null || echo '[]')
|
|
145
|
+
EXIST_TABS=$(echo "$EX_TABS_JSON" | jq -r '[.result.records[]?.OmniSupervisorTabType] | @json' 2>/dev/null || echo '[]')
|
|
146
|
+
MAX_ACTION_ORDER=$(echo "$EX_ACTIONS_JSON" | jq -r '[.result.records[]?.DisplayOrder // 0] | max // 0' 2>/dev/null || echo 0)
|
|
147
|
+
MAX_TAB_ORDER=$(echo "$EX_TABS_JSON" | jq -r '[.result.records[]?.DisplayOrder // 0] | max // 0' 2>/dev/null || echo 0)
|
|
148
|
+
|
|
149
|
+
has_existing() { echo "$1" | jq -e --arg v "$2" 'index($v) != null' >/dev/null 2>&1; }
|
|
150
|
+
|
|
151
|
+
# PLAN_ONLY - report intent, no write.
|
|
152
|
+
if [ "${PLAN_ONLY:-}" = "1" ]; then
|
|
153
|
+
EMITTED=1
|
|
154
|
+
jq -n --arg cdn "$CONFIG_DN" --arg cid "$CONFIG_ID" \
|
|
155
|
+
--argjson ra "$(printf '%s\n' "${REQ_ACTIONS[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \
|
|
156
|
+
--argjson rt "$(printf '%s\n' "${REQ_TABS[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \
|
|
157
|
+
--argjson ea "$EXIST_ACTIONS" --argjson et "$EXIST_TABS" \
|
|
158
|
+
'{skill:"service-omni-supervisor-surface-deploy",status:"action_needed",plan_mode:true,
|
|
159
|
+
config:{developer_name:$cdn,id:$cid},
|
|
160
|
+
plan_detail:("Would ensure \($ra|length) action type(s) and \($rt|length) tab type(s) on OmniSupervisorConfig \($cdn); existing actions=\($ea|length), tabs=\($et|length). Inserts only the missing types."),
|
|
161
|
+
actions:{requested:$ra,created:[],reused:$ea,count:($ea|length)},
|
|
162
|
+
tabs:{requested:$rt,created:[],reused:$et,count:($et|length)},
|
|
163
|
+
manual_actions:[],blocking_issue:null}'
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Step 4 - insert missing action rows
|
|
168
|
+
CREATED_ACTIONS="[]"; REUSED_ACTIONS="[]"; ORDER=$MAX_ACTION_ORDER
|
|
169
|
+
for a in "${REQ_ACTIONS[@]:-}"; do
|
|
170
|
+
[ -z "$a" ] && continue
|
|
171
|
+
if has_existing "$EXIST_ACTIONS" "$a"; then
|
|
172
|
+
REUSED_ACTIONS=$(echo "$REUSED_ACTIONS" | jq --arg v "$a" '. + [$v]')
|
|
173
|
+
continue
|
|
174
|
+
fi
|
|
175
|
+
ORDER=$((ORDER + 1))
|
|
176
|
+
RES=$(sf data create record --target-org "$ORG" --sobject OmniSupervisorConfigAction \
|
|
177
|
+
--values "OmniSupervisorConfigId=$CONFIG_ID OmniSupervisorActionType=$a DisplayOrder=$ORDER" --json 2>/dev/null || true)
|
|
178
|
+
OKID=$(echo "$RES" | jq -r '.result.id // ""' 2>/dev/null)
|
|
179
|
+
if [ -z "$OKID" ] || [ "$OKID" = "null" ]; then
|
|
180
|
+
MSG=$(echo "$RES" | jq -r '.message // "unknown insert error"' 2>/dev/null | tr -d '\n' | head -c 300)
|
|
181
|
+
emit_blocked "Failed to insert OmniSupervisorConfigAction '$a': $MSG"
|
|
182
|
+
fi
|
|
183
|
+
CREATED_ACTIONS=$(echo "$CREATED_ACTIONS" | jq --arg v "$a" '. + [$v]')
|
|
184
|
+
done
|
|
185
|
+
|
|
186
|
+
# Step 5 - insert missing tab rows
|
|
187
|
+
CREATED_TABS="[]"; REUSED_TABS="[]"; ORDER=$MAX_TAB_ORDER
|
|
188
|
+
for t in "${REQ_TABS[@]:-}"; do
|
|
189
|
+
[ -z "$t" ] && continue
|
|
190
|
+
if has_existing "$EXIST_TABS" "$t"; then
|
|
191
|
+
REUSED_TABS=$(echo "$REUSED_TABS" | jq --arg v "$t" '. + [$v]')
|
|
192
|
+
continue
|
|
193
|
+
fi
|
|
194
|
+
ORDER=$((ORDER + 1))
|
|
195
|
+
RES=$(sf data create record --target-org "$ORG" --sobject OmniSupervisorConfigTab \
|
|
196
|
+
--values "OmniSupervisorConfigId=$CONFIG_ID OmniSupervisorTabType=$t DisplayOrder=$ORDER" --json 2>/dev/null || true)
|
|
197
|
+
OKID=$(echo "$RES" | jq -r '.result.id // ""' 2>/dev/null)
|
|
198
|
+
if [ -z "$OKID" ] || [ "$OKID" = "null" ]; then
|
|
199
|
+
MSG=$(echo "$RES" | jq -r '.message // "unknown insert error"' 2>/dev/null | tr -d '\n' | head -c 300)
|
|
200
|
+
emit_blocked "Failed to insert OmniSupervisorConfigTab '$t': $MSG"
|
|
201
|
+
fi
|
|
202
|
+
CREATED_TABS=$(echo "$CREATED_TABS" | jq --arg v "$t" '. + [$v]')
|
|
203
|
+
done
|
|
204
|
+
|
|
205
|
+
CREATED_TOTAL=$(( $(echo "$CREATED_ACTIONS" | jq 'length') + $(echo "$CREATED_TABS" | jq 'length') ))
|
|
206
|
+
STATUS="reused"; [ "$CREATED_TOTAL" -gt 0 ] && STATUS="created"
|
|
207
|
+
|
|
208
|
+
EMITTED=1
|
|
209
|
+
jq -n --arg status "$STATUS" --arg cdn "$CONFIG_DN" --arg cid "$CONFIG_ID" \
|
|
210
|
+
--argjson ca "$CREATED_ACTIONS" --argjson rua "$REUSED_ACTIONS" \
|
|
211
|
+
--argjson ct "$CREATED_TABS" --argjson rut "$REUSED_TABS" \
|
|
212
|
+
'{skill:"service-omni-supervisor-surface-deploy",status:$status,
|
|
213
|
+
config:{developer_name:$cdn,id:$cid},
|
|
214
|
+
actions:{requested:(($ca+$rua)|sort),created:$ca,reused:$rua,count:(($ca+$rua)|length)},
|
|
215
|
+
tabs:{requested:(($ct+$rut)|sort),created:$ct,reused:$rut,count:(($ct+$rut)|length)},
|
|
216
|
+
manual_actions:[],blocking_issue:null}'
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Shared paths + fake-`sf` harness for the supervisor-surface-deploy contract suite.
|
|
2
|
+
|
|
3
|
+
The fake serves the org guard query, config resolution, existing-surface reads, and record
|
|
4
|
+
inserts from env tunables. Org-free and network-free.
|
|
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-supervisor-surface-deploy"
|
|
15
|
+
SCRIPT = f"{SKILL_DIR}/scripts/deploy-and-report.sh"
|
|
16
|
+
|
|
17
|
+
# Tunables (env):
|
|
18
|
+
# FAKE_SANDBOX = true|false (safe_to_write guard; default true)
|
|
19
|
+
# FAKE_CFG_COUNT = 0|1|2 (OmniSupervisorConfig rows; default 1)
|
|
20
|
+
# FAKE_EXIST_ACTIONS = JSON array of types (existing OmniSupervisorConfigAction; default [])
|
|
21
|
+
# FAKE_EXIST_TABS = JSON array of types (existing OmniSupervisorConfigTab; default [])
|
|
22
|
+
# FAKE_CREATE_OK = true|false (record insert result; default true)
|
|
23
|
+
_FAKE_SF = r"""#!/usr/bin/env bash
|
|
24
|
+
sub="${1:-} ${2:-}"; args="$*"
|
|
25
|
+
SANDBOX="${FAKE_SANDBOX:-true}"
|
|
26
|
+
CFG_COUNT="${FAKE_CFG_COUNT:-1}"
|
|
27
|
+
EXIST_ACTIONS="${FAKE_EXIST_ACTIONS:-[]}"
|
|
28
|
+
EXIST_TABS="${FAKE_EXIST_TABS:-[]}"
|
|
29
|
+
CREATE_OK="${FAKE_CREATE_OK:-true}"
|
|
30
|
+
|
|
31
|
+
case "$sub" in
|
|
32
|
+
"org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
|
|
33
|
+
"data query")
|
|
34
|
+
if printf '%s' "$args" | grep -q "FROM Organization"; then
|
|
35
|
+
if [ "$SANDBOX" = "true" ]; then OT="Developer Edition"; else OT="Enterprise Edition"; fi
|
|
36
|
+
echo "{\"status\":0,\"result\":{\"records\":[{\"IsSandbox\":$SANDBOX,\"TrialExpirationDate\":null,\"OrganizationType\":\"$OT\"}]}}"
|
|
37
|
+
elif printf '%s' "$args" | grep -q "FROM OmniSupervisorConfig\b"; then
|
|
38
|
+
case "$CFG_COUNT" in
|
|
39
|
+
0) echo '{"status":0,"result":{"records":[]}}';;
|
|
40
|
+
2) echo '{"status":0,"result":{"records":[{"Id":"0Q2xx0000000001AAA","DeveloperName":"Omni_Supervisor"},{"Id":"0Q2xx0000000002AAA","DeveloperName":"Second_Config"}]}}';;
|
|
41
|
+
*) echo '{"status":0,"result":{"records":[{"Id":"0Q2xx0000000001AAA","DeveloperName":"Omni_Supervisor"}]}}';;
|
|
42
|
+
esac
|
|
43
|
+
elif printf '%s' "$args" | grep -q "FROM OmniSupervisorConfigAction"; then
|
|
44
|
+
echo "{\"status\":0,\"result\":{\"records\":$(printf '%s' "$EXIST_ACTIONS" | jq -c 'to_entries | map({OmniSupervisorActionType:.value, DisplayOrder:(.key+1)})')}}"
|
|
45
|
+
elif printf '%s' "$args" | grep -q "FROM OmniSupervisorConfigTab"; then
|
|
46
|
+
echo "{\"status\":0,\"result\":{\"records\":$(printf '%s' "$EXIST_TABS" | jq -c 'to_entries | map({OmniSupervisorTabType:.value, DisplayOrder:(.key+1)})')}}"
|
|
47
|
+
else
|
|
48
|
+
echo '{"status":0,"result":{"records":[]}}'
|
|
49
|
+
fi
|
|
50
|
+
exit 0;;
|
|
51
|
+
"data create")
|
|
52
|
+
if [ "$CREATE_OK" = "true" ]; then
|
|
53
|
+
echo '{"status":0,"result":{"id":"0Q3xx0000000001AAA","success":true}}'
|
|
54
|
+
else
|
|
55
|
+
echo '{"status":1,"name":"FIELD_INTEGRITY_EXCEPTION","message":"bad value for restricted picklist field"}'
|
|
56
|
+
fi
|
|
57
|
+
exit 0;;
|
|
58
|
+
*) echo '{"status":0,"result":{}}'; exit 0;;
|
|
59
|
+
esac
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
|
|
64
|
+
env = dict(os.environ)
|
|
65
|
+
if scrub_sf:
|
|
66
|
+
kept = [d for d in env.get("PATH", "").split(os.pathsep)
|
|
67
|
+
if d and not os.path.exists(os.path.join(d, "sf"))]
|
|
68
|
+
env["PATH"] = os.pathsep.join(kept)
|
|
69
|
+
if path_prefix:
|
|
70
|
+
env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
|
|
71
|
+
if extra_env:
|
|
72
|
+
env.update({k: str(v) for k, v in extra_env.items()})
|
|
73
|
+
proc = subprocess.run(
|
|
74
|
+
["bash", str(SKILLS_ROOT / SCRIPT), *args],
|
|
75
|
+
capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
|
|
76
|
+
)
|
|
77
|
+
return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FakeSf:
|
|
81
|
+
def __init__(self, body=_FAKE_SF):
|
|
82
|
+
self._body = body
|
|
83
|
+
self._tmp = None
|
|
84
|
+
|
|
85
|
+
def __enter__(self):
|
|
86
|
+
self._tmp = tempfile.mkdtemp(prefix="supsurface-fake-sf-")
|
|
87
|
+
sf = Path(self._tmp) / "sf"
|
|
88
|
+
sf.write_text(self._body)
|
|
89
|
+
sf.chmod(0o755)
|
|
90
|
+
return self._tmp
|
|
91
|
+
|
|
92
|
+
def __exit__(self, *exc):
|
|
93
|
+
import shutil
|
|
94
|
+
if self._tmp:
|
|
95
|
+
shutil.rmtree(self._tmp, ignore_errors=True)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Contract tests for service-omni-supervisor-surface-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 FakeSf, _FAKE_SF, run # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _last_json(out: str) -> dict:
|
|
15
|
+
dec = json.JSONDecoder()
|
|
16
|
+
obj, i, n = None, 0, len(out)
|
|
17
|
+
while i < n:
|
|
18
|
+
if out[i] in "{[":
|
|
19
|
+
try:
|
|
20
|
+
cand, end = dec.raw_decode(out, i)
|
|
21
|
+
if isinstance(cand, dict):
|
|
22
|
+
obj = cand
|
|
23
|
+
i = end
|
|
24
|
+
continue
|
|
25
|
+
except json.JSONDecodeError:
|
|
26
|
+
pass
|
|
27
|
+
i += 1
|
|
28
|
+
assert obj is not None, f"no JSON object found in output:\n{out}"
|
|
29
|
+
return obj
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _detect(env=None, args=("org",)):
|
|
33
|
+
with FakeSf(_FAKE_SF) as p:
|
|
34
|
+
return run(list(args), path_prefix=p, extra_env=env or {})
|
|
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_bad_config_name_rejected(self):
|
|
44
|
+
rc, out = run(["org", "1badname"], scrub_sf=True)
|
|
45
|
+
self.assertEqual(rc, 1, out)
|
|
46
|
+
self.assertIn("Invalid config_developer_name", out)
|
|
47
|
+
|
|
48
|
+
def test_reference_required_action_blocked(self):
|
|
49
|
+
rc, out = _detect({"SUPERVISOR_ACTIONS": "AllAgents.CustomAction"})
|
|
50
|
+
self.assertEqual(rc, 1, out)
|
|
51
|
+
v = _last_json(out)
|
|
52
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
53
|
+
self.assertIn("external reference", v["blocking_issue"], out)
|
|
54
|
+
|
|
55
|
+
def test_reference_required_tab_blocked(self):
|
|
56
|
+
rc, out = _detect({"SUPERVISOR_TABS": "FlexipageType"})
|
|
57
|
+
self.assertEqual(rc, 1, out)
|
|
58
|
+
v = _last_json(out)
|
|
59
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
60
|
+
|
|
61
|
+
def test_unsupported_type_blocked(self):
|
|
62
|
+
rc, out = _detect({"SUPERVISOR_TABS": "NotARealTab"})
|
|
63
|
+
self.assertEqual(rc, 1, out)
|
|
64
|
+
v = _last_json(out)
|
|
65
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
66
|
+
self.assertIn("Unsupported tab type", v["blocking_issue"], out)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SafetyAndResolutionTests(unittest.TestCase):
|
|
70
|
+
def test_production_org_blocked(self):
|
|
71
|
+
rc, out = _detect({"FAKE_SANDBOX": "false"})
|
|
72
|
+
self.assertEqual(rc, 1, out)
|
|
73
|
+
v = _last_json(out)
|
|
74
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
75
|
+
self.assertIn("production", v["blocking_issue"], out)
|
|
76
|
+
|
|
77
|
+
def test_no_config_blocks_with_pointer(self):
|
|
78
|
+
rc, out = _detect({"FAKE_CFG_COUNT": "0"})
|
|
79
|
+
self.assertEqual(rc, 1, out)
|
|
80
|
+
v = _last_json(out)
|
|
81
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
82
|
+
self.assertTrue(any(a.get("target_skill") == "service-omni-supervisor-config-deploy"
|
|
83
|
+
for a in v["manual_actions"]), out)
|
|
84
|
+
|
|
85
|
+
def test_multiple_configs_without_name_blocks(self):
|
|
86
|
+
rc, out = _detect({"FAKE_CFG_COUNT": "2"})
|
|
87
|
+
self.assertEqual(rc, 1, out)
|
|
88
|
+
v = _last_json(out)
|
|
89
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
90
|
+
self.assertIn("Multiple", v["blocking_issue"], out)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class StateModelTests(unittest.TestCase):
|
|
94
|
+
def test_created_on_empty_surface(self):
|
|
95
|
+
rc, out = _detect({"FAKE_EXIST_ACTIONS": "[]", "FAKE_EXIST_TABS": "[]"})
|
|
96
|
+
self.assertEqual(rc, 0, out)
|
|
97
|
+
v = _last_json(out)
|
|
98
|
+
self.assertEqual(v["status"], "created", out)
|
|
99
|
+
self.assertEqual(len(v["actions"]["created"]), 5, out)
|
|
100
|
+
self.assertEqual(len(v["tabs"]["created"]), 5, out)
|
|
101
|
+
self.assertEqual(v["actions"]["reused"], [], out)
|
|
102
|
+
|
|
103
|
+
def test_reused_when_all_present(self):
|
|
104
|
+
acts = json.dumps(["AllAgents.ChangeQueues", "AllAgents.ChangeSkills",
|
|
105
|
+
"AllAgents.ChangeGroups", "AllAgents.AssignLearning",
|
|
106
|
+
"QueuesBacklog.ManageQueues"])
|
|
107
|
+
tabs = json.dumps(["Wallboard", "Agents", "QueuesBacklog", "AssignedWork", "SkillsBacklog"])
|
|
108
|
+
rc, out = _detect({"FAKE_EXIST_ACTIONS": acts, "FAKE_EXIST_TABS": tabs})
|
|
109
|
+
self.assertEqual(rc, 0, out)
|
|
110
|
+
v = _last_json(out)
|
|
111
|
+
self.assertEqual(v["status"], "reused", out)
|
|
112
|
+
self.assertEqual(v["actions"]["created"], [], out)
|
|
113
|
+
self.assertEqual(v["tabs"]["created"], [], out)
|
|
114
|
+
|
|
115
|
+
def test_partial_diff_inserts_only_missing(self):
|
|
116
|
+
rc, out = _detect({
|
|
117
|
+
"FAKE_EXIST_ACTIONS": json.dumps(["AllAgents.ChangeQueues"]),
|
|
118
|
+
"FAKE_EXIST_TABS": json.dumps(["Wallboard"]),
|
|
119
|
+
})
|
|
120
|
+
self.assertEqual(rc, 0, out)
|
|
121
|
+
v = _last_json(out)
|
|
122
|
+
self.assertEqual(v["status"], "created", out)
|
|
123
|
+
self.assertIn("AllAgents.ChangeQueues", v["actions"]["reused"], out)
|
|
124
|
+
self.assertNotIn("AllAgents.ChangeQueues", v["actions"]["created"], out)
|
|
125
|
+
self.assertEqual(len(v["actions"]["created"]), 4, out)
|
|
126
|
+
self.assertEqual(len(v["tabs"]["created"]), 4, out)
|
|
127
|
+
|
|
128
|
+
def test_plan_only_makes_no_writes(self):
|
|
129
|
+
rc, out = _detect({"PLAN_ONLY": "1"})
|
|
130
|
+
self.assertEqual(rc, 0, out)
|
|
131
|
+
v = _last_json(out)
|
|
132
|
+
self.assertEqual(v["status"], "action_needed", out)
|
|
133
|
+
self.assertTrue(v["plan_mode"], out)
|
|
134
|
+
self.assertEqual(v["actions"]["created"], [], out)
|
|
135
|
+
|
|
136
|
+
def test_insert_failure_blocks(self):
|
|
137
|
+
rc, out = _detect({"FAKE_CREATE_OK": "false"})
|
|
138
|
+
self.assertEqual(rc, 1, out)
|
|
139
|
+
v = _last_json(out)
|
|
140
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
141
|
+
self.assertIn("Failed to insert", v["blocking_issue"], out)
|
|
142
|
+
|
|
143
|
+
def test_custom_tab_set_honored(self):
|
|
144
|
+
rc, out = _detect({"SUPERVISOR_TABS": "Wallboard,Reports,Alerts",
|
|
145
|
+
"SUPERVISOR_ACTIONS": "AllAgents.ChangeQueues"})
|
|
146
|
+
self.assertEqual(rc, 0, out)
|
|
147
|
+
v = _last_json(out)
|
|
148
|
+
self.assertEqual(v["status"], "created", out)
|
|
149
|
+
self.assertEqual(sorted(v["tabs"]["created"]), ["Alerts", "Reports", "Wallboard"], out)
|
|
150
|
+
self.assertEqual(v["actions"]["created"], ["AllAgents.ChangeQueues"], out)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: service-omni-supervisor-users-create
|
|
3
|
+
description: "Use to create N Omni-Channel supervisor users on a Salesforce org via Anonymous Apex, using the supervisor{i}.<suffix>@example.com pattern with SOQL-based idempotency (re-runs skip existing usernames). Passwords are set via System.setPassword and handled fail-closed: the wrapper proves no active debug TraceFlag before setting a password, and otherwise leaves the user ACTIVE and reset_required. Triggers: create supervisor users, provision supervisor accounts, scaffold supervisor personas. Do not use on production customer orgs (blocked by the safe_to_write guard), to bind supervisors to OmniSupervisorConfig (service-omni-supervisor-config-deploy), or to assign the ContactCenterSupervisor permission set (service-omni-supervisor-permset-assign)."
|
|
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-supervisor-config-deploy"
|
|
12
|
+
- "service-omni-supervisor-permset-assign"
|
|
13
|
+
accessCheck:
|
|
14
|
+
- type: license
|
|
15
|
+
value: ServiceCloud
|
|
16
|
+
cliTools:
|
|
17
|
+
- tool: ["jq"]
|
|
18
|
+
semver: ">=1.6"
|
|
19
|
+
- tool: ["sf"]
|
|
20
|
+
semver: ">=2.139.6"
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# service-omni-supervisor-users-create
|
|
24
|
+
|
|
25
|
+
Create N supervisor users on a Salesforce org for the classic Omni-Channel Supervisor Configuration (`OmniSupervisorConfig`), which binds named user records via `OmniSupervisorConfigUser`. Users follow a deterministic `supervisor{i}.<suffix>@example.com` pattern so the coordinator can rediscover them across runs. It is the supervisor counterpart to `service-omni-agent-users-create` and shares its detection, password, and idempotency model; the only differences are the username/alias prefixes and the debug-log marker. Binding these users into a config (`service-omni-supervisor-config-deploy`) and granting them supervisor access (`service-omni-supervisor-permset-assign`) are separate leaves.
|
|
26
|
+
|
|
27
|
+
## Inputs
|
|
28
|
+
|
|
29
|
+
Confirm once, up front:
|
|
30
|
+
|
|
31
|
+
- `org-alias` (required, no default) — must resolve via `sf org display`.
|
|
32
|
+
- Supervisor count (optional, default `1`, range `1..5`; the coordinator typically requests 1).
|
|
33
|
+
- Profile name (optional, default `Standard User`). The coordinator overrides this to a Service Cloud profile so supervisors consume Service Cloud licenses; supervisor access itself comes from the standard `ContactCenterSupervisor` permission set assigned later.
|
|
34
|
+
|
|
35
|
+
Usernames and passwords are never accepted from the operator — both are generated (usernames from the org suffix, passwords via Anonymous Apex).
|
|
36
|
+
|
|
37
|
+
## Preconditions and safety
|
|
38
|
+
|
|
39
|
+
- Target org authenticated via `sf` CLI (My Domain URL, not `.lightning.force.com`), Service Cloud license present, `sf` CLI ≥ 2.139.6.
|
|
40
|
+
- The executing user has `PermissionsModifyAllData` and `PermissionsManagePasswordPolicies` (standard on System Administrator).
|
|
41
|
+
- **Production guardrail:** the detect script computes `safe_to_write` as `IsSandbox` OR `TrialExpirationDate != null` OR `OrganizationType` in {Developer Edition, Base Edition}, and the skill blocks with no override when it is false. CDOs, scratch orgs, and dev orgs are permitted.
|
|
42
|
+
|
|
43
|
+
**Password handling (fail-closed).** Passwords are set by Anonymous Apex `System.setPassword` (`sf user password generate` cannot target Apex-inserted users). The literal appears in the inline executeAnonymous debug log and, only when a debug-log TraceFlag is active for the running user, in a queryable `ApexLog`. The wrapper therefore fails closed *before* the first `System.setPassword`: it proves via a SOQL-filtered Tooling API query (`ExpirationDate > now`) that no active TraceFlag exists. If safety cannot be positively proven, it sets no password at all; the user is left ACTIVE, flagged `password_status:"reset_required"`, and a `security_warning` explains why. It never deletes logs. A user whose password could not be set is kept ACTIVE and flagged for reset — never deactivated.
|
|
44
|
+
|
|
45
|
+
## Run
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# read-only preview (never writes)
|
|
49
|
+
bash scripts/detect-and-create.sh plan <org-alias> [count] [profile-name]
|
|
50
|
+
# detect, enforce safe_to_write, then insert only the missing supervisors
|
|
51
|
+
bash scripts/detect-and-create.sh run <org-alias> [count=1] [profile-name="Standard User"]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`detect-and-create.sh` is the canonical entry point: it re-runs detection, enforces the production guard, and only then inserts. Do not call `scripts/run-create.sh` directly — it is internal and does not enforce the guard.
|
|
55
|
+
|
|
56
|
+
## Behavior
|
|
57
|
+
|
|
58
|
+
**Detection.** The detector derives an 8-char suffix from `Organization.Id`, resolves the profile by name, and queries `User` for `supervisor{i}.<suffix>@example.com`, splitting occupied slots into active `existing_users` and `inactive_users`.
|
|
59
|
+
|
|
60
|
+
**Insertion.** The Apex loads `assets/create-supervisors.apex.template`, substitutes `__COUNT__`/`__PROFILE_ID__`/`__SUFFIX__`, and inserts only the missing indexes, re-checking inside the transaction to prevent a single run from double-inserting; across *concurrent* runs this check is not a guarantee (both can pass their pre-query before either commits), so duplicate protection there relies on the global username-uniqueness constraint plus the `DUPLICATE_USERNAME` retry (see references/apex-template-notes.md). Created users get the Service Cloud feature (`UserPermissionsSupportUser=true`); if the profile's license does not allow it, the Apex strips the flag and retries (the permset assign will then block until the user is on a suitable license). Each created user is reported via `SUPERVISOR_CREATED|<id>|<username>|<email>` (no password in the marker — it is set by the separate `System.setPassword` submission).
|
|
61
|
+
|
|
62
|
+
**Inactive occupants.** An inactive user occupying a supervisor slot is not a reusable supervisor and cannot be recreated (usernames are globally unique). It is surfaced as a required manual reactivation and never counted toward the requested slots — counting it would under-provision the config.
|
|
63
|
+
|
|
64
|
+
**Verification.** After insertion the detector re-runs and must show `missing_count == 0`.
|
|
65
|
+
|
|
66
|
+
## Output contract
|
|
67
|
+
|
|
68
|
+
`detect-and-create.sh` emits a single JSON object with `status` ∈ `created` | `partial` | `reused` | `action_needed` | `blocked`, plus `detect`, `create` (`null` when nothing was created), `created_count`, `reused_count`, `total_present_after`, `users_needing_password_reset`, `action_required`, and `safe_to_write`.
|
|
69
|
+
|
|
70
|
+
- `created` — every missing index landed with a working password.
|
|
71
|
+
- `partial` — some landed but not all, any `System.setPassword` failed (kept ACTIVE, listed in `users_needing_password_reset`), or a slot is occupied by an inactive user needing reactivation.
|
|
72
|
+
- `reused` — all requested slots already existed; no DML.
|
|
73
|
+
- `action_needed` — plan mode only.
|
|
74
|
+
- `blocked` — precondition failed.
|
|
75
|
+
|
|
76
|
+
`create.created_users[].password` is populated only for users created this run whose `System.setPassword` succeeded. `create.rolled_back_users` is always empty (this skill never deactivates a user). Generated passwords are a secret: the returned JSON is the only place they appear; a caller that persists stdout must write it only to a restricted `CREDENTIALS.json` (mode 0600), redact it elsewhere, and delete it after distribution — the coordinator does this automatically.
|
|
77
|
+
|
|
78
|
+
## Limitations
|
|
79
|
+
|
|
80
|
+
- Username pattern and org suffix are fixed and never operator-configurable — that determinism is what enables idempotent re-runs.
|
|
81
|
+
- Creates only supervisor users; it never deletes or deactivates users, including orphaned supervisors from prior runs.
|
|
82
|
+
- Common User errors (`DUPLICATE_USERNAME`, `INVALID_EMAIL`, `LICENSE_LIMIT_EXCEEDED`) are translated into operator-friendly messages rather than surfaced raw.
|
|
83
|
+
|
|
84
|
+
## References
|
|
85
|
+
|
|
86
|
+
| File | When to read |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `references/apex-patterns.md` | Before running the Apex — Apex structure, User field defaults, password policy, and the profile-localization risk |
|
|
89
|
+
| `references/apex-template-notes.md` | When creation returns a duplicate, license, password, or trace-safety error |
|
|
90
|
+
| `assets/create-supervisors.apex.template` | Loaded by `scripts/run-create.sh` when missing supervisor users must be inserted |
|
|
91
|
+
| `scripts/detect-existing.sh` | Loaded by `scripts/detect-and-create.sh` for the read-only org, profile, safety, and existing-user checks |
|
|
92
|
+
| `scripts/run-create.sh` | Internal writer loaded by `scripts/detect-and-create.sh` only after guard checks pass |
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// service-omni-supervisor-users-create - Anonymous Apex asset template. Tokens __COUNT__/__PROFILE_ID__/__SUFFIX__ resolved by run-create.sh. Idempotency, password/TraceFlag security note, output contract: references/apex-template-notes.md.
|
|
2
|
+
|
|
3
|
+
Integer requested = __COUNT__;
|
|
4
|
+
String profileId = '__PROFILE_ID__';
|
|
5
|
+
String suffix = '__SUFFIX__';
|
|
6
|
+
|
|
7
|
+
Map<String, Integer> expectedByUsername = new Map<String, Integer>();
|
|
8
|
+
for (Integer i = 1; i <= requested; i++) {
|
|
9
|
+
expectedByUsername.put('supervisor' + i + '.' + suffix + '@example.com', i);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Set<String> existingUsernames = new Set<String>();
|
|
13
|
+
for (User u : [SELECT Username FROM User WHERE Username IN :expectedByUsername.keySet()]) {
|
|
14
|
+
existingUsernames.add(u.Username.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
List<User> toInsert = new List<User>();
|
|
18
|
+
|
|
19
|
+
for (Integer i = 1; i <= requested; i++) {
|
|
20
|
+
String uname = 'supervisor' + i + '.' + suffix + '@example.com';
|
|
21
|
+
if (existingUsernames.contains(uname.toLowerCase())) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Alias must be <= 8 chars - construct as omsu{i}{last-3-of-suffix}
|
|
26
|
+
// For i=1..9: 'omsu' (4) + i (1) + last-3-suffix (3) = 8 chars. OK.
|
|
27
|
+
String aliasStr = 'omsu' + i + suffix.substring(suffix.length() - 3);
|
|
28
|
+
|
|
29
|
+
User u = new User(
|
|
30
|
+
FirstName = 'Omni',
|
|
31
|
+
LastName = 'Supervisor' + i,
|
|
32
|
+
Email = uname,
|
|
33
|
+
Username = uname,
|
|
34
|
+
Alias = aliasStr,
|
|
35
|
+
ProfileId = profileId,
|
|
36
|
+
IsActive = true,
|
|
37
|
+
// Service Cloud feature license - REQUIRED for the ContactCenterSupervisor permset
|
|
38
|
+
// (IsContactCenterSupervisor is license-gated). Only allowed on user licenses that
|
|
39
|
+
// support it (e.g. Salesforce); the try/catch strips it and retries when the profile's
|
|
40
|
+
// user license rejects it (e.g. the limited "Service Cloud" user license). In that
|
|
41
|
+
// fallback the supervisor permset assign will later block until the user is moved to a
|
|
42
|
+
// Salesforce license + Service Cloud feature.
|
|
43
|
+
UserPermissionsSupportUser = true,
|
|
44
|
+
TimeZoneSidKey = 'America/Los_Angeles',
|
|
45
|
+
LocaleSidKey = 'en_US',
|
|
46
|
+
EmailEncodingKey = 'UTF-8',
|
|
47
|
+
LanguageLocaleKey = 'en_US'
|
|
48
|
+
);
|
|
49
|
+
toInsert.add(u);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (toInsert.isEmpty()) {
|
|
53
|
+
System.debug('SUPERVISORS_ALL_EXIST - no inserts required');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
insert toInsert;
|
|
59
|
+
System.debug('SUPERVISORS_SERVICE_CLOUD_FEATURE|enabled');
|
|
60
|
+
} catch (DmlException e) {
|
|
61
|
+
if (e.getMessage() != null && e.getMessage().contains('Service Cloud User')) {
|
|
62
|
+
for (User u : toInsert) { u.UserPermissionsSupportUser = false; }
|
|
63
|
+
insert toInsert;
|
|
64
|
+
System.debug('SUPERVISORS_SERVICE_CLOUD_FEATURE|skipped_license_not_allowed');
|
|
65
|
+
} else {
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (User u : toInsert) {
|
|
71
|
+
System.debug('SUPERVISOR_CREATED|' + u.Id + '|' + u.Username + '|' + u.Email);
|
|
72
|
+
}
|