@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.
- package/package.json +1 -1
- package/skills/agentforce-architecture-analyze/SKILL.md +66 -17
- package/skills/agentforce-architecture-analyze/assets/cli/show_access_token.yaml +4 -7
- package/skills/agentforce-architecture-analyze/references/contract.json +1 -1
- package/skills/agentforce-architecture-analyze/scripts/emit_env.py +2 -2
- package/skills/agentforce-architecture-analyze/scripts/main.py +40 -44
- package/skills/agentforce-architecture-analyze/scripts/rest_client.py +11 -3
- package/skills/agentforce-architecture-analyze/scripts/sf_cli.py +77 -10
- package/skills/agentforce-architecture-analyze/scripts/tests/test_end_to_end_fixture.py +1 -0
- package/skills/agentforce-architecture-analyze/scripts/tests/test_flow_children_inflation.py +1 -0
- package/skills/agentforce-architecture-analyze/scripts/tests/test_main_pipeline.py +53 -2
- package/skills/agentforce-architecture-analyze/scripts/tests/test_resolve_creds.py +184 -36
- package/skills/agentforce-architecture-analyze/scripts/tests/test_rest_client.py +17 -0
- package/skills/agentforce-architecture-analyze/scripts/tests/test_sf_cli.py +165 -11
- package/skills/agentforce-architecture-analyze/scripts/tests/test_skill_md_pipeline_block.py +319 -0
- package/skills/agentforce-d360-analyze/SKILL.md +28 -10
- package/skills/agentforce-d360-analyze/scripts/dc.py +200 -50
- package/skills/agentforce-d360-analyze/scripts/discover_sessions.py +10 -6
- package/skills/agentforce-d360-analyze/scripts/fetch_dc.py +22 -6
- package/skills/agentforce-d360-analyze/scripts/resolve_session.py +21 -13
- package/skills/agentforce-d360-analyze/scripts/tests/test_dc_and_resolve_session.py +298 -76
- package/skills/agentforce-d360-analyze/scripts/tests/test_default_target_org.py +432 -0
- package/skills/agentforce-d360-analyze/scripts/tests/test_discover_sessions.py +1 -1
- package/skills/agentforce-d360-analyze/scripts/tests/test_resolve_session_main.py +9 -3
- package/skills/agentforce-d360-analyze/scripts/tests/test_skill_md_prefix_block.py +176 -0
- package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +17 -27
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +7 -17
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +47 -63
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +10 -13
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -46
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +4 -21
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +58 -82
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +16 -315
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +9 -24
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -44
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +11 -63
- package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +2 -1
- package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +2 -1
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +2 -1
- package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +2 -1
- package/skills/data360-schema-get/SKILL.md +3 -3
- package/skills/design-systems-slds-validate/references/report-format.md +1 -1
- package/skills/dx-org-manage/SKILL.md +1 -0
- package/skills/dx-org-shape-manage/SKILL.md +1 -0
- package/skills/dx-org-snapshot-manage/SKILL.md +172 -0
- package/skills/dx-org-snapshot-manage/examples/create_error_output.json +16 -0
- package/skills/dx-org-snapshot-manage/examples/create_success_output.json +19 -0
- package/skills/dx-org-snapshot-manage/examples/delete_output.json +9 -0
- package/skills/dx-org-snapshot-manage/examples/get_output.json +19 -0
- package/skills/dx-org-snapshot-manage/examples/list_output.json +36 -0
- package/skills/dx-org-snapshot-manage/references/cli_flags.md +179 -0
- package/skills/dx-org-snapshot-manage/references/snapshot_usage.md +55 -0
- package/skills/dx-org-trial-expiration-check/SKILL.md +1 -1
- package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +2 -2
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +7 -5
- package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +11 -11
- package/skills/experience-content-media-image-generate/SKILL.md +83 -0
- package/skills/experience-content-media-image-generate/scripts/download-image.sh +84 -0
- package/skills/external-diagram-mermaid-generate/SKILL.md +12 -12
- package/skills/field-service-data-capture-migrate/references/component-mapping.md +1 -1
- package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +1 -2
- package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +1 -0
- package/skills/field-service-data-capture-migrate/references/transformer-rules.md +1 -1
- package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +12 -1
- package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +4 -1
- package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -1
- package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +233 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/accounting-setup.md +117 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/billing-rules-config.md +237 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/billing-settings.md +271 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/demo-policy-config.md +138 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/org-defaults.md +64 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/permissions-config.md +226 -0
- package/skills/insurance-brokerage-agency-billing-configure/references/prerequisites.md +142 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +3 -2
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +3 -3
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +68 -45
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +17 -18
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +16 -3
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +16 -13
- package/skills/marketing-message-test/SKILL.md +194 -0
- package/skills/marketing-message-test/references/api-contract.md +333 -0
- package/skills/omnistudio-callable-apex-generate/SKILL.md +3 -3
- package/skills/omnistudio-datamapper-generate/SKILL.md +6 -6
- package/skills/omnistudio-datapacks-deploy/SKILL.md +11 -11
- package/skills/omnistudio-dependencies-analyze/SKILL.md +5 -5
- package/skills/omnistudio-epc-catalog-generate/SKILL.md +6 -6
- package/skills/omnistudio-flexcard-generate/SKILL.md +6 -6
- package/skills/omnistudio-integration-procedure-generate/SKILL.md +6 -6
- package/skills/omnistudio-omniscript-generate/SKILL.md +6 -5
- package/skills/platform-apex-logs-debug/SKILL.md +1 -0
- package/skills/platform-metadata-deploy/SKILL.md +1 -0
- package/skills/platform-models-api-configure/SKILL.md +5 -5
- package/skills/platform-soql-query/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +17 -6
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +107 -192
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +6 -6
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +49 -20
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +4 -4
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +224 -290
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/workflow.md +295 -0
- package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -1
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +2 -1
- package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +2 -2
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +20 -13
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +5 -55
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +0 -38
- package/skills/service-omni-command-center-analyze/SKILL.md +2 -3
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
- package/skills/tableau-next-asset-create/README.md +65 -0
- package/skills/tableau-next-asset-create/SKILL.md +409 -0
- package/skills/tableau-next-asset-create/references/admin-users.md +149 -0
- package/skills/tableau-next-asset-create/references/ai-readiness.md +355 -0
- package/skills/tableau-next-asset-create/references/alerts.md +111 -0
- package/skills/tableau-next-asset-create/references/build-workflow.md +819 -0
- package/skills/tableau-next-asset-create/references/dashboard-authoring.md +302 -0
- package/skills/tableau-next-asset-create/references/dashboard-design-principles.md +352 -0
- package/skills/tableau-next-asset-create/references/data-transform-gotchas.md +210 -0
- package/skills/tableau-next-asset-create/references/data-understanding.md +387 -0
- package/skills/tableau-next-asset-create/references/deep-reference-index.md +74 -0
- package/skills/tableau-next-asset-create/references/destructive-operations.md +62 -0
- package/skills/tableau-next-asset-create/references/edit-dashboard.md +273 -0
- package/skills/tableau-next-asset-create/references/edit-visualization.md +529 -0
- package/skills/tableau-next-asset-create/references/empty-source-handling.md +104 -0
- package/skills/tableau-next-asset-create/references/ingest-and-metric-gotchas.md +638 -0
- package/skills/tableau-next-asset-create/references/insight-bundle.md +69 -0
- package/skills/tableau-next-asset-create/references/large-flat-file-handling.md +331 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_envelope.json +54 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_shared-widget-fields.json +122 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/add_page.json +29 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/delete_page.json +27 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/discard_dashboard.json +11 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget.json +30 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget_to_page.json +30 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/place_widget_on_page.json +30 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_dashboard.json +16 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_page.json +26 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/rename_page.json +28 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/save_dashboard.json +18 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/undo.json +11 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_button_widget.json +69 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_container_widget.json +88 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_filter_widget.json +146 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_metric_widget.json +78 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_navigation_widget.json +80 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_parameter_widget.json +160 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_text_widget.json +103 -0
- package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_visualization_widget.json +68 -0
- package/skills/tableau-next-asset-create/references/sdm-tool-reference.md +721 -0
- package/skills/tableau-next-asset-create/references/semantic-query-and-enrichment.md +355 -0
- package/skills/tableau-next-asset-create/references/shared-gates.md +322 -0
- package/skills/tableau-next-asset-create/references/sharing-and-promotion.md +306 -0
- package/skills/tableau-next-asset-create/references/tasks/ai-readiness-audit.md +51 -0
- package/skills/tableau-next-asset-create/references/tasks/analyze-data.md +48 -0
- package/skills/tableau-next-asset-create/references/tasks/build-dashboard.md +66 -0
- package/skills/tableau-next-asset-create/references/tasks/build-end-to-end.md +73 -0
- package/skills/tableau-next-asset-create/references/tasks/create-data-transform.md +51 -0
- package/skills/tableau-next-asset-create/references/tasks/create-viz.md +58 -0
- package/skills/tableau-next-asset-create/references/tasks/edit-dashboard.md +80 -0
- package/skills/tableau-next-asset-create/references/tasks/edit-sdm.md +106 -0
- package/skills/tableau-next-asset-create/references/tasks/edit-viz.md +40 -0
- package/skills/tableau-next-asset-create/references/tasks/enrich-model.md +68 -0
- package/skills/tableau-next-asset-create/references/tasks/ingest-flat-file.md +68 -0
- package/skills/tableau-next-asset-create/references/tasks/manage-alert.md +35 -0
- package/skills/tableau-next-asset-create/references/tasks/profile-dataset.md +47 -0
- package/skills/tableau-next-asset-create/references/tasks/promote-or-reuse.md +46 -0
- package/skills/tableau-next-asset-create/references/tasks/provision-user.md +39 -0
- package/skills/tableau-next-asset-create/references/tasks/query-model.md +92 -0
- package/skills/tableau-next-asset-create/references/tasks/review-verified-questions.md +42 -0
- package/skills/tableau-next-asset-create/references/tasks/share-asset.md +37 -0
- package/skills/tableau-next-asset-create/references/verified-questions.md +125 -0
- package/skills/tableau-next-asset-create/references/viz-authoring.md +992 -0
- package/skills/service-de-channel-consent-configure/SKILL.md +0 -230
- 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`
|
|
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
|
|
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
|
-
|
|
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()
|