@salesforce/afv-skills 1.59.0 → 1.60.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 (173) 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-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +0 -102
@@ -64,18 +64,27 @@ class ParseTests(unittest.TestCase):
64
64
 
65
65
 
66
66
  class ResolveOrgTests(unittest.TestCase):
67
- """Tests for the two-path access-token retrieval per forcedotcom/cli#3560.
67
+ """Tests for fail-fast access-token retrieval per forcedotcom/cli#3560.
68
68
 
69
- Path 1 (primary): ``sf org auth show-access-token --json --no-prompt``
70
- Path 2 (fallback): ``sf org display`` + ``SF_TEMP_SHOW_SECRETS=true``
69
+ The token comes ONLY from ``sf org auth show-access-token --json
70
+ --no-prompt``; ``sf org display`` supplies instanceUrl only. The
71
+ display stub deliberately carries a non-empty ``TOKEN_FROM_DISPLAY``
72
+ so any regression to a display-token fallback is caught.
71
73
 
72
- Most tests mock ``subprocess.run`` with a side-effect callable that
73
- returns a different stub depending on which sf subcommand was invoked,
74
- so we can exercise primary success, primary failure → fallback, and
75
- full-failure paths without spawning real processes.
74
+ Tests mock ``subprocess.run`` with a side-effect callable that routes
75
+ by argv shape (preflight ``--help`` / display / show-access-token)
76
+ without spawning real processes.
76
77
  """
77
78
 
78
79
  REDACTED_TOKEN = "[REDACTED] Use 'sf org auth show-access-token' to view"
80
+ PREFLIGHT_ARGV = ["sf", "org", "auth", "show-access-token", "--help"]
81
+ # Shape of real ``--help`` output (sf CLI 2.150.6).
82
+ REAL_HELP = "USAGE\n $ sf org auth show-access-token -o <value> [--json]\n"
83
+
84
+ def setUp(self):
85
+ # The preflight caches success process-wide; isolate every test.
86
+ dc._show_access_token_capability_ok = False
87
+ self.addCleanup(setattr, dc, "_show_access_token_capability_ok", False)
79
88
 
80
89
  def _cp(self, stdout: str, *, returncode: int = 0, stderr: str = ""):
81
90
  return SimpleNamespace(
@@ -91,36 +100,38 @@ class ResolveOrgTests(unittest.TestCase):
91
100
  def _show_token_payload(self, *, access_token: str = "TOKEN_FROM_SHOW") -> str:
92
101
  return json.dumps({"result": {"accessToken": access_token}})
93
102
 
94
- def _route(self, primary_ok=True, primary_unknown=False,
95
- primary_redacted=False, display_redacted=False):
103
+ def _route(self, *, preflight_missing=False, primary_fails=False,
104
+ primary_token="TOKEN_FROM_SHOW", captured=None):
96
105
  """Build a fake_run that routes by argv shape.
97
106
 
98
- - primary_ok=True → show-access-token returns clean token
99
- - primary_unknown → show-access-token raises CalledProcessError
100
- (older sf CLI without the subcommand)
101
- - primary_redacted → show-access-token returns the placeholder
102
- - display_redacted → org_display token field is the placeholder
103
- (so fallback also fails — test full-failure)
107
+ - preflight_missing → ``show-access-token --help`` raises
108
+ CalledProcessError (sf CLI lacks the command)
109
+ - primary_fails → token call raises CalledProcessError
110
+ (e.g. org not authenticated)
111
+ - primary_token → accessToken returned by the token call
112
+ - captured → optional list; each (argv, kwargs) is appended
104
113
  """
105
114
  def fake_run(argv, **kwargs):
106
- # display call
115
+ if captured is not None:
116
+ captured.append((argv, kwargs))
117
+ if "show-access-token" in argv and "--help" in argv:
118
+ if preflight_missing:
119
+ raise subprocess.CalledProcessError(
120
+ returncode=1, cmd=argv,
121
+ stderr="show-access-token is not a sf command",
122
+ )
123
+ return self._cp(self.REAL_HELP)
107
124
  if "display" in argv:
108
- return self._cp(
109
- self._display_payload(
110
- access_token=(self.REDACTED_TOKEN if display_redacted
111
- else "TOKEN_FROM_DISPLAY"),
112
- ),
113
- )
114
- # show-access-token call
125
+ return self._cp(self._display_payload())
115
126
  if "show-access-token" in argv:
116
- if primary_unknown:
127
+ if primary_fails:
117
128
  raise subprocess.CalledProcessError(
118
129
  returncode=1, cmd=argv,
119
- stderr="show-access-token is not a sf command",
130
+ stderr="NoOrgAuthenticationError",
120
131
  )
121
- token = (self.REDACTED_TOKEN if primary_redacted
122
- else "TOKEN_FROM_SHOW")
123
- return self._cp(self._show_token_payload(access_token=token))
132
+ return self._cp(
133
+ self._show_token_payload(access_token=primary_token),
134
+ )
124
135
  raise AssertionError(f"unexpected argv: {argv}")
125
136
  return fake_run
126
137
 
@@ -131,40 +142,133 @@ class ResolveOrgTests(unittest.TestCase):
131
142
  self.assertEqual(url, "https://example.salesforce.com")
132
143
  self.assertEqual(token, "TOKEN_FROM_SHOW")
133
144
 
134
- def test_primary_unknown_falls_back_to_display_token(self):
135
- """Older sf CLI: show-access-token unknown → use display payload."""
145
+ def test_primary_failure_raises_without_display_fallback(self):
146
+ """Token command failing is terminal — the display payload's
147
+ accessToken must NOT be used as a fallback."""
136
148
  with mock.patch.object(
137
- dc.subprocess, "run",
138
- side_effect=self._route(primary_unknown=True),
149
+ dc.subprocess, "run", side_effect=self._route(primary_fails=True),
139
150
  ):
140
- url, token = dc.resolve_org("my-org")
141
- self.assertEqual(url, "https://example.salesforce.com")
142
- self.assertEqual(token, "TOKEN_FROM_DISPLAY")
151
+ with self.assertRaises(SystemExit) as ctx:
152
+ dc.resolve_org("my-org")
153
+ msg = str(ctx.exception)
154
+ self.assertIn("sf org auth show-access-token", msg)
155
+ self.assertNotIn("TOKEN_FROM_DISPLAY", msg)
156
+
157
+ def test_primary_non_json_output_raises(self):
158
+ def fake_run(argv, **kwargs):
159
+ if "--help" in argv:
160
+ return self._cp(self.REAL_HELP)
161
+ if "display" in argv:
162
+ return self._cp(self._display_payload())
163
+ return self._cp("Warning: something not json")
164
+
165
+ with mock.patch.object(dc.subprocess, "run", side_effect=fake_run):
166
+ with self.assertRaises(SystemExit) as ctx:
167
+ dc.resolve_org("my-org")
168
+ self.assertIn("sf org auth show-access-token", str(ctx.exception))
143
169
 
144
- def test_primary_redacted_falls_back_to_display_token(self):
145
- """Edge case: dedicated command returns the placeholder string —
146
- treat as failure and try the display fallback."""
170
+ def test_primary_redacted_raises(self):
171
+ """Dedicated command returns the placeholder — fail fast rather than
172
+ handing the redaction string to downstream callers (which would
173
+ cause INVALID_AUTH_HEADER 401 on every call)."""
147
174
  with mock.patch.object(
148
175
  dc.subprocess, "run",
149
- side_effect=self._route(primary_redacted=True),
176
+ side_effect=self._route(primary_token=self.REDACTED_TOKEN),
150
177
  ):
151
- url, token = dc.resolve_org("my-org")
152
- self.assertEqual(token, "TOKEN_FROM_DISPLAY")
178
+ with self.assertRaises(SystemExit) as ctx:
179
+ dc.resolve_org("my-org")
180
+ self.assertIn("could not retrieve a usable access token", str(ctx.exception))
153
181
 
154
- def test_both_paths_redacted_raises(self):
155
- """If both paths return the placeholder, surface a clean SystemExit
156
- rather than handing back the redaction string to downstream callers
157
- (which would cause INVALID_AUTH_HEADER 401 on every Tooling/REST call)."""
182
+ def test_primary_empty_token_raises(self):
158
183
  with mock.patch.object(
159
- dc.subprocess, "run",
160
- side_effect=self._route(primary_redacted=True, display_redacted=True),
184
+ dc.subprocess, "run", side_effect=self._route(primary_token=""),
161
185
  ):
162
186
  with self.assertRaises(SystemExit) as ctx:
163
187
  dc.resolve_org("my-org")
164
188
  self.assertIn("could not retrieve a usable access token", str(ctx.exception))
165
189
 
190
+ def test_capability_preflight_failure_raises_clear_error(self):
191
+ """sf CLI without `sf org auth show-access-token` → actionable
192
+ upgrade message, and the token call is never attempted."""
193
+ captured: list = []
194
+ with mock.patch.object(
195
+ dc.subprocess, "run",
196
+ side_effect=self._route(preflight_missing=True, captured=captured),
197
+ ):
198
+ with self.assertRaises(SystemExit) as ctx:
199
+ dc.resolve_org("my-org")
200
+ msg = str(ctx.exception)
201
+ self.assertIn("missing required command", msg)
202
+ self.assertIn("sf org auth show-access-token", msg)
203
+ self.assertIn("Upgrade/reinstall Salesforce CLI", msg)
204
+ token_calls = [a for a, _ in captured
205
+ if "show-access-token" in a and "--no-prompt" in a]
206
+ self.assertEqual(token_calls, [])
207
+
208
+ def test_zero_exit_without_help_signature_fails_and_is_not_cached(self):
209
+ """A CLI that answers an unknown command with exit 0 must NOT be
210
+ treated as capable; the failure is not cached, so the next call
211
+ re-probes."""
212
+ bogus = self._cp(
213
+ "\x1b[1mUSAGE\x1b[22m\n $ sf [COMMAND]\n",
214
+ stderr='{"accessToken":"SECRET_Z"}',
215
+ )
216
+ with mock.patch.object(
217
+ dc.subprocess, "run", side_effect=[bogus, self._cp(self.REAL_HELP)],
218
+ ) as run:
219
+ with self.assertRaises(SystemExit) as ctx:
220
+ dc._assert_show_access_token_capability()
221
+ msg = str(ctx.exception)
222
+ self.assertIn("missing required command", msg)
223
+ self.assertNotIn("SECRET_Z", msg)
224
+ self.assertFalse(dc._show_access_token_capability_ok)
225
+ dc._assert_show_access_token_capability()
226
+ self.assertEqual(run.call_count, 2)
227
+ self.assertTrue(dc._show_access_token_capability_ok)
228
+
229
+ def test_zero_exit_without_help_signature_blocks_resolve_org(self):
230
+ captured: list = []
231
+ route = self._route(captured=captured)
232
+
233
+ def fake_run(argv, **kwargs):
234
+ if "--help" in argv:
235
+ captured.append((argv, kwargs))
236
+ return self._cp("")
237
+ return route(argv, **kwargs)
238
+
239
+ with mock.patch.object(dc.subprocess, "run", side_effect=fake_run):
240
+ with self.assertRaises(SystemExit) as ctx:
241
+ dc.resolve_org("my-org")
242
+ self.assertIn("lacked the command signature", str(ctx.exception))
243
+ self.assertEqual([a for a, _ in captured], [self.PREFLIGHT_ARGV])
244
+
245
+ def test_zero_exit_unknown_command_notice_echoing_name_fails(self):
246
+ """Unknown-command notices contain the bare command name; only the
247
+ USAGE form (`$ sf org auth show-access-token`) may pass."""
248
+ for out in (
249
+ "Error: Command org:auth:show-access-token not found.",
250
+ "Warning: show-access-token is not a sf command.",
251
+ "Did you mean org auth show-access-token? Run sf help for options.",
252
+ ):
253
+ with self.subTest(out=out):
254
+ dc._show_access_token_capability_ok = False
255
+ with mock.patch.object(
256
+ dc.subprocess, "run", return_value=self._cp("", stderr=out),
257
+ ):
258
+ with self.assertRaises(SystemExit) as ctx:
259
+ dc._assert_show_access_token_capability()
260
+ self.assertIn("missing required command", str(ctx.exception))
261
+ self.assertFalse(dc._show_access_token_capability_ok)
262
+
263
+ def test_real_help_signature_passes_preflight(self):
264
+ with mock.patch.object(
265
+ dc.subprocess, "run", return_value=self._cp(self.REAL_HELP),
266
+ ):
267
+ dc._assert_show_access_token_capability()
268
+ self.assertTrue(dc._show_access_token_capability_ok)
269
+
166
270
  def test_raises_systemexit_when_sf_cli_missing(self):
167
- """sf binary not on PATH — bail with the upgrade hint."""
271
+ """sf binary not on PATH — bail with the install hint."""
168
272
  with mock.patch.object(
169
273
  dc.subprocess, "run", side_effect=FileNotFoundError("sf"),
170
274
  ):
@@ -182,49 +286,167 @@ class ResolveOrgTests(unittest.TestCase):
182
286
  dc.resolve_org("my-org")
183
287
 
184
288
  def test_primary_path_argv_contains_show_access_token(self):
185
- """Tripwire — primary call MUST be `sf org auth show-access-token`
289
+ """Tripwire — token call MUST be `sf org auth show-access-token`
186
290
  with `--no-prompt`. Without `--no-prompt` the command blocks on a
187
291
  confirmation banner that --json doesn't suppress on its own."""
188
- captured_argvs: list[list[str]] = []
292
+ captured: list = []
293
+ with mock.patch.object(
294
+ dc.subprocess, "run", side_effect=self._route(captured=captured),
295
+ ):
296
+ dc.resolve_org("my-org")
297
+
298
+ argvs = [a for a, _ in captured]
299
+ # display call is still required for non-secret metadata.
300
+ self.assertTrue(any("display" in a for a in argvs))
301
+ primary = next(
302
+ a for a in argvs if "show-access-token" in a and "--no-prompt" in a
303
+ )
304
+ self.assertIn("--json", primary)
305
+ self.assertIn("--target-org", primary)
306
+
307
+ def test_does_not_inject_show_secrets_env(self):
308
+ """Tripwire: no sf subprocess may rely on SF_TEMP_SHOW_SECRETS
309
+ (removed from sf CLI per forcedotcom/cli#3560)."""
310
+ captured: list = []
311
+ with mock.patch.object(
312
+ dc.subprocess, "run", side_effect=self._route(captured=captured),
313
+ ):
314
+ dc.resolve_org("my-org")
315
+
316
+ self.assertTrue(captured)
317
+ for argv, kwargs in captured:
318
+ env = kwargs.get("env") or {}
319
+ self.assertNotIn("SF_TEMP_SHOW_SECRETS", env, msg=argv)
320
+
321
+ def test_preflight_is_first_sf_call(self):
322
+ captured: list = []
323
+ with mock.patch.object(
324
+ dc.subprocess, "run", side_effect=self._route(captured=captured),
325
+ ):
326
+ dc.resolve_org("my-org")
327
+ argvs = [a for a, _ in captured]
328
+ self.assertEqual(argvs[0], self.PREFLIGHT_ARGV)
329
+ self.assertIn("display", argvs[1])
330
+
331
+ def test_preflight_failure_never_invokes_display(self):
332
+ captured: list = []
333
+ with mock.patch.object(
334
+ dc.subprocess, "run",
335
+ side_effect=self._route(preflight_missing=True, captured=captured),
336
+ ):
337
+ with self.assertRaises(SystemExit):
338
+ dc.resolve_org("my-org")
339
+ self.assertEqual([a for a, _ in captured], [self.PREFLIGHT_ARGV])
340
+
341
+ def test_missing_sf_reports_preflight_error_not_display(self):
342
+ captured: list = []
189
343
 
190
344
  def fake_run(argv, **kwargs):
191
- captured_argvs.append(argv)
192
- if "display" in argv:
193
- return self._cp(self._display_payload())
194
- return self._cp(self._show_token_payload())
345
+ captured.append(argv)
346
+ raise FileNotFoundError("sf")
195
347
 
196
348
  with mock.patch.object(dc.subprocess, "run", side_effect=fake_run):
197
- dc.resolve_org("my-org")
349
+ with self.assertRaises(SystemExit) as ctx:
350
+ dc.resolve_org("my-org")
351
+ self.assertIn("sf CLI not found", str(ctx.exception))
352
+ self.assertEqual(captured, [self.PREFLIGHT_ARGV])
198
353
 
199
- # display call should still pass --verbose + SF_TEMP_SHOW_SECRETS
200
- # (so the fallback path stays viable on this same invocation).
201
- self.assertTrue(any("display" in a for a in captured_argvs))
202
- # primary call shape:
203
- primary = next(a for a in captured_argvs if "show-access-token" in a)
204
- self.assertIn("--no-prompt", primary)
205
- self.assertIn("--json", primary)
206
- self.assertIn("--target-org", primary)
354
+ def test_preflight_runs_once_across_two_resolves(self):
355
+ captured: list = []
356
+ with mock.patch.object(
357
+ dc.subprocess, "run", side_effect=self._route(captured=captured),
358
+ ):
359
+ dc.resolve_org("my-org")
360
+ dc.resolve_org("my-org")
361
+ argvs = [a for a, _ in captured]
362
+ self.assertEqual(argvs.count(self.PREFLIGHT_ARGV), 1)
363
+ self.assertEqual(sum(1 for a in argvs if "display" in a), 2)
207
364
 
208
- def test_display_call_still_passes_verbose_and_show_secrets_env(self):
209
- """The legacy fallback path stays armed: org_display still runs
210
- with `--verbose` (so accessToken is emitted) and
211
- `SF_TEMP_SHOW_SECRETS=true` (so the token isn't redacted on sf
212
- CLI versions that still honour the workaround)."""
213
- captured = {}
365
+ def test_failed_preflight_is_retried_on_next_resolve(self):
366
+ captured: list = []
367
+ with mock.patch.object(
368
+ dc.subprocess, "run",
369
+ side_effect=self._route(preflight_missing=True, captured=captured),
370
+ ):
371
+ with self.assertRaises(SystemExit):
372
+ dc.resolve_org("my-org")
373
+ with mock.patch.object(
374
+ dc.subprocess, "run", side_effect=self._route(captured=captured),
375
+ ):
376
+ _url, token = dc.resolve_org("my-org")
377
+ self.assertEqual(token, "TOKEN_FROM_SHOW")
378
+ argvs = [a for a, _ in captured]
379
+ self.assertEqual(argvs.count(self.PREFLIGHT_ARGV), 2)
214
380
 
381
+ def _token_call_fails_with(self, *, stdout: str, stderr: str):
215
382
  def fake_run(argv, **kwargs):
383
+ if "--help" in argv:
384
+ return self._cp(self.REAL_HELP)
216
385
  if "display" in argv:
217
- captured["argv"] = argv
218
- captured["env"] = kwargs.get("env")
219
386
  return self._cp(self._display_payload())
220
- return self._cp(self._show_token_payload())
387
+ raise subprocess.CalledProcessError(
388
+ returncode=1, cmd=argv, output=stdout, stderr=stderr,
389
+ )
390
+ return fake_run
221
391
 
222
- with mock.patch.object(dc.subprocess, "run", side_effect=fake_run):
392
+ def test_token_call_failure_output_is_redacted(self):
393
+ """A failing `show-access-token --json` can echo the token on
394
+ stdout (or a bearer header on stderr); neither may reach the exit
395
+ message."""
396
+ cases = {
397
+ "stdout_only": ('{"accessToken":"SECRET_X"}', ""),
398
+ "stderr_and_stdout": (
399
+ '{"accessToken":"SECRET_X"}',
400
+ "Error: request failed\nBearer SECRET_Y\n",
401
+ ),
402
+ "auth_header_and_qs": (
403
+ "", "Authorization: Bearer SECRET_Y accessToken=SECRET_X",
404
+ ),
405
+ }
406
+ for label, (stdout, stderr) in cases.items():
407
+ with self.subTest(label):
408
+ with mock.patch.object(
409
+ dc.subprocess, "run",
410
+ side_effect=self._token_call_fails_with(
411
+ stdout=stdout, stderr=stderr,
412
+ ),
413
+ ):
414
+ with self.assertRaises(SystemExit) as ctx:
415
+ dc.resolve_org("my-org")
416
+ msg = str(ctx.exception)
417
+ self.assertNotIn("SECRET_X", msg)
418
+ self.assertNotIn("SECRET_Y", msg)
419
+ self.assertIn("<redacted>", msg)
420
+
421
+ def test_preflight_failure_output_is_redacted(self):
422
+ for label, (stdout, stderr) in {
423
+ "stdout_only": ('{"accessToken":"SECRET_X"}', ""),
424
+ "stderr": ("", "Bearer SECRET_Y"),
425
+ }.items():
426
+ with self.subTest(label):
427
+ dc._show_access_token_capability_ok = False
428
+ err = subprocess.CalledProcessError(
429
+ returncode=1, cmd=self.PREFLIGHT_ARGV,
430
+ output=stdout, stderr=stderr,
431
+ )
432
+ with mock.patch.object(dc.subprocess, "run", side_effect=err):
433
+ with self.assertRaises(SystemExit) as ctx:
434
+ dc.resolve_org("my-org")
435
+ msg = str(ctx.exception)
436
+ self.assertIn("missing required command", msg)
437
+ self.assertNotIn("SECRET_X", msg)
438
+ self.assertNotIn("SECRET_Y", msg)
439
+
440
+ def test_display_call_still_passes_verbose(self):
441
+ """org_display still includes --verbose for metadata shape stability."""
442
+ captured: list = []
443
+ with mock.patch.object(
444
+ dc.subprocess, "run", side_effect=self._route(captured=captured),
445
+ ):
223
446
  dc.resolve_org("my-org")
224
447
 
225
- self.assertIn("--verbose", captured["argv"])
226
- self.assertIsNotNone(captured["env"])
227
- self.assertEqual(captured["env"].get("SF_TEMP_SHOW_SECRETS"), "true")
448
+ display_argv = next(a for a, _ in captured if "display" in a)
449
+ self.assertIn("--verbose", display_argv)
228
450
 
229
451
 
230
452
  # -----------------------------------------------------------------------------