@salesforce/afv-skills 1.59.0 → 1.61.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 (174) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-architecture-analyze/SKILL.md +66 -17
  3. package/skills/agentforce-architecture-analyze/assets/cli/show_access_token.yaml +4 -7
  4. package/skills/agentforce-architecture-analyze/references/contract.json +1 -1
  5. package/skills/agentforce-architecture-analyze/scripts/emit_env.py +2 -2
  6. package/skills/agentforce-architecture-analyze/scripts/main.py +40 -44
  7. package/skills/agentforce-architecture-analyze/scripts/rest_client.py +11 -3
  8. package/skills/agentforce-architecture-analyze/scripts/sf_cli.py +77 -10
  9. package/skills/agentforce-architecture-analyze/scripts/tests/test_end_to_end_fixture.py +1 -0
  10. package/skills/agentforce-architecture-analyze/scripts/tests/test_flow_children_inflation.py +1 -0
  11. package/skills/agentforce-architecture-analyze/scripts/tests/test_main_pipeline.py +53 -2
  12. package/skills/agentforce-architecture-analyze/scripts/tests/test_resolve_creds.py +184 -36
  13. package/skills/agentforce-architecture-analyze/scripts/tests/test_rest_client.py +17 -0
  14. package/skills/agentforce-architecture-analyze/scripts/tests/test_sf_cli.py +165 -11
  15. package/skills/agentforce-architecture-analyze/scripts/tests/test_skill_md_pipeline_block.py +319 -0
  16. package/skills/agentforce-d360-analyze/SKILL.md +28 -10
  17. package/skills/agentforce-d360-analyze/scripts/dc.py +200 -50
  18. package/skills/agentforce-d360-analyze/scripts/discover_sessions.py +10 -6
  19. package/skills/agentforce-d360-analyze/scripts/fetch_dc.py +22 -6
  20. package/skills/agentforce-d360-analyze/scripts/resolve_session.py +21 -13
  21. package/skills/agentforce-d360-analyze/scripts/tests/test_dc_and_resolve_session.py +298 -76
  22. package/skills/agentforce-d360-analyze/scripts/tests/test_default_target_org.py +432 -0
  23. package/skills/agentforce-d360-analyze/scripts/tests/test_discover_sessions.py +1 -1
  24. package/skills/agentforce-d360-analyze/scripts/tests/test_resolve_session_main.py +9 -3
  25. package/skills/agentforce-d360-analyze/scripts/tests/test_skill_md_prefix_block.py +176 -0
  26. package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +17 -27
  27. package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +7 -17
  28. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +47 -63
  29. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +10 -13
  30. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -46
  31. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +4 -21
  32. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +58 -82
  33. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +16 -315
  34. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +9 -24
  35. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -44
  36. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +11 -63
  37. package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +2 -1
  38. package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +2 -1
  39. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +2 -1
  40. package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +2 -1
  41. package/skills/data360-schema-get/SKILL.md +3 -3
  42. package/skills/design-systems-slds-validate/references/report-format.md +1 -1
  43. package/skills/dx-org-manage/SKILL.md +1 -0
  44. package/skills/dx-org-shape-manage/SKILL.md +1 -0
  45. package/skills/dx-org-snapshot-manage/SKILL.md +172 -0
  46. package/skills/dx-org-snapshot-manage/examples/create_error_output.json +16 -0
  47. package/skills/dx-org-snapshot-manage/examples/create_success_output.json +19 -0
  48. package/skills/dx-org-snapshot-manage/examples/delete_output.json +9 -0
  49. package/skills/dx-org-snapshot-manage/examples/get_output.json +19 -0
  50. package/skills/dx-org-snapshot-manage/examples/list_output.json +36 -0
  51. package/skills/dx-org-snapshot-manage/references/cli_flags.md +179 -0
  52. package/skills/dx-org-snapshot-manage/references/snapshot_usage.md +55 -0
  53. package/skills/dx-org-trial-expiration-check/SKILL.md +1 -1
  54. package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +2 -2
  55. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +7 -5
  56. package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +11 -11
  57. package/skills/experience-content-media-image-generate/SKILL.md +83 -0
  58. package/skills/experience-content-media-image-generate/scripts/download-image.sh +84 -0
  59. package/skills/external-diagram-mermaid-generate/SKILL.md +12 -12
  60. package/skills/field-service-data-capture-migrate/references/component-mapping.md +1 -1
  61. package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +1 -2
  62. package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +1 -0
  63. package/skills/field-service-data-capture-migrate/references/transformer-rules.md +1 -1
  64. package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +12 -1
  65. package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +4 -1
  66. package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -1
  67. package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +233 -0
  68. package/skills/insurance-brokerage-agency-billing-configure/references/accounting-setup.md +117 -0
  69. package/skills/insurance-brokerage-agency-billing-configure/references/billing-rules-config.md +237 -0
  70. package/skills/insurance-brokerage-agency-billing-configure/references/billing-settings.md +271 -0
  71. package/skills/insurance-brokerage-agency-billing-configure/references/demo-policy-config.md +138 -0
  72. package/skills/insurance-brokerage-agency-billing-configure/references/org-defaults.md +64 -0
  73. package/skills/insurance-brokerage-agency-billing-configure/references/permissions-config.md +226 -0
  74. package/skills/insurance-brokerage-agency-billing-configure/references/prerequisites.md +142 -0
  75. package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +3 -2
  76. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +3 -3
  77. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +68 -45
  78. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +17 -18
  79. package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +16 -3
  80. package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +16 -13
  81. package/skills/marketing-message-test/SKILL.md +194 -0
  82. package/skills/marketing-message-test/references/api-contract.md +333 -0
  83. package/skills/omnistudio-callable-apex-generate/SKILL.md +3 -3
  84. package/skills/omnistudio-datamapper-generate/SKILL.md +6 -6
  85. package/skills/omnistudio-datapacks-deploy/SKILL.md +11 -11
  86. package/skills/omnistudio-dependencies-analyze/SKILL.md +5 -5
  87. package/skills/omnistudio-epc-catalog-generate/SKILL.md +6 -6
  88. package/skills/omnistudio-flexcard-generate/SKILL.md +6 -6
  89. package/skills/omnistudio-integration-procedure-generate/SKILL.md +6 -6
  90. package/skills/omnistudio-omniscript-generate/SKILL.md +6 -5
  91. package/skills/platform-apex-logs-debug/SKILL.md +1 -0
  92. package/skills/platform-metadata-deploy/SKILL.md +1 -0
  93. package/skills/platform-models-api-configure/SKILL.md +5 -5
  94. package/skills/platform-soql-query/SKILL.md +1 -0
  95. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +17 -6
  96. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +107 -192
  97. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +6 -6
  98. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +49 -20
  99. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +4 -4
  100. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +224 -290
  101. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/workflow.md +295 -0
  102. package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -1
  103. package/skills/service-itsm-slack-itservice-configure/SKILL.md +2 -1
  104. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +2 -2
  105. package/skills/service-omni-channel-setup-coordinate/SKILL.md +20 -13
  106. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +5 -55
  107. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +0 -38
  108. package/skills/service-omni-command-center-analyze/SKILL.md +2 -3
  109. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
  110. package/skills/tableau-next-asset-create/README.md +65 -0
  111. package/skills/tableau-next-asset-create/SKILL.md +409 -0
  112. package/skills/tableau-next-asset-create/references/admin-users.md +149 -0
  113. package/skills/tableau-next-asset-create/references/ai-readiness.md +355 -0
  114. package/skills/tableau-next-asset-create/references/alerts.md +111 -0
  115. package/skills/tableau-next-asset-create/references/build-workflow.md +819 -0
  116. package/skills/tableau-next-asset-create/references/dashboard-authoring.md +302 -0
  117. package/skills/tableau-next-asset-create/references/dashboard-design-principles.md +352 -0
  118. package/skills/tableau-next-asset-create/references/data-transform-gotchas.md +210 -0
  119. package/skills/tableau-next-asset-create/references/data-understanding.md +387 -0
  120. package/skills/tableau-next-asset-create/references/deep-reference-index.md +74 -0
  121. package/skills/tableau-next-asset-create/references/destructive-operations.md +62 -0
  122. package/skills/tableau-next-asset-create/references/edit-dashboard.md +273 -0
  123. package/skills/tableau-next-asset-create/references/edit-visualization.md +529 -0
  124. package/skills/tableau-next-asset-create/references/empty-source-handling.md +104 -0
  125. package/skills/tableau-next-asset-create/references/ingest-and-metric-gotchas.md +638 -0
  126. package/skills/tableau-next-asset-create/references/insight-bundle.md +69 -0
  127. package/skills/tableau-next-asset-create/references/large-flat-file-handling.md +331 -0
  128. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_envelope.json +54 -0
  129. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_shared-widget-fields.json +122 -0
  130. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/add_page.json +29 -0
  131. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/delete_page.json +27 -0
  132. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/discard_dashboard.json +11 -0
  133. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget.json +30 -0
  134. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget_to_page.json +30 -0
  135. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/place_widget_on_page.json +30 -0
  136. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_dashboard.json +16 -0
  137. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_page.json +26 -0
  138. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/rename_page.json +28 -0
  139. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/save_dashboard.json +18 -0
  140. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/undo.json +11 -0
  141. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_button_widget.json +69 -0
  142. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_container_widget.json +88 -0
  143. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_filter_widget.json +146 -0
  144. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_metric_widget.json +78 -0
  145. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_navigation_widget.json +80 -0
  146. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_parameter_widget.json +160 -0
  147. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_text_widget.json +103 -0
  148. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_visualization_widget.json +68 -0
  149. package/skills/tableau-next-asset-create/references/sdm-tool-reference.md +721 -0
  150. package/skills/tableau-next-asset-create/references/semantic-query-and-enrichment.md +355 -0
  151. package/skills/tableau-next-asset-create/references/shared-gates.md +322 -0
  152. package/skills/tableau-next-asset-create/references/sharing-and-promotion.md +306 -0
  153. package/skills/tableau-next-asset-create/references/tasks/ai-readiness-audit.md +51 -0
  154. package/skills/tableau-next-asset-create/references/tasks/analyze-data.md +48 -0
  155. package/skills/tableau-next-asset-create/references/tasks/build-dashboard.md +66 -0
  156. package/skills/tableau-next-asset-create/references/tasks/build-end-to-end.md +73 -0
  157. package/skills/tableau-next-asset-create/references/tasks/create-data-transform.md +51 -0
  158. package/skills/tableau-next-asset-create/references/tasks/create-viz.md +58 -0
  159. package/skills/tableau-next-asset-create/references/tasks/edit-dashboard.md +80 -0
  160. package/skills/tableau-next-asset-create/references/tasks/edit-sdm.md +106 -0
  161. package/skills/tableau-next-asset-create/references/tasks/edit-viz.md +40 -0
  162. package/skills/tableau-next-asset-create/references/tasks/enrich-model.md +68 -0
  163. package/skills/tableau-next-asset-create/references/tasks/ingest-flat-file.md +68 -0
  164. package/skills/tableau-next-asset-create/references/tasks/manage-alert.md +35 -0
  165. package/skills/tableau-next-asset-create/references/tasks/profile-dataset.md +47 -0
  166. package/skills/tableau-next-asset-create/references/tasks/promote-or-reuse.md +46 -0
  167. package/skills/tableau-next-asset-create/references/tasks/provision-user.md +39 -0
  168. package/skills/tableau-next-asset-create/references/tasks/query-model.md +92 -0
  169. package/skills/tableau-next-asset-create/references/tasks/review-verified-questions.md +42 -0
  170. package/skills/tableau-next-asset-create/references/tasks/share-asset.md +37 -0
  171. package/skills/tableau-next-asset-create/references/verified-questions.md +125 -0
  172. package/skills/tableau-next-asset-create/references/viz-authoring.md +992 -0
  173. package/skills/service-de-channel-consent-configure/SKILL.md +0 -230
  174. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +0 -102
@@ -0,0 +1,432 @@
1
+ """``--org`` is optional: entry points fall back to the sf CLI default org.
2
+
3
+ Covers ``dc.default_target_org`` (mocked at the subprocess boundary) and the
4
+ three user entry points that take ``--org`` — ``discover_sessions``,
5
+ ``fetch_dc`` and ``resolve_session`` — with and without an explicit alias.
6
+
7
+ ``default_target_org`` runs the show-access-token capability preflight
8
+ (``sf org auth show-access-token --help``) before ``sf config get``, so every
9
+ fake here routes by argv: the preflight gets real-shaped USAGE help (or a
10
+ "command missing" answer), and ``sf config get`` gets the canned payload.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import io
15
+ import json
16
+ import subprocess
17
+ import unittest
18
+ from types import SimpleNamespace
19
+ from unittest import mock
20
+
21
+ from . import _bootstrap # noqa: F401 — sys.path setup
22
+
23
+ import dc # type: ignore
24
+ import discover_sessions # type: ignore
25
+ import fetch_dc # type: ignore
26
+ import resolve_session # type: ignore
27
+ from .fixtures.synthetic_session import IDS # type: ignore
28
+ from .test_fetch_dc_main import _MainHarness as _FetchHarness # type: ignore
29
+
30
+ _CONFIG_ARGV = ["sf", "config", "get", "target-org", "--json"]
31
+ _PREFLIGHT_ARGV = ["sf", "org", "auth", "show-access-token", "--help"]
32
+ # Shape of real ``--help`` output (sf CLI 2.150.6).
33
+ _REAL_HELP = "USAGE\n $ sf org auth show-access-token -o <value> [--json]\n"
34
+ _UPGRADE_MSG = "Upgrade/reinstall Salesforce CLI"
35
+ _NO_ORG_MSG = "no --org given and no default target org set"
36
+
37
+
38
+ def _config_payload(value: str | None) -> str:
39
+ entry = {"name": "target-org", "location": "Local", "success": True}
40
+ if value is not None:
41
+ entry["value"] = value
42
+ return json.dumps({"status": 0, "result": [entry], "warnings": []})
43
+
44
+
45
+ def _cp(stdout: str) -> SimpleNamespace:
46
+ return SimpleNamespace(returncode=0, stdout=stdout, stderr="")
47
+
48
+
49
+ def _sf_run(value: str | None = None, *, config=None, preflight: str = "ok",
50
+ calls: list | None = None):
51
+ """subprocess.run stand-in routing by argv.
52
+
53
+ ``preflight``: "ok" → real USAGE help; "nonzero" → the CLI rejects the
54
+ unknown command (CalledProcessError); "no-usage" → exit 0 without the
55
+ USAGE line (an old CLI echoing a not-found notice).
56
+ ``config``: a callable/exception overriding the ``sf config get`` answer;
57
+ default is the canned payload for ``value``. ``calls`` records argv.
58
+ """
59
+ def fake_run(argv, **kwargs):
60
+ if calls is not None:
61
+ calls.append((argv, kwargs))
62
+ if argv == _PREFLIGHT_ARGV:
63
+ if preflight == "ok":
64
+ return _cp(_REAL_HELP)
65
+ if preflight == "nonzero":
66
+ raise subprocess.CalledProcessError(
67
+ returncode=127, cmd=argv,
68
+ stderr="Warning: show-access-token is not a sf command.",
69
+ )
70
+ if preflight == "no-usage":
71
+ return _cp("Command org:auth:show-access-token not found.")
72
+ raise AssertionError(f"bad preflight mode {preflight!r}")
73
+ if argv == _CONFIG_ARGV:
74
+ if isinstance(config, BaseException):
75
+ raise config
76
+ if callable(config):
77
+ return config(argv, **kwargs)
78
+ return _cp(_config_payload(value))
79
+ raise AssertionError(f"unexpected argv: {argv}")
80
+ return fake_run
81
+
82
+
83
+ def _sf_config_run(value: str | None):
84
+ """A CLI with a working preflight whose default org is ``value``."""
85
+ return _sf_run(value)
86
+
87
+
88
+ # A CLI lacking show-access-token, in both shapes the preflight rejects.
89
+ _MISSING_CMD_MODES = ("nonzero", "no-usage")
90
+
91
+
92
+ class _FreshPreflight(unittest.TestCase):
93
+ """The preflight caches success process-wide; isolate every test."""
94
+
95
+ def setUp(self):
96
+ dc._show_access_token_capability_ok = False
97
+ self.addCleanup(setattr, dc, "_show_access_token_capability_ok", False)
98
+
99
+
100
+ # -----------------------------------------------------------------------------
101
+ # dc.default_target_org
102
+ # -----------------------------------------------------------------------------
103
+
104
+
105
+ class DefaultTargetOrgTests(_FreshPreflight):
106
+
107
+ def test_returns_configured_value_with_timeout(self):
108
+ calls: list = []
109
+ with mock.patch.object(
110
+ dc.subprocess, "run", side_effect=_sf_run("my-default", calls=calls),
111
+ ):
112
+ self.assertEqual(dc.default_target_org(), "my-default")
113
+ self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV, _CONFIG_ARGV])
114
+ config_kwargs = calls[1][1]
115
+ self.assertTrue(config_kwargs.get("timeout"))
116
+ self.assertTrue(config_kwargs.get("check"))
117
+
118
+ def test_missing_show_access_token_fails_before_config_get(self):
119
+ for mode in _MISSING_CMD_MODES:
120
+ with self.subTest(preflight=mode):
121
+ dc._show_access_token_capability_ok = False
122
+ calls: list = []
123
+ with mock.patch.object(
124
+ dc.subprocess, "run",
125
+ side_effect=_sf_run(None, preflight=mode, calls=calls),
126
+ ):
127
+ with self.assertRaises(SystemExit) as ctx:
128
+ dc.default_target_org()
129
+ msg = str(ctx.exception)
130
+ self.assertIn("sf org auth show-access-token", msg)
131
+ self.assertIn(_UPGRADE_MSG, msg)
132
+ self.assertNotIn(_NO_ORG_MSG, msg)
133
+ self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV])
134
+ self.assertFalse(dc._show_access_token_capability_ok)
135
+
136
+ def test_preflight_cached_across_default_org_and_resolve_org(self):
137
+ calls: list = []
138
+ base = _sf_run("my-default", calls=calls)
139
+
140
+ def fake_run(argv, **kwargs):
141
+ if argv[:3] == ["sf", "org", "display"]:
142
+ calls.append((argv, kwargs))
143
+ return _cp(json.dumps({"result": {"instanceUrl": "https://x"}}))
144
+ if argv[:4] == ["sf", "org", "auth", "show-access-token"] and "--help" not in argv:
145
+ calls.append((argv, kwargs))
146
+ return _cp(json.dumps({"result": {"accessToken": "TOK"}}))
147
+ return base(argv, **kwargs)
148
+
149
+ with mock.patch.object(dc.subprocess, "run", side_effect=fake_run):
150
+ org = dc.default_target_org()
151
+ self.assertEqual(dc.default_target_org(), org)
152
+ self.assertEqual(dc.resolve_org(org), ("https://x", "TOK"))
153
+ argvs = [a for a, _ in calls]
154
+ self.assertEqual(argvs[0], _PREFLIGHT_ARGV)
155
+ self.assertEqual(argvs.count(_PREFLIGHT_ARGV), 1, argvs)
156
+
157
+ def test_empty_result_raises_clear_error(self):
158
+ payloads = [
159
+ _config_payload(None), # key unset
160
+ _config_payload(""), # blank value
161
+ json.dumps({"status": 0, "result": []}), # no entries
162
+ json.dumps({"status": 0}), # no result at all
163
+ ]
164
+ for payload in payloads:
165
+ with self.subTest(payload=payload):
166
+ with mock.patch.object(
167
+ dc.subprocess, "run",
168
+ side_effect=_sf_run(config=lambda *a, _p=payload, **k: _cp(_p)),
169
+ ):
170
+ with self.assertRaises(SystemExit) as ctx:
171
+ dc.default_target_org()
172
+ msg = str(ctx.exception)
173
+ self.assertIn("no --org given and no default target org set", msg)
174
+ self.assertIn("sf config set target-org <alias>", msg)
175
+
176
+ def test_sf_failure_raises_redacted_error(self):
177
+ err = subprocess.CalledProcessError(
178
+ returncode=1, cmd=_CONFIG_ARGV,
179
+ stderr='boom Authorization: Bearer 00DSECRET!tok "accessToken": "00DLEAK"',
180
+ )
181
+ with mock.patch.object(dc.subprocess, "run", side_effect=_sf_run(config=err)):
182
+ with self.assertRaises(SystemExit) as ctx:
183
+ dc.default_target_org()
184
+ msg = str(ctx.exception)
185
+ self.assertIn("no --org given and no default target org set", msg)
186
+ self.assertIn("sf config get target-org", msg)
187
+ self.assertIn("boom", msg)
188
+ self.assertNotIn("00DSECRET", msg)
189
+ self.assertNotIn("00DLEAK", msg)
190
+ self.assertIn("<redacted>", msg)
191
+
192
+ def test_sf_missing_raises_clear_error(self):
193
+ # No sf at all: the preflight is the first call and reports it.
194
+ calls: list = []
195
+
196
+ def fake_run(argv, **kwargs):
197
+ calls.append(argv)
198
+ raise FileNotFoundError("sf")
199
+
200
+ with mock.patch.object(dc.subprocess, "run", side_effect=fake_run):
201
+ with self.assertRaises(SystemExit) as ctx:
202
+ dc.default_target_org()
203
+ self.assertIn("sf CLI not found on PATH", str(ctx.exception))
204
+ self.assertEqual(calls, [_PREFLIGHT_ARGV])
205
+
206
+ def test_sf_vanishes_before_config_get_raises_clear_error(self):
207
+ with mock.patch.object(
208
+ dc.subprocess, "run", side_effect=_sf_run(config=FileNotFoundError("sf")),
209
+ ):
210
+ with self.assertRaises(SystemExit) as ctx:
211
+ dc.default_target_org()
212
+ self.assertIn("sf CLI not found on PATH", str(ctx.exception))
213
+
214
+ def test_timeout_raises_clear_error(self):
215
+ with mock.patch.object(
216
+ dc.subprocess, "run",
217
+ side_effect=_sf_run(
218
+ config=subprocess.TimeoutExpired(cmd=_CONFIG_ARGV, timeout=30),
219
+ ),
220
+ ):
221
+ with self.assertRaises(SystemExit) as ctx:
222
+ dc.default_target_org()
223
+ msg = str(ctx.exception)
224
+ self.assertIn("`sf config get target-org` timed out", msg)
225
+ self.assertIn(_NO_ORG_MSG, msg)
226
+
227
+ def test_non_json_output_raises_clear_error(self):
228
+ with mock.patch.object(
229
+ dc.subprocess, "run",
230
+ side_effect=_sf_run(config=lambda *a, **k: _cp("Warning: not json")),
231
+ ):
232
+ with self.assertRaises(SystemExit) as ctx:
233
+ dc.default_target_org()
234
+ self.assertIn("non-JSON", str(ctx.exception))
235
+
236
+
237
+ # -----------------------------------------------------------------------------
238
+ # discover_sessions
239
+ # -----------------------------------------------------------------------------
240
+
241
+
242
+ class DiscoverSessionsOrgDefaultTests(_FreshPreflight):
243
+
244
+ def _run(self, *argv: str, run_side_effect):
245
+ with mock.patch.object(discover_sessions.sys, "argv",
246
+ ["discover_sessions.py", *argv]):
247
+ with mock.patch.object(dc.subprocess, "run",
248
+ side_effect=run_side_effect) as run:
249
+ with mock.patch.object(
250
+ discover_sessions, "resolve_org",
251
+ return_value=("https://x", "T"),
252
+ ) as resolve:
253
+ with mock.patch.object(discover_sessions, "post", return_value=[]):
254
+ with mock.patch("sys.stdout", io.StringIO()) as out:
255
+ rc = discover_sessions.main()
256
+ return rc, run, resolve, out.getvalue()
257
+
258
+ def test_no_org_uses_default_target_org(self):
259
+ rc, run, resolve, out = self._run(
260
+ "--grep", "refund", "--limit", "5",
261
+ run_side_effect=_sf_config_run("def-org"),
262
+ )
263
+ self.assertEqual(rc, 2) # zero rows
264
+ self.assertEqual(
265
+ [c.args[0] for c in run.call_args_list], [_PREFLIGHT_ARGV, _CONFIG_ARGV],
266
+ )
267
+ resolve.assert_called_once_with("def-org")
268
+ self.assertIn("def-org", out)
269
+
270
+ def test_explicit_org_wins_and_helper_not_called(self):
271
+ with mock.patch.object(discover_sessions, "default_target_org") as helper:
272
+ rc, run, resolve, _ = self._run(
273
+ "--org", "explicit-org",
274
+ run_side_effect=AssertionError("sf must not be called"),
275
+ )
276
+ self.assertEqual(rc, 2)
277
+ helper.assert_not_called()
278
+ run.assert_not_called()
279
+ resolve.assert_called_once_with("explicit-org")
280
+
281
+ def test_no_org_and_no_default_exits_with_clear_message(self):
282
+ with self.assertRaises(SystemExit) as ctx:
283
+ self._run(run_side_effect=_sf_config_run(None))
284
+ self.assertIn("no --org given and no default target org set", str(ctx.exception))
285
+
286
+ def test_no_org_on_cli_lacking_show_access_token_reports_upgrade(self):
287
+ # No default org AND no show-access-token: the upgrade message wins,
288
+ # `sf config get` is never run, and no org resolution is attempted.
289
+ for mode in _MISSING_CMD_MODES:
290
+ with self.subTest(preflight=mode):
291
+ dc._show_access_token_capability_ok = False
292
+ calls: list = []
293
+ with mock.patch.object(discover_sessions.sys, "argv",
294
+ ["discover_sessions.py"]):
295
+ with mock.patch.object(
296
+ dc.subprocess, "run",
297
+ side_effect=_sf_run(None, preflight=mode, calls=calls),
298
+ ):
299
+ with mock.patch.object(discover_sessions, "resolve_org") as resolve:
300
+ with self.assertRaises(SystemExit) as ctx:
301
+ discover_sessions.main()
302
+ msg = str(ctx.exception)
303
+ self.assertIn(_UPGRADE_MSG, msg)
304
+ self.assertNotIn(_NO_ORG_MSG, msg)
305
+ self.assertNotIn(_CONFIG_ARGV, [a for a, _ in calls])
306
+ self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV])
307
+ resolve.assert_not_called()
308
+
309
+
310
+ # -----------------------------------------------------------------------------
311
+ # fetch_dc
312
+ # -----------------------------------------------------------------------------
313
+
314
+
315
+ class FetchDcOrgDefaultTests(_FreshPreflight):
316
+
317
+ _BASE = ["fetch_dc.py", "--session", IDS.SID, "--no-assemble", "--no-render"]
318
+
319
+ def test_no_org_uses_default_target_org(self):
320
+ captured = {}
321
+
322
+ def capture(ctx):
323
+ captured.update(ctx)
324
+ ctx["org_id_15"] = IDS.ORG_ID_15
325
+ ctx["agent_api_name"] = IDS.AGENT_API
326
+ ctx["agent_version"] = IDS.AGENT_VERSION
327
+
328
+ with _FetchHarness():
329
+ with mock.patch.object(fetch_dc, "_run_waterfall", side_effect=capture):
330
+ with mock.patch.object(dc.subprocess, "run",
331
+ side_effect=_sf_config_run("def-org")) as run:
332
+ with mock.patch.object(fetch_dc.sys, "argv", list(self._BASE)):
333
+ rc = fetch_dc.main()
334
+ fetch_dc.preflight_dc_access.assert_called_once_with(IDS.SID, "def-org")
335
+ self.assertEqual(rc, 0)
336
+ self.assertEqual(
337
+ [c.args[0] for c in run.call_args_list], [_PREFLIGHT_ARGV, _CONFIG_ARGV],
338
+ )
339
+ self.assertEqual(captured["org_alias"], "def-org")
340
+
341
+ def test_explicit_org_wins_and_helper_not_called(self):
342
+ with _FetchHarness():
343
+ with mock.patch.object(fetch_dc, "default_target_org") as helper:
344
+ with mock.patch.object(
345
+ fetch_dc.sys, "argv", [*self._BASE, "--org", "explicit-org"],
346
+ ):
347
+ rc = fetch_dc.main()
348
+ fetch_dc.preflight_dc_access.assert_called_once_with(
349
+ IDS.SID, "explicit-org",
350
+ )
351
+ self.assertEqual(rc, 0)
352
+ helper.assert_not_called()
353
+
354
+ def test_no_default_routes_through_dc_access_denied(self):
355
+ with _FetchHarness(is_tty=False):
356
+ with mock.patch.object(dc.subprocess, "run",
357
+ side_effect=_sf_config_run(None)):
358
+ with mock.patch.object(fetch_dc.sys, "argv", list(self._BASE)):
359
+ with mock.patch("sys.stdout", io.StringIO()) as out:
360
+ rc = fetch_dc.main()
361
+ fetch_dc.preflight_dc_access.assert_not_called()
362
+ self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)
363
+ payload = json.loads(out.getvalue().strip().splitlines()[-1])
364
+ self.assertEqual(payload["status"], "DC_ACCESS_DENIED")
365
+ self.assertEqual(payload["reason"], "no_org")
366
+ self.assertIn("no default target org set", payload["detail"])
367
+
368
+ def test_no_org_on_cli_lacking_show_access_token_reports_upgrade(self):
369
+ # Headless contract is unchanged (DC_ACCESS_DENIED, exit 10, same
370
+ # reason code the explicit-org preflight failure already uses), but the
371
+ # detail is the upgrade message and `sf config get` never runs.
372
+ for mode in _MISSING_CMD_MODES:
373
+ with self.subTest(preflight=mode):
374
+ dc._show_access_token_capability_ok = False
375
+ calls: list = []
376
+ with _FetchHarness(is_tty=False):
377
+ with mock.patch.object(
378
+ dc.subprocess, "run",
379
+ side_effect=_sf_run(None, preflight=mode, calls=calls),
380
+ ):
381
+ with mock.patch.object(fetch_dc.sys, "argv", list(self._BASE)):
382
+ with mock.patch("sys.stdout", io.StringIO()) as out:
383
+ rc = fetch_dc.main()
384
+ fetch_dc.preflight_dc_access.assert_not_called()
385
+ self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)
386
+ payload = json.loads(out.getvalue().strip().splitlines()[-1])
387
+ self.assertEqual(payload["status"], "DC_ACCESS_DENIED")
388
+ self.assertIn(_UPGRADE_MSG, payload["detail"])
389
+ self.assertNotIn(_NO_ORG_MSG, payload["detail"])
390
+ self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV])
391
+
392
+
393
+ # -----------------------------------------------------------------------------
394
+ # resolve_session (live lookup only needs an org on a disk miss)
395
+ # -----------------------------------------------------------------------------
396
+
397
+
398
+ class ResolveSessionOrgDefaultTests(unittest.TestCase):
399
+
400
+ _MSG = "0Mw000000000000"
401
+
402
+ def _run(self, *argv: str):
403
+ with mock.patch.object(resolve_session.sys, "argv",
404
+ ["resolve_session.py", "--id", self._MSG, *argv]):
405
+ with mock.patch.object(resolve_session, "resolve_from_disk", return_value=None):
406
+ with mock.patch.object(
407
+ resolve_session, "resolve", return_value="resolved-uuid",
408
+ ) as resolve:
409
+ with mock.patch("sys.stdout", io.StringIO()) as out:
410
+ rc = resolve_session.main()
411
+ return rc, resolve, out.getvalue()
412
+
413
+ def test_disk_miss_without_org_uses_default(self):
414
+ with mock.patch.object(
415
+ resolve_session, "default_target_org", return_value="def-org",
416
+ ) as helper:
417
+ rc, resolve, out = self._run()
418
+ self.assertEqual(rc, 0)
419
+ helper.assert_called_once_with()
420
+ resolve.assert_called_once_with(self._MSG, org="def-org")
421
+ self.assertIn("resolved-uuid", out)
422
+
423
+ def test_explicit_org_wins_and_helper_not_called(self):
424
+ with mock.patch.object(resolve_session, "default_target_org") as helper:
425
+ rc, resolve, _ = self._run("--org", "explicit-org")
426
+ self.assertEqual(rc, 0)
427
+ helper.assert_not_called()
428
+ resolve.assert_called_once_with(self._MSG, org="explicit-org")
429
+
430
+
431
+ if __name__ == "__main__":
432
+ unittest.main()
@@ -399,7 +399,7 @@ class RenderPickerTests(unittest.TestCase):
399
399
  class MainTests(unittest.TestCase):
400
400
 
401
401
  def _argv(self, *extra: str) -> list[str]:
402
- # `--org` is required; everything else has a default or is optional.
402
+ # Explicit `--org` so no test shells out for the sf default target org.
403
403
  return ["discover_sessions.py", "--org", "my-org", *extra]
404
404
 
405
405
  def test_main_exit_zero_when_rows_returned(self):
@@ -5,7 +5,7 @@ Argparse-driven — exercises every branch:
5
5
  - --disk-only hit / miss
6
6
  - live path with --org
7
7
  - disk-first fallback when no --org
8
- - not-found-no-org error
8
+ - not-found-no-org error (no sf default target org either)
9
9
  """
10
10
  from __future__ import annotations
11
11
 
@@ -135,14 +135,20 @@ class DiskFirstFallbackTests(unittest.TestCase):
135
135
  self.assertIn(IDS.SID, h.stdout.getvalue())
136
136
 
137
137
  def test_disk_miss_no_org_returns_two_with_hint(self):
138
- # No --org AND disk miss → exit 2 + helpful stderr.
138
+ # No --org, disk miss, AND no sf default target org → exit 2 +
139
+ # helpful stderr.
139
140
  with _MainHarness(
140
141
  argv=["--id", "0Mw000000000000"],
141
142
  ) as h:
142
- rc = resolve_session.main()
143
+ with mock.patch.object(
144
+ resolve_session, "default_target_org",
145
+ side_effect=SystemExit("no --org given and no default target org set"),
146
+ ):
147
+ rc = resolve_session.main()
143
148
  self.assertEqual(rc, 2)
144
149
  self.assertIn("not found on disk", h.stderr.getvalue())
145
150
  self.assertIn("--org <alias> required", h.stderr.getvalue())
151
+ self.assertIn("no default target org set", h.stderr.getvalue())
146
152
 
147
153
 
148
154
  if __name__ == "__main__":
@@ -0,0 +1,176 @@
1
+ """Run SKILL.md's "## Resolving the script prefix" bash block under bash and zsh.
2
+
3
+ The block is the contract the agent executes verbatim, so these tests extract
4
+ it from SKILL.md (no copy to drift) and run it under ``env -i`` against fake
5
+ install layouts, then call ``python3 "$prefix/fetch_dc.py"`` the way every
6
+ later SKILL.md invocation does. The stub fetch_dc.py records its own path so
7
+ the test can assert which copy actually ran.
8
+
9
+ Scenarios: scripts at the candidate root (shipped layout), scripts only under
10
+ ``<candidate>/artifacts/scripts/`` (ADK eval staging layout) via an exported
11
+ SKILL_ROOT and via the install-root probe, root preferred over its own
12
+ artifacts/, candidate order preserved, and nothing found (guard message).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ import unittest
23
+ from pathlib import Path
24
+
25
+ _SKILL_NAME = "agentforce-d360-analyze"
26
+
27
+
28
+ def _find_skill_md() -> Path:
29
+ # contributed-skills: <skill>/scripts/tests/ -> <skill>/SKILL.md
30
+ # outputs/unified-sors: <skill>/artifacts/scripts/tests/ -> <skill>/SKILL.md
31
+ for parent in Path(__file__).resolve().parents:
32
+ candidate = parent / "SKILL.md"
33
+ if candidate.is_file():
34
+ return candidate
35
+ raise FileNotFoundError("SKILL.md not found above " + __file__)
36
+
37
+
38
+ def _extract_prefix_block(skill_md: str) -> str:
39
+ section = skill_md.split("## Resolving the script prefix", 1)[1]
40
+ match = re.search(r"```bash\n(.*?)\n```", section, re.S)
41
+ if match is None:
42
+ raise ValueError("no ```bash block under '## Resolving the script prefix'")
43
+ return match.group(1) + "\n"
44
+
45
+
46
+ _FETCH_STUB = """\
47
+ import json, os, sys
48
+ with open({record!r}, "a") as fh:
49
+ fh.write(json.dumps({{"path": os.path.realpath(__file__),
50
+ "argv": sys.argv[1:]}}) + "\\n")
51
+ """
52
+
53
+ # Appended after the extracted block: report what it resolved, then invoke the
54
+ # entry script exactly as the rest of SKILL.md does.
55
+ _EPILOGUE = """
56
+ printf 'SKILL_ROOT=%s\\nprefix=%s\\n' "$SKILL_ROOT" "$prefix"
57
+ python3 "$prefix/fetch_dc.py" --session probe-test
58
+ """
59
+
60
+
61
+ class PrefixBlockTest(unittest.TestCase):
62
+ SHELLS = [s for s in ("bash", "zsh") if shutil.which(s)]
63
+
64
+ @classmethod
65
+ def setUpClass(cls) -> None:
66
+ cls.block = _extract_prefix_block(_find_skill_md().read_text())
67
+
68
+ def setUp(self) -> None:
69
+ self.tmp = Path(tempfile.mkdtemp(prefix="d360-prefix-test-"))
70
+ self.addCleanup(shutil.rmtree, self.tmp, True)
71
+ self.record = self.tmp / "calls.jsonl"
72
+ self.home = self.tmp / "home"
73
+ self.home.mkdir()
74
+ self.bin = self.tmp / "bin"
75
+ self.bin.mkdir()
76
+ (self.bin / "python3").symlink_to(sys.executable)
77
+ self.script = self.tmp / "block.sh"
78
+ self.script.write_text(self.block + _EPILOGUE)
79
+
80
+ # -- helpers ---------------------------------------------------------
81
+
82
+ def _make_scripts(self, root: Path) -> Path:
83
+ scripts = root / "scripts"
84
+ scripts.mkdir(parents=True)
85
+ (scripts / "fetch_dc.py").write_text(_FETCH_STUB.format(record=str(self.record)))
86
+ return scripts
87
+
88
+ def _run(self, shell: str, skill_root: str | None):
89
+ if self.record.exists():
90
+ self.record.unlink()
91
+ env = {"PATH": f"{self.bin}:/usr/bin:/bin", "HOME": str(self.home)}
92
+ if skill_root is not None:
93
+ env["SKILL_ROOT"] = skill_root
94
+ cmd = ["env", "-i", *(f"{k}={v}" for k, v in env.items()),
95
+ shell, str(self.script)]
96
+ proc = subprocess.run(
97
+ cmd, capture_output=True, text=True, timeout=60, cwd=self.tmp,
98
+ )
99
+ calls = []
100
+ if self.record.exists():
101
+ calls = [json.loads(l) for l in self.record.read_text().splitlines()]
102
+ return proc, calls
103
+
104
+ def _assert_resolved(self, proc, calls, root: Path) -> None:
105
+ self.assertEqual(proc.returncode, 0, proc.stderr)
106
+ self.assertIn(f"SKILL_ROOT={root}\n", proc.stdout)
107
+ self.assertIn(f"prefix={root}/scripts\n", proc.stdout)
108
+ self.assertEqual(len(calls), 1, calls)
109
+ self.assertEqual(
110
+ calls[0]["path"], str((root / "scripts" / "fetch_dc.py").resolve())
111
+ )
112
+ self.assertEqual(calls[0]["argv"], ["--session", "probe-test"])
113
+
114
+ # -- scenarios -------------------------------------------------------
115
+
116
+ def test_shells_available(self) -> None:
117
+ self.assertIn("bash", self.SHELLS)
118
+
119
+ def test_a_shipped_layout_exported_root(self) -> None:
120
+ root = self.tmp / "skill"
121
+ self._make_scripts(root)
122
+ for shell in self.SHELLS:
123
+ with self.subTest(shell=shell):
124
+ self._assert_resolved(*self._run(shell, str(root)), root)
125
+
126
+ def test_b_scripts_only_under_artifacts_of_exported_root(self) -> None:
127
+ staged = self.tmp / "staged" / _SKILL_NAME
128
+ self._make_scripts(staged / "artifacts")
129
+ (staged / "SKILL.md").write_text("# staged\n")
130
+ for shell in self.SHELLS:
131
+ with self.subTest(shell=shell):
132
+ self._assert_resolved(
133
+ *self._run(shell, str(staged)), staged / "artifacts"
134
+ )
135
+
136
+ def test_c_artifacts_found_via_install_root_probe(self) -> None:
137
+ staged = self.home / ".claude" / "skills" / _SKILL_NAME
138
+ self._make_scripts(staged / "artifacts")
139
+ for shell in self.SHELLS:
140
+ with self.subTest(shell=shell):
141
+ self._assert_resolved(
142
+ *self._run(shell, None), staged / "artifacts"
143
+ )
144
+
145
+ def test_d_candidate_root_preferred_over_its_artifacts(self) -> None:
146
+ root = self.tmp / "skill"
147
+ self._make_scripts(root)
148
+ self._make_scripts(root / "artifacts")
149
+ for shell in self.SHELLS:
150
+ with self.subTest(shell=shell):
151
+ self._assert_resolved(*self._run(shell, str(root)), root)
152
+
153
+ def test_e_earlier_candidate_artifacts_beats_later_candidate_root(self) -> None:
154
+ staged = self.tmp / "staged" / _SKILL_NAME
155
+ self._make_scripts(staged / "artifacts")
156
+ self._make_scripts(self.home / ".claude" / "skills" / _SKILL_NAME)
157
+ for shell in self.SHELLS:
158
+ with self.subTest(shell=shell):
159
+ self._assert_resolved(
160
+ *self._run(shell, str(staged)), staged / "artifacts"
161
+ )
162
+
163
+ def test_f_no_scripts_anywhere_reports_guard_message(self) -> None:
164
+ empty = self.tmp / "empty" / _SKILL_NAME
165
+ (empty / "artifacts").mkdir(parents=True)
166
+ for shell in self.SHELLS:
167
+ with self.subTest(shell=shell):
168
+ proc, calls = self._run(shell, str(empty))
169
+ self.assertEqual(proc.returncode, 1)
170
+ self.assertIn(f"{_SKILL_NAME}: scripts not found", proc.stderr)
171
+ self.assertNotIn("prefix=", proc.stdout)
172
+ self.assertEqual(calls, [])
173
+
174
+
175
+ if __name__ == "__main__":
176
+ unittest.main()