@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,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: service-omni-command-center-analyze
|
|
3
|
+
description: "Use to analyze whether an org should use Command Center for Service V2 or classic Omni Supervisor and recommend the next action without making changes. Triggers: detect Command Center V2 versus classic, check supervisor readiness, choose a supervisor experience, diagnose Command Center availability, assess Omni Supervisor setup. Do not use to enable V2, assign permissions, or deploy configuration."
|
|
4
|
+
allowed-tools: Bash Read Grep Glob
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0"
|
|
7
|
+
domains: ["Service"]
|
|
8
|
+
minApiVersion: "66.0"
|
|
9
|
+
relatedSkills:
|
|
10
|
+
- "service-omni-channel-setup-coordinate"
|
|
11
|
+
- "service-omni-supervisor-config-deploy"
|
|
12
|
+
accessCheck:
|
|
13
|
+
- type: license
|
|
14
|
+
value: ServiceCloud
|
|
15
|
+
cliTools:
|
|
16
|
+
- tool: ["jq"]
|
|
17
|
+
semver: ">=1.6"
|
|
18
|
+
- tool: ["python3"]
|
|
19
|
+
semver: ">=3.8"
|
|
20
|
+
- tool: ["sf"]
|
|
21
|
+
semver: ">=2.139.6"
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# service-omni-command-center-analyze
|
|
25
|
+
|
|
26
|
+
Decide, **without changing anything**, which supervisor experience an org should use and what to do next. Command Center for Service V2 is gated on three independent dimensions — org **capability** (release gater), the **`CommandCenterForServiceV2` org preference** (whose ON-flip seeds the `CommandCenterForServiceV2_L` FlexiPage and registers the V2 tab), and the per-supervisor **`CommandCenterForServiceUser`** permission. A single boolean cannot express that, so this skill returns an explicit **state** plus the recommended follow-up skill (e.g. `service-omni-supervisor-config-deploy` for the classic path). It is the entry point of the Command Center flow — `service-omni-channel-setup-coordinate` runs it first — and is safe to run on any org, including production, because it only reads.
|
|
27
|
+
|
|
28
|
+
## Inputs
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bash scripts/analyze.sh <org-alias> [supervisor_username_or_id]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `org-alias` (required).
|
|
35
|
+
- `supervisor_username_or_id` (optional). A Username (`…@…`) or 15/18-char User Id (`005…`). When given, the per-user `CommandCenterForServiceUser` permission is evaluated; when omitted, the permission dimension is reported as not-checked and readiness is org-level only.
|
|
36
|
+
|
|
37
|
+
## Preconditions and safety
|
|
38
|
+
|
|
39
|
+
- Target org authenticated via `sf` CLI, Service Cloud license, `sf` CLI ≥ 2.139.6.
|
|
40
|
+
- **Read-only** — no `safe_to_write` guard is needed; the skill issues only SOQL/Tooling queries.
|
|
41
|
+
|
|
42
|
+
## Run
|
|
43
|
+
|
|
44
|
+
`analyze.sh` gathers observable signals and maps them to a state:
|
|
45
|
+
|
|
46
|
+
| Signal | Source | Meaning |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| V2 capability | `SELECT Id FROM PermissionSet WHERE PermissionsCommandCenterForServiceUser = true` probe | If the user-permission column does not exist on the org schema, V2 is not available. |
|
|
49
|
+
| Seeded FlexiPage | Tooling: `FlexiPage` where `DeveloperName='CommandCenterForServiceV2_L'` | Present ⇒ the org preference was enabled and the page was seeded. |
|
|
50
|
+
| V2 tab | `TabDefinition` (best-effort) | Cross-checks the seed; a seed/tab mismatch indicates a partial provision. |
|
|
51
|
+
| Supervisor permission | `PermissionSetAssignment` joined on `PermissionSet.PermissionsCommandCenterForServiceUser` | Whether the named supervisor can use V2. |
|
|
52
|
+
| Legacy config | Tooling: `OmniSupervisorConfig` count | Informational for the classic path. |
|
|
53
|
+
|
|
54
|
+
## Behavior
|
|
55
|
+
|
|
56
|
+
**States and recommended next action:**
|
|
57
|
+
|
|
58
|
+
| State | Meaning | Recommended skill |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `v2_ready` | Capability + seed present (and supervisor has permission, if checked) | verify only (coordinator proceeds to V2 verification) |
|
|
61
|
+
| `v2_permission_missing` | V2 enabled but the named supervisor lacks `CommandCenterForServiceUser` | Manual — assign `CommandCenterForServiceUser` (headless-capable via PermissionSet; packaging pending) |
|
|
62
|
+
| `v2_seed_incomplete` | Capability present but the FlexiPage/tab provisioning is inconsistent | Manual — re-check in Setup → Omni-Channel → Supervisor Settings (no supported public write API) |
|
|
63
|
+
| `v2_available_not_enabled` | Org supports V2 but the preference is off | Manual — enable in Setup → Omni-Channel → Supervisor Settings (no supported Metadata/Tooling write API) |
|
|
64
|
+
| `legacy_selected` | V2 capability absent | `service-omni-supervisor-config-deploy` |
|
|
65
|
+
| `ambiguous` | A required signal could not be read through a supported API | none — **blocks**, do not guess |
|
|
66
|
+
|
|
67
|
+
`ambiguous` is the only state that exits non-zero: the review's rule is to block rather than infer when state cannot be proven read-only. All other states exit 0 with `status:"detected"`.
|
|
68
|
+
|
|
69
|
+
## Output contract
|
|
70
|
+
|
|
71
|
+
A single JSON object: `status` (`detected` | `blocked`), `state` (one of the six above), `recommended_action`, `recommended_skill`, `signals` (`v2_capability`, `seed_flexipage_present`, `v2_tab_present`, `supervisor{identifier,checked,has_command_center_permission}`, `legacy_omnisupervisorconfig_count`), `manual_actions`, `blocking_issue`.
|
|
72
|
+
|
|
73
|
+
## Limitations
|
|
74
|
+
|
|
75
|
+
- The `CommandCenterForServiceV2` org preference has no supported public read; its state is **inferred** from the seeded FlexiPage (the platform seeds on the ON-flip). This is called out in `signals` and is why a bare capability-without-seed reads as `v2_available_not_enabled`.
|
|
76
|
+
- The release gater cannot be read directly; capability is inferred from the presence of the user-permission column on the org schema.
|
|
77
|
+
- Tab detection is best-effort; when the tab query cannot run, `v2_tab_present` is `"unknown"` and state is derived from the remaining signals.
|
|
78
|
+
- Read-only: it never enables V2, seeds pages, assigns permissions, or deploys config.
|
|
79
|
+
|
|
80
|
+
## References
|
|
81
|
+
|
|
82
|
+
| File | When to read |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `references/api-notes.md` | The three-dimensional V2 gate, why each signal is a proxy, and the exact queries used |
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# API notes — service-omni-command-center-analyze
|
|
2
|
+
|
|
3
|
+
## The three-dimensional V2 gate
|
|
4
|
+
|
|
5
|
+
Command Center for Service V2 renders only when **all** of the following hold, per the runtime
|
|
6
|
+
access check on the seeded page (`OmniChannel.userCanUseCommandCenterForServiceV2`):
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
orgHasCommandCenterForServiceV2Enabled
|
|
10
|
+
= release Gater
|
|
11
|
+
AND CommandCenterForServiceV2 org preference
|
|
12
|
+
AND NOT StandardOmniEOLExtension
|
|
13
|
+
AND UserPermissions.CommandCenterForServiceUser (per user)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
A single boolean cannot capture this, so the detector reports an explicit **state** across the
|
|
17
|
+
capability, preference/seed, and per-user permission dimensions.
|
|
18
|
+
|
|
19
|
+
## Why the preference is read via a proxy
|
|
20
|
+
|
|
21
|
+
The `CommandCenterForServiceV2` org preference is flipped only by an **internal** Setup
|
|
22
|
+
`SettingMutator` (`OmniChannelSettings.OrgPreferences.commandCenterForServiceV2`) and is **not**
|
|
23
|
+
exposed by the v66 Metadata API `OmniChannelSettings` (nor any other proven public write/read
|
|
24
|
+
surface). Its ON-flip, however, has an observable side effect: the platform seeds the
|
|
25
|
+
`CommandCenterForServiceV2_L` FlexiPage into a DB row and registers the V2 tab (idempotently). The
|
|
26
|
+
detector therefore treats the **seeded FlexiPage** as the proxy for "preference is ON":
|
|
27
|
+
|
|
28
|
+
- capability present + seed present ⇒ preference was enabled.
|
|
29
|
+
- capability present + seed absent ⇒ preference is off (`v2_available_not_enabled`).
|
|
30
|
+
|
|
31
|
+
This proxy is called out in the output; the detector does not claim to read the preference directly.
|
|
32
|
+
|
|
33
|
+
## Signals and queries
|
|
34
|
+
|
|
35
|
+
| Signal | Query | Notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| V2 capability | `SELECT Id FROM PermissionSet WHERE PermissionsCommandCenterForServiceUser = true LIMIT 1` | A `No such column`/`INVALID_FIELD` error ⇒ capability **absent** (org schema has no such user permission). Success ⇒ capability present. Any other error ⇒ `unknown` ⇒ `ambiguous`. |
|
|
38
|
+
| Seeded FlexiPage | Tooling: `SELECT Id FROM FlexiPage WHERE DeveloperName='CommandCenterForServiceV2_L'` | Presence ⇒ preference enabled + page seeded. Query error ⇒ `unknown` ⇒ `ambiguous`. |
|
|
39
|
+
| V2 tab | `SELECT Name FROM TabDefinition WHERE Name='standard-commandcenterforservicev2'` | Best-effort cross-check. Error ⇒ `"unknown"`; state falls back to capability + seed. |
|
|
40
|
+
| Supervisor permission | `SELECT Id FROM PermissionSetAssignment WHERE AssigneeId='…' AND PermissionSet.PermissionsCommandCenterForServiceUser = true` | Catches both permission-set and profile-granted permission (profiles surface as owned permission sets). Only run when a supervisor is supplied. |
|
|
41
|
+
| Legacy config | Tooling: `SELECT Id FROM OmniSupervisorConfig` | Informational count for the classic path. |
|
|
42
|
+
|
|
43
|
+
## Seed/tab consistency
|
|
44
|
+
|
|
45
|
+
The enable hook performs **two** side effects — tab registration and FlexiPage seed. A partial
|
|
46
|
+
failure can leave exactly one present. When both signals are known and disagree, the detector
|
|
47
|
+
returns `v2_seed_incomplete` (re-provision or apply the manual Setup step) rather than a misleading
|
|
48
|
+
`v2_ready`.
|
|
49
|
+
|
|
50
|
+
## Why `ambiguous` blocks
|
|
51
|
+
|
|
52
|
+
The review's rule is explicit: when a required signal cannot be read through a supported API, **block
|
|
53
|
+
and do not guess**. `ambiguous` is the only state that exits non-zero, so a coordinator will not
|
|
54
|
+
proceed to enable or configure on unproven state.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# analyze.sh - read-only analysis of the Omni supervisor experience (Command Center V2 vs classic) + recommended next action; SOQL/Tooling reads only, no writes. Args, states, exit codes: SKILL.md.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ $# -lt 1 ]; then
|
|
7
|
+
echo '{"error":"Usage: bash analyze.sh <org-alias> [supervisor_username_or_id]"}' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
ORG="$1"
|
|
12
|
+
SUP="${2:-}"
|
|
13
|
+
|
|
14
|
+
AUTH_CHECK=$(sf org display --target-org "$ORG" --json 2>/dev/null || true)
|
|
15
|
+
if [ -z "$AUTH_CHECK" ]; then
|
|
16
|
+
echo "{\"error\":\"Org alias '$ORG' is not authenticated. Run: sf org login web -a $ORG -r <my-domain-url>\"}" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Best-effort query helper: echoes the raw JSON; caller inspects .status and payload.
|
|
21
|
+
soql() { sf data query --target-org "$ORG" --json --query "$1" 2>/dev/null || true; }
|
|
22
|
+
soql_tooling() { sf data query --target-org "$ORG" --use-tooling-api --json --query "$1" 2>/dev/null || true; }
|
|
23
|
+
|
|
24
|
+
json_status() { echo "$1" | jq -r '.status // 1' 2>/dev/null || echo 1; }
|
|
25
|
+
json_total() { echo "$1" | jq -r '.result.totalSize // (.result.records | length) // 0' 2>/dev/null || echo 0; }
|
|
26
|
+
|
|
27
|
+
# Signal 1 - V2 capability (does the user-permission column exist on the org schema?)
|
|
28
|
+
CAP_JSON=$(soql "SELECT Id FROM PermissionSet WHERE PermissionsCommandCenterForServiceUser = true LIMIT 1")
|
|
29
|
+
if [ "$(json_status "$CAP_JSON")" = "0" ]; then
|
|
30
|
+
CAPABILITY=true
|
|
31
|
+
elif echo "$CAP_JSON" | grep -qiE "No such column|INVALID_FIELD|PermissionsCommandCenterForServiceUser"; then
|
|
32
|
+
CAPABILITY=false
|
|
33
|
+
else
|
|
34
|
+
CAPABILITY=unknown
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Signal 2 - Seeded FlexiPage (proxy for the CommandCenterForServiceV2 preference being ON)
|
|
38
|
+
SEED_PRESENT=unknown
|
|
39
|
+
if [ "$CAPABILITY" = "true" ]; then
|
|
40
|
+
SEED_JSON=$(soql_tooling "SELECT Id FROM FlexiPage WHERE DeveloperName='CommandCenterForServiceV2_L'")
|
|
41
|
+
if [ "$(json_status "$SEED_JSON")" = "0" ]; then
|
|
42
|
+
if [ "$(json_total "$SEED_JSON")" -gt 0 ]; then SEED_PRESENT=true; else SEED_PRESENT=false; fi
|
|
43
|
+
else
|
|
44
|
+
SEED_PRESENT=unknown
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Signal 3 - V2 tab (best-effort cross-check)
|
|
49
|
+
TAB_PRESENT=unknown
|
|
50
|
+
if [ "$CAPABILITY" = "true" ]; then
|
|
51
|
+
TAB_JSON=$(soql "SELECT Name FROM TabDefinition WHERE Name='standard-commandcenterforservicev2' LIMIT 1")
|
|
52
|
+
if [ "$(json_status "$TAB_JSON")" = "0" ]; then
|
|
53
|
+
if [ "$(json_total "$TAB_JSON")" -gt 0 ]; then TAB_PRESENT=true; else TAB_PRESENT=false; fi
|
|
54
|
+
else
|
|
55
|
+
TAB_PRESENT=unknown
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Signal 4 - Legacy OmniSupervisorConfig count (informational)
|
|
60
|
+
LEG_JSON=$(soql_tooling "SELECT Id FROM OmniSupervisorConfig")
|
|
61
|
+
if [ "$(json_status "$LEG_JSON")" = "0" ]; then LEG_COUNT=$(json_total "$LEG_JSON"); else LEG_COUNT="null"; fi
|
|
62
|
+
|
|
63
|
+
# Signal 5 - Supervisor permission (only when a supervisor is supplied)
|
|
64
|
+
SUP_CHECKED=false
|
|
65
|
+
SUP_HAS_PERM=unknown
|
|
66
|
+
SUP_ID=""
|
|
67
|
+
SUP_IDENT="null"
|
|
68
|
+
SUP_CHECK_ERROR=""
|
|
69
|
+
if [ -n "$SUP" ]; then
|
|
70
|
+
SUP_IDENT="$SUP"
|
|
71
|
+
if [[ "$SUP" =~ ^005[A-Za-z0-9]{12}([A-Za-z0-9]{3})?$ ]]; then
|
|
72
|
+
SUP_QUERY="SELECT Id FROM User WHERE Id='$SUP' LIMIT 1"
|
|
73
|
+
elif [[ "$SUP" =~ ^[^,\'\"[:space:]]+@[^,\'\"[:space:]]+$ ]]; then
|
|
74
|
+
SUP_QUERY="SELECT Id FROM User WHERE Username='$SUP' LIMIT 1"
|
|
75
|
+
else
|
|
76
|
+
echo "{\"error\":\"supervisor_username_or_id '$SUP' is neither a Username (...@...) nor a 15/18-char User Id (005...).\"}" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
if [ "$CAPABILITY" = "true" ]; then
|
|
81
|
+
UID_JSON=$(soql "$SUP_QUERY")
|
|
82
|
+
if [ "$(json_status "$UID_JSON")" != "0" ]; then
|
|
83
|
+
SUP_CHECK_ERROR="Supplied supervisor '$SUP_IDENT' could not be read through a supported API. Not guessing; verify User read access and retry."
|
|
84
|
+
elif [ "$(json_total "$UID_JSON")" -ne 1 ]; then
|
|
85
|
+
SUP_CHECK_ERROR="Supplied supervisor '$SUP_IDENT' was not found. Verify the username or User Id and retry."
|
|
86
|
+
else
|
|
87
|
+
SUP_ID=$(echo "$UID_JSON" | jq -r '.result.records[0].Id // ""' 2>/dev/null)
|
|
88
|
+
if [ -z "$SUP_ID" ]; then
|
|
89
|
+
SUP_CHECK_ERROR="Supplied supervisor '$SUP_IDENT' lookup returned no User Id. Not guessing; verify User read access and retry."
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [ -z "$SUP_CHECK_ERROR" ]; then
|
|
94
|
+
PERM_JSON=$(soql "SELECT Id FROM PermissionSetAssignment WHERE AssigneeId='$SUP_ID' AND PermissionSet.PermissionsCommandCenterForServiceUser = true LIMIT 1")
|
|
95
|
+
if [ "$(json_status "$PERM_JSON")" = "0" ]; then
|
|
96
|
+
SUP_CHECKED=true
|
|
97
|
+
if [ "$(json_total "$PERM_JSON")" -gt 0 ]; then SUP_HAS_PERM=true; else SUP_HAS_PERM=false; fi
|
|
98
|
+
else
|
|
99
|
+
SUP_CHECK_ERROR="CommandCenterForServiceUser permission could not be read for supplied supervisor '$SUP_IDENT'. Not guessing; verify PermissionSetAssignment read access and retry."
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Derive state
|
|
106
|
+
STATE=""; REC_ACTION=""; REC_SKILL="null"; STATUS="detected"; BLOCKING="null"
|
|
107
|
+
|
|
108
|
+
if [ "$CAPABILITY" = "unknown" ]; then
|
|
109
|
+
STATE="ambiguous"; STATUS="blocked"
|
|
110
|
+
REC_ACTION="V2 capability could not be read through a supported API. Not guessing; re-run with an authenticated org that permits these read queries."
|
|
111
|
+
BLOCKING="\"$REC_ACTION\""
|
|
112
|
+
elif [ "$CAPABILITY" = "false" ]; then
|
|
113
|
+
STATE="legacy_selected"
|
|
114
|
+
REC_ACTION="Command Center for Service V2 capability is not present on this org; configure the classic Omni Supervisor experience."
|
|
115
|
+
REC_SKILL="\"service-omni-supervisor-config-deploy\""
|
|
116
|
+
elif [ -n "$SUP_CHECK_ERROR" ]; then
|
|
117
|
+
STATE="ambiguous"; STATUS="blocked"
|
|
118
|
+
REC_ACTION="$SUP_CHECK_ERROR"
|
|
119
|
+
BLOCKING="\"$REC_ACTION\""
|
|
120
|
+
elif [ "$SEED_PRESENT" = "unknown" ]; then
|
|
121
|
+
STATE="ambiguous"; STATUS="blocked"
|
|
122
|
+
REC_ACTION="V2 capability is present but the seeded FlexiPage could not be read through a supported API. Not guessing; re-run with an org that permits the Tooling query."
|
|
123
|
+
BLOCKING="\"$REC_ACTION\""
|
|
124
|
+
else
|
|
125
|
+
# capability true, seed known (true/false)
|
|
126
|
+
if [ "$SEED_PRESENT" = "true" ] && { [ "$TAB_PRESENT" = "true" ] || [ "$TAB_PRESENT" = "unknown" ]; }; then
|
|
127
|
+
if [ "$SUP_CHECKED" = "true" ] && [ "$SUP_HAS_PERM" = "false" ]; then
|
|
128
|
+
STATE="v2_permission_missing"
|
|
129
|
+
REC_ACTION="V2 is enabled and seeded, but supervisor '$SUP_IDENT' lacks the CommandCenterForServiceUser permission. Assign it via a PermissionSet (headless-capable; see manual_actions for packaging)."
|
|
130
|
+
REC_SKILL="null"
|
|
131
|
+
else
|
|
132
|
+
STATE="v2_ready"
|
|
133
|
+
if [ "$SUP_CHECKED" = "true" ]; then
|
|
134
|
+
REC_ACTION="V2 is enabled and seeded and supervisor '$SUP_IDENT' has the CommandCenterForServiceUser permission. Verify the seeded page and tab."
|
|
135
|
+
else
|
|
136
|
+
REC_ACTION="V2 is enabled and seeded at the org level. Supply a supervisor to verify per-user permission; otherwise verify the seeded page and tab."
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
elif { [ "$SEED_PRESENT" = "true" ] && [ "$TAB_PRESENT" = "false" ]; } || { [ "$SEED_PRESENT" = "false" ] && [ "$TAB_PRESENT" = "true" ]; }; then
|
|
140
|
+
STATE="v2_seed_incomplete"
|
|
141
|
+
REC_ACTION="V2 capability is present but page/tab provisioning is inconsistent (seed_flexipage_present=$SEED_PRESENT, v2_tab_present=$TAB_PRESENT). Re-check in Setup → Omni-Channel → Supervisor Settings; there is no supported public write API to re-provision headlessly."
|
|
142
|
+
REC_SKILL="null"
|
|
143
|
+
else
|
|
144
|
+
STATE="v2_available_not_enabled"
|
|
145
|
+
REC_ACTION="Org supports V2 but the CommandCenterForServiceV2 preference is off (no seeded page). Enable it manually in Setup → Omni-Channel → Supervisor Settings; there is no supported Metadata/Tooling write API for this preference."
|
|
146
|
+
REC_SKILL="null"
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# manual_actions: for permission_missing and ambiguous surface a concrete pointer.
|
|
151
|
+
MANUAL_ACTIONS='[]'
|
|
152
|
+
case "$STATE" in
|
|
153
|
+
v2_permission_missing)
|
|
154
|
+
MANUAL_ACTIONS=$(jq -n '[{id:"ASSIGN_V2_PERM",title:"Assign the CommandCenterForServiceUser permission to the supervisor (headless-capable via PermissionSet; packaging pending)",target_skill:null}]') ;;
|
|
155
|
+
v2_available_not_enabled|v2_seed_incomplete)
|
|
156
|
+
MANUAL_ACTIONS=$(jq -n '[{id:"ENABLE_V2_PREF",title:"Enable Command Center for Service V2 in Setup → Omni-Channel → Supervisor Settings (no supported public write API; manual)",target_skill:null}]') ;;
|
|
157
|
+
esac
|
|
158
|
+
|
|
159
|
+
# Emit
|
|
160
|
+
LEG_JQ="$LEG_COUNT"; [ "$LEG_COUNT" = "null" ] && LEG_JQ=null
|
|
161
|
+
|
|
162
|
+
jq -n \
|
|
163
|
+
--arg status "$STATUS" \
|
|
164
|
+
--arg state "$STATE" \
|
|
165
|
+
--arg rec_action "$REC_ACTION" \
|
|
166
|
+
--argjson rec_skill "$REC_SKILL" \
|
|
167
|
+
--arg cap "$CAPABILITY" \
|
|
168
|
+
--arg seed "$SEED_PRESENT" \
|
|
169
|
+
--arg tab "$TAB_PRESENT" \
|
|
170
|
+
--argjson sup_ident "$( [ "$SUP_IDENT" = "null" ] && echo null || jq -n --arg s "$SUP_IDENT" '$s' )" \
|
|
171
|
+
--argjson sup_checked "$SUP_CHECKED" \
|
|
172
|
+
--arg sup_perm "$SUP_HAS_PERM" \
|
|
173
|
+
--argjson leg "$LEG_JQ" \
|
|
174
|
+
--argjson manual "$MANUAL_ACTIONS" \
|
|
175
|
+
--argjson blocking "$BLOCKING" \
|
|
176
|
+
'{
|
|
177
|
+
skill: "service-omni-command-center-analyze",
|
|
178
|
+
status: $status,
|
|
179
|
+
state: $state,
|
|
180
|
+
recommended_action: $rec_action,
|
|
181
|
+
recommended_skill: $rec_skill,
|
|
182
|
+
signals: {
|
|
183
|
+
v2_capability: $cap,
|
|
184
|
+
seed_flexipage_present: $seed,
|
|
185
|
+
v2_tab_present: $tab,
|
|
186
|
+
supervisor: {
|
|
187
|
+
identifier: $sup_ident,
|
|
188
|
+
checked: $sup_checked,
|
|
189
|
+
has_command_center_permission: (if $sup_checked then ($sup_perm=="true") else $sup_perm end)
|
|
190
|
+
},
|
|
191
|
+
legacy_omnisupervisorconfig_count: $leg
|
|
192
|
+
},
|
|
193
|
+
manual_actions: $manual,
|
|
194
|
+
blocking_issue: $blocking
|
|
195
|
+
}'
|
|
196
|
+
|
|
197
|
+
[ "$STATUS" = "blocked" ] && exit 1
|
|
198
|
+
exit 0
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Shared paths + fake-`sf` harness for the command-center-analyze contract suite.
|
|
2
|
+
|
|
3
|
+
Read-only skill: the fake serves SOQL/Tooling query responses only. Org-free and network-free.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
SKILLS_ROOT = Path(__file__).resolve().parents[3]
|
|
13
|
+
SKILL_DIR = "service-omni-command-center-analyze"
|
|
14
|
+
SCRIPT = f"{SKILL_DIR}/scripts/analyze.sh"
|
|
15
|
+
|
|
16
|
+
# Tunables (env):
|
|
17
|
+
# FAKE_CAP = true|false|unknown (V2 capability probe on PermissionSet)
|
|
18
|
+
# FAKE_SEED = true|false|unknown (seeded FlexiPage, tooling)
|
|
19
|
+
# FAKE_TAB = true|false|unknown (V2 TabDefinition)
|
|
20
|
+
# FAKE_LEG = integer (OmniSupervisorConfig count; default 0)
|
|
21
|
+
# FAKE_SUP_LOOKUP = true|missing|unknown (supervisor User lookup)
|
|
22
|
+
# FAKE_SUP_PERM = true|false|unknown (supervisor permission query)
|
|
23
|
+
_FAKE_SF = r"""#!/usr/bin/env bash
|
|
24
|
+
sub="${1:-} ${2:-}"; args="$*"
|
|
25
|
+
CAP="${FAKE_CAP:-true}"
|
|
26
|
+
SEED="${FAKE_SEED:-false}"
|
|
27
|
+
TAB="${FAKE_TAB:-false}"
|
|
28
|
+
LEG="${FAKE_LEG:-0}"
|
|
29
|
+
SUP_PERM="${FAKE_SUP_PERM:-false}"
|
|
30
|
+
SUP_LOOKUP="${FAKE_SUP_LOOKUP:-true}"
|
|
31
|
+
emit_ok() { echo "{\"status\":0,\"result\":{\"records\":$1,\"totalSize\":$2}}"; }
|
|
32
|
+
case "$sub" in
|
|
33
|
+
"org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
|
|
34
|
+
"data query")
|
|
35
|
+
if printf '%s' "$args" | grep -q -- "--use-tooling-api"; then
|
|
36
|
+
if printf '%s' "$args" | grep -q "FROM FlexiPage"; then
|
|
37
|
+
case "$SEED" in
|
|
38
|
+
true) emit_ok '[{"Id":"0M0xx0000000001AAA"}]' 1;;
|
|
39
|
+
false) emit_ok '[]' 0;;
|
|
40
|
+
*) echo '{"status":1,"name":"UNEXPECTED","message":"tooling read failed"}';;
|
|
41
|
+
esac
|
|
42
|
+
elif printf '%s' "$args" | grep -q "FROM OmniSupervisorConfig"; then
|
|
43
|
+
if [ "$LEG" -gt 0 ] 2>/dev/null; then emit_ok '[{"Id":"0L0xx0000000001AAA"}]' "$LEG"; else emit_ok '[]' 0; fi
|
|
44
|
+
else
|
|
45
|
+
emit_ok '[]' 0
|
|
46
|
+
fi
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
# non-tooling SOQL
|
|
50
|
+
if printf '%s' "$args" | grep -q "FROM PermissionSetAssignment"; then
|
|
51
|
+
case "$SUP_PERM" in
|
|
52
|
+
true) emit_ok '[{"Id":"0Paxx0000000001AAA"}]' 1;;
|
|
53
|
+
false) emit_ok '[]' 0;;
|
|
54
|
+
*) echo '{"status":1,"name":"UNEXPECTED","message":"permission read failed"}';;
|
|
55
|
+
esac
|
|
56
|
+
elif printf '%s' "$args" | grep -q "FROM PermissionSet"; then
|
|
57
|
+
case "$CAP" in
|
|
58
|
+
true) emit_ok '[{"Id":"0PSxx0000000001AAA"}]' 1;;
|
|
59
|
+
false) echo '{"status":1,"name":"INVALID_FIELD","message":"No such column '"'"'PermissionsCommandCenterForServiceUser'"'"' on entity PermissionSet"}';;
|
|
60
|
+
*) echo '{"status":1,"name":"UNEXPECTED","message":"connection reset"}';;
|
|
61
|
+
esac
|
|
62
|
+
elif printf '%s' "$args" | grep -q "FROM TabDefinition"; then
|
|
63
|
+
case "$TAB" in
|
|
64
|
+
true) emit_ok '[{"Name":"standard-commandcenterforservicev2"}]' 1;;
|
|
65
|
+
false) emit_ok '[]' 0;;
|
|
66
|
+
*) echo '{"status":1,"name":"UNEXPECTED","message":"tab read failed"}';;
|
|
67
|
+
esac
|
|
68
|
+
elif printf '%s' "$args" | grep -q "FROM User"; then
|
|
69
|
+
case "$SUP_LOOKUP" in
|
|
70
|
+
true) emit_ok '[{"Id":"0050000000000009AAA"}]' 1;;
|
|
71
|
+
missing) emit_ok '[]' 0;;
|
|
72
|
+
*) echo '{"status":1,"name":"UNEXPECTED","message":"user read failed"}';;
|
|
73
|
+
esac
|
|
74
|
+
else
|
|
75
|
+
emit_ok '[]' 0
|
|
76
|
+
fi
|
|
77
|
+
exit 0;;
|
|
78
|
+
*) echo '{"status":0,"result":{}}'; exit 0;;
|
|
79
|
+
esac
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
|
|
84
|
+
env = dict(os.environ)
|
|
85
|
+
if scrub_sf:
|
|
86
|
+
kept = [d for d in env.get("PATH", "").split(os.pathsep)
|
|
87
|
+
if d and not os.path.exists(os.path.join(d, "sf"))]
|
|
88
|
+
env["PATH"] = os.pathsep.join(kept)
|
|
89
|
+
if path_prefix:
|
|
90
|
+
env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
|
|
91
|
+
if extra_env:
|
|
92
|
+
env.update({k: str(v) for k, v in extra_env.items()})
|
|
93
|
+
proc = subprocess.run(
|
|
94
|
+
["bash", str(SKILLS_ROOT / SCRIPT), *args],
|
|
95
|
+
capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
|
|
96
|
+
)
|
|
97
|
+
return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class FakeSf:
|
|
101
|
+
def __init__(self, body=_FAKE_SF):
|
|
102
|
+
self._body = body
|
|
103
|
+
self._tmp = None
|
|
104
|
+
|
|
105
|
+
def __enter__(self):
|
|
106
|
+
self._tmp = tempfile.mkdtemp(prefix="ccdetect-fake-sf-")
|
|
107
|
+
sf = Path(self._tmp) / "sf"
|
|
108
|
+
sf.write_text(self._body)
|
|
109
|
+
sf.chmod(0o755)
|
|
110
|
+
return self._tmp
|
|
111
|
+
|
|
112
|
+
def __exit__(self, *exc):
|
|
113
|
+
import shutil
|
|
114
|
+
if self._tmp:
|
|
115
|
+
shutil.rmtree(self._tmp, ignore_errors=True)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Contract tests for service-omni-command-center-analyze/scripts/analyze.sh (read-only state model)."""
|
|
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
|
+
class InputContractTests(unittest.TestCase):
|
|
33
|
+
def test_missing_args_rejected(self):
|
|
34
|
+
rc, out = run([], scrub_sf=True)
|
|
35
|
+
self.assertEqual(rc, 1, out)
|
|
36
|
+
self.assertIn("Usage", out)
|
|
37
|
+
|
|
38
|
+
def test_bad_supervisor_token_rejected(self):
|
|
39
|
+
with FakeSf(_FAKE_SF) as p:
|
|
40
|
+
rc, out = run(["org", "not-a-user-or-id"], path_prefix=p, extra_env={"FAKE_CAP": "true"})
|
|
41
|
+
self.assertEqual(rc, 1, out)
|
|
42
|
+
self.assertIn("neither a Username", out)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StateModelTests(unittest.TestCase):
|
|
46
|
+
def _detect(self, env, args=("org",)):
|
|
47
|
+
with FakeSf(_FAKE_SF) as p:
|
|
48
|
+
return run(list(args), path_prefix=p, extra_env=env)
|
|
49
|
+
|
|
50
|
+
def test_legacy_when_capability_absent(self):
|
|
51
|
+
rc, out = self._detect({"FAKE_CAP": "false"})
|
|
52
|
+
self.assertEqual(rc, 0, out)
|
|
53
|
+
v = _last_json(out)
|
|
54
|
+
self.assertEqual(v["state"], "legacy_selected", out)
|
|
55
|
+
self.assertEqual(v["recommended_skill"], "service-omni-supervisor-config-deploy", out)
|
|
56
|
+
self.assertEqual(v["signals"]["v2_capability"], "false", out)
|
|
57
|
+
|
|
58
|
+
def test_ambiguous_when_capability_unknown_blocks(self):
|
|
59
|
+
rc, out = self._detect({"FAKE_CAP": "unknown"})
|
|
60
|
+
self.assertEqual(rc, 1, out)
|
|
61
|
+
v = _last_json(out)
|
|
62
|
+
self.assertEqual(v["state"], "ambiguous", out)
|
|
63
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
64
|
+
|
|
65
|
+
def test_ambiguous_when_seed_unreadable_blocks(self):
|
|
66
|
+
rc, out = self._detect({"FAKE_CAP": "true", "FAKE_SEED": "unknown"})
|
|
67
|
+
self.assertEqual(rc, 1, out)
|
|
68
|
+
v = _last_json(out)
|
|
69
|
+
self.assertEqual(v["state"], "ambiguous", out)
|
|
70
|
+
|
|
71
|
+
def test_available_not_enabled_when_no_seed(self):
|
|
72
|
+
rc, out = self._detect({"FAKE_CAP": "true", "FAKE_SEED": "false", "FAKE_TAB": "false"})
|
|
73
|
+
self.assertEqual(rc, 0, out)
|
|
74
|
+
v = _last_json(out)
|
|
75
|
+
self.assertEqual(v["state"], "v2_available_not_enabled", out)
|
|
76
|
+
self.assertTrue(any(a["id"] == "ENABLE_V2_PREF" for a in v["manual_actions"]), out)
|
|
77
|
+
|
|
78
|
+
def test_ready_org_level_without_supervisor(self):
|
|
79
|
+
rc, out = self._detect({"FAKE_CAP": "true", "FAKE_SEED": "true", "FAKE_TAB": "true"})
|
|
80
|
+
self.assertEqual(rc, 0, out)
|
|
81
|
+
v = _last_json(out)
|
|
82
|
+
self.assertEqual(v["state"], "v2_ready", out)
|
|
83
|
+
self.assertFalse(v["signals"]["supervisor"]["checked"], out)
|
|
84
|
+
|
|
85
|
+
def test_ready_with_supervisor_having_permission(self):
|
|
86
|
+
rc, out = self._detect(
|
|
87
|
+
{"FAKE_CAP": "true", "FAKE_SEED": "true", "FAKE_TAB": "true", "FAKE_SUP_PERM": "true"},
|
|
88
|
+
args=("org", "sup@example.com"))
|
|
89
|
+
self.assertEqual(rc, 0, out)
|
|
90
|
+
v = _last_json(out)
|
|
91
|
+
self.assertEqual(v["state"], "v2_ready", out)
|
|
92
|
+
self.assertTrue(v["signals"]["supervisor"]["checked"], out)
|
|
93
|
+
self.assertTrue(v["signals"]["supervisor"]["has_command_center_permission"], out)
|
|
94
|
+
|
|
95
|
+
def test_permission_missing_when_supervisor_lacks_perm(self):
|
|
96
|
+
rc, out = self._detect(
|
|
97
|
+
{"FAKE_CAP": "true", "FAKE_SEED": "true", "FAKE_TAB": "true", "FAKE_SUP_PERM": "false"},
|
|
98
|
+
args=("org", "005000000000002AAA"))
|
|
99
|
+
self.assertEqual(rc, 0, out)
|
|
100
|
+
v = _last_json(out)
|
|
101
|
+
self.assertEqual(v["state"], "v2_permission_missing", out)
|
|
102
|
+
self.assertIsNone(v["recommended_skill"], out)
|
|
103
|
+
self.assertTrue(any(a["id"] == "ASSIGN_V2_PERM" for a in v["manual_actions"]), out)
|
|
104
|
+
self.assertFalse(v["signals"]["supervisor"]["has_command_center_permission"], out)
|
|
105
|
+
|
|
106
|
+
def test_missing_supplied_supervisor_blocks(self):
|
|
107
|
+
rc, out = self._detect(
|
|
108
|
+
{
|
|
109
|
+
"FAKE_CAP": "true",
|
|
110
|
+
"FAKE_SEED": "true",
|
|
111
|
+
"FAKE_TAB": "true",
|
|
112
|
+
"FAKE_SUP_LOOKUP": "missing",
|
|
113
|
+
},
|
|
114
|
+
args=("org", "sup@example.com"))
|
|
115
|
+
self.assertEqual(rc, 1, out)
|
|
116
|
+
v = _last_json(out)
|
|
117
|
+
self.assertEqual(v["state"], "ambiguous", out)
|
|
118
|
+
self.assertEqual(v["status"], "blocked", out)
|
|
119
|
+
self.assertIn("was not found", v["blocking_issue"], out)
|
|
120
|
+
self.assertFalse(v["signals"]["supervisor"]["checked"], out)
|
|
121
|
+
|
|
122
|
+
def test_unreadable_supplied_supervisor_blocks(self):
|
|
123
|
+
rc, out = self._detect(
|
|
124
|
+
{
|
|
125
|
+
"FAKE_CAP": "true",
|
|
126
|
+
"FAKE_SEED": "true",
|
|
127
|
+
"FAKE_TAB": "true",
|
|
128
|
+
"FAKE_SUP_LOOKUP": "unknown",
|
|
129
|
+
},
|
|
130
|
+
args=("org", "005000000000002AAA"))
|
|
131
|
+
self.assertEqual(rc, 1, out)
|
|
132
|
+
v = _last_json(out)
|
|
133
|
+
self.assertEqual(v["state"], "ambiguous", out)
|
|
134
|
+
self.assertIn("could not be read", v["blocking_issue"], out)
|
|
135
|
+
|
|
136
|
+
def test_unreadable_supervisor_permission_blocks(self):
|
|
137
|
+
rc, out = self._detect(
|
|
138
|
+
{
|
|
139
|
+
"FAKE_CAP": "true",
|
|
140
|
+
"FAKE_SEED": "true",
|
|
141
|
+
"FAKE_TAB": "true",
|
|
142
|
+
"FAKE_SUP_PERM": "unknown",
|
|
143
|
+
},
|
|
144
|
+
args=("org", "sup@example.com"))
|
|
145
|
+
self.assertEqual(rc, 1, out)
|
|
146
|
+
v = _last_json(out)
|
|
147
|
+
self.assertEqual(v["state"], "ambiguous", out)
|
|
148
|
+
self.assertIn("permission could not be read", v["blocking_issue"], out)
|
|
149
|
+
|
|
150
|
+
def test_seed_incomplete_when_seed_without_tab(self):
|
|
151
|
+
rc, out = self._detect({"FAKE_CAP": "true", "FAKE_SEED": "true", "FAKE_TAB": "false"})
|
|
152
|
+
self.assertEqual(rc, 0, out)
|
|
153
|
+
v = _last_json(out)
|
|
154
|
+
self.assertEqual(v["state"], "v2_seed_incomplete", out)
|
|
155
|
+
|
|
156
|
+
def test_ready_when_tab_unknown(self):
|
|
157
|
+
rc, out = self._detect({"FAKE_CAP": "true", "FAKE_SEED": "true", "FAKE_TAB": "unknown"})
|
|
158
|
+
self.assertEqual(rc, 0, out)
|
|
159
|
+
v = _last_json(out)
|
|
160
|
+
self.assertEqual(v["state"], "v2_ready", out)
|
|
161
|
+
self.assertEqual(v["signals"]["v2_tab_present"], "unknown", out)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
unittest.main(verbosity=2)
|