@salesforce/afv-skills 1.47.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/package.json +1 -1
  2. package/skills/education-cloud-domain-configure/SKILL.md +234 -0
  3. package/skills/education-cloud-domain-configure/references/dependent-features.md +198 -0
  4. package/skills/education-cloud-domain-configure/references/domain-features/academic-operations.md +386 -0
  5. package/skills/education-cloud-domain-configure/references/domain-features/alumni-relations.md +155 -0
  6. package/skills/education-cloud-domain-configure/references/domain-features/cross-domain-dependencies.md +18 -0
  7. package/skills/education-cloud-domain-configure/references/domain-features/industriessettings-toggle-reference.md +43 -0
  8. package/skills/education-cloud-domain-configure/references/domain-features/mentoring.md +198 -0
  9. package/skills/education-cloud-domain-configure/references/domain-features/recruitment-admissions.md +315 -0
  10. package/skills/education-cloud-domain-configure/references/domain-features/student-management.md +155 -0
  11. package/skills/education-cloud-domain-configure/references/domain-features/student-success.md +249 -0
  12. package/skills/education-cloud-domain-configure/references/domain-features.md +27 -0
  13. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +23 -0
  14. package/skills/education-cloud-domain-configure/references/toggle-write-path.md +67 -0
  15. package/skills/education-cloud-domain-configure/scripts/check_license_provisioned.py +62 -0
  16. package/skills/education-cloud-domain-configure/scripts/check_org_edition.py +44 -0
  17. package/skills/education-cloud-domain-configure/scripts/check_permission_assigned.py +39 -0
  18. package/skills/education-cloud-multi-campus-configure/SKILL.md +7 -0
  19. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +4 -3
  20. package/skills/service-agentforce-channel-configure/SKILL.md +33 -28
  21. package/skills/service-agentforce-human-escalation-configure/SKILL.md +205 -0
  22. package/skills/service-agentforce-human-escalation-configure/assets/escalation-thresholds.instructions.md +37 -0
  23. package/skills/service-agentforce-human-escalation-configure/assets/force-app/main/default/flows/Human_Escalation_Outbound_Flow.flow-meta.xml +173 -0
  24. package/skills/service-agentforce-human-escalation-configure/assets/package.xml +8 -0
  25. package/skills/service-agentforce-human-escalation-configure/assets/sfdx-project.json +5 -0
  26. package/skills/service-agentforce-human-escalation-configure/references/classifier-contracts.md +103 -0
  27. package/skills/service-agentforce-human-escalation-configure/references/escalation-mechanism.md +63 -0
  28. package/skills/service-agentforce-human-escalation-configure/references/failure-policy.md +44 -0
  29. package/skills/service-agentforce-human-escalation-configure/references/nga-escalation.md +57 -0
  30. package/skills/service-agentforce-human-escalation-configure/references/runtime-verification.md +72 -0
  31. package/skills/service-agentforce-human-escalation-configure/scripts/classify-agent-active.mjs +49 -0
  32. package/skills/service-agentforce-human-escalation-configure/scripts/classify-nga-escalation.mjs +166 -0
  33. package/skills/service-agentforce-human-escalation-configure/scripts/extract-outbound-route.mjs +67 -0
  34. package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs +131 -0
  35. package/skills/service-agentforce-human-escalation-configure/scripts/tests/__init__.py +0 -0
  36. package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +249 -0
  37. package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +899 -0
  38. package/skills/service-agentforce-human-escalation-configure/scripts/verify-and-configure.sh +705 -0
  39. package/skills/service-agentforce-human-escalation-configure/scripts/verify-escalation-config.mjs +89 -0
  40. package/skills/service-de-channel-routing-configure/SKILL.md +13 -5
  41. package/skills/service-de-channel-routing-configure/scripts/tests/test_skill_contract.py +25 -0
  42. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +23 -10
  43. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -48
  44. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +6 -2
  45. package/skills/service-omni-agent-users-create/SKILL.md +96 -0
  46. package/skills/service-omni-agent-users-create/assets/create-users.apex.template +80 -0
  47. package/skills/service-omni-agent-users-create/references/apex-patterns.md +135 -0
  48. package/skills/service-omni-agent-users-create/references/apex-template-notes.md +40 -0
  49. package/skills/service-omni-agent-users-create/scripts/detect-and-create.sh +206 -0
  50. package/skills/service-omni-agent-users-create/scripts/detect-existing.sh +122 -0
  51. package/skills/service-omni-agent-users-create/scripts/run-create.sh +298 -0
  52. package/skills/service-omni-agent-users-create/scripts/tests/test_user_create_security.py +171 -0
  53. package/skills/service-omni-agent-work-sharing-configure/SKILL.md +78 -0
  54. package/skills/service-omni-agent-work-sharing-configure/references/api-notes.md +31 -0
  55. package/skills/service-omni-agent-work-sharing-configure/scripts/configure-and-report.sh +186 -0
  56. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/_bootstrap.py +120 -0
  57. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/test_agent_work_sharing_contracts.py +93 -0
  58. package/skills/service-omni-base-settings-configure/SKILL.md +77 -0
  59. package/skills/service-omni-base-settings-configure/assets/force-app/main/default/settings/OmniChannel.settings-meta.xml +8 -0
  60. package/skills/service-omni-base-settings-configure/assets/package.xml +8 -0
  61. package/skills/service-omni-base-settings-configure/assets/sfdx-project.json +12 -0
  62. package/skills/service-omni-base-settings-configure/references/api-notes.md +121 -0
  63. package/skills/service-omni-base-settings-configure/scripts/configure-and-report.sh +164 -0
  64. package/skills/service-omni-base-settings-configure/scripts/detect-existing.sh +69 -0
  65. package/skills/service-omni-channel-setup-coordinate/SKILL.md +168 -0
  66. package/skills/service-omni-channel-setup-coordinate/examples/output-templates.md +161 -0
  67. package/skills/service-omni-channel-setup-coordinate/references/gap-catalog.md +99 -0
  68. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +1542 -0
  69. package/skills/service-omni-channel-setup-coordinate/scripts/tests/__init__.py +0 -0
  70. package/skills/service-omni-channel-setup-coordinate/scripts/tests/_bootstrap.py +223 -0
  71. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +804 -0
  72. package/skills/service-omni-command-center-analyze/SKILL.md +84 -0
  73. package/skills/service-omni-command-center-analyze/references/api-notes.md +54 -0
  74. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +198 -0
  75. package/skills/service-omni-command-center-analyze/scripts/tests/_bootstrap.py +115 -0
  76. package/skills/service-omni-command-center-analyze/scripts/tests/test_command_center_analyze_contracts.py +165 -0
  77. package/skills/service-omni-permission-set-assign/SKILL.md +88 -0
  78. package/skills/service-omni-permission-set-assign/assets/package.xml +8 -0
  79. package/skills/service-omni-permission-set-assign/references/api-notes.md +143 -0
  80. package/skills/service-omni-permission-set-assign/scripts/verify-and-assign.sh +397 -0
  81. package/skills/service-omni-presence-status-deploy/SKILL.md +87 -0
  82. package/skills/service-omni-presence-status-deploy/assets/package.xml +12 -0
  83. package/skills/service-omni-presence-status-deploy/references/api-notes.md +115 -0
  84. package/skills/service-omni-presence-status-deploy/scripts/deploy-and-report.sh +422 -0
  85. package/skills/service-omni-presence-user-config-deploy/SKILL.md +80 -0
  86. package/skills/service-omni-presence-user-config-deploy/references/api-notes.md +60 -0
  87. package/skills/service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh +261 -0
  88. package/skills/service-omni-presence-user-config-deploy/scripts/tests/_bootstrap.py +106 -0
  89. package/skills/service-omni-presence-user-config-deploy/scripts/tests/test_presence_user_config_contracts.py +142 -0
  90. package/skills/service-omni-queue-deploy/SKILL.md +91 -0
  91. package/skills/service-omni-queue-deploy/references/api-notes.md +140 -0
  92. package/skills/service-omni-queue-deploy/scripts/verify-and-align.sh +470 -0
  93. package/skills/service-omni-queue-members-assign/SKILL.md +93 -0
  94. package/skills/service-omni-queue-members-assign/references/api-notes.md +144 -0
  95. package/skills/service-omni-queue-members-assign/scripts/tests/_bootstrap.py +67 -0
  96. package/skills/service-omni-queue-members-assign/scripts/tests/test_queue_members_contracts.py +73 -0
  97. package/skills/service-omni-queue-members-assign/scripts/verify-and-bind.sh +362 -0
  98. package/skills/service-omni-queue-routing-config-deploy/SKILL.md +101 -0
  99. package/skills/service-omni-queue-routing-config-deploy/references/api-notes.md +127 -0
  100. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/_bootstrap.py +90 -0
  101. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/test_queue_routing_config_contracts.py +70 -0
  102. package/skills/service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh +538 -0
  103. package/skills/service-omni-routing-flow-deploy/SKILL.md +111 -0
  104. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml +94 -0
  105. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.flow-meta.xml +88 -0
  106. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Cases.flow-meta.xml +144 -0
  107. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml +94 -0
  108. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.flow-meta.xml +88 -0
  109. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCalls.flow-meta.xml +144 -0
  110. package/skills/service-omni-routing-flow-deploy/assets/package.xml +11 -0
  111. package/skills/service-omni-routing-flow-deploy/assets/sfdx-project.json +11 -0
  112. package/skills/service-omni-routing-flow-deploy/references/api-notes.md +265 -0
  113. package/skills/service-omni-routing-flow-deploy/scripts/deploy-and-report.sh +684 -0
  114. package/skills/service-omni-service-channel-configure/SKILL.md +83 -0
  115. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Cases.serviceChannel-meta.xml +9 -0
  116. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Incidents.serviceChannel-meta.xml +9 -0
  117. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/MessagingSessions.serviceChannel-meta.xml +9 -0
  118. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/VoiceCalls.serviceChannel-meta.xml +9 -0
  119. package/skills/service-omni-service-channel-configure/assets/package.xml +11 -0
  120. package/skills/service-omni-service-channel-configure/assets/sfdx-project.json +12 -0
  121. package/skills/service-omni-service-channel-configure/references/api-notes.md +84 -0
  122. package/skills/service-omni-service-channel-configure/scripts/deploy-and-report.sh +383 -0
  123. package/skills/service-omni-sidebar-configure/SKILL.md +77 -0
  124. package/skills/service-omni-sidebar-configure/references/api-notes.md +51 -0
  125. package/skills/service-omni-sidebar-configure/scripts/enable-and-report.sh +315 -0
  126. package/skills/service-omni-sidebar-configure/scripts/tests/_bootstrap.py +142 -0
  127. package/skills/service-omni-sidebar-configure/scripts/tests/test_sidebar_contracts.py +139 -0
  128. package/skills/service-omni-skills-based-routing-configure/SKILL.md +81 -0
  129. package/skills/service-omni-skills-based-routing-configure/references/api-notes.md +50 -0
  130. package/skills/service-omni-skills-based-routing-configure/scripts/configure-and-report.sh +270 -0
  131. package/skills/service-omni-skills-based-routing-configure/scripts/tests/_bootstrap.py +120 -0
  132. package/skills/service-omni-skills-based-routing-configure/scripts/tests/test_sbr_contracts.py +129 -0
  133. package/skills/service-omni-supervisor-config-deploy/SKILL.md +97 -0
  134. package/skills/service-omni-supervisor-config-deploy/assets/force-app/main/default/omniSupervisorConfigs/Omni_Supervisor.omniSupervisorConfig-meta.xml +9 -0
  135. package/skills/service-omni-supervisor-config-deploy/assets/package.xml +8 -0
  136. package/skills/service-omni-supervisor-config-deploy/assets/sfdx-project.json +12 -0
  137. package/skills/service-omni-supervisor-config-deploy/references/api-notes.md +205 -0
  138. package/skills/service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh +586 -0
  139. package/skills/service-omni-supervisor-config-deploy/scripts/tests/_bootstrap.py +153 -0
  140. package/skills/service-omni-supervisor-config-deploy/scripts/tests/test_supervisor_config_contracts.py +116 -0
  141. package/skills/service-omni-supervisor-permset-assign/SKILL.md +88 -0
  142. package/skills/service-omni-supervisor-permset-assign/references/api-notes.md +48 -0
  143. package/skills/service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh +316 -0
  144. package/skills/service-omni-supervisor-surface-deploy/SKILL.md +85 -0
  145. package/skills/service-omni-supervisor-surface-deploy/references/api-notes.md +47 -0
  146. package/skills/service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh +216 -0
  147. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/_bootstrap.py +95 -0
  148. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/test_supervisor_surface_contracts.py +154 -0
  149. package/skills/service-omni-supervisor-users-create/SKILL.md +92 -0
  150. package/skills/service-omni-supervisor-users-create/assets/create-supervisors.apex.template +72 -0
  151. package/skills/service-omni-supervisor-users-create/references/apex-patterns.md +73 -0
  152. package/skills/service-omni-supervisor-users-create/references/apex-template-notes.md +41 -0
  153. package/skills/service-omni-supervisor-users-create/scripts/detect-and-create.sh +197 -0
  154. package/skills/service-omni-supervisor-users-create/scripts/detect-existing.sh +112 -0
  155. package/skills/service-omni-supervisor-users-create/scripts/run-create.sh +296 -0
  156. package/skills/service-omni-work-skill-routing-configure/SKILL.md +80 -0
  157. package/skills/service-omni-work-skill-routing-configure/references/api-notes.md +67 -0
  158. package/skills/service-omni-work-skill-routing-configure/scripts/configure-and-report.sh +262 -0
  159. package/skills/service-omni-work-skill-routing-configure/scripts/tests/_bootstrap.py +121 -0
  160. package/skills/service-omni-work-skill-routing-configure/scripts/tests/test_work_skill_routing_contracts.py +161 -0
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ // Idempotent in-place patcher for canEscalate + outboundRoute surfaces; refuses (exit 4) if no safe insertion point. XSD order + modes: references/classifier-contracts.md.
3
+
4
+ import { readFileSync, writeFileSync } from 'node:fs';
5
+
6
+ const [mode, filePath, flowName, escalationMessageArg] = process.argv.slice(2);
7
+ if (!mode || !filePath) {
8
+ process.stderr.write('usage: node patch-escalation-surfaces.mjs <canEscalate|outboundRoute> <file> [flowApiName] [escalationMessage]\n');
9
+ process.exit(2);
10
+ }
11
+
12
+ const xmlEscape = (s) => String(s)
13
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
14
+
15
+ let text;
16
+ try {
17
+ text = readFileSync(filePath, 'utf8');
18
+ } catch {
19
+ process.stderr.write(`error: cannot read ${filePath}\n`);
20
+ process.exit(2);
21
+ }
22
+
23
+ const done = (summary) => { process.stdout.write(JSON.stringify(summary) + '\n'); process.exit(0); };
24
+ const refuse = (msg) => { process.stderr.write(`refuse: ${msg}\n`); process.exit(4); };
25
+
26
+ // Return {index, indent} of the earliest top-level opening tag from `tags` found in `hay`
27
+ // (searched from offset `from`), along with the leading whitespace on that tag's line.
28
+ const firstTag = (hay, tags, from = 0) => {
29
+ const scope = hay.slice(from);
30
+ let best = null;
31
+ for (const t of tags) {
32
+ const re = new RegExp(`([ \\t]*)<${t}(?=[ >/])`, 'g');
33
+ for (const m of scope.matchAll(re)) {
34
+ const idx = from + m.index;
35
+ if (best === null || idx < best.index) best = { index: idx, indent: m[1] || '' };
36
+ break; // first match of this tag is enough
37
+ }
38
+ }
39
+ return best;
40
+ };
41
+
42
+ if (mode === 'canEscalate') {
43
+ // Idempotent: normalize an existing <canEscalate> to true.
44
+ if (/<canEscalate>[\s\S]*?<\/canEscalate>/.test(text)) {
45
+ const next = text.replace(/<canEscalate>[\s\S]*?<\/canEscalate>/, '<canEscalate>true</canEscalate>');
46
+ if (next !== text) writeFileSync(filePath, next);
47
+ done({ mode, changed: next !== text, action: 'set-existing' });
48
+ }
49
+ const rootOpen = text.match(/<GenAiPlugin\b[^>]*>/);
50
+ if (!rootOpen) refuse('no <GenAiPlugin> root element found');
51
+ let searchFrom = rootOpen.index + rootOpen[0].length;
52
+ // Skip PAST any <aiPluginUtterances> blocks first - they legally precede canEscalate and each
53
+ // one carries a nested <developerName>, which would otherwise be mistaken for the top-level anchor.
54
+ const utteranceCloses = [...text.matchAll(/<\/aiPluginUtterances>/g)];
55
+ if (utteranceCloses.length) {
56
+ const last = utteranceCloses[utteranceCloses.length - 1];
57
+ searchFrom = last.index + '</aiPluginUtterances>'.length;
58
+ }
59
+ // canEscalate must come BEFORE the first of these top-level siblings.
60
+ const anchor = firstTag(text, [
61
+ 'description', 'developerName', 'genAiFunctions', 'genAiPluginInstructions',
62
+ 'language', 'localActionLinks', 'localActions', 'localDeveloperName',
63
+ 'masterLabel', 'pluginType', 'scope', 'source',
64
+ ], searchFrom);
65
+ if (!anchor) refuse('no post-canEscalate sibling (description/developerName/masterLabel/...) found; refusing to guess placement');
66
+ const indent = anchor.indent || ' ';
67
+ const insertion = `${indent}<canEscalate>true</canEscalate>\n`;
68
+ const followingTag = text.slice(anchor.index).match(/<([A-Za-z]+)/)[1];
69
+ const next = text.slice(0, anchor.index) + insertion + text.slice(anchor.index);
70
+ writeFileSync(filePath, next);
71
+ done({ mode, changed: true, action: 'inserted-before-' + followingTag });
72
+ }
73
+
74
+ if (mode === 'outboundRoute') {
75
+ if (!flowName) { process.stderr.write('error: outboundRoute requires <flowApiName>\n'); process.exit(2); }
76
+ const escMsg = xmlEscape(escalationMessageArg || 'Transferring you to a live support agent.');
77
+ const MESSAGING_SURFACES = new Set(['Messaging', 'CustomerWebClient']);
78
+
79
+ const surfaces = [...text.matchAll(/<plannerSurfaces>([\s\S]*?)<\/plannerSurfaces>/g)];
80
+ if (surfaces.length === 0) refuse('no <plannerSurfaces> block found; refusing to invent one');
81
+ const chosen = surfaces.find((m) => {
82
+ const t = m[1].match(/<surfaceType>\s*([^<]*?)\s*<\/surfaceType>/);
83
+ return t && MESSAGING_SURFACES.has(t[1].trim());
84
+ });
85
+ if (!chosen) refuse('no Messaging-class planner surface (surfaceType Messaging|CustomerWebClient) found');
86
+
87
+ const innerStart = chosen.index + '<plannerSurfaces>'.length;
88
+ const innerEnd = chosen.index + chosen[0].length - '</plannerSurfaces>'.length;
89
+ const inner = text.slice(innerStart, innerEnd);
90
+
91
+ // Idempotent: an existing outboundRouteConfigs already targeting this flow -> normalize the type only.
92
+ const existing = [...inner.matchAll(/<outboundRouteConfigs>([\s\S]*?)<\/outboundRouteConfigs>/g)]
93
+ .find((c) => {
94
+ const n = c[1].match(/<outboundRouteName>\s*([^<]*?)\s*<\/outboundRouteName>/);
95
+ return n && n[1].trim() === flowName;
96
+ });
97
+ if (existing) {
98
+ const block = existing[0];
99
+ let newBlock = block;
100
+ if (/<outboundRouteType>[\s\S]*?<\/outboundRouteType>/.test(block)) {
101
+ newBlock = block.replace(/<outboundRouteType>[\s\S]*?<\/outboundRouteType>/, '<outboundRouteType>OmniChannelFlow</outboundRouteType>');
102
+ } else {
103
+ newBlock = block.replace(/<\/outboundRouteConfigs>/, ' <outboundRouteType>OmniChannelFlow</outboundRouteType>\n </outboundRouteConfigs>');
104
+ }
105
+ if (newBlock === block) done({ mode, changed: false, action: 'already-configured' });
106
+ const newInner = inner.replace(block, newBlock);
107
+ const next = text.slice(0, innerStart) + newInner + text.slice(innerEnd);
108
+ writeFileSync(filePath, next);
109
+ done({ mode, changed: true, action: 'normalized-type' });
110
+ }
111
+
112
+ // Insert a fresh block BEFORE <surface> to honor the AiPlannerSurfaceDef XSD sequence
113
+ // (...outboundRouteConfigs*, surface, surfaceType). <surface> is required, so it must be present.
114
+ const surfaceAnchor = firstTag(inner, ['surface']);
115
+ if (!surfaceAnchor) refuse('Messaging planner surface has no <surface> element; refusing to guess insertion point');
116
+ const indent = surfaceAnchor.indent || ' ';
117
+ const childIndent = indent + ' ';
118
+ const block =
119
+ `${indent}<outboundRouteConfigs>\n` +
120
+ `${childIndent}<escalationMessage>${escMsg}</escalationMessage>\n` +
121
+ `${childIndent}<outboundRouteName>${xmlEscape(flowName)}</outboundRouteName>\n` +
122
+ `${childIndent}<outboundRouteType>OmniChannelFlow</outboundRouteType>\n` +
123
+ `${indent}</outboundRouteConfigs>\n`;
124
+ const newInner = inner.slice(0, surfaceAnchor.index) + block + inner.slice(surfaceAnchor.index);
125
+ const next = text.slice(0, innerStart) + newInner + text.slice(innerEnd);
126
+ writeFileSync(filePath, next);
127
+ done({ mode, changed: true, action: 'inserted-before-surface' });
128
+ }
129
+
130
+ process.stderr.write(`error: unknown mode '${mode}'\n`);
131
+ process.exit(2);
@@ -0,0 +1,249 @@
1
+ """Shared paths + fake-`sf` harness for the escalation-configure contract suite (org-free, network-free; drives the bash+Node skill via subprocess, asserting exit codes + emitted JSON)."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ # skills/ root is three levels up (.../<skill>/scripts/tests/_bootstrap.py).
10
+ SKILLS_ROOT = Path(__file__).resolve().parents[3]
11
+
12
+ SKILL_DIR = "service-agentforce-human-escalation-configure"
13
+ SCRIPT = f"{SKILL_DIR}/scripts/verify-and-configure.sh"
14
+ # The ITSM coordinator's advancement gate - exercised for the escalation verdict mapping.
15
+ VERDICT_SCRIPT = "service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs"
16
+
17
+ # A fake `sf` that reports a PRODUCTION customer org so the safe_to_write guard must refuse.
18
+ # A fake `project deploy` returns a sentinel so a guard bypass would surface loudly.
19
+ _FAKE_SF_PROD = r"""#!/usr/bin/env bash
20
+ sub="${1:-} ${2:-}"; args="$*"
21
+ case "$sub" in
22
+ "org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
23
+ "data query")
24
+ if printf '%s' "$args" | grep -q "FROM Organization"; then
25
+ echo '{"status":0,"result":{"records":[{"Id":"00Dxx0000000000EAA","IsSandbox":false,"TrialExpirationDate":null,"OrganizationType":"Enterprise Edition"}]}}'
26
+ else
27
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
28
+ fi
29
+ exit 0;;
30
+ "project deploy")
31
+ echo '{"status":1,"result":{"success":false},"message":"FAKE_SF_DEPLOY_SHOULD_NOT_BE_REACHED"}'; exit 1;;
32
+ *) echo '{"status":0,"result":{}}'; exit 0;;
33
+ esac
34
+ """
35
+
36
+ # A fake `sf` for a SANDBOX org where every deterministic surface already resolves (active agent,
37
+ # MessagingChannel, queue + QueueSobject + QRC, active RoutingFlow) and `project retrieve` writes
38
+ # canEscalate=true / an outboundRouteName so the doc-driven surfaces verify true. The FAKE_* env vars
39
+ # below toggle each surface off to exercise the block/create paths; a GroupMember POST flips membership
40
+ # via a marker file (stateful convergence); canonical QRC id 0RC000000000001AAA makes create+adopt converge.
41
+ _FAKE_SF_SANDBOX = r"""#!/usr/bin/env bash
42
+ sub="${1:-} ${2:-}"; args="$*"
43
+ SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
44
+ AGENT_ACTIVE="${FAKE_AGENT_ACTIVE:-1}"
45
+ MC_PRESENT="${FAKE_MC_PRESENT:-1}"
46
+ QS_PRESENT="${FAKE_QS_PRESENT:-1}"
47
+ QRC_PRESENT="${FAKE_QRC_PRESENT:-1}"
48
+ QUEUE_PRESENT="${FAKE_QUEUE_PRESENT:-1}"
49
+ QUEUE_BOUND="${FAKE_QUEUE_BOUND:-1}"
50
+ QUEUE_MEMBERS="${FAKE_QUEUE_MEMBERS:-1}"
51
+ ROUTE_TYPE="${FAKE_ROUTE_TYPE:-OmniChannelFlow}"
52
+ ROUTE_SURFACE="${FAKE_ROUTE_SURFACE:-Messaging}"
53
+ # FAKE_GENAI_MD=0 → org exposes no GenAiPlugin/GenAiPlannerBundle metadata types (the escalation
54
+ # preflight must then BLOCK before any write). Default 1 → the authoring metadata is exposed.
55
+ GENAI_MD="${FAKE_GENAI_MD:-1}"
56
+ # FAKE_MODEL pins the authoring model the org advertises: classic (GenAiPlugin+GenAiPlannerBundle),
57
+ # nga (AiAuthoringBundle, no GenAiPlannerBundle), or none. Empty => derive from GENAI_MD (1=classic,0=none).
58
+ MODEL="${FAKE_MODEL:-}"
59
+ if [ -z "$MODEL" ]; then [ "$GENAI_MD" = "1" ] && MODEL="classic" || MODEL="none"; fi
60
+ TARGET_MODEL="${FAKE_TARGET_MODEL:-$MODEL}"
61
+ # FAKE_NGA_SURFACE=1 => retrieved AiAuthoringBundle contains connection messaging: + @utils.escalate
62
+ # (Service escalation surface present). 0 => neither (surface missing => INCOMPLETE on nga).
63
+ NGA_SURFACE="${FAKE_NGA_SURFACE:-1}"
64
+ RC_ID="0RC000000000001AAA"
65
+ # Opt-in call log: when FAKE_SF_CALLLOG points at a file, append every invocation's args so a
66
+ # test can prove which mutations did (or did NOT) happen - e.g. idempotent re-runs create nothing.
67
+ if [ -n "${FAKE_SF_CALLLOG:-}" ]; then printf '%s\n' "$args" >> "$FAKE_SF_CALLLOG"; fi
68
+ _has_members() { [ "$QUEUE_MEMBERS" = "1" ] || [ -f "$SELF_DIR/.gm_added" ]; }
69
+ case "$sub" in
70
+ "org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
71
+ "org list")
72
+ if printf '%s' "$args" | grep -q "metadata-types"; then
73
+ case "$MODEL" in
74
+ classic) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"GenAiPlugin"},{"xmlName":"GenAiPlannerBundle"}]}}';;
75
+ nga) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"AiAuthoringBundle"},{"xmlName":"GenAiPlugin"}]}}';;
76
+ both) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"AiAuthoringBundle"},{"xmlName":"GenAiPlugin"},{"xmlName":"GenAiPlannerBundle"}]}}';;
77
+ *) echo '{"status":0,"result":{"metadataObjects":[{"xmlName":"Bot"},{"xmlName":"BotTemplate"},{"xmlName":"BotBlock"}]}}';;
78
+ esac
79
+ else
80
+ echo '{"status":0,"result":{}}'
81
+ fi
82
+ exit 0;;
83
+ "data query")
84
+ if printf '%s' "$args" | grep -q "FROM Organization"; then
85
+ echo '{"status":0,"result":{"records":[{"Id":"00Dxx0000000000EAA","IsSandbox":true,"TrialExpirationDate":null,"OrganizationType":"Developer Edition"}]}}'
86
+ elif printf '%s' "$args" | grep -q "FROM BotDefinition"; then
87
+ if [ "$AGENT_ACTIVE" = "1" ]; then
88
+ echo '{"status":0,"result":{"records":[{"Id":"0Xx000000000001AAA","DeveloperName":"Support_Agent","MasterLabel":"Support Agent","BotVersions":{"records":[{"Id":"04V000000000001AAA","Status":"Active"}]}}],"totalSize":1}}'
89
+ else
90
+ echo '{"status":0,"result":{"records":[{"Id":"0Xx000000000001AAA","DeveloperName":"Support_Agent","MasterLabel":"Support Agent","BotVersions":{"records":[{"Id":"04V000000000001AAA","Status":"Inactive"}]}}],"totalSize":1}}'
91
+ fi
92
+ elif printf '%s' "$args" | grep -q "FROM MessagingChannel"; then
93
+ if [ "$MC_PRESENT" = "1" ]; then
94
+ echo '{"status":0,"result":{"records":[{"Id":"0M9000000000001AAA","DeveloperName":"Support_Chat"}],"totalSize":1}}'
95
+ else
96
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
97
+ fi
98
+ elif printf '%s' "$args" | grep -q "FROM QueueSobject"; then
99
+ if [ "$QS_PRESENT" = "1" ]; then
100
+ echo '{"status":0,"result":{"records":[{"Id":"0Q9000000000001AAA"}],"totalSize":1}}'
101
+ else
102
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
103
+ fi
104
+ elif printf '%s' "$args" | grep -q "FROM QueueRoutingConfig"; then
105
+ if [ "$QRC_PRESENT" = "1" ]; then
106
+ echo '{"status":0,"result":{"records":[{"Id":"'"$RC_ID"'","DeveloperName":"Human_Support_Queue_RC"}],"totalSize":1}}'
107
+ else
108
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
109
+ fi
110
+ elif printf '%s' "$args" | grep -q "FROM FlowDefinitionView"; then
111
+ echo '{"status":0,"result":{"records":[{"ApiName":"Human_Escalation_Outbound_Flow","ActiveVersionId":"301000000000001AAA"}],"totalSize":1}}'
112
+ elif printf '%s' "$args" | grep -q "FROM GroupMember"; then
113
+ if _has_members; then
114
+ echo '{"status":0,"result":{"records":[{"UserOrGroupId":"005000000000001AAA"}],"totalSize":1}}'
115
+ else
116
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
117
+ fi
118
+ elif printf '%s' "$args" | grep -q "FROM User"; then
119
+ if printf '%s' "$args" | grep -q "COUNT(Id)"; then
120
+ if _has_members; then
121
+ echo '{"status":0,"result":{"records":[{"c":1}],"totalSize":1}}'
122
+ else
123
+ echo '{"status":0,"result":{"records":[{"c":0}],"totalSize":1}}'
124
+ fi
125
+ else
126
+ # Id-by-username (assignment path) → an active user row.
127
+ echo '{"status":0,"result":{"records":[{"Id":"005000000000002AAA"}],"totalSize":1}}'
128
+ fi
129
+ elif printf '%s' "$args" | grep -q "FROM Group"; then
130
+ if [ "$QUEUE_PRESENT" = "1" ]; then
131
+ if [ "$QUEUE_BOUND" = "1" ]; then
132
+ echo '{"status":0,"result":{"records":[{"Id":"00G000000000001AAA","QueueRoutingConfigId":"'"$RC_ID"'"}],"totalSize":1}}'
133
+ else
134
+ echo '{"status":0,"result":{"records":[{"Id":"00G000000000001AAA","QueueRoutingConfigId":null}],"totalSize":1}}'
135
+ fi
136
+ else
137
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
138
+ fi
139
+ else
140
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
141
+ fi
142
+ exit 0;;
143
+ "api request")
144
+ # Data API POST/PATCH (QueueSobject bind, QueueRoutingConfig create, Group.QueueRoutingConfigId bind).
145
+ if printf '%s' "$args" | grep -q "sobjects/QueueRoutingConfig"; then
146
+ echo '{"success":true,"id":"'"$RC_ID"'"}'
147
+ elif printf '%s' "$args" | grep -q "sobjects/QueueSobject"; then
148
+ echo '{"success":true,"id":"0Q9000000000009AAA"}'
149
+ elif printf '%s' "$args" | grep -q "sobjects/GroupMember"; then
150
+ touch "$SELF_DIR/.gm_added"
151
+ echo '{"success":true,"id":"0GM00000000001AAA"}'
152
+ else
153
+ # Group PATCH → 204 empty body on success.
154
+ echo ''
155
+ fi
156
+ exit 0;;
157
+ "project retrieve")
158
+ dir=$(printf '%s' "$args" | sed -n 's/.*--target-metadata-dir[ =]\([^ ]*\).*/\1/p'); dir="${dir:-.}"
159
+ mkdir -p "$dir"
160
+ if printf '%s' "$args" | grep -q "AiAuthoringBundle:"; then
161
+ [ "$TARGET_MODEL" = "nga" ] || [ "$TARGET_MODEL" = "both" ] || exit 1
162
+ if [ "$NGA_SURFACE" = "1" ]; then
163
+ cat > "$dir/Support_Agent.agent" <<'AGENT'
164
+ start_agent support_agent:
165
+ connection messaging:
166
+ end_user_client: customer_web_client
167
+ reasoning:
168
+ actions:
169
+ human_handoff: @utils.escalate
170
+ description: "Transfer the user to a human agent."
171
+ AGENT
172
+ else
173
+ cat > "$dir/Support_Agent.agent" <<'AGENT'
174
+ start_agent support_agent:
175
+ reasoning:
176
+ actions:
177
+ answer: @actions.AnswerQuestionWithKnowledge
178
+ AGENT
179
+ fi
180
+ elif printf '%s' "$args" | grep -q "GenAiPlugin:"; then
181
+ echo '<canEscalate>true</canEscalate>' > "$dir/plugin.xml"
182
+ elif printf '%s' "$args" | grep -q "GenAiPlannerBundle:"; then
183
+ [ "$TARGET_MODEL" = "classic" ] || [ "$TARGET_MODEL" = "both" ] || exit 1
184
+ cat > "$dir/bundle.xml" <<BXML
185
+ <GenAiPlannerBundle>
186
+ <plannerSurfaces>
187
+ <surfaceType>${ROUTE_SURFACE}</surfaceType>
188
+ <outboundRouteConfigs>
189
+ <escalationMessage>Transferring you to a live support agent.</escalationMessage>
190
+ <outboundRouteName>Human_Escalation_Outbound_Flow</outboundRouteName>
191
+ <outboundRouteType>${ROUTE_TYPE}</outboundRouteType>
192
+ </outboundRouteConfigs>
193
+ </plannerSurfaces>
194
+ </GenAiPlannerBundle>
195
+ BXML
196
+ fi
197
+ echo '{"status":0,"result":{"success":true}}'; exit 0;;
198
+ "project deploy")
199
+ echo '{"status":0,"result":{"success":true,"files":[{"fullName":"Human_Escalation_Outbound_Flow","type":"Flow","state":"Created"}]}}'; exit 0;;
200
+ *) echo '{"status":0,"result":{}}'; exit 0;;
201
+ esac
202
+ """
203
+
204
+
205
+ def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
206
+ """Run the orchestrator; return (returncode, merged stdout+stderr)."""
207
+ env = dict(os.environ)
208
+ if scrub_sf:
209
+ kept = [d for d in env.get("PATH", "").split(os.pathsep)
210
+ if d and not os.path.exists(os.path.join(d, "sf"))]
211
+ env["PATH"] = os.pathsep.join(kept)
212
+ if path_prefix:
213
+ env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
214
+ if extra_env:
215
+ env.update({k: str(v) for k, v in extra_env.items()})
216
+ proc = subprocess.run(
217
+ ["bash", str(SKILLS_ROOT / SCRIPT), *args],
218
+ capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
219
+ )
220
+ return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
221
+
222
+
223
+ def run_node(script_rel, args):
224
+ """Run a bundled Node helper; return (returncode, stdout)."""
225
+ proc = subprocess.run(
226
+ ["node", str(SKILLS_ROOT / script_rel), *args],
227
+ capture_output=True, text=True, cwd=str(SKILLS_ROOT),
228
+ )
229
+ return proc.returncode, (proc.stdout or "")
230
+
231
+
232
+ class FakeSf:
233
+ """Context manager: a temp dir holding an executable fake `sf` first on PATH."""
234
+
235
+ def __init__(self, body=_FAKE_SF_PROD):
236
+ self._body = body
237
+ self._tmp = None
238
+
239
+ def __enter__(self):
240
+ self._tmp = tempfile.mkdtemp(prefix="esc-fake-sf-")
241
+ sf = Path(self._tmp) / "sf"
242
+ sf.write_text(self._body)
243
+ sf.chmod(0o755)
244
+ return self._tmp
245
+
246
+ def __exit__(self, *exc):
247
+ import shutil
248
+ if self._tmp:
249
+ shutil.rmtree(self._tmp, ignore_errors=True)