@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,804 @@
|
|
|
1
|
+
"""Org-independent contract tests for the service-omni-* skills: bash -n syntax, input-contract rejection before the first sf call, safe_to_write refusal, Apex-template token fidelity, and packaged asset well-formedness - all org-free and network-free (CI-safe)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import unittest
|
|
9
|
+
import xml.dom.minidom
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Hyphenated dir name blocks relative imports; add our dir to sys.path so this file runs standalone.
|
|
13
|
+
_THIS_DIR = Path(__file__).resolve().parent
|
|
14
|
+
if str(_THIS_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_THIS_DIR))
|
|
16
|
+
|
|
17
|
+
import _bootstrap # noqa: E402
|
|
18
|
+
from _bootstrap import SKILLS_ROOT, SCRIPTS, _FakeSfDir, _FAKE_SF_SANDBOX, _FAKE_SF_VOICE, _run # noqa: E402
|
|
19
|
+
|
|
20
|
+
_FLOW_SCRIPT = "service-omni-routing-flow-deploy/scripts/deploy-and-report.sh"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SyntaxTests(unittest.TestCase):
|
|
24
|
+
"""(1) Every script must parse under `bash -n`."""
|
|
25
|
+
|
|
26
|
+
def test_all_scripts_parse(self):
|
|
27
|
+
for script in SCRIPTS:
|
|
28
|
+
with self.subTest(script=script):
|
|
29
|
+
proc = subprocess.run(
|
|
30
|
+
["bash", "-n", str(SKILLS_ROOT / script)],
|
|
31
|
+
capture_output=True, text=True,
|
|
32
|
+
)
|
|
33
|
+
self.assertEqual(
|
|
34
|
+
proc.returncode, 0,
|
|
35
|
+
f"{script} failed bash -n: {proc.stderr.strip()}",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InputContractTests(unittest.TestCase):
|
|
40
|
+
"""(2) Deterministic rejection BEFORE the first sf call.
|
|
41
|
+
|
|
42
|
+
Every case runs with `sf` scrubbed off PATH - if a script reached an sf call it
|
|
43
|
+
would fail differently (command-not-found), so a clean expected-exit + expected
|
|
44
|
+
error substring proves the guard fires first.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def assert_reject(self, script, args, want_exit, needle):
|
|
48
|
+
rc, out = _run(script, args, extra_path_scrub=True)
|
|
49
|
+
self.assertEqual(rc, want_exit, f"{script} {args} → exit {rc} (want {want_exit}); out={out[:200]}")
|
|
50
|
+
self.assertRegex(out, re.compile(re.escape(needle), re.IGNORECASE),
|
|
51
|
+
f"{script} {args} output missing '{needle}'; out={out[:200]}")
|
|
52
|
+
|
|
53
|
+
# --- missing required org-alias / too few args ---
|
|
54
|
+
def test_missing_org_rejected(self):
|
|
55
|
+
cases = [
|
|
56
|
+
("service-omni-base-settings-configure/scripts/detect-existing.sh", [], 1, "error"),
|
|
57
|
+
("service-omni-agent-users-create/scripts/detect-existing.sh", [], 1, "error"),
|
|
58
|
+
("service-omni-agent-users-create/scripts/run-create.sh", [], 1, "Usage"),
|
|
59
|
+
("service-omni-supervisor-users-create/scripts/run-create.sh", [], 1, "Usage"),
|
|
60
|
+
("service-omni-permission-set-assign/scripts/verify-and-assign.sh", [], 1, "Usage"),
|
|
61
|
+
("service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh", [], 1, "Usage"),
|
|
62
|
+
("service-omni-queue-members-assign/scripts/verify-and-bind.sh", [], 1, "Usage"),
|
|
63
|
+
("service-omni-queue-deploy/scripts/verify-and-align.sh", [], 1, "Missing"),
|
|
64
|
+
("service-omni-service-channel-configure/scripts/deploy-and-report.sh", [], 1, "Missing"),
|
|
65
|
+
("service-omni-presence-status-deploy/scripts/deploy-and-report.sh", [], 1, "Missing"),
|
|
66
|
+
("service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh", [], 1, "Missing"),
|
|
67
|
+
("service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh", [], 1, "Missing"),
|
|
68
|
+
("service-omni-base-settings-configure/scripts/configure-and-report.sh", [], 1, "Missing"),
|
|
69
|
+
("service-omni-agent-users-create/scripts/detect-and-create.sh", [], 1, "Missing"),
|
|
70
|
+
("service-omni-supervisor-users-create/scripts/detect-and-create.sh", [], 1, "Missing"),
|
|
71
|
+
(_FLOW_SCRIPT, [], 1, "Missing"),
|
|
72
|
+
]
|
|
73
|
+
for script, args, want, needle in cases:
|
|
74
|
+
with self.subTest(script=script):
|
|
75
|
+
self.assert_reject(script, args, want, needle)
|
|
76
|
+
|
|
77
|
+
# --- invalid mode (plan|run) ---
|
|
78
|
+
def test_invalid_mode_rejected(self):
|
|
79
|
+
for script in [
|
|
80
|
+
"service-omni-base-settings-configure/scripts/configure-and-report.sh",
|
|
81
|
+
"service-omni-agent-users-create/scripts/detect-and-create.sh",
|
|
82
|
+
]:
|
|
83
|
+
with self.subTest(script=script):
|
|
84
|
+
self.assert_reject(script, ["bogus", "myorg"], 1, "Invalid mode")
|
|
85
|
+
|
|
86
|
+
# --- coordinator flag / mode / arg validation (exit 2) ---
|
|
87
|
+
def test_coordinator_arg_validation(self):
|
|
88
|
+
drv = "service-omni-channel-setup-coordinate/scripts/integration-driver.sh"
|
|
89
|
+
self.assert_reject(drv, [], 2, "plan or --run")
|
|
90
|
+
self.assert_reject(drv, ["--frobnicate", "myorg"], 2, "Unknown flag")
|
|
91
|
+
self.assert_reject(drv, ["--run"], 2, "Missing required")
|
|
92
|
+
self.assert_reject(drv, ["--run", "org;rm -rf /"], 2, "Invalid org-alias")
|
|
93
|
+
self.assert_reject(drv, ["--run", "myorg", "0"], 2, "Invalid count")
|
|
94
|
+
self.assert_reject(drv, ["--run", "myorg", "3", "Widget"], 2, "Unsupported")
|
|
95
|
+
|
|
96
|
+
# --- out-of-range count ---
|
|
97
|
+
def test_out_of_range_count_rejected(self):
|
|
98
|
+
cases = [
|
|
99
|
+
("service-omni-agent-users-create/scripts/detect-existing.sh", ["myorg", "0"], "range"),
|
|
100
|
+
("service-omni-agent-users-create/scripts/detect-existing.sh", ["myorg", "11"], "range"),
|
|
101
|
+
("service-omni-permission-set-assign/scripts/verify-and-assign.sh", ["myorg", "11"], "Invalid count"),
|
|
102
|
+
("service-omni-queue-members-assign/scripts/verify-and-bind.sh", ["myorg", "CaseQueue", "11"], "Invalid count"),
|
|
103
|
+
("service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh", ["myorg", "6"], "Invalid count"),
|
|
104
|
+
("service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh", ["myorg", "6"], "Invalid supervisor_count"),
|
|
105
|
+
]
|
|
106
|
+
for script, args, needle in cases:
|
|
107
|
+
with self.subTest(script=script, args=args):
|
|
108
|
+
self.assert_reject(script, args, 1, needle)
|
|
109
|
+
|
|
110
|
+
# --- injection-shaped DeveloperName rejected before any sf call ---
|
|
111
|
+
def test_injection_queue_name_rejected(self):
|
|
112
|
+
self.assert_reject(
|
|
113
|
+
"service-omni-queue-deploy/scripts/verify-and-align.sh",
|
|
114
|
+
["myorg", "Case", "Case_Routing_Config", "bad name;DROP"],
|
|
115
|
+
1, "Invalid queue_developer_name",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# --- F10: injection-shaped routing_config_dn (3rd positional) rejected before any sf call ---
|
|
119
|
+
def test_injection_routing_config_dn_rejected(self):
|
|
120
|
+
# A non-empty 3rd arg that is not a well-formed DeveloperName must reject up front.
|
|
121
|
+
self.assert_reject(
|
|
122
|
+
"service-omni-queue-deploy/scripts/verify-and-align.sh",
|
|
123
|
+
["myorg", "Case", "Case_Routing'; DROP", "CaseQueue"],
|
|
124
|
+
1, "Invalid routing_config_developer_name",
|
|
125
|
+
)
|
|
126
|
+
# Leading digit is also invalid (DeveloperName must start with a letter).
|
|
127
|
+
self.assert_reject(
|
|
128
|
+
"service-omni-queue-deploy/scripts/verify-and-align.sh",
|
|
129
|
+
["myorg", "Case", "9bad", "CaseQueue"],
|
|
130
|
+
1, "Invalid routing_config_developer_name",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# --- injection-shaped overflow-assignee (fallback user) rejected before any sf call ---
|
|
134
|
+
def test_injection_overflow_assignee_rejected(self):
|
|
135
|
+
# 7th positional is the overflow/fallback assignee; a non-Username/Id token must reject
|
|
136
|
+
# up front (it is interpolated into a User SOQL WHERE), before the auth/org query.
|
|
137
|
+
self.assert_reject(
|
|
138
|
+
"service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh",
|
|
139
|
+
["myorg", "MostAvailable", "Case_Routing_Config", "Case Routing Config", "5", "1", "bad;DROP TABLE"],
|
|
140
|
+
1, "Invalid overflow-assignee",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# --- F10: injection-shaped queue_developer_name in queue-members rejected before any sf call ---
|
|
144
|
+
def test_injection_queue_member_queue_dn_rejected(self):
|
|
145
|
+
self.assert_reject(
|
|
146
|
+
"service-omni-queue-members-assign/scripts/verify-and-bind.sh",
|
|
147
|
+
["myorg", "bad name;DROP", "3"],
|
|
148
|
+
1, "Invalid queue_developer_name",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# --- F10: injection-shaped permission-set name rejected before the org-mutating query ---
|
|
152
|
+
# The PS-name guard sits AFTER the `sf org display` auth check, so a fake sf must be present
|
|
153
|
+
# for auth to pass; the guard then rejects an injection-shaped name before any data query.
|
|
154
|
+
def test_injection_permission_set_name_rejected(self):
|
|
155
|
+
cases = [
|
|
156
|
+
"service-omni-permission-set-assign/scripts/verify-and-assign.sh",
|
|
157
|
+
"service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh",
|
|
158
|
+
]
|
|
159
|
+
with _FakeSfDir(body=_FAKE_SF_SANDBOX) as fake_dir:
|
|
160
|
+
for script in cases:
|
|
161
|
+
with self.subTest(script=script):
|
|
162
|
+
rc, out = _run(script, ["myorg", "1", "Bad Name; DROP"], path_prefix=fake_dir)
|
|
163
|
+
self.assertEqual(rc, 1, f"{script} → exit {rc} (want 1); out={out[:200]}")
|
|
164
|
+
self.assertRegex(
|
|
165
|
+
out, re.compile("Invalid permission-set name", re.IGNORECASE),
|
|
166
|
+
f"{script} output missing injection rejection; out={out[:200]}",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# --- unsupported sObject type (exit 2) ---
|
|
170
|
+
def test_unsupported_sobject_rejected(self):
|
|
171
|
+
cases = [
|
|
172
|
+
"service-omni-service-channel-configure/scripts/deploy-and-report.sh",
|
|
173
|
+
"service-omni-presence-status-deploy/scripts/deploy-and-report.sh",
|
|
174
|
+
"service-omni-queue-deploy/scripts/verify-and-align.sh",
|
|
175
|
+
]
|
|
176
|
+
for script in cases:
|
|
177
|
+
with self.subTest(script=script):
|
|
178
|
+
self.assert_reject(script, ["myorg", "Widget"], 2, "Unsupported")
|
|
179
|
+
|
|
180
|
+
# --- routing-flow: invalid --target (exit 2) + injection-shaped flow name (exit 1) ---
|
|
181
|
+
def test_routing_flow_invalid_target_rejected(self):
|
|
182
|
+
self.assert_reject(_FLOW_SCRIPT, ["myorg", "--target", "Widget", "--trigger"], 2, "Invalid --target")
|
|
183
|
+
|
|
184
|
+
def test_routing_flow_injection_flow_name_rejected(self):
|
|
185
|
+
self.assert_reject(_FLOW_SCRIPT, ["myorg", "bad;name", "--trigger"], 1, "Invalid flow_developer_name")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class SafeToWriteTests(unittest.TestCase):
|
|
189
|
+
"""(3) A production customer org is hard-refused before any mutation."""
|
|
190
|
+
|
|
191
|
+
#: (label, script, args) - every mutating skill + the coordinator.
|
|
192
|
+
REFUSERS = [
|
|
193
|
+
("base-settings (run)", "service-omni-base-settings-configure/scripts/configure-and-report.sh", ["run", "myorg"]),
|
|
194
|
+
("agent-users (run)", "service-omni-agent-users-create/scripts/detect-and-create.sh", ["run", "myorg", "3"]),
|
|
195
|
+
("supervisor-users (run)", "service-omni-supervisor-users-create/scripts/detect-and-create.sh", ["run", "myorg", "1"]),
|
|
196
|
+
("service-channel", "service-omni-service-channel-configure/scripts/deploy-and-report.sh", ["myorg", "Case"]),
|
|
197
|
+
("qrc", "service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh", ["myorg"]),
|
|
198
|
+
("queue-deploy", "service-omni-queue-deploy/scripts/verify-and-align.sh", ["myorg", "Case"]),
|
|
199
|
+
("queue-members", "service-omni-queue-members-assign/scripts/verify-and-bind.sh", ["myorg", "CaseQueue", "3"]),
|
|
200
|
+
("presence", "service-omni-presence-status-deploy/scripts/deploy-and-report.sh", ["myorg", "Case"]),
|
|
201
|
+
("permset-assign", "service-omni-permission-set-assign/scripts/verify-and-assign.sh", ["myorg", "3"]),
|
|
202
|
+
("supervisor-permset", "service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh", ["myorg", "1"]),
|
|
203
|
+
("supervisor-config", "service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh", ["myorg", "1"]),
|
|
204
|
+
("routing-flow (trigger)", "service-omni-routing-flow-deploy/scripts/deploy-and-report.sh", ["myorg", "--target", "VoiceCall", "--trigger"]),
|
|
205
|
+
("coordinator (run)", "service-omni-channel-setup-coordinate/scripts/integration-driver.sh", ["--run", "myorg", "3", "Case"]),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
_REFUSAL_RE = re.compile(r'refus|production|"?safe_to_write"?[:= ]*false', re.IGNORECASE)
|
|
209
|
+
|
|
210
|
+
def test_production_org_refused(self):
|
|
211
|
+
with _FakeSfDir() as fake_dir:
|
|
212
|
+
for label, script, args in self.REFUSERS:
|
|
213
|
+
with self.subTest(skill=label):
|
|
214
|
+
rc, out = _run(script, args, path_prefix=fake_dir)
|
|
215
|
+
self.assertNotEqual(rc, 0, f"{label}: expected non-zero (refusal), got 0; out={out[:200]}")
|
|
216
|
+
self.assertRegex(out, self._REFUSAL_RE,
|
|
217
|
+
f"{label}: non-zero exit but no refusal signal; out={out[:220]}")
|
|
218
|
+
self.assertNotIn("FAKE_SF_DEPLOY_SHOULD_NOT_BE_REACHED", out,
|
|
219
|
+
f"{label}: reached a deploy despite production guard")
|
|
220
|
+
|
|
221
|
+
def test_sandbox_org_not_refused_negative_control(self):
|
|
222
|
+
"""Discriminating check: a SANDBOX org must NOT trip the production refusal.
|
|
223
|
+
Proves the assertion above isn't trivially green. We pick a skill whose guard
|
|
224
|
+
is query-only (qrc) so on a sandbox it proceeds past the guard (and fails
|
|
225
|
+
later on missing fake data) rather than refusing."""
|
|
226
|
+
with _FakeSfDir(body=_FAKE_SF_SANDBOX) as fake_dir:
|
|
227
|
+
rc, out = _run(
|
|
228
|
+
"service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh",
|
|
229
|
+
["myorg"], path_prefix=fake_dir,
|
|
230
|
+
)
|
|
231
|
+
self.assertNotRegex(
|
|
232
|
+
out, re.compile(r'refus|"?safe_to_write"?[:= ]*false', re.IGNORECASE),
|
|
233
|
+
f"sandbox org unexpectedly triggered a production refusal; out={out[:220]}",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ApexTemplateTests(unittest.TestCase):
|
|
238
|
+
"""(4) User-create Apex templates: token coverage + clean resolution."""
|
|
239
|
+
|
|
240
|
+
TEMPLATES = [
|
|
241
|
+
"service-omni-agent-users-create/assets/create-users.apex.template",
|
|
242
|
+
"service-omni-supervisor-users-create/assets/create-supervisors.apex.template",
|
|
243
|
+
]
|
|
244
|
+
EXPECTED_TOKENS = {"__COUNT__", "__PROFILE_ID__", "__SUFFIX__"}
|
|
245
|
+
|
|
246
|
+
def test_token_set_matches_substitutions(self):
|
|
247
|
+
for tpl in self.TEMPLATES:
|
|
248
|
+
with self.subTest(template=tpl):
|
|
249
|
+
text = (SKILLS_ROOT / tpl).read_text()
|
|
250
|
+
tokens = set(re.findall(r"__[A-Z_]+__", text))
|
|
251
|
+
self.assertEqual(
|
|
252
|
+
tokens, self.EXPECTED_TOKENS,
|
|
253
|
+
f"{tpl} tokens {tokens} != run-create.sh substitutions {self.EXPECTED_TOKENS}",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def test_resolves_to_token_free_apex(self):
|
|
257
|
+
for tpl in self.TEMPLATES:
|
|
258
|
+
with self.subTest(template=tpl):
|
|
259
|
+
text = (SKILLS_ROOT / tpl).read_text()
|
|
260
|
+
resolved = (text.replace("__COUNT__", "3")
|
|
261
|
+
.replace("__PROFILE_ID__", "00exx0000000000AAA")
|
|
262
|
+
.replace("__SUFFIX__", "abc12345"))
|
|
263
|
+
leftover = re.findall(r"__[A-Z_]+__", resolved)
|
|
264
|
+
self.assertEqual(leftover, [], f"{tpl} left residual tokens: {set(leftover)}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class DeployAssetTests(unittest.TestCase):
|
|
268
|
+
"""(5) Packaged deploy assets are well-formed."""
|
|
269
|
+
|
|
270
|
+
def test_all_xml_well_formed(self):
|
|
271
|
+
xmls = sorted(SKILLS_ROOT.glob("service-omni-*/assets/**/*.xml"))
|
|
272
|
+
self.assertTrue(xmls, "expected at least one packaged .xml asset")
|
|
273
|
+
for asset in xmls:
|
|
274
|
+
with self.subTest(asset=str(asset.relative_to(SKILLS_ROOT))):
|
|
275
|
+
try:
|
|
276
|
+
xml.dom.minidom.parse(str(asset))
|
|
277
|
+
except Exception as e: # noqa: BLE001 - surface any parse failure as a test failure
|
|
278
|
+
self.fail(f"{asset} is not well-formed XML: {e}")
|
|
279
|
+
|
|
280
|
+
def test_all_json_parses(self):
|
|
281
|
+
jsons = sorted(SKILLS_ROOT.glob("service-omni-*/assets/**/*.json"))
|
|
282
|
+
self.assertTrue(jsons, "expected at least one packaged .json asset")
|
|
283
|
+
for j in jsons:
|
|
284
|
+
with self.subTest(asset=str(j.relative_to(SKILLS_ROOT))):
|
|
285
|
+
try:
|
|
286
|
+
json.loads(j.read_text())
|
|
287
|
+
except Exception as e: # noqa: BLE001
|
|
288
|
+
self.fail(f"{j} is not valid JSON: {e}")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class VoiceRoutingFlowTests(unittest.TestCase):
|
|
292
|
+
"""(6) Voice steel-thread behaviors for service-omni-routing-flow-deploy, driven by the
|
|
293
|
+
parameterized _FAKE_SF_VOICE sandbox org (org-free, network-free, deterministic).
|
|
294
|
+
|
|
295
|
+
Covers:
|
|
296
|
+
- fail closed when the ServiceChannel does not resolve.
|
|
297
|
+
- reuse only when the active flow's embedded bindings still match; redeploy on drift.
|
|
298
|
+
- runtime proof is blocking under --require-proof, and the throwaway record is cleaned up.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
#: Cross-skill handoff the coordinator supplies - the SAME channel/queue/QRC discovered upstream.
|
|
302
|
+
HANDOFF = {
|
|
303
|
+
"SERVICE_CHANNEL_DEVELOPER_NAME": "sfdc_phone",
|
|
304
|
+
"QUEUE_DEVELOPER_NAME": "VoiceQueue",
|
|
305
|
+
"ROUTING_CONFIG_DEVELOPER_NAME": "Voice_Routing_Config",
|
|
306
|
+
# Drive the async poll with zero real waiting.
|
|
307
|
+
"OMNI_PROOF_POLL_TRIES": "1",
|
|
308
|
+
"OMNI_PROOF_POLL_SLEEP": "0",
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
def _run_flow(self, args, env):
|
|
312
|
+
merged = dict(self.HANDOFF)
|
|
313
|
+
merged.update(env)
|
|
314
|
+
with _FakeSfDir(body=_FAKE_SF_VOICE) as fake_dir:
|
|
315
|
+
return _run(_FLOW_SCRIPT, args, path_prefix=fake_dir, extra_env=merged)
|
|
316
|
+
|
|
317
|
+
# --- unresolved ServiceChannel must block, never deploy a flow with an empty channel id ---
|
|
318
|
+
def test_p0_fail_closed_when_service_channel_missing(self):
|
|
319
|
+
rc, out = self._run_flow(
|
|
320
|
+
["myorg", "--target", "VoiceCall", "--trigger"],
|
|
321
|
+
{"FAKE_SC_PRESENT": "0"},
|
|
322
|
+
)
|
|
323
|
+
self.assertEqual(rc, 1, f"expected block on missing ServiceChannel; out={out[:300]}")
|
|
324
|
+
self.assertRegex(out, re.compile(r'ServiceChannel .*not found', re.IGNORECASE), out[:300])
|
|
325
|
+
self.assertIn('"status": "blocked"', out)
|
|
326
|
+
|
|
327
|
+
# --- happy deploy: channel present, no active flow yet → Created ---
|
|
328
|
+
def test_deploys_when_channel_present(self):
|
|
329
|
+
rc, out = self._run_flow(
|
|
330
|
+
["myorg", "--target", "VoiceCall", "--trigger"],
|
|
331
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "0"},
|
|
332
|
+
)
|
|
333
|
+
self.assertEqual(rc, 0, f"expected clean deploy; out={out[:300]}")
|
|
334
|
+
self.assertRegex(out, re.compile(r'"status":\s*"created"'), out[:300])
|
|
335
|
+
# The resolved ServiceChannel id (0N9...) from the handoff channel must be embedded, not empty.
|
|
336
|
+
self.assertIn("0N9000000000001AAA", out)
|
|
337
|
+
# P1 round-trip proof: the reported active version id must come from FlowDefinitionView.
|
|
338
|
+
self.assertIn("301000000000009AAA", out)
|
|
339
|
+
self.assertRegex(out, re.compile(r'"flow_active_version_id":\s*"301000000000009AAA"'), out[:600])
|
|
340
|
+
|
|
341
|
+
# --- P1 round-trip proof: deploy ok but NO active flow version → block (never claim active) ---
|
|
342
|
+
def test_blocks_when_no_active_flow_version_after_deploy(self):
|
|
343
|
+
rc, out = self._run_flow(
|
|
344
|
+
["myorg", "--target", "VoiceCall", "--trigger"],
|
|
345
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "0", "FAKE_FLOW_ACTIVE_RT": "0"},
|
|
346
|
+
)
|
|
347
|
+
self.assertEqual(rc, 1, f"expected block when no active version; out={out[:300]}")
|
|
348
|
+
self.assertRegex(out, re.compile(r'"status":\s*"blocked"'), out[:300])
|
|
349
|
+
self.assertRegex(out, re.compile(r'active version', re.IGNORECASE), out[:500])
|
|
350
|
+
|
|
351
|
+
def test_uses_tooling_flow_when_flow_definition_view_fails(self):
|
|
352
|
+
rc, out = self._run_flow(
|
|
353
|
+
["myorg", "--target", "VoiceCall", "--trigger"],
|
|
354
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "1", "FAKE_FLOW_VIEW_ERROR": "1"},
|
|
355
|
+
)
|
|
356
|
+
self.assertEqual(rc, 0, f"expected Tooling Flow fallback; out={out[:500]}")
|
|
357
|
+
self.assertRegex(out, re.compile(r'"status":\s*"reused"'), out[:500])
|
|
358
|
+
self.assertRegex(out, re.compile(r'"flow_active_version_id":\s*"301000000000001AAA"'), out[:700])
|
|
359
|
+
|
|
360
|
+
# --- P1 idempotency: active flow whose bindings still match → reused (no redeploy) ---
|
|
361
|
+
def test_reuse_when_active_bindings_match(self):
|
|
362
|
+
rc, out = self._run_flow(
|
|
363
|
+
["myorg", "--target", "VoiceCall", "--trigger"],
|
|
364
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "1"}, # default FAKE_ACTIVE_IDS = current ids
|
|
365
|
+
)
|
|
366
|
+
self.assertEqual(rc, 0, f"expected reuse; out={out[:300]}")
|
|
367
|
+
self.assertRegex(out, re.compile(r'"status":\s*"reused"'), out[:300])
|
|
368
|
+
|
|
369
|
+
# --- P1 stale repair: active flow bound to DIFFERENT ids → must redeploy, not reuse ---
|
|
370
|
+
def test_redeploy_when_active_bindings_stale(self):
|
|
371
|
+
rc, out = self._run_flow(
|
|
372
|
+
["myorg", "--target", "VoiceCall", "--trigger"],
|
|
373
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "1",
|
|
374
|
+
"FAKE_ACTIVE_IDS": "00GSTALE0000001AAA 0RCSTALE000001AAA 0N9STALE0000001AAA"},
|
|
375
|
+
)
|
|
376
|
+
self.assertEqual(rc, 0, f"expected redeploy on drift; out={out[:300]}")
|
|
377
|
+
self.assertNotRegex(out, re.compile(r'"status":\s*"reused"'),
|
|
378
|
+
f"stale bindings must NOT be reused; out={out[:300]}")
|
|
379
|
+
self.assertRegex(out, re.compile(r'"status":\s*"created"'), out[:300])
|
|
380
|
+
|
|
381
|
+
# --- P1 runtime proof BLOCKING: --require-proof with no PSR/AW must block ---
|
|
382
|
+
def test_require_proof_blocks_without_routing_evidence(self):
|
|
383
|
+
rc, out = self._run_flow(
|
|
384
|
+
["myorg", "--target", "VoiceCall", "--require-proof"],
|
|
385
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "0", "FAKE_PROOF": "0"},
|
|
386
|
+
)
|
|
387
|
+
self.assertEqual(rc, 1, f"expected block when proof required but absent; out={out[:300]}")
|
|
388
|
+
self.assertRegex(out, re.compile(r'REQUIRED but not observed', re.IGNORECASE), out[:300])
|
|
389
|
+
|
|
390
|
+
# --- P1 runtime proof SUCCESS: --require-proof with PSR present → success + record cleaned up ---
|
|
391
|
+
def test_require_proof_succeeds_and_cleans_up_record(self):
|
|
392
|
+
import tempfile, os as _os
|
|
393
|
+
log = tempfile.NamedTemporaryFile(prefix="omni-del-", delete=False)
|
|
394
|
+
log.close()
|
|
395
|
+
try:
|
|
396
|
+
rc, out = self._run_flow(
|
|
397
|
+
["myorg", "--target", "VoiceCall", "--require-proof"],
|
|
398
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "0", "FAKE_PROOF": "1",
|
|
399
|
+
"FAKE_DELETE_LOG": log.name},
|
|
400
|
+
)
|
|
401
|
+
self.assertEqual(rc, 0, f"expected success with proof present; out={out[:300]}")
|
|
402
|
+
self.assertRegex(out, re.compile(r'"success":\s*true'), out[:300])
|
|
403
|
+
# The throwaway VoiceCall (id 500...) must have been deleted by the cleanup trap.
|
|
404
|
+
with open(log.name) as fh:
|
|
405
|
+
deleted = fh.read()
|
|
406
|
+
self.assertIn("500000000000001AAA", deleted,
|
|
407
|
+
"runtime-proof record was not cleaned up (no delete logged)")
|
|
408
|
+
finally:
|
|
409
|
+
_os.unlink(log.name)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class SkillsBasedRoutingFlowTests(unittest.TestCase):
|
|
413
|
+
"""(6b) Skills-based routing variant for service-omni-routing-flow-deploy (W-24069467).
|
|
414
|
+
|
|
415
|
+
The base trigger flows hardcode routingType=QueueBased, so a skills-based-routing org
|
|
416
|
+
config never takes effect at runtime. This variant emits routingType=SkillsBased with a
|
|
417
|
+
non-null skillOption (RunSBRRules default → platform evaluates WorkSkillRouting rules and
|
|
418
|
+
attaches SkillRequirement rows to the PSR). A null skillOption is exactly what NPEs the
|
|
419
|
+
platform routeWork action and rolls back the triggering insert (W-24069761), so the deploy
|
|
420
|
+
script refuses to emit one and the asset always carries it.
|
|
421
|
+
|
|
422
|
+
Org-free/network-free: template well-formedness + input-contract rejection + the SkillsBased
|
|
423
|
+
deploy path driven through the parameterized _FAKE_SF_VOICE sandbox.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
FLOWS_DIR = (SKILLS_ROOT / "service-omni-routing-flow-deploy"
|
|
427
|
+
/ "assets/force-app/main/default/flows")
|
|
428
|
+
SKILLSBASED_TEMPLATES = [
|
|
429
|
+
"Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml",
|
|
430
|
+
"Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml",
|
|
431
|
+
]
|
|
432
|
+
QUEUEBASED_TEMPLATES = [
|
|
433
|
+
"Omni_Route_Case_Trigger.flow-meta.xml",
|
|
434
|
+
"Omni_Route_VoiceCall_Trigger.flow-meta.xml",
|
|
435
|
+
]
|
|
436
|
+
#: The tokens deploy-and-report.sh substitutes for the record-triggered variants.
|
|
437
|
+
SED_SUBSTITUTIONS = {
|
|
438
|
+
"__SERVICE_CHANNEL_ID__", "__SERVICE_CHANNEL_DEVNAME__", "__SERVICE_CHANNEL_LABEL__",
|
|
439
|
+
"__ROUTING_CONFIG_ID__", "__QUEUE_ID__", "__SKILL_OPTION__",
|
|
440
|
+
}
|
|
441
|
+
DRIVER = (SKILLS_ROOT / "service-omni-channel-setup-coordinate"
|
|
442
|
+
/ "scripts/integration-driver.sh")
|
|
443
|
+
COORDINATOR_DOC = SKILLS_ROOT / "service-omni-channel-setup-coordinate/SKILL.md"
|
|
444
|
+
|
|
445
|
+
# --- template well-formedness: SkillsBased routingType + a NON-NULL skillOption token ---
|
|
446
|
+
def test_skillsbased_templates_are_skillsbased_with_nonnull_skilloption(self):
|
|
447
|
+
for name in self.SKILLSBASED_TEMPLATES:
|
|
448
|
+
with self.subTest(template=name):
|
|
449
|
+
path = self.FLOWS_DIR / name
|
|
450
|
+
self.assertTrue(path.exists(), f"missing SkillsBased template {name}")
|
|
451
|
+
text = path.read_text()
|
|
452
|
+
self.assertRegex(
|
|
453
|
+
text, re.compile(r"<name>routingType</name>\s*<value>\s*<stringValue>SkillsBased</stringValue>"),
|
|
454
|
+
f"{name} must emit routingType=SkillsBased",
|
|
455
|
+
)
|
|
456
|
+
# skillOption must be present and carry the resolvable token (never a null/empty value).
|
|
457
|
+
self.assertRegex(
|
|
458
|
+
text, re.compile(r"<name>skillOption</name>\s*<value>\s*<stringValue>__SKILL_OPTION__</stringValue>"),
|
|
459
|
+
f"{name} must carry a tokenized non-null skillOption (W-24069761)",
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# --- regression guard: the QueueBased base flows stay QueueBased and never grow a skillOption ---
|
|
463
|
+
def test_queuebased_templates_unchanged(self):
|
|
464
|
+
for name in self.QUEUEBASED_TEMPLATES:
|
|
465
|
+
with self.subTest(template=name):
|
|
466
|
+
text = (self.FLOWS_DIR / name).read_text()
|
|
467
|
+
self.assertIn("<stringValue>QueueBased</stringValue>", text,
|
|
468
|
+
f"{name} must stay routingType=QueueBased")
|
|
469
|
+
self.assertNotIn("SkillsBased", text, f"{name} must not become SkillsBased")
|
|
470
|
+
self.assertNotIn("skillOption", text,
|
|
471
|
+
f"{name} (QueueBased) must not carry skillOption - QueueBased ignores it")
|
|
472
|
+
|
|
473
|
+
# --- every token in the SkillsBased template is one the deploy script substitutes (no residue) ---
|
|
474
|
+
def test_skillsbased_tokens_resolve_cleanly(self):
|
|
475
|
+
for name in self.SKILLSBASED_TEMPLATES:
|
|
476
|
+
with self.subTest(template=name):
|
|
477
|
+
text = (self.FLOWS_DIR / name).read_text()
|
|
478
|
+
tokens = set(re.findall(r"__[A-Z_]+__", text))
|
|
479
|
+
self.assertIn("__SKILL_OPTION__", tokens, f"{name} must tokenize skillOption")
|
|
480
|
+
unknown = tokens - self.SED_SUBSTITUTIONS
|
|
481
|
+
self.assertEqual(unknown, set(),
|
|
482
|
+
f"{name} has tokens deploy-and-report.sh does not substitute: {unknown}")
|
|
483
|
+
|
|
484
|
+
# --- input contract (sf scrubbed): invalid routing-type is rejected before any sf call ---
|
|
485
|
+
def test_invalid_routing_type_rejected(self):
|
|
486
|
+
rc, out = _run(_FLOW_SCRIPT, ["myorg", "--trigger", "--routing-type", "Widget"],
|
|
487
|
+
extra_path_scrub=True)
|
|
488
|
+
self.assertEqual(rc, 2, f"expected exit 2 on bad routing-type; out={out[:300]}")
|
|
489
|
+
self.assertRegex(out, re.compile(r"Invalid --routing-type", re.IGNORECASE), out[:300])
|
|
490
|
+
|
|
491
|
+
# --- 761 guard: SkillsBased with an explicitly empty skillOption is refused (never emit null) ---
|
|
492
|
+
def test_skillsbased_empty_skill_option_rejected(self):
|
|
493
|
+
rc, out = _run(_FLOW_SCRIPT, ["myorg", "--trigger", "--routing-type=SkillsBased", "--skill-option="],
|
|
494
|
+
extra_path_scrub=True)
|
|
495
|
+
self.assertEqual(rc, 2, f"expected exit 2 on empty skill-option; out={out[:300]}")
|
|
496
|
+
self.assertIn("W-24069761", out)
|
|
497
|
+
|
|
498
|
+
# --- 761 guard: SkillsBased with an out-of-enum skillOption is refused ---
|
|
499
|
+
def test_skillsbased_invalid_skill_option_rejected(self):
|
|
500
|
+
rc, out = _run(_FLOW_SCRIPT, ["myorg", "--trigger", "--routing-type", "SkillsBased",
|
|
501
|
+
"--skill-option", "Bogus"], extra_path_scrub=True)
|
|
502
|
+
self.assertEqual(rc, 2, f"expected exit 2 on bad skill-option; out={out[:300]}")
|
|
503
|
+
self.assertRegex(out, re.compile(r"Invalid --skill-option", re.IGNORECASE), out[:300])
|
|
504
|
+
|
|
505
|
+
# --- SkillsBased with no --skill-option defaults to a safe non-null value (RunSBRRules) ---
|
|
506
|
+
def test_skillsbased_defaults_to_runsbrrules(self):
|
|
507
|
+
rc, out = self._run_flow(
|
|
508
|
+
["myorg", "--target", "VoiceCall", "--trigger", "--routing-type", "SkillsBased"],
|
|
509
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "0"},
|
|
510
|
+
)
|
|
511
|
+
self.assertEqual(rc, 0, f"expected clean SkillsBased deploy; out={out[:400]}")
|
|
512
|
+
self.assertRegex(out, re.compile(r'"routing_type":\s*"SkillsBased"'), out[:600])
|
|
513
|
+
self.assertRegex(out, re.compile(r'"skill_option":\s*"RunSBRRules"'), out[:600])
|
|
514
|
+
|
|
515
|
+
# --- SkillsBased runtime proof: the PSR's SkillRequirement rows are queried and reported ---
|
|
516
|
+
def test_skillsbased_require_proof_reports_skill_requirements(self):
|
|
517
|
+
rc, out = self._run_flow(
|
|
518
|
+
["myorg", "--target", "VoiceCall", "--require-proof", "--routing-type", "SkillsBased"],
|
|
519
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "0", "FAKE_PROOF": "1", "FAKE_SKILLREQ": "1"},
|
|
520
|
+
)
|
|
521
|
+
self.assertEqual(rc, 0, f"expected success with proof + skill requirement; out={out[:400]}")
|
|
522
|
+
self.assertRegex(out, re.compile(r'"skill_requirement_count":\s*1'), out[:800])
|
|
523
|
+
self.assertIn("0PS00000000001AAA", out) # the SkillId the platform attached to the PSR
|
|
524
|
+
|
|
525
|
+
def test_skillsbased_request_redeploys_active_queuebased_flow(self):
|
|
526
|
+
rc, out = self._run_flow(
|
|
527
|
+
["myorg", "--target", "VoiceCall", "--trigger",
|
|
528
|
+
"--routing-type", "SkillsBased"],
|
|
529
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "1",
|
|
530
|
+
"ACTIVE_ROUTING_TYPE": "QueueBased"},
|
|
531
|
+
)
|
|
532
|
+
self.assertEqual(rc, 0, out[:600])
|
|
533
|
+
self.assertRegex(out, re.compile(r'"status":\s*"created"'), out[:600])
|
|
534
|
+
|
|
535
|
+
def test_skillsbased_request_reuses_matching_active_flow(self):
|
|
536
|
+
rc, out = self._run_flow(
|
|
537
|
+
["myorg", "--target", "VoiceCall", "--trigger",
|
|
538
|
+
"--routing-type", "SkillsBased"],
|
|
539
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "1",
|
|
540
|
+
"ACTIVE_ROUTING_TYPE": "SkillsBased",
|
|
541
|
+
"ACTIVE_SKILL_OPTION": "RunSBRRules"},
|
|
542
|
+
)
|
|
543
|
+
self.assertEqual(rc, 0, out[:600])
|
|
544
|
+
self.assertRegex(out, re.compile(r'"status":\s*"reused"'), out[:600])
|
|
545
|
+
|
|
546
|
+
def test_required_skillsbased_proof_rejects_zero_skill_requirements(self):
|
|
547
|
+
rc, out = self._run_flow(
|
|
548
|
+
["myorg", "--target", "VoiceCall", "--require-proof",
|
|
549
|
+
"--routing-type", "SkillsBased"],
|
|
550
|
+
{"FAKE_SC_PRESENT": "1", "FAKE_FLOW_ACTIVE": "0", "FAKE_PROOF": "1"},
|
|
551
|
+
)
|
|
552
|
+
self.assertEqual(rc, 1, out[:600])
|
|
553
|
+
self.assertIn("SkillRequirement", out)
|
|
554
|
+
|
|
555
|
+
def test_coordinator_infers_and_forwards_skillsbased_routing(self):
|
|
556
|
+
source = self.DRIVER.read_text()
|
|
557
|
+
self.assertIn('ROUTING_TYPE="${OMNI_ROUTING_TYPE:-}"', source)
|
|
558
|
+
self.assertIn('ROUTING_TYPE="SkillsBased"', source)
|
|
559
|
+
self.assertIn('--routing-type "$ROUTING_TYPE"', source)
|
|
560
|
+
self.assertIn('--skill-option "$SKILL_OPTION"', source)
|
|
561
|
+
|
|
562
|
+
def test_coordinator_sequences_wsr_before_routing_flow(self):
|
|
563
|
+
source = self.DRIVER.read_text()
|
|
564
|
+
wsr_index = source.index('run_skill "service-omni-work-skill-routing-configure"')
|
|
565
|
+
flow_index = source.index('# ---- Routing flow')
|
|
566
|
+
self.assertLess(wsr_index, flow_index)
|
|
567
|
+
|
|
568
|
+
def test_coordinator_documents_sbr_routing_inputs(self):
|
|
569
|
+
text = self.COORDINATOR_DOC.read_text()
|
|
570
|
+
self.assertIn("OMNI_ROUTING_TYPE", text)
|
|
571
|
+
self.assertIn("OMNI_SKILL_OPTION", text)
|
|
572
|
+
self.assertIn("RunSBRRules", text)
|
|
573
|
+
|
|
574
|
+
def _run_flow(self, args, env):
|
|
575
|
+
# Cross-skill handoff the coordinator supplies; the VOICE fake ignores the devname values
|
|
576
|
+
# and returns canonical channel/queue/QRC ids regardless, so these just satisfy resolution.
|
|
577
|
+
merged = {
|
|
578
|
+
"SERVICE_CHANNEL_DEVELOPER_NAME": "sfdc_phone",
|
|
579
|
+
"QUEUE_DEVELOPER_NAME": "VoiceQueue",
|
|
580
|
+
"ROUTING_CONFIG_DEVELOPER_NAME": "Voice_Routing_Config",
|
|
581
|
+
"OMNI_PROOF_POLL_TRIES": "1",
|
|
582
|
+
"OMNI_PROOF_POLL_SLEEP": "0",
|
|
583
|
+
}
|
|
584
|
+
merged.update(env)
|
|
585
|
+
with _FakeSfDir(body=_FAKE_SF_VOICE) as fake_dir:
|
|
586
|
+
return _run(_FLOW_SCRIPT, args, path_prefix=fake_dir, extra_env=merged)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class VoiceMembershipContractTests(unittest.TestCase):
|
|
590
|
+
"""(7) Explicit real-agent membership contract for service-omni-queue-members-assign."""
|
|
591
|
+
|
|
592
|
+
SCRIPT = "service-omni-queue-members-assign/scripts/verify-and-bind.sh"
|
|
593
|
+
|
|
594
|
+
# A malformed explicit token (neither Username nor 005-Id) must block after resolving the queue.
|
|
595
|
+
def test_explicit_bad_token_blocked(self):
|
|
596
|
+
with _FakeSfDir(body=_FAKE_SF_VOICE) as fake_dir:
|
|
597
|
+
rc, out = _run(self.SCRIPT, ["myorg", "VoiceQueue", "", "not-an-email,also_bad"],
|
|
598
|
+
path_prefix=fake_dir)
|
|
599
|
+
self.assertEqual(rc, 1, f"expected block on bad explicit token; out={out[:300]}")
|
|
600
|
+
self.assertRegex(out, re.compile(r'neither a Username', re.IGNORECASE), out[:300])
|
|
601
|
+
|
|
602
|
+
# A well-formed explicit username list drives the explicit source (member_source=explicit).
|
|
603
|
+
def test_explicit_username_list_uses_explicit_source(self):
|
|
604
|
+
with _FakeSfDir(body=_FAKE_SF_VOICE) as fake_dir:
|
|
605
|
+
rc, out = _run(self.SCRIPT, ["myorg", "VoiceQueue", "", "agent1@example.com,agent2@example.com"],
|
|
606
|
+
path_prefix=fake_dir)
|
|
607
|
+
# Whatever the final bind/re-query tally, the run must NOT block on parsing and must record
|
|
608
|
+
# the explicit source (never silently fall back to the demo agent pattern).
|
|
609
|
+
self.assertIn('"member_source": "explicit"', out, f"out={out[:400]}")
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class CoordinatorSupervisorPermsetTests(unittest.TestCase):
|
|
613
|
+
"""(8) The coordinator must invoke the supervisor-permset leaf with the STANDARD
|
|
614
|
+
ContactCenterSupervisor permission set - never the incompatible custom Omni_Supervisor
|
|
615
|
+
(which re-declares license-gated perms and fails assignment on standard licenses)."""
|
|
616
|
+
|
|
617
|
+
DRIVER = SKILLS_ROOT / "service-omni-channel-setup-coordinate" / "scripts" / "integration-driver.sh"
|
|
618
|
+
|
|
619
|
+
def _driver_src(self) -> str:
|
|
620
|
+
return self.DRIVER.read_text()
|
|
621
|
+
|
|
622
|
+
def test_supervisor_permset_invocations_use_contact_center_supervisor(self):
|
|
623
|
+
src = self._driver_src()
|
|
624
|
+
# Every verify-and-assign invocation that supplies $SUPERVISOR_COUNT (the supervisor leaf)
|
|
625
|
+
# must pass ContactCenterSupervisor as the permission-set argument.
|
|
626
|
+
sup_invocations = re.findall(
|
|
627
|
+
r'verify-and-assign\.sh\s+"\$ORG"\s+"\$SUPERVISOR_COUNT"\s+"([^"]+)"', src)
|
|
628
|
+
self.assertTrue(sup_invocations, "no supervisor-permset invocation found in coordinator")
|
|
629
|
+
for permset in sup_invocations:
|
|
630
|
+
self.assertEqual(
|
|
631
|
+
permset, "ContactCenterSupervisor",
|
|
632
|
+
f"coordinator invokes supervisor-permset leaf with '{permset}', "
|
|
633
|
+
f"expected 'ContactCenterSupervisor'")
|
|
634
|
+
|
|
635
|
+
def test_coordinator_never_passes_custom_omni_supervisor_permset(self):
|
|
636
|
+
src = self._driver_src()
|
|
637
|
+
self.assertNotIn(
|
|
638
|
+
'verify-and-assign.sh "$ORG" "$SUPERVISOR_COUNT" "Omni_Supervisor"', src,
|
|
639
|
+
"coordinator must not assign the custom Omni_Supervisor permission set "
|
|
640
|
+
"(license-gated perms fail on standard licenses)")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class CoordinatorRepExperienceTests(unittest.TestCase):
|
|
644
|
+
"""(9) Assert the optional rep-experience wiring and the independent SBR gate."""
|
|
645
|
+
|
|
646
|
+
DRIVER = SKILLS_ROOT / "service-omni-channel-setup-coordinate" / "scripts" / "integration-driver.sh"
|
|
647
|
+
|
|
648
|
+
def setUp(self):
|
|
649
|
+
self.src = self.DRIVER.read_text()
|
|
650
|
+
|
|
651
|
+
def test_rep_stage_gated_on_flag(self):
|
|
652
|
+
self.assertRegex(
|
|
653
|
+
self.src, r'\[\s*"\$\{OMNI_REP_EXPERIENCE:-0\}"\s*=\s*"1"\s*\]',
|
|
654
|
+
"rep-experience stage must be gated behind OMNI_REP_EXPERIENCE=1")
|
|
655
|
+
|
|
656
|
+
def test_rep_stage_invokes_all_three_leaves(self):
|
|
657
|
+
for leaf, script in (
|
|
658
|
+
("service-omni-presence-user-config-deploy", "deploy-and-report.sh"),
|
|
659
|
+
("service-omni-skills-based-routing-configure", "configure-and-report.sh"),
|
|
660
|
+
("service-omni-sidebar-configure", "enable-and-report.sh"),
|
|
661
|
+
):
|
|
662
|
+
self.assertIn(leaf, self.src, f"coordinator never invokes {leaf}")
|
|
663
|
+
self.assertIn(script, self.src, f"coordinator never calls {leaf} via {script}")
|
|
664
|
+
|
|
665
|
+
def test_sbr_gated_on_routing_type(self):
|
|
666
|
+
self.assertRegex(
|
|
667
|
+
self.src, r'\[\s*"\$ROUTING_TYPE"\s*=\s*"SkillsBased"\s*\]',
|
|
668
|
+
"skills-based prerequisites must run only for SkillsBased routing")
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class CoordinatorOverflowAssigneeTests(unittest.TestCase):
|
|
672
|
+
"""(10) Optional fallback routing threads end-to-end: the coordinator input
|
|
673
|
+
OMNI_OVERFLOW_ASSIGNEE is exported as QRC_OVERFLOW_ASSIGNEE, and the QRC leaf reads
|
|
674
|
+
that same env var onto QueueRoutingConfig.OverflowAssigneeId. Neither side's suite
|
|
675
|
+
can prove the handoff alone."""
|
|
676
|
+
|
|
677
|
+
DRIVER = SKILLS_ROOT / "service-omni-channel-setup-coordinate" / "scripts" / "integration-driver.sh"
|
|
678
|
+
QRC_LEAF = SKILLS_ROOT / "service-omni-queue-routing-config-deploy" / "scripts" / "upsert-and-report.sh"
|
|
679
|
+
|
|
680
|
+
def test_coordinator_exports_overflow_to_qrc_env(self):
|
|
681
|
+
self.assertRegex(
|
|
682
|
+
self.DRIVER.read_text(),
|
|
683
|
+
r'export\s+QRC_OVERFLOW_ASSIGNEE="\$OMNI_OVERFLOW_ASSIGNEE"',
|
|
684
|
+
"coordinator must export OMNI_OVERFLOW_ASSIGNEE as QRC_OVERFLOW_ASSIGNEE")
|
|
685
|
+
|
|
686
|
+
def test_qrc_leaf_consumes_overflow_env(self):
|
|
687
|
+
self.assertIn(
|
|
688
|
+
"QRC_OVERFLOW_ASSIGNEE", self.QRC_LEAF.read_text(),
|
|
689
|
+
"QRC leaf must read QRC_OVERFLOW_ASSIGNEE so the coordinator handoff lands")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class ConfirmedGapRegressionTests(unittest.TestCase):
|
|
693
|
+
"""Cross-skill contracts confirmed by live-org validation."""
|
|
694
|
+
|
|
695
|
+
DRIVER = SKILLS_ROOT / "service-omni-channel-setup-coordinate" / "scripts" / "integration-driver.sh"
|
|
696
|
+
AGENTS = SKILLS_ROOT / "service-omni-agent-users-create" / "scripts" / "detect-and-create.sh"
|
|
697
|
+
QRC = SKILLS_ROOT / "service-omni-queue-routing-config-deploy" / "scripts" / "upsert-and-report.sh"
|
|
698
|
+
BASE = SKILLS_ROOT / "service-omni-base-settings-configure" / "scripts" / "configure-and-report.sh"
|
|
699
|
+
|
|
700
|
+
def test_mixed_runs_propagate_reused_agent_usernames(self):
|
|
701
|
+
self.assertIn("reused_users", self.AGENTS.read_text())
|
|
702
|
+
self.assertIn(".detect.existing_users", self.DRIVER.read_text())
|
|
703
|
+
|
|
704
|
+
def test_coordinator_exposes_per_target_queue_overrides(self):
|
|
705
|
+
source = self.DRIVER.read_text()
|
|
706
|
+
for var in (
|
|
707
|
+
"OMNI_CASE_QUEUE_DEVELOPER_NAME",
|
|
708
|
+
"OMNI_VOICE_QUEUE_DEVELOPER_NAME",
|
|
709
|
+
"OMNI_INCIDENT_QUEUE_DEVELOPER_NAME",
|
|
710
|
+
"OMNI_MESSAGING_QUEUE_DEVELOPER_NAME",
|
|
711
|
+
):
|
|
712
|
+
self.assertIn(var, source)
|
|
713
|
+
self.assertIn("queue_override_for_target", source)
|
|
714
|
+
|
|
715
|
+
def test_coordinator_adopts_existing_or_explicit_qrc(self):
|
|
716
|
+
source = self.DRIVER.read_text()
|
|
717
|
+
for var in (
|
|
718
|
+
"OMNI_CASE_ROUTING_CONFIG_DEVELOPER_NAME",
|
|
719
|
+
"OMNI_VOICE_ROUTING_CONFIG_DEVELOPER_NAME",
|
|
720
|
+
):
|
|
721
|
+
self.assertIn(var, source)
|
|
722
|
+
self.assertNotIn(
|
|
723
|
+
"SELECT QueueRoutingConfig.DeveloperName FROM Group",
|
|
724
|
+
source,
|
|
725
|
+
"Group.QueueRoutingConfig is not a queryable relationship on supported orgs",
|
|
726
|
+
)
|
|
727
|
+
self.assertIn("SELECT QueueRoutingConfigId FROM Group", source)
|
|
728
|
+
self.assertIn("SELECT DeveloperName FROM QueueRoutingConfig WHERE Id=", source)
|
|
729
|
+
self.assertIn('QRC_DEVELOPER_NAME="$qrc_dn"', source)
|
|
730
|
+
|
|
731
|
+
def test_qrc_push_timeout_is_written_and_verified(self):
|
|
732
|
+
source = self.QRC.read_text()
|
|
733
|
+
self.assertIn("QRC_PUSH_TIMEOUT", source)
|
|
734
|
+
self.assertIn("{PushTimeout: $pt}", source)
|
|
735
|
+
self.assertIn("PushTimeout(got=", source)
|
|
736
|
+
|
|
737
|
+
def test_status_capacity_setting_uses_explicit_source_deploy(self):
|
|
738
|
+
source = self.BASE.read_text()
|
|
739
|
+
self.assertIn("--source-dir", source)
|
|
740
|
+
self.assertIn("OmniChannel.settings-meta.xml", source)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class CoordinatorSupervisorSurfaceTests(unittest.TestCase):
|
|
744
|
+
"""(11) The supervisor surface + Command Center advisory + field-based routing are
|
|
745
|
+
coordinator-only wiring the per-leaf suites cannot see. Assert: the read-only analysis
|
|
746
|
+
always runs (never gated on the opt-in flag), the surface leaf runs after the config and
|
|
747
|
+
is gated on the config disposition, and field-based WorkSkillRouting is gated on the
|
|
748
|
+
four OMNI_WSR_* inputs."""
|
|
749
|
+
|
|
750
|
+
DRIVER = SKILLS_ROOT / "service-omni-channel-setup-coordinate" / "scripts" / "integration-driver.sh"
|
|
751
|
+
|
|
752
|
+
def setUp(self):
|
|
753
|
+
self.src = self.DRIVER.read_text()
|
|
754
|
+
|
|
755
|
+
def test_command_center_analyze_invoked(self):
|
|
756
|
+
self.assertIn("service-omni-command-center-analyze", self.src,
|
|
757
|
+
"coordinator never invokes command-center-analyze")
|
|
758
|
+
self.assertIn("analyze.sh", self.src,
|
|
759
|
+
"coordinator never calls command-center-analyze via analyze.sh")
|
|
760
|
+
|
|
761
|
+
def test_command_center_analyze_not_gated_on_rep_flag(self):
|
|
762
|
+
# The advisory read-only step must run regardless of the opt-in rep-experience flag: its
|
|
763
|
+
# run_skill call is top-level (column 0), not indented inside the rep-experience `if` block.
|
|
764
|
+
self.assertRegex(
|
|
765
|
+
self.src, r'(?m)^run_skill "service-omni-command-center-analyze"',
|
|
766
|
+
"command-center-analyze must be invoked top-level, outside the OMNI_REP_EXPERIENCE gate")
|
|
767
|
+
|
|
768
|
+
def test_supervisor_surface_invoked_after_config(self):
|
|
769
|
+
self.assertIn("service-omni-supervisor-surface-deploy", self.src,
|
|
770
|
+
"coordinator never invokes supervisor-surface-deploy")
|
|
771
|
+
cfg_at = self.src.index("service-omni-supervisor-config-deploy")
|
|
772
|
+
surface_at = self.src.index("service-omni-supervisor-surface-deploy")
|
|
773
|
+
self.assertLess(cfg_at, surface_at,
|
|
774
|
+
"supervisor-surface-deploy must be sequenced after supervisor-config-deploy")
|
|
775
|
+
|
|
776
|
+
def test_supervisor_surface_gated_on_config_disposition(self):
|
|
777
|
+
self.assertIn("SUPERVISOR_CONFIG_OK", self.src,
|
|
778
|
+
"surface stage must gate on the run-mode supervisor-config disposition")
|
|
779
|
+
self.assertIn("SUPERVISOR_CONFIG_PRESENT", self.src,
|
|
780
|
+
"surface stage must gate on the plan-mode supervisor-config presence")
|
|
781
|
+
|
|
782
|
+
def test_selected_supervisor_config_is_forwarded_to_both_leaves(self):
|
|
783
|
+
self.assertIn("OMNI_SUPERVISOR_CONFIG_DEVELOPER_NAME", self.src)
|
|
784
|
+
self.assertIn(
|
|
785
|
+
'deploy-and-report.sh "$ORG" "$SUPERVISOR_COUNT" "$DISCOVERED_QUEUES_CSV" "" "" "$SUPERVISOR_CONFIG_DN"',
|
|
786
|
+
self.src,
|
|
787
|
+
"coordinator must pass the selected DeveloperName to supervisor-config-deploy",
|
|
788
|
+
)
|
|
789
|
+
self.assertIn(
|
|
790
|
+
'deploy-and-report.sh "$ORG" "$SUPERVISOR_CONFIG_RESOLVED_DN"',
|
|
791
|
+
self.src,
|
|
792
|
+
"coordinator must pass the resolved DeveloperName to supervisor-surface-deploy",
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
def test_work_skill_routing_gated_on_wsr_inputs(self):
|
|
796
|
+
self.assertIn("service-omni-work-skill-routing-configure", self.src,
|
|
797
|
+
"coordinator never invokes work-skill-routing-configure")
|
|
798
|
+
for var in ("OMNI_WSR_ENTITY", "OMNI_WSR_FIELD", "OMNI_WSR_SKILL", "OMNI_WSR_VALUE"):
|
|
799
|
+
self.assertIn(var, self.src,
|
|
800
|
+
f"field-based routing must be gated on {var}")
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
if __name__ == "__main__":
|
|
804
|
+
unittest.main(verbosity=2)
|