afv-library/plugins/builder/salesforce-development/scripts/test/test_sf_context.py
2026-08-10 16:05:50 +00:00

2053 lines
105 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Unit tests for the cross-platform executable resolver in sf_context.py
(WIN-026) and the deterministic setup/org reporting (WIN-027).
These are the evidence for the Windows fix: they simulate Windows resolution of a
`.cmd`/`.bat` shim (via a faked `shutil.which`) and assert that a COMSPEC-wrapped
ARGV ARRAY is built — never a shell string — while POSIX paths spawn directly.
They also assert that a genuinely-missing tool is reported FAILED (not silently
empty, not green) and that failure diagnostics never leak tokens/secrets.
Offline: no live org, no real subprocess spawn (subprocess.run / shutil.which are
mocked). Stdlib unittest only (no pytest/PyYAML) so it runs anywhere Python does,
including the 3.9 baseline.
Run: python3 plugins/builder/salesforce-development/scripts/test/test_sf_context.py
"""
from __future__ import annotations
import importlib.util
import json
import io
import os
import stat
import tempfile
import types
import unittest
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
from unittest import mock
# sf_context.py is the sibling of this test's parent dir: scripts/test/ → scripts/.
# (The runtime lives under scripts/ rather than bin/ because this repo's
# .gitignore blocks bin/ — see the bin/README.md note.)
_MODULE_PATH = Path(__file__).resolve().parent.parent / "sf_context.py"
def _load_module():
spec = importlib.util.spec_from_file_location("sf_context_under_test", _MODULE_PATH)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module
sfx = _load_module()
def _completed(stdout="", returncode=0, stderr=""):
"""A stand-in for subprocess.CompletedProcess (only the fields run() reads)."""
return types.SimpleNamespace(stdout=stdout, returncode=returncode, stderr=stderr)
class ResolveExecutableTests(unittest.TestCase):
def test_delegates_to_shutil_which(self):
with mock.patch.object(sfx.shutil, "which", return_value="/usr/local/bin/sf") as which:
self.assertEqual(sfx.resolve_executable("sf"), "/usr/local/bin/sf")
which.assert_called_once_with("sf")
def test_windows_shim_found_via_pathext(self):
# shutil.which honors PATHEXT on Windows, so a bare "sf" resolves to sf.cmd.
with mock.patch.object(sfx.shutil, "which", return_value=r"C:\tools\sf\bin\sf.cmd"):
self.assertEqual(sfx.resolve_executable("sf"), r"C:\tools\sf\bin\sf.cmd")
def test_missing_returns_none(self):
with mock.patch.object(sfx.shutil, "which", return_value=None):
self.assertIsNone(sfx.resolve_executable("definitely-not-a-tool"))
def test_empty_name_returns_none(self):
self.assertIsNone(sfx.resolve_executable(""))
class BuildCommandTests(unittest.TestCase):
def test_posix_spawns_resolved_path_directly(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
argv = sfx.build_command("sf", ["config", "get", "target-org", "--json"])
self.assertEqual(argv, ["/usr/local/bin/sf", "config", "get", "target-org", "--json"])
# A plain argv array, first element the resolved binary (no cmd wrapper).
self.assertIsInstance(argv, list)
self.assertNotIn("/c", argv)
def test_windows_cmd_shim_wrapped_with_comspec(self):
resolved = r"C:\Program Files\sf\bin\sf.cmd"
with mock.patch.object(sfx, "resolve_executable", return_value=resolved), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": r"C:\Windows\System32\cmd.exe"}, clear=False):
argv = sfx.build_command("sf", ["config", "get", "target-org"])
self.assertEqual(
argv,
[r"C:\Windows\System32\cmd.exe", "/c", resolved, "config", "get", "target-org"],
)
def test_windows_bat_shim_wrapped(self):
resolved = r"C:\tools\npm.bat"
with mock.patch.object(sfx, "resolve_executable", return_value=resolved), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": r"C:\Windows\System32\cmd.exe"}, clear=False):
argv = sfx.build_command("npm", ["--version"])
self.assertEqual(argv, [r"C:\Windows\System32\cmd.exe", "/c", resolved, "--version"])
def test_comspec_falls_back_to_cmd_exe(self):
resolved = r"C:\tools\sf.cmd"
env_without_comspec = {k: v for k, v in sfx.os.environ.items() if k != "COMSPEC"}
with mock.patch.object(sfx, "resolve_executable", return_value=resolved), \
mock.patch.dict(sfx.os.environ, env_without_comspec, clear=True):
argv = sfx.build_command("sf", ["version"])
self.assertEqual(argv, ["cmd.exe", "/c", resolved, "version"])
def test_missing_tool_returns_none(self):
with mock.patch.object(sfx, "resolve_executable", return_value=None):
self.assertIsNone(sfx.build_command("sf", ["version"]))
def test_never_builds_a_shell_string(self):
# The crux of the .cmd case: "no shell" and "injection-safe" are reconciled
# by keeping an ARGV ARRAY. Assert the result is always a list of tokens,
# never a single concatenated command string.
for resolved in (r"C:\tools\sf.cmd", "/usr/local/bin/sf"):
with mock.patch.object(sfx, "resolve_executable", return_value=resolved):
argv = sfx.build_command("sf", ["config", "get"])
self.assertIsInstance(argv, list)
for token in argv:
self.assertIsInstance(token, str)
def test_cmd_shim_refuses_metacharacter_args(self):
# cmd.exe re-parses its command line, so an arg with a shell metacharacter
# must NOT reach a batch shim. build_command fails closed (returns None).
for bad in ("safe&whoami", "a|b", "x>y", "a<b", "p^q", "%PATH%", 'a"b',
"a!b", "a(b", "a)b", "a\nb", "a\rb"):
with mock.patch.object(sfx, "resolve_executable", return_value=r"C:\tools\sf.cmd"), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False):
argv = sfx.build_command("sf", ["config", "get", bad])
self.assertIsNone(argv, f"metacharacter arg should be refused: {bad!r}")
def test_cmd_shim_refuses_dangerous_path(self):
# A reparse-dangerous char in the resolved shim PATH is also refused
# (e.g. `%` env-expansion or unquoted `&`), fail closed.
for bad_path in (r"C:\a&b\sf.cmd", r"C:\weird%dir\sf.cmd", r"C:\x!y\sf.cmd"):
with mock.patch.object(sfx, "resolve_executable", return_value=bad_path), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False):
self.assertIsNone(sfx.build_command("sf", ["version"]),
f"dangerous shim path should be refused: {bad_path!r}")
def test_cmd_shim_allows_program_files_x86_path(self):
# `(` `)` appear in legitimate install paths, so the PATH guard must NOT
# reject them (they're only rejected in ARGS).
resolved = r"C:\Program Files (x86)\sf\bin\sf.cmd"
with mock.patch.object(sfx, "resolve_executable", return_value=resolved), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False):
argv = sfx.build_command("sf", ["version"])
self.assertEqual(argv, ["cmd.exe", "/c", resolved, "version"])
def test_posix_path_allows_metacharacters(self):
# The reparse hazard is cmd.exe-specific; a direct shell=False spawn of a
# POSIX/.exe path is not subject to it, so args pass through unchanged.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
argv = sfx.build_command("sf", ["config", "get", "safe&whoami"])
self.assertEqual(argv, ["/usr/local/bin/sf", "config", "get", "safe&whoami"])
def test_cmd_shim_allows_ordinary_args(self):
# A normal alias/flag arg (no metacharacters, spaces ok) still runs.
with mock.patch.object(sfx, "resolve_executable", return_value=r"C:\tools\sf.cmd"), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False):
argv = sfx.build_command("sf", ["org", "display", "--target-org", "my-scratch"])
self.assertEqual(argv, ["cmd.exe", "/c", r"C:\tools\sf.cmd", "org", "display", "--target-org", "my-scratch"])
def test_run_refuses_metacharacter_arg_and_never_spawns(self):
with mock.patch.object(sfx, "resolve_executable", return_value=r"C:\tools\sf.cmd"), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False), \
mock.patch.object(sfx.subprocess, "run") as spawn:
self.assertEqual(sfx.run(["sf", "config", "get", "a&b"]), "")
spawn.assert_not_called()
def test_preserves_argv_boundaries(self):
# A SOQL query with spaces must remain ONE argv element (no concatenation),
# both on POSIX and inside the cmd wrapper.
query = "SELECT Id FROM Account WHERE Name = 'Acme Inc'"
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
argv = sfx.build_command("sf", ["data", "query", "--query", query])
self.assertIn(query, argv)
self.assertEqual(argv[-1], query)
with mock.patch.object(sfx, "resolve_executable", return_value=r"C:\tools\sf.cmd"), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False):
argv = sfx.build_command("sf", ["data", "query", "--query", query])
self.assertEqual(argv[-1], query)
self.assertEqual(argv[:2], ["cmd.exe", "/c"])
class RunTests(unittest.TestCase):
def test_run_spawns_comspec_argv_with_shell_false(self):
resolved = r"C:\tools\sf.cmd"
captured = {}
def fake_run(argv, **kwargs):
captured["argv"] = argv
captured["kwargs"] = kwargs
return _completed(stdout="ok-out", returncode=0)
with mock.patch.object(sfx, "resolve_executable", return_value=resolved), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False), \
mock.patch.object(sfx.subprocess, "run", side_effect=fake_run):
out = sfx.run(["sf", "config", "get", "target-org", "--json"])
self.assertEqual(out, "ok-out")
self.assertEqual(captured["argv"], ["cmd.exe", "/c", resolved, "config", "get", "target-org", "--json"])
self.assertIsInstance(captured["argv"], list)
self.assertIs(captured["kwargs"].get("shell"), False)
def test_run_posix_spawns_resolved_path(self):
captured = {}
def fake_run(argv, **kwargs):
captured["argv"] = argv
return _completed(stdout="v1", returncode=0)
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/git"), \
mock.patch.object(sfx.subprocess, "run", side_effect=fake_run):
out = sfx.run(["git", "--version"])
self.assertEqual(out, "v1")
self.assertEqual(captured["argv"], ["/usr/local/bin/git", "--version"])
def test_run_missing_tool_returns_empty_and_never_spawns(self):
with mock.patch.object(sfx, "resolve_executable", return_value=None), \
mock.patch.object(sfx.subprocess, "run") as spawn:
self.assertEqual(sfx.run(["sf", "version"]), "")
spawn.assert_not_called()
def test_run_nonzero_returncode_returns_empty(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run", return_value=_completed(stdout="x", returncode=1)):
self.assertEqual(sfx.run(["sf", "version"]), "")
def test_run_timeout_returns_empty(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
side_effect=sfx.subprocess.TimeoutExpired(cmd="sf", timeout=1)):
self.assertEqual(sfx.run(["sf", "version"]), "")
def test_run_applies_platform_default_timeout(self):
# An unspecified timeout resolves to the platform-aware _cli_timeout()
# (longer on Windows to survive slow cold `sf.cmd` startup under load).
captured = {}
def fake_run(argv, **kwargs):
captured["timeout"] = kwargs.get("timeout")
return _completed(stdout="x", returncode=0)
with mock.patch.object(sfx, "resolve_executable", return_value=r"C:\tools\sf.cmd"), \
mock.patch.object(sfx, "_is_windows", return_value=True), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False), \
mock.patch.object(sfx.subprocess, "run", side_effect=fake_run):
sfx.run(["sf", "config", "get", "target-org"])
self.assertEqual(captured["timeout"], 30)
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx, "_is_windows", return_value=False), \
mock.patch.object(sfx.subprocess, "run", side_effect=fake_run):
sfx.run(["sf", "version"])
self.assertEqual(captured["timeout"], 10)
class PlatformTuningTests(unittest.TestCase):
def test_cli_timeout_scales_on_windows(self):
with mock.patch.object(sfx, "_is_windows", return_value=True):
self.assertEqual(sfx._cli_timeout(), 30)
with mock.patch.object(sfx, "_is_windows", return_value=False):
self.assertEqual(sfx._cli_timeout(), 10)
def test_check_tools_workers_reduced_on_windows(self):
with mock.patch.object(sfx, "_is_windows", return_value=True):
self.assertEqual(sfx._check_tools_workers(), 3)
with mock.patch.object(sfx, "_is_windows", return_value=False):
self.assertEqual(sfx._check_tools_workers(), 7)
class ForceUtf8StdioTests(unittest.TestCase):
def test_reconfigures_stdout_and_stderr_to_utf8(self):
# Windows cp1252 consoles can't encode the box-drawing glyphs the status
# commands print; startup reconfigures the streams to UTF-8.
calls = []
class FakeStream:
def reconfigure(self, **kw):
calls.append(kw)
with mock.patch.object(sfx.sys, "stdout", FakeStream()), \
mock.patch.object(sfx.sys, "stderr", FakeStream()):
sfx._force_utf8_stdio()
self.assertEqual(calls, [{"encoding": "utf-8"}, {"encoding": "utf-8"}])
def test_stream_without_reconfigure_is_safe(self):
class NoReconfigure:
pass
with mock.patch.object(sfx.sys, "stdout", NoReconfigure()), \
mock.patch.object(sfx.sys, "stderr", NoReconfigure()):
sfx._force_utf8_stdio() # must not raise
def test_reconfigure_error_is_swallowed(self):
class BadStream:
def reconfigure(self, **kw):
raise ValueError("boom")
with mock.patch.object(sfx.sys, "stdout", BadStream()), \
mock.patch.object(sfx.sys, "stderr", BadStream()):
sfx._force_utf8_stdio() # must not raise
class GetTargetOrgTests(unittest.TestCase):
_CONFIG_JSON = json.dumps(
{"result": [{"name": "target-org", "value": "myScratch"}]}
)
_NO_ORG_JSON = json.dumps({"result": [{"name": "target-org"}]})
def setUp(self):
# get_target_org_detailed now honors SF_TARGET_ORG / SFDX_TARGET_ORG before
# the CLI config (matching `sf` and the proxy). Scrub them so the CLI-mock
# cases below observe the config path, not the runner's ambient env.
patcher = mock.patch.dict(sfx.os.environ, {}, clear=False)
patcher.start()
self.addCleanup(patcher.stop)
sfx.os.environ.pop("SF_TARGET_ORG", None)
sfx.os.environ.pop("SFDX_TARGET_ORG", None)
def test_succeeds_when_sf_resolves_to_cmd_shim(self):
# The Windows regression: get_target_org() returned "" because sf.cmd
# could not be launched. With the resolver it must succeed.
with mock.patch.object(sfx, "resolve_executable", return_value=r"C:\tools\sf.cmd"), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=self._CONFIG_JSON, returncode=0)):
self.assertEqual(sfx.get_target_org(), "myScratch")
def test_missing_cli_reports_no_org_not_a_crash(self):
with mock.patch.object(sfx, "resolve_executable", return_value=None):
self.assertEqual(sfx.get_target_org(), "")
def test_detailed_distinguishes_no_org_from_cli_failure(self):
# CLI ran, org set → (alias, "").
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=self._CONFIG_JSON, returncode=0)):
self.assertEqual(sfx.get_target_org_detailed(), ("myScratch", ""))
# CLI ran, no org set → ("", "") (empty reason = genuinely no org).
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=self._NO_ORG_JSON, returncode=0)):
self.assertEqual(sfx.get_target_org_detailed(), ("", ""))
# CLI present but query failed (nonzero) → ("", "nonzero"), NOT a false no-org.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout="", returncode=1)):
alias, reason = sfx.get_target_org_detailed()
self.assertEqual(alias, "")
self.assertEqual(reason, "nonzero")
# CLI query timed out → ("", "timeout").
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
side_effect=sfx.subprocess.TimeoutExpired(cmd="sf", timeout=1)):
self.assertEqual(sfx.get_target_org_detailed(), ("", "timeout"))
def test_detailed_flags_invalid_output_on_exit_zero(self):
# sf exits 0 but the payload can't be trusted → "invalid-output", never a
# false "no org configured" and never a crash on an unexpected shape.
cases = {
"malformed JSON": "{not valid json",
"empty stdout": "",
"non-object root (array)": "[1, 2, 3]",
"non-object root (scalar)": "\"hi\"",
"missing result field": json.dumps({"status": 0}),
"result not a list (object)": json.dumps({"result": {}}),
"result not a list (string)": json.dumps({"result": "x"}),
}
for label, payload in cases.items():
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=payload, returncode=0)):
alias, reason = sfx.get_target_org_detailed()
self.assertEqual(alias, "", f"{label}: alias should be empty")
self.assertEqual(reason, "invalid-output", f"{label}: reason should be invalid-output")
def test_detailed_well_formed_no_entry_is_no_org(self):
# A well-formed empty/other-key result is genuinely "no org" (not invalid),
# and non-dict entries in the list don't crash.
for payload in (json.dumps({"result": []}),
json.dumps({"result": [{"name": "other"}]}),
json.dumps({"result": ["stringy", 5, None]})):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=payload, returncode=0)):
self.assertEqual(sfx.get_target_org_detailed(), ("", ""))
def test_env_target_org_takes_precedence_over_config(self):
# SF_TARGET_ORG / SFDX_TARGET_ORG must win over the CLI config, matching
# how `sf` itself and the proxy's resolveTargetOrg() resolve the org. If
# the consumer read config here while the proxy stamped sidecars with the
# env org, the MCP-health filter would reject valid sidecars.
for var in ("SF_TARGET_ORG", "SFDX_TARGET_ORG"):
with self.subTest(var=var):
# The CLI would say "myScratch"; the env override must win — and
# subprocess.run must not even be consulted (short-circuit).
run_spy = mock.MagicMock(
return_value=_completed(stdout=self._CONFIG_JSON, returncode=0))
with mock.patch.dict(sfx.os.environ, {var: "envOrg"}, clear=False), \
mock.patch.object(sfx.subprocess, "run", run_spy):
self.assertEqual(sfx.get_target_org_detailed(), ("envOrg", ""))
run_spy.assert_not_called()
def test_sf_target_org_wins_over_sfdx_target_org(self):
# When both are set, SF_TARGET_ORG takes priority (same order as the proxy).
with mock.patch.dict(sfx.os.environ,
{"SF_TARGET_ORG": "sfOrg", "SFDX_TARGET_ORG": "sfdxOrg"},
clear=False):
self.assertEqual(sfx.get_target_org_detailed(), ("sfOrg", ""))
class RunResultTests(unittest.TestCase):
def test_ok_result(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout="hi", returncode=0)):
res = sfx.run_result(["sf", "version"])
self.assertTrue(res.ok)
self.assertEqual(res.stdout, "hi")
self.assertEqual(res.reason, "")
def test_unresolved_reason(self):
with mock.patch.object(sfx, "resolve_executable", return_value=None):
res = sfx.run_result(["sf", "version"])
self.assertFalse(res.ok)
self.assertEqual(res.reason, "unresolved")
def test_nonzero_reason(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout="", returncode=2)):
res = sfx.run_result(["sf", "version"])
self.assertFalse(res.ok)
self.assertEqual(res.reason, "nonzero")
self.assertEqual(res.returncode, 2)
def test_timeout_reason(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
side_effect=sfx.subprocess.TimeoutExpired(cmd="sf", timeout=1)):
self.assertEqual(sfx.run_result(["sf", "version"]).reason, "timeout")
def test_run_wrapper_preserves_empty_on_failure(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout="junk", returncode=1)):
self.assertEqual(sfx.run(["sf", "version"]), "")
class CheckToolsTests(unittest.TestCase):
def setUp(self):
# cmd_check_tools now writes a readiness verdict to ./.sf/ as a side effect,
# so isolate the cwd in a temp dir — otherwise the run litters the invoker's
# directory. The direct `_check_*` unit tests don't touch cwd, so this is
# harmless for them.
self._prev_cwd = os.getcwd()
self._tmp = tempfile.TemporaryDirectory()
os.chdir(self._tmp.name)
def tearDown(self):
os.chdir(self._prev_cwd)
self._tmp.cleanup()
def _mock_all_checks(self, git=None, cli=None):
"""Patch every _check_* to a green row (info for the MCP process row) so a
cmd_check_tools run is deterministic and offline. `git`/`cli` override those
rows for the not-ready cases."""
ok = lambda name: {"name": name, "status": "ok", "version": "x", "message": "Installed"}
return [
mock.patch.object(sfx, "_check_sf_cli", return_value=cli or ok("Salesforce CLI")),
mock.patch.object(sfx, "_check_code_analyzer", return_value=ok("Code Analyzer plugin")),
mock.patch.object(sfx, "_check_node", return_value=ok("Node.js")),
mock.patch.object(sfx, "_check_npm", return_value=ok("NPM")),
mock.patch.object(sfx, "_check_git", return_value=git or ok("Git")),
mock.patch.object(sfx, "_check_source_tracking", return_value=ok("Source Tracking")),
mock.patch.object(sfx, "_check_mcp", return_value=[
{"name": "Salesforce MCP (config)", "status": "ok"},
{"name": "Salesforce MCP (process)", "status": "info"},
]),
mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/tool"),
]
def _run_check_tools(self):
buf = io.StringIO()
with redirect_stdout(buf):
sfx.cmd_check_tools()
return json.loads(buf.getvalue())
def _read_verdict(self):
return json.loads((Path(".sf") / "environment-readiness.json").read_text())
def _read_report(self):
return json.loads((Path(".sf") / "environment-readiness-report.json").read_text())
def _run_readiness_banner(self):
buf, err = io.StringIO(), io.StringIO()
with redirect_stdout(buf), redirect_stderr(err):
code = sfx.cmd_readiness_banner()
return code, buf.getvalue(), err.getvalue()
def test_readiness_banner_prints_the_deterministic_render_from_the_persisted_report(self):
# The platform-environment-validate paint fallback: after a check-tools scan
# persists the report, `readiness-banner` prints exactly render_readiness_text
# from that same report — so the skill never hand-renders the banner from JSON.
patches = self._mock_all_checks()
for p in patches:
p.start()
try:
self._run_check_tools()
finally:
for p in patches:
p.stop()
code, out, err = self._run_readiness_banner()
self.assertEqual(code, 0)
self.assertEqual(err, "")
self.assertEqual(out, sfx.render_readiness_text(self._read_report(), color=False) + "\n")
self.assertIn("Ready to build on Salesforce?", out)
def test_readiness_banner_fails_open_with_a_pointer_when_no_report_exists(self):
# No check-tools run in this cwd → no persisted report. The command stays
# fail-open: nothing on stdout, a one-line pointer to check-tools on stderr,
# exit 2. The check-tools JSON, not this banner, is the authoritative result.
code, out, err = self._run_readiness_banner()
self.assertEqual(code, 2)
self.assertEqual(out, "")
self.assertIn("check-tools", err)
def test_check_tools_writes_ready_verdict_when_all_green(self):
# No critical and no warn rows (the MCP process row is info, which does NOT
# count) → readiness is a pass, with a toolchain signature and timestamp.
patches = self._mock_all_checks()
for p in patches:
p.start()
try:
report = self._run_check_tools()
finally:
for p in patches:
p.stop()
self.assertNotIn("diagnostic", report) # nothing critical
verdict = self._read_verdict()
self.assertTrue(verdict["ready"])
self.assertEqual(verdict["needsAttention"], [])
self.assertIn("signature", verdict)
self.assertIn("checkedAt", verdict)
def test_check_tools_writes_not_ready_when_a_tool_is_critical(self):
git_missing = {"name": "Git", "status": "critical", "version": None, "message": "Not found"}
patches = self._mock_all_checks(git=git_missing)
for p in patches:
p.start()
try:
self._run_check_tools()
finally:
for p in patches:
p.stop()
verdict = self._read_verdict()
self.assertFalse(verdict["ready"])
self.assertIn("Git", verdict["needsAttention"])
def test_check_tools_warn_row_is_ready_but_flagged(self):
# A 🟡 warn (a non-LTS Node, an outdated-but-working CLI) is ADVISORY, not a
# blocker: it is surfaced in needsAttention for the banner, but readiness
# stays True and blockers is empty, so the scaffold gate never blocks on it.
cli_outdated = {"name": "Salesforce CLI", "status": "warn", "version": "2.1",
"message": "Version 2.1 is outdated"}
patches = self._mock_all_checks(cli=cli_outdated)
for p in patches:
p.start()
try:
self._run_check_tools()
finally:
for p in patches:
p.stop()
verdict = self._read_verdict()
self.assertTrue(verdict["ready"]) # warn alone never blocks
self.assertEqual(verdict["blockers"], []) # nothing critical
self.assertIn("Salesforce CLI", verdict["needsAttention"]) # still surfaced
def test_check_tools_blockers_are_critical_only_not_warnings(self):
# When a critical AND a warn coexist, ready is False (the critical blocks)
# but `blockers` names ONLY the critical — the warn stays advisory in
# needsAttention so the scaffold-gate block never misattributes it.
git_missing = {"name": "Git", "status": "critical", "version": None, "message": "Not found"}
cli_warn = {"name": "Salesforce CLI", "status": "warn", "version": "2.1",
"message": "Version 2.1 is outdated"}
patches = self._mock_all_checks(git=git_missing, cli=cli_warn)
for p in patches:
p.start()
try:
self._run_check_tools()
finally:
for p in patches:
p.stop()
verdict = self._read_verdict()
self.assertFalse(verdict["ready"])
self.assertEqual(verdict["blockers"], ["Git"]) # critical only
self.assertIn("Git", verdict["needsAttention"])
self.assertIn("Salesforce CLI", verdict["needsAttention"]) # warn surfaced, not a blocker
def test_check_tools_persists_the_full_report_for_the_paint_hook(self):
# The readiness-paint PostToolUse hook renders the banner from this file — a
# PostToolUse payload carries only the executed command, never the scan's
# stdout, so the report must be persisted for the hook to read back. It is
# the same object the scan prints to stdout.
patches = self._mock_all_checks()
for p in patches:
p.start()
try:
report = self._run_check_tools()
finally:
for p in patches:
p.stop()
self.assertEqual(self._read_report(), report)
self.assertIn("tools", self._read_report())
def test_sf_cli_ok_when_cmd_shim_resolves(self):
version_out = "@salesforce/cli/2.100.0 win32-x64 node-v20.0.0"
with mock.patch.object(sfx, "resolve_executable", return_value=r"C:\tools\sf.cmd"), \
mock.patch.dict(sfx.os.environ, {"COMSPEC": "cmd.exe"}, clear=False), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=version_out, returncode=0)):
result = sfx._check_sf_cli()
self.assertEqual(result["status"], "ok")
self.assertEqual(result["version"], "2.100.0")
def test_sf_cli_warns_when_update_available(self):
# Readiness = latest. The cached oclif notice (on `sf version` stderr)
# reports a newer release, so an installed-but-outdated CLI is 🟡, not 🟢.
version_out = "@salesforce/cli/2.130.9 darwin-arm64 node-v22.0.0"
stderr_out = " Warning: @salesforce/cli update available from 2.130.9 to 2.144.6."
env = {k: v for k, v in sfx.os.environ.items() if k != sfx._UPDATE_CHECK_ENV}
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.dict(sfx.os.environ, env, clear=True), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=version_out, stderr=stderr_out, returncode=0)):
result = sfx._check_sf_cli()
self.assertEqual(result["status"], "warn")
self.assertEqual(result["version"], "2.130.9")
self.assertIn("2.144.6", result["message"])
def test_sf_cli_ok_when_up_to_date(self):
# No update notice on stderr → the CLI is current → 🟢.
version_out = "@salesforce/cli/2.144.6 darwin-arm64 node-v22.0.0"
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=version_out, stderr="", returncode=0)):
result = sfx._check_sf_cli()
self.assertEqual(result["status"], "ok")
self.assertEqual(result["version"], "2.144.6")
def test_sf_cli_update_check_opt_out_stays_ok(self):
# SFDX_SKIP_CLI_UPDATE_CHECK=1 disables the readiness warn even when an
# update notice is present — a user who opted out never sees the 🟡.
version_out = "@salesforce/cli/2.130.9 darwin-arm64 node-v22.0.0"
stderr_out = " Warning: @salesforce/cli update available from 2.130.9 to 2.144.6."
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"), \
mock.patch.dict(sfx.os.environ, {sfx._UPDATE_CHECK_ENV: "1"}, clear=False), \
mock.patch.object(sfx.subprocess, "run",
return_value=_completed(stdout=version_out, stderr=stderr_out, returncode=0)):
result = sfx._check_sf_cli()
self.assertEqual(result["status"], "ok")
def test_missing_sf_cli_reported_critical_not_silently_empty(self):
# A genuinely-missing tool: resolver finds nothing, run() returns "".
# WIN-027: this must be reported FAILED, never silently empty or green.
with mock.patch.object(sfx, "resolve_executable", return_value=None):
result = sfx._check_sf_cli()
self.assertEqual(result["status"], "critical")
self.assertNotEqual(result["status"], "ok")
self.assertIn("Not found", result["message"])
def test_missing_npm_reported_critical(self):
with mock.patch.object(sfx, "resolve_executable", return_value=None):
result = sfx._check_npm()
self.assertEqual(result["status"], "critical")
def test_mcp_check_keeps_three_concerns_distinct(self):
# WIN-027: config presence, per-server platform-MCP health (WIN-033/040),
# and process health are separate rows; process health is NEVER inferred
# green from config.
with mock.patch.object(sfx, "_probe_server",
side_effect=lambda slug, timeout=None: {
"name": sfx._mcp_row_name(slug), "status": "warn",
"version": None, "message": "stubbed"}):
rows = sfx._check_mcp()
names = [r["name"] for r in rows]
self.assertIn("Salesforce MCP (config)", names)
self.assertIn("Salesforce MCP (salesforce-api-context)", names)
self.assertIn("Salesforce MCP (metadata-experts)", names)
self.assertIn("Salesforce MCP (process)", names)
process_row = next(r for r in rows if r["name"] == "Salesforce MCP (process)")
# Process health is informational (not a warning), so a healthy setup can
# read fully green — but it is never inferred "ok" from config/endpoint.
self.assertEqual(process_row["status"], "info")
self.assertNotEqual(process_row["status"], "ok")
def test_code_analyzer_installed_reports_ok(self):
# Physically installed → `sf plugins inspect` returns a real version.
inspect_out = json.dumps([{"name": "@salesforce/plugin-code-analyzer", "version": "5.11.1"}])
def fake_run(argv, **_):
if "inspect" in argv:
return inspect_out
return ""
with mock.patch.object(sfx, "run", side_effect=fake_run):
result = sfx._check_code_analyzer()
self.assertEqual(result["status"], "ok")
self.assertEqual(result["version"], "5.11.1")
self.assertEqual(result["message"], "Installed")
def test_code_analyzer_jit_registered_reports_ok_not_critical(self):
# NOT physically installed → `inspect` fails (JIT plugins yield {"error": {}}
# and exit 1, so run() returns ""). But the CLI registers it as a JIT plugin,
# so it auto-installs on first use — this is AVAILABLE, never critical.
plugins_out = json.dumps([
{"name": "@salesforce/plugin-org", "version": "3.0.0"},
{
"name": "@salesforce/cli",
"options": {"isRoot": True},
"pjson": {"oclif": {"jitPlugins": {"@salesforce/plugin-code-analyzer": "5.11.1"}}},
},
])
def fake_run(argv, **_):
if "inspect" in argv:
return "" # JIT plugin not yet installed
if "--json" in argv:
return plugins_out
return ""
with mock.patch.object(sfx, "run", side_effect=fake_run):
result = sfx._check_code_analyzer()
self.assertEqual(result["status"], "ok")
self.assertNotEqual(result["status"], "critical")
self.assertEqual(result["version"], "5.11.1")
self.assertIn("JIT", result["message"])
def test_code_analyzer_genuinely_absent_reports_critical(self):
# inspect fails AND the plugin is not in the CLI's jitPlugins registry →
# genuinely missing, so report critical with the install hint.
plugins_out = json.dumps([
{"name": "@salesforce/cli", "options": {"isRoot": True},
"pjson": {"oclif": {"jitPlugins": {"@salesforce/plugin-signups": "2.0.0"}}}},
])
def fake_run(argv, **_):
if "inspect" in argv:
return ""
if "--json" in argv:
return plugins_out
return ""
with mock.patch.object(sfx, "run", side_effect=fake_run):
result = sfx._check_code_analyzer()
self.assertEqual(result["status"], "critical")
self.assertIsNone(result["version"])
self.assertIn("plugins install", result["message"])
def test_check_tools_attaches_diagnostic_on_failure(self):
# All native tools missing → several critical rows → a diagnostic block is
# attached so the failure is understandable (and not flipped green).
with mock.patch.object(sfx, "resolve_executable", return_value=None), \
mock.patch.object(sfx, "get_target_org", return_value=""):
buf = io.StringIO()
with redirect_stdout(buf):
sfx.cmd_check_tools()
report = json.loads(buf.getvalue())
self.assertIn("tools", report)
self.assertTrue(any(t["status"] == "critical" for t in report["tools"]))
self.assertIn("diagnostic", report)
diag = report["diagnostic"]
for key in ("platform", "shell", "cwd", "pluginRoot", "resolvedExecutables"):
self.assertIn(key, diag)
class ReadinessStateTests(unittest.TestCase):
"""The cached readiness verdict mirrors the CLI-update state: cwd-relative
.sf/ JSON, fail-open read, signature-gated freshness. The load-bearing rule is
the honesty invariant — an absent or corrupt verdict, or a not-ready one, is
NEVER treated as a pass."""
def setUp(self):
self._prev_cwd = os.getcwd()
self._tmp = tempfile.TemporaryDirectory()
os.chdir(self._tmp.name)
def tearDown(self):
os.chdir(self._prev_cwd)
self._tmp.cleanup()
def _readiness_files(self):
directory = Path(".sf")
return sorted(path.name for path in directory.iterdir()) if directory.exists() else []
def test_record_and_load_roundtrip(self):
self.assertTrue(sfx._record_readiness_verdict(True, [], "sig-1"))
state = sfx._load_readiness_state()
self.assertTrue(state["ready"])
self.assertEqual(state["signature"], "sig-1")
self.assertEqual(state["needsAttention"], [])
self.assertIn("checkedAt", state)
self.assertEqual(self._readiness_files(), ["environment-readiness.json"])
def test_blockers_default_to_needs_attention_when_omitted(self):
# Back-compat: a caller that doesn't distinguish severities (no blockers arg)
# gets blockers == needsAttention, so the gate still names those.
sfx._record_readiness_verdict(False, ["Git"], "sig-1")
self.assertEqual(sfx._load_readiness_state()["blockers"], ["Git"])
def test_blockers_recorded_distinct_from_needs_attention(self):
# A warn-only verdict: not-green (needsAttention) yet no blockers, so ready
# can honestly be True — this is the shape a warn-only scan writes.
sfx._record_readiness_verdict(True, ["Node.js"], "sig-1", blockers=[])
state = sfx._load_readiness_state()
self.assertTrue(state["ready"])
self.assertEqual(state["needsAttention"], ["Node.js"])
self.assertEqual(state["blockers"], [])
def test_is_fresh_requires_a_pass_and_matching_signature(self):
sfx._record_readiness_verdict(True, [], "sig-1")
self.assertTrue(sfx._readiness_is_fresh("sig-1"))
# Toolchain changed since the scan → the cached green no longer applies.
self.assertFalse(sfx._readiness_is_fresh("sig-2"))
def test_not_ready_verdict_is_never_fresh(self):
sfx._record_readiness_verdict(False, ["Git"], "sig-1")
self.assertFalse(sfx._readiness_is_fresh("sig-1"))
def test_absent_verdict_reads_empty_and_is_never_a_pass(self):
# No file written yet → unchecked → honest {} → never fresh (never green).
self.assertEqual(sfx._load_readiness_state(), {})
self.assertFalse(sfx._readiness_is_fresh("anything"))
def test_corrupt_verdict_reads_empty(self):
Path(".sf").mkdir(parents=True, exist_ok=True)
(Path(".sf") / "environment-readiness.json").write_text("{ not json")
self.assertEqual(sfx._load_readiness_state(), {})
self.assertFalse(sfx._readiness_is_fresh("anything"))
def test_oversized_verdict_reads_empty(self):
Path(".sf").mkdir(parents=True, exist_ok=True)
padding = "x" * sfx._READINESS_JSON_MAX_BYTES
(Path(".sf") / "environment-readiness.json").write_text(
json.dumps({"ready": True, "signature": "sig-1", "padding": padding})
)
self.assertEqual(sfx._load_readiness_state(), {})
self.assertFalse(sfx._readiness_is_fresh("sig-1"))
def test_unreadable_verdict_reads_empty(self):
with mock.patch.object(sfx.os, "open", side_effect=OSError("unreadable")) as opened:
self.assertEqual(sfx._load_readiness_state(), {})
opened.assert_called_once()
def test_reader_rejects_fifo_mode_without_reading_and_uses_safe_flags(self):
binary_flag = 1 << 29
fifo_stat = type("FifoStat", (), {"st_mode": stat.S_IFIFO, "st_size": 0})()
with mock.patch.object(sfx.os, "O_BINARY", binary_flag, create=True), \
mock.patch.object(sfx.os, "open", return_value=71) as opened, \
mock.patch.object(sfx.os, "fstat", return_value=fifo_stat), \
mock.patch.object(sfx.os, "read") as read, \
mock.patch.object(sfx.os, "close") as close:
self.assertEqual(sfx._load_bounded_small_json(Path("readiness-fifo")), {})
flags = opened.call_args.args[1]
for name in ("O_NOFOLLOW", "O_NONBLOCK", "O_CLOEXEC", "O_BINARY"):
flag = getattr(sfx.os, name, 0)
if flag:
self.assertTrue(flags & flag, name)
read.assert_not_called()
close.assert_called_once_with(71)
def test_reader_does_not_follow_symlink_when_no_follow_is_supported(self):
if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "symlink"):
self.skipTest("no-follow symlink opens are not supported")
target = Path("readiness-target.json")
target.write_text(json.dumps({"ready": True}))
link = Path("readiness-link.json")
try:
link.symlink_to(target.name)
except OSError as error:
self.skipTest(f"symlink creation is unavailable: {error}")
self.assertEqual(sfx._load_bounded_small_json(link), {})
self.assertEqual(json.loads(target.read_text()), {"ready": True})
def test_symlinked_readiness_parent_fails_open_and_writes_fail_silent(self):
if not hasattr(os, "symlink"):
self.skipTest("symlinks are not supported")
outside = Path(self._tmp.name).parent / f"{Path(self._tmp.name).name}-outside-sf"
outside.mkdir()
target = outside / "environment-readiness-report.json"
original = json.dumps({"tools": [{"name": "outside"}]}).encode()
target.write_bytes(original)
try:
Path(".sf").symlink_to(outside, target_is_directory=True)
except OSError as error:
target.unlink()
outside.rmdir()
self.skipTest(f"directory symlink creation is unavailable: {error}")
try:
self.assertEqual(sfx._load_readiness_report(), {})
self.assertFalse(sfx._record_readiness_report({"tools": [{"name": "inside"}]}))
# Simulate Python's native-Windows no-dir-fd path: identity and reparse
# checks must reject the same hostile parent without touching its target.
with mock.patch.object(sfx, "_PHASE_DIR_FD_SUPPORTED", False):
self.assertEqual(sfx._load_readiness_report(), {})
self.assertFalse(sfx._record_readiness_report({"tools": [{"name": "fallback"}]}))
self.assertEqual(target.read_bytes(), original)
self.assertEqual(sorted(path.name for path in outside.iterdir()), [target.name])
finally:
Path(".sf").unlink()
target.unlink()
outside.rmdir()
def test_valid_non_object_json_roots_read_empty(self):
Path(".sf").mkdir(parents=True, exist_ok=True)
path = Path(".sf") / "environment-readiness.json"
for value in ([{"ready": True}], "ready", 1, True, None):
with self.subTest(value=value):
path.write_text(json.dumps(value))
self.assertEqual(sfx._load_readiness_state(), {})
def test_deeply_nested_json_recursion_reads_empty(self):
Path(".sf").mkdir(parents=True, exist_ok=True)
path = Path(".sf") / "environment-readiness.json"
path.write_text("[" * 10000 + "{}" + "]" * 10000)
self.assertEqual(sfx._load_readiness_state(), {})
def test_report_record_and_load_roundtrip(self):
# The FULL report is persisted next to the coarse verdict so the readiness-
# paint hook can render the banner from it (the hook payload carries only the
# command, never the scan's stdout). Same cwd-relative .sf/, fail-open read.
report = {"tools": [{"name": "Git", "status": "ok", "version": "git version 2.50.1"}]}
self.assertTrue(sfx._record_readiness_report(report))
self.assertEqual(sfx._load_readiness_report(), report)
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_atomic_replace_exposes_only_complete_old_or_new_report(self):
old = {"tools": [{"name": "Git", "status": "warn", "message": "old"}]}
new = {"tools": [{"name": "Git", "status": "ok", "message": "new"}]}
self.assertTrue(sfx._record_readiness_report(old))
path = Path(".sf") / "environment-readiness-report.json"
prior = path.read_bytes()
real_replace = os.replace
observations = []
def observe_replace(source, destination, **kwargs):
source_path = path.parent / source if kwargs.get("src_dir_fd") is not None else Path(source)
observations.append((path.read_bytes(), source_path.read_bytes()))
real_replace(source, destination, **kwargs)
with mock.patch.object(sfx.os, "replace", side_effect=observe_replace):
self.assertTrue(sfx._record_readiness_report(new))
self.assertEqual(observations[0][0], prior)
self.assertEqual(json.loads(observations[0][1]), new)
self.assertEqual(sfx._load_readiness_report(), new)
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_readiness_temp_is_exclusive_owner_only_and_cleaned_after_success(self):
report = {"tools": [{"name": "Git", "status": "ok"}]}
real_open = os.open
real_replace = os.replace
temp_modes = []
def observe_replace(source, destination, **kwargs):
stat_kwargs = ({"dir_fd": kwargs["src_dir_fd"]}
if kwargs.get("src_dir_fd") is not None else {})
temp_modes.append(os.stat(source, **stat_kwargs).st_mode & 0o777)
real_replace(source, destination, **kwargs)
with mock.patch.object(sfx.os, "open", wraps=real_open) as opened, \
mock.patch.object(sfx.os, "replace", side_effect=observe_replace):
self.assertTrue(sfx._record_readiness_report(report))
temp_open = next(
call for call in opened.call_args_list
if Path(call.args[0]).name.startswith(".environment-readiness-report.json.")
)
self.assertTrue(temp_open.args[1] & os.O_EXCL)
self.assertEqual(temp_open.args[2], 0o600)
self.assertEqual(temp_modes, [0o600])
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_writer_uses_binary_flag_when_available(self):
binary_flag = 1 << 29
directory = types.SimpleNamespace(fd=None)
with mock.patch.object(sfx.os, "O_BINARY", binary_flag, create=True), \
mock.patch.object(sfx, "_open_phase_directory", return_value=directory), \
mock.patch.object(
sfx, "_open_phase_child", side_effect=OSError("stop")
) as opened:
self.assertFalse(sfx._record_readiness_report({"tools": []}))
self.assertTrue(opened.call_args.args[2] & binary_flag)
def test_temp_file_collision_is_preserved_and_destination_unchanged(self):
old = {"tools": [{"name": "Git", "status": "warn", "message": "old"}]}
self.assertTrue(sfx._record_readiness_report(old))
destination = Path(".sf") / "environment-readiness-report.json"
prior = destination.read_bytes()
collision = destination.with_name(f".{destination.name}.collision.tmp")
collision_bytes = b"owned by another writer"
collision.write_bytes(collision_bytes)
with mock.patch.object(sfx.secrets, "token_hex", return_value="collision"), \
mock.patch.object(sfx.os, "replace") as replace:
self.assertFalse(sfx._record_readiness_report({"tools": [{"name": "Node.js"}]}))
replace.assert_not_called()
self.assertEqual(destination.read_bytes(), prior)
self.assertEqual(collision.read_bytes(), collision_bytes)
def test_temp_directory_collision_is_not_removed(self):
destination = Path(".sf") / "environment-readiness-report.json"
destination.parent.mkdir(parents=True)
collision = destination.with_name(f".{destination.name}.collision.tmp")
collision.mkdir()
with mock.patch.object(sfx.secrets, "token_hex", return_value="collision"):
self.assertFalse(sfx._record_readiness_report({"tools": []}))
self.assertTrue(collision.is_dir())
self.assertFalse(destination.exists())
def test_temp_symlink_collision_is_not_removed_or_followed(self):
if not hasattr(os, "symlink"):
self.skipTest("symlinks are not supported")
destination = Path(".sf") / "environment-readiness-report.json"
destination.parent.mkdir(parents=True)
target = destination.parent / "collision-target"
target_bytes = b"must remain untouched"
target.write_bytes(target_bytes)
collision = destination.with_name(f".{destination.name}.collision.tmp")
try:
collision.symlink_to(target.name)
except OSError as error:
self.skipTest(f"symlink creation is unavailable: {error}")
with mock.patch.object(sfx.secrets, "token_hex", return_value="collision"):
self.assertFalse(sfx._record_readiness_report({"tools": []}))
self.assertTrue(collision.is_symlink())
self.assertEqual(target.read_bytes(), target_bytes)
self.assertFalse(destination.exists())
def test_partial_writes_are_completed_before_replace(self):
report = {"tools": [{"name": "Git", "status": "ok", "message": "complete"}]}
real_write = os.write
writes = []
def write_small_chunk(fd, value):
chunk = bytes(value[:min(7, len(value))])
writes.append(chunk)
return real_write(fd, chunk)
with mock.patch.object(sfx.os, "write", side_effect=write_small_chunk):
self.assertTrue(sfx._record_readiness_report(report))
self.assertGreater(len(writes), 1)
self.assertEqual(sfx._load_readiness_report(), report)
def test_failed_short_write_preserves_prior_report_and_cleans_temp(self):
old = {"tools": [{"name": "Git", "status": "warn", "message": "old"}]}
self.assertTrue(sfx._record_readiness_report(old))
path = Path(".sf") / "environment-readiness-report.json"
prior = path.read_bytes()
real_write = os.write
first_write = True
def short_then_stop(fd, value):
nonlocal first_write
if first_write:
first_write = False
chunk = bytes(value[:max(1, len(value) // 2)])
return real_write(fd, chunk)
return 0
with mock.patch.object(sfx.os, "write", side_effect=short_then_stop):
self.assertFalse(sfx._record_readiness_report({"tools": [{"name": "Node.js"}]}))
self.assertEqual(path.read_bytes(), prior)
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_failed_fsync_preserves_prior_report_and_cleans_temp(self):
old = {"tools": [{"name": "Git", "status": "warn", "message": "old"}]}
self.assertTrue(sfx._record_readiness_report(old))
path = Path(".sf") / "environment-readiness-report.json"
prior = path.read_bytes()
with mock.patch.object(sfx.os, "fsync", side_effect=OSError("fsync failed")):
self.assertFalse(sfx._record_readiness_report({"tools": [{"name": "Node.js"}]}))
self.assertEqual(path.read_bytes(), prior)
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_failed_replace_preserves_prior_report_and_cleans_temp(self):
old = {"tools": [{"name": "Git", "status": "warn", "message": "old"}]}
self.assertTrue(sfx._record_readiness_report(old))
path = Path(".sf") / "environment-readiness-report.json"
prior = path.read_bytes()
with mock.patch.object(sfx.os, "replace", side_effect=OSError("replace failed")):
self.assertFalse(sfx._record_readiness_report({"tools": [{"name": "Node.js"}]}))
self.assertEqual(path.read_bytes(), prior)
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_directory_sync_failure_is_reported_and_temp_is_cleaned(self):
with mock.patch.object(sfx, "_sync_phase_directory", return_value=False) as sync:
self.assertFalse(sfx._record_readiness_report({"tools": []}))
sync.assert_called_once()
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_recursive_report_is_rejected_before_filesystem_mutation(self):
recursive = {"tools": []}
cursor = recursive
for _ in range(10000):
child = {}
cursor["child"] = child
cursor = child
with mock.patch.object(sfx.os, "open", wraps=os.open) as opened:
self.assertFalse(sfx._record_readiness_report(recursive))
opened.assert_not_called()
self.assertFalse(Path(".sf").exists())
def test_oversized_report_write_is_rejected_before_mutation(self):
old = {"tools": [{"name": "Git", "status": "ok"}]}
self.assertTrue(sfx._record_readiness_report(old))
path = Path(".sf") / "environment-readiness-report.json"
prior = path.read_bytes()
oversized = {"tools": [], "padding": "x" * sfx._READINESS_JSON_MAX_BYTES}
with mock.patch.object(sfx.os, "open", wraps=os.open) as opened:
self.assertFalse(sfx._record_readiness_report(oversized))
self.assertEqual(path.read_bytes(), prior)
opened.assert_not_called()
self.assertEqual(self._readiness_files(), ["environment-readiness-report.json"])
def test_absent_report_reads_empty(self):
# No scan has run yet → honest {} (the paint hook then stays silent).
self.assertEqual(sfx._load_readiness_report(), {})
def test_corrupt_report_reads_empty(self):
Path(".sf").mkdir(parents=True, exist_ok=True)
(Path(".sf") / "environment-readiness-report.json").write_text("{ not json")
self.assertEqual(sfx._load_readiness_report(), {})
def test_oversized_report_reads_empty(self):
Path(".sf").mkdir(parents=True, exist_ok=True)
padding = "x" * sfx._READINESS_JSON_MAX_BYTES
(Path(".sf") / "environment-readiness-report.json").write_text(
json.dumps({"tools": [], "padding": padding})
)
self.assertEqual(sfx._load_readiness_report(), {})
def test_unreadable_report_reads_empty(self):
with mock.patch.object(sfx.os, "open", side_effect=OSError("unreadable")) as opened:
self.assertEqual(sfx._load_readiness_report(), {})
opened.assert_called_once()
class WelcomeReadinessTests(unittest.TestCase):
"""`_welcome_readiness` is the cheap 3-way signal the front-of-journey surfaces
read: it resolves `sf` on PATH and consults a SESSION-SCOPED env-verified marker,
with NO subprocess. "ready" is earned only by a check-tools pass THIS session
(recorded by the readiness-paint hook), so a new session re-verifies — readiness
is a current property, never trusted from a durable cross-session cache."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self._orig_marker_dir = sfx._WELCOME_MARKER_DIR
self._orig_sid = sfx._CURRENT_SESSION_ID
sfx._WELCOME_MARKER_DIR = Path(self._tmp.name)
sfx._CURRENT_SESSION_ID = "sess-1"
def tearDown(self):
sfx._WELCOME_MARKER_DIR = self._orig_marker_dir
sfx._CURRENT_SESSION_ID = self._orig_sid
self._tmp.cleanup()
def test_absent_when_sf_not_on_path(self):
with mock.patch.object(sfx, "resolve_executable", return_value=None):
self.assertEqual(sfx._welcome_readiness(), "absent")
def test_unverified_when_present_but_not_checked_this_session(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
# No env-verified marker recorded for this session yet.
self.assertEqual(sfx._welcome_readiness(), "unverified")
def test_ready_when_checked_this_session(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
sfx._record_env_verified("sess-1")
self.assertEqual(sfx._welcome_readiness(), "ready")
def test_marker_from_another_session_does_not_carry_over(self):
# Readiness is session-scoped: a pass recorded under a DIFFERENT session id
# never counts for this one, so a fresh session honestly re-verifies.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
sfx._record_env_verified("sess-OTHER")
self.assertEqual(sfx._welcome_readiness(), "unverified")
def test_absent_wins_even_with_a_marker(self):
# `sf` off PATH is definitively not-ready, whatever any marker says.
sfx._record_env_verified("sess-1")
with mock.patch.object(sfx, "resolve_executable", return_value=None):
self.assertEqual(sfx._welcome_readiness(), "absent")
def test_no_session_id_reads_unverified(self):
# The non-hook Bash-subcommand path carries no session id; with nothing to
# key a marker on, readiness reads conservatively as unverified (never ready).
sfx._CURRENT_SESSION_ID = ""
sfx._record_env_verified("sess-1") # a marker exists, but not for ""
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
self.assertEqual(sfx._welcome_readiness(), "unverified")
class HasAuthedOrgTests(unittest.TestCase):
"""`_has_authed_org` is the cheap, subprocess-free "has the user ever authed an
org" signal. It is auth HISTORY — deliberately DISTINCT from `_has_target_org`
(the current-target signal that lights the Connect stage); this one only tunes
the Connect CTA copy (a returning developer with orgs authed is invited to pick
one as the target; a first-timer to authenticate one). It lists the global auth
store (~/.sfdx) — a per-USER, cwd-independent fact — and counts a *.json off the
non-auth denylist ONLY when its content carries a stored credential. A tokenless
cache the CLI co-locates there (notably the org-id-keyed *.sandbox.json sandbox-
process record, which survives `sf org logout --all`) must NOT read as an org, or
the returning-developer CTA would show falsely. Home is patched to a temp dir so
the real store is never read (determinism on any machine / CI)."""
# A credential-bearing auth file (the OAuth shape); any of _AUTH_CREDENTIAL_KEYS
# would do — this mirrors what `sf org login web` persists.
AUTH_CONTENT = {"accessToken": "00Dxx!redacted", "refreshToken": "5Aep!redacted",
"orgId": "00Dxx0000001gPFEAY", "instanceUrl": "https://x.my.salesforce.com"}
# The exact key set sf writes into the tokenless *.sandbox.json process cache —
# note `username` is present (so a "has a username" heuristic would false-positive)
# but NONE of _AUTH_CREDENTIAL_KEYS is.
SANDBOX_CACHE = {"prodOrgUsername": "admin@acme.com", "sandboxInfoId": "0GRxx",
"sandboxName": "mySandbox", "sandboxOrgId": "00Dxx", "sandboxProcessId": "0GQxx",
"sandboxUsername": "admin@acme.com.mysandbox", "timestamp": "2026-07-27T00:00:00Z",
"username": "admin@acme.com.mysandbox"}
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.home = Path(self._tmp.name)
self.sfdx = self.home / ".sfdx"
self._home_patch = mock.patch.object(sfx.Path, "home", return_value=self.home)
self._home_patch.start()
def tearDown(self):
self._home_patch.stop()
self._tmp.cleanup()
def _write(self, name, *, as_dir=False, content=None):
"""Create ~/.sfdx/<name>. Files default to a credential-bearing auth body so a
plain _write() is a real authentication; pass content={...} for a tokenless
cache or content='...' for raw (non-JSON) bytes."""
target = self.sfdx / name
self.sfdx.mkdir(parents=True, exist_ok=True)
if as_dir:
target.mkdir()
return
if content is None:
content = self.AUTH_CONTENT
body = json.dumps(content) if isinstance(content, (dict, list)) else str(content)
target.write_text(body, encoding="utf-8")
def test_true_when_a_username_keyed_auth_file_present(self):
self._write("jdoe@acme.example.com.json")
self.assertTrue(sfx._has_authed_org())
def test_true_for_org_id_and_scratch_keyed_auth(self):
# Auth files are keyed by username, org-id, or scratch-org id — the KEY shape is
# irrelevant; a credential in the body is what counts, so a new key shape is
# still a connection. Presence is monotonic, so each shape in turn stays True.
for key in ("00Dxx0000001gPFEAY.json", "test-abc123@example.com.json"):
with self.subTest(key=key):
self._write(key)
self.assertTrue(sfx._has_authed_org())
def test_true_for_jwt_and_password_only_credentials(self):
# JWT persists a private key (no refresh token); username-password / scratch
# orgs persist a password. Either alone is a real, durable authentication.
for content in ({"privateKey": "-----BEGIN-redacted", "username": "svc@acme.com"},
{"password": "!redacted", "username": "test@scratch.com"}):
with self.subTest(cred=sorted(content)[0]):
self.sfdx.mkdir(parents=True, exist_ok=True)
for stale in self.sfdx.glob("*.json"):
stale.unlink()
self._write("cred.json", content=content)
self.assertTrue(sfx._has_authed_org())
def test_false_for_tokenless_sandbox_process_cache(self):
# THE N4 regression: an org-id-keyed *.sandbox.json is off the denylist and
# is_file()==True, but it carries no credential, so it must not light Connect.
# This is the state left behind by `sf org create sandbox` + `sf org logout`.
self._write("00DXK0000011cVh2AI.sandbox.json", content=self.SANDBOX_CACHE)
self.assertFalse(sfx._has_authed_org())
def test_false_for_credential_less_json(self):
# A *.json off the denylist that carries no credential (e.g. a stray metadata
# blob) is not an authentication — content, not filename, is the gate.
self._write("orphan.json", content={"orgId": "00Dxx", "username": "a@b.c"})
self.assertFalse(sfx._has_authed_org())
def test_sandbox_cache_alongside_a_real_auth_returns_true(self):
# The real auth file still wins — the tokenless cache neither adds nor masks.
self._write("00DXK0000011cVh2AI.sandbox.json", content=self.SANDBOX_CACHE)
self._write("jdoe@acme.example.com.json")
self.assertTrue(sfx._has_authed_org())
def test_false_when_only_non_auth_files_present(self):
# The bookkeeping files sf drops next to auth entries must NOT read as an org.
for name in sfx._NON_AUTH_SFDX_FILES:
self._write(name)
self.assertFalse(sfx._has_authed_org())
def test_mixed_auth_and_non_auth_returns_true(self):
for name in sfx._NON_AUTH_SFDX_FILES:
self._write(name)
self._write("jdoe@acme.example.com.json")
self.assertTrue(sfx._has_authed_org())
def test_false_when_sfdx_dir_absent(self):
# No ~/.sfdx at all → iterdir raises → fails soft to False, never raises.
self.assertFalse(self.sfdx.exists())
self.assertFalse(sfx._has_authed_org())
def test_false_when_sfdx_dir_empty(self):
self.sfdx.mkdir(parents=True)
self.assertFalse(sfx._has_authed_org())
def test_corrupt_or_oversized_json_fails_soft_to_false(self):
# An unreadable / non-JSON *.json off the denylist must be skipped, never raise.
self._write("broken.json", content="{not: valid json")
self.assertFalse(sfx._has_authed_org())
def test_non_json_files_and_json_subdirectories_do_not_count(self):
# A .json-suffixed *directory* (is_file() False) and a non-json file must both
# be ignored — only regular *.json auth entries light Connect.
self._write("notes.txt")
self._write("scratch-orgs.json", as_dir=True)
self.assertFalse(sfx._has_authed_org())
class HasTargetOrgTests(unittest.TestCase):
"""`_has_target_org` is the CURRENT-target signal that lights the Connect stage —
"is an org set as the default/target right now", distinct from `_has_authed_org`'s
auth history. Subprocess-free: it reads the local project config first, then the
global user config, honoring the modern `sf` `target-org` key and the legacy sfdx
`defaultusername`. A configured-but-offline target still counts as set; a missing /
empty / corrupt config fails soft to False. Home AND the project root are temp
dirs so the real config is never read (determinism on any machine / CI)."""
def setUp(self):
self._home_tmp = tempfile.TemporaryDirectory()
self._root_tmp = tempfile.TemporaryDirectory()
self.home = Path(self._home_tmp.name)
self.root = Path(self._root_tmp.name)
self._home_patch = mock.patch.object(sfx.Path, "home", return_value=self.home)
self._home_patch.start()
def tearDown(self):
self._home_patch.stop()
self._home_tmp.cleanup()
self._root_tmp.cleanup()
def _write(self, base, rel, content):
path = base / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(content) if isinstance(content, dict) else str(content),
encoding="utf-8")
def test_false_when_no_config_anywhere(self):
self.assertFalse(sfx._has_target_org(self.root))
def test_true_from_local_sf_config(self):
self._write(self.root, ".sf/config.json", {"target-org": "acme-dev"})
self.assertTrue(sfx._has_target_org(self.root))
def test_true_from_global_sf_config(self):
self._write(self.home, ".sf/config.json", {"target-org": "acme-dev"})
self.assertTrue(sfx._has_target_org(self.root))
def test_true_from_legacy_sfdx_defaultusername(self):
# A project configured by older sfdx tooling still counts as having a target.
self._write(self.root, ".sfdx/sfdx-config.json", {"defaultusername": "a@b.c"})
self.assertTrue(sfx._has_target_org(self.root))
def test_false_when_config_present_but_no_target_key(self):
# An empty config, or one carrying only unrelated keys, is not a target.
self._write(self.root, ".sf/config.json", {})
self._write(self.home, ".sf/config.json", {"org-api-version": "60.0"})
self.assertFalse(sfx._has_target_org(self.root))
def test_false_when_target_value_is_empty(self):
# A present-but-empty target-org must not read as set.
self._write(self.root, ".sf/config.json", {"target-org": ""})
self.assertFalse(sfx._has_target_org(self.root))
def test_corrupt_config_fails_soft_to_false(self):
self._write(self.root, ".sf/config.json", "{ not json")
self.assertFalse(sfx._has_target_org(self.root))
def test_local_target_counts_even_when_global_is_empty(self):
# A configured-but-offline target is still "set" — reachability isn't tested
# here; the org band annotates that separately.
self._write(self.home, ".sf/config.json", {})
self._write(self.root, ".sf/config.json", {"target-org": "offline-org"})
self.assertTrue(sfx._has_target_org(self.root))
def test_configured_alias_returns_the_target_name(self):
# _has_target_org is a thin boolean over _configured_target_alias, which returns
# the NAME so the org band can show *which* org is targeted (not just that one is).
self._write(self.root, ".sf/config.json", {"target-org": "acme-dev"})
self.assertEqual(sfx._configured_target_alias(self.root), "acme-dev")
def test_configured_alias_is_none_when_nothing_is_set(self):
self.assertIsNone(sfx._configured_target_alias(self.root))
def test_configured_alias_prefers_local_over_global(self):
self._write(self.home, ".sf/config.json", {"target-org": "global-org"})
self._write(self.root, ".sf/config.json", {"target-org": "local-org"})
self.assertEqual(sfx._configured_target_alias(self.root), "local-org")
def test_configured_alias_ignores_empty_and_whitespace_values(self):
self._write(self.root, ".sf/config.json", {"target-org": " "})
self.assertIsNone(sfx._configured_target_alias(self.root))
class ToolchainSignatureTests(unittest.TestCase):
"""The freshness signature must be STABLE across shells: per-shell version-manager
shims (fnm, nvm, pyenv) resolve to different symlink paths per invocation but point
at the same real executable. Canonicalizing with realpath collapses them, so a
cached 'ready' verdict isn't spuriously invalidated between the scan and a later
welcome — while a genuine version change (a new realpath target) still invalidates."""
def test_signature_canonicalizes_symlinks_to_the_real_binary(self):
with tempfile.TemporaryDirectory() as d:
real = Path(d) / "sf-real"
real.write_text("#!/bin/sh\n")
# Two distinct shim paths that both point at the same real binary — the
# shape of per-shell version-manager churn.
shim_a = Path(d) / "shim-a"
shim_b = Path(d) / "shim-b"
os.symlink(real, shim_a)
os.symlink(real, shim_b)
with mock.patch.object(
sfx, "resolve_executable",
side_effect=lambda t: str(shim_a) if t == "sf" else None,
):
sig_a = sfx._toolchain_signature()
with mock.patch.object(
sfx, "resolve_executable",
side_effect=lambda t: str(shim_b) if t == "sf" else None,
):
sig_b = sfx._toolchain_signature()
# Different shims, same real binary → identical signature (the stability).
self.assertEqual(sig_a, sig_b)
self.assertIn(os.path.realpath(str(real)), sig_a) # keyed on the target
self.assertNotIn("shim-a", sig_a) # not on the volatile shim
def test_missing_tool_contributes_empty_segment_not_a_crash(self):
# resolve_executable → None for every tool must yield a stable all-empty
# signature (no realpath call on a falsy path), never an exception.
with mock.patch.object(sfx, "resolve_executable", return_value=None):
self.assertEqual(sfx._toolchain_signature(), "|||")
class ScaffoldGateTests(unittest.TestCase):
"""The PreToolUse backstop on `sf project generate` — the scaffold chokepoint of
the front-of-journey readiness floor. It NEVER runs the scan (PATH lookup + one
small verdict read only), self-gates on the command, and grades block/warn/allow
by how cheaply it can prove the environment broken. Fails OPEN on any error."""
def setUp(self):
self._prev_cwd = os.getcwd()
self._tmp = tempfile.TemporaryDirectory()
os.chdir(self._tmp.name)
def tearDown(self):
os.chdir(self._prev_cwd)
self._tmp.cleanup()
def run_gate(self, command):
payload = io.StringIO(json.dumps({"tool_input": {"command": command}}))
out = io.StringIO()
with mock.patch.object(sfx.sys, "stdin", payload), redirect_stdout(out):
code = sfx.cmd_scaffold_gate()
return code, json.loads(out.getvalue())
def _decision(self, result):
return result.get("hookSpecificOutput", {}).get("permissionDecision")
def test_non_scaffold_command_stays_silent_without_touching_path_or_verdict(self):
# Some Claude Code builds fire every Bash PreToolUse hook — the self-gate
# must let unrelated commands through without even resolving the CLI.
for cmd in ("cd /tmp && ls", "sf org list", "sf project deploy start -o x", ""):
with self.subTest(cmd=cmd):
with mock.patch.object(sfx, "resolve_executable") as rex, \
mock.patch.object(sfx, "_load_readiness_state") as lrs:
code, result = self.run_gate(cmd)
self.assertEqual((code, result), (0, {"continue": True}))
rex.assert_not_called()
lrs.assert_not_called()
def test_absent_cli_denies_with_remediation(self):
with mock.patch.object(sfx, "resolve_executable", return_value=None):
_, result = self.run_gate("sf project generate --name acme")
self.assertEqual(self._decision(result), "deny")
reason = result["hookSpecificOutput"]["permissionDecisionReason"]
self.assertIn("platform-environment-validate", reason)
self.assertRegex(reason, r"(?i)isn't on your path")
def test_ran_and_failed_verdict_for_this_toolchain_denies(self):
# A scan that RAN and FAILED under the CURRENT signature is known-broken →
# block, naming what needs attention.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
sfx._record_readiness_verdict(False, ["Git", "Node.js"], sfx._toolchain_signature())
_, result = self.run_gate("sf project generate --name acme")
self.assertEqual(self._decision(result), "deny")
reason = result["hookSpecificOutput"]["permissionDecisionReason"]
self.assertIn("Git", reason)
self.assertIn("Node.js", reason)
def test_fresh_pass_allows_silently(self):
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
sfx._record_readiness_verdict(True, [], sfx._toolchain_signature())
_, result = self.run_gate("sf project generate --name acme")
self.assertEqual(result, {"continue": True})
def test_warn_only_verdict_allows_silently(self):
# THE field regression: a scan that recorded warnings but no blockers is
# ready=True, so scaffolding passes through untouched. This is the non-LTS
# Node / indeterminate source-tracking case — advisory warns must never gate.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
sfx._record_readiness_verdict(True, ["Node.js", "Source Tracking"],
sfx._toolchain_signature(), blockers=[])
_, result = self.run_gate("sf project generate --name acme")
self.assertEqual(result, {"continue": True})
self.assertIsNone(self._decision(result))
def test_block_names_only_blockers_not_advisory_warnings(self):
# When a real blocker and an advisory warn coexist, the deny reason names the
# blocker (Git) and NOT the warn (Node.js) — a block never reads as though a
# warning were the thing standing in the way.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
sfx._record_readiness_verdict(False, ["Git", "Node.js"],
sfx._toolchain_signature(), blockers=["Git"])
_, result = self.run_gate("sf project generate --name acme")
self.assertEqual(self._decision(result), "deny")
reason = result["hookSpecificOutput"]["permissionDecisionReason"]
self.assertIn("Git", reason)
self.assertNotIn("Node.js", reason)
def test_unverified_allows_but_nudges_the_check(self):
# `sf` present, no verdict → can't prove broken → ALLOW, but the model note
# steers toward verifying first. Never a deny.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
_, result = self.run_gate("sf project generate --name acme")
self.assertTrue(result.get("continue"))
self.assertIsNone(self._decision(result))
note = result["hookSpecificOutput"]["additionalContext"]
self.assertIn("platform-environment-validate", note)
def test_stale_failed_verdict_does_not_block(self):
# A failure recorded under a DIFFERENT (since-changed) toolchain no longer
# describes this machine — we can't prove it's broken now, so warn, not block.
with mock.patch.object(sfx, "resolve_executable", return_value="/usr/local/bin/sf"):
sfx._record_readiness_verdict(False, ["Git"], "some-other-signature")
_, result = self.run_gate("sf project generate --name acme")
self.assertTrue(result.get("continue"))
self.assertIsNone(self._decision(result))
def test_crash_fails_open(self):
with mock.patch.object(sfx, "_read_hook_payload", side_effect=RuntimeError("boom")):
_, result = self.run_gate("sf project generate --name acme")
self.assertEqual(result, {"continue": True})
class McpHealthContractTests(unittest.TestCase):
"""WIN-033 (passive sidecar read) + WIN-040 (active --probe) — see
CONTRACT-mcp-health.md. The consumer owns the server-key -> slug-arg
mapping; sidecar filename AND --probe arg both use the SLUG ARG
("metadata-experts"), never the .mcp.json server key
("salesforce-metadata-experts")."""
def test_slug_mapping_uses_slug_arg_not_server_key(self):
self.assertEqual(sfx._MCP_SERVER_SLUGS["salesforce-api-context"], "salesforce-api-context")
self.assertEqual(sfx._MCP_SERVER_SLUGS["salesforce-metadata-experts"], "metadata-experts")
self.assertNotIn("salesforce-metadata-experts", sfx._MCP_SERVER_SLUGS.values())
def test_state_table_matches_contract(self):
self.assertEqual(sfx._render_mcp_state_row("s", "ok")["status"], "ok")
self.assertEqual(sfx._render_mcp_state_row("s", "inactive")["status"], "critical")
self.assertEqual(sfx._render_mcp_state_row("s", "auth")["status"], "warn")
self.assertEqual(sfx._render_mcp_state_row("s", "env-not-ready")["status"], "warn")
self.assertEqual(sfx._render_mcp_state_row("s", "unreachable")["status"], "warn")
def test_unknown_state_renders_neutral_warn_not_crash(self):
row = sfx._render_mcp_state_row("metadata-experts", "some-future-state")
self.assertEqual(row["status"], "warn")
self.assertIn("Unrecognized", row["message"])
def test_missing_state_renders_neutral_warn_not_crash(self):
row = sfx._render_mcp_state_row("metadata-experts", None)
self.assertEqual(row["status"], "warn")
def test_passive_row_absent_sidecar_is_neutral_not_invented(self):
with mock.patch.object(sfx, "_read_health_sidecar", return_value=None):
row = sfx._passive_mcp_row("metadata-experts")
self.assertEqual(row["status"], "info")
self.assertIn("not yet observed", row["message"].lower())
def test_passive_row_present_sidecar_renders_from_state(self):
with mock.patch.object(sfx, "_read_health_sidecar",
return_value={"slug": "metadata-experts", "state": "inactive",
"detail": "HTTP 404 Server definition not found"}):
row = sfx._passive_mcp_row("metadata-experts")
self.assertEqual(row["status"], "critical")
self.assertIn("not activated", row["message"])
def test_read_health_sidecar_reads_slug_named_file(self):
# The sidecar path MUST be keyed by the slug arg, not the server key.
with mock.patch.object(Path, "exists", return_value=True), \
mock.patch.object(Path, "read_text", return_value=json.dumps({"state": "ok"})) as read_text:
data = sfx._read_health_sidecar("metadata-experts")
self.assertEqual(data, {"state": "ok"})
# read_text was called on a Path ending in metadata-experts.json.
self.assertTrue(read_text.call_count >= 1)
def test_read_health_sidecar_bad_json_returns_none_not_crash(self):
with mock.patch.object(Path, "exists", return_value=True), \
mock.patch.object(Path, "read_text", return_value="{not valid json"):
self.assertIsNone(sfx._read_health_sidecar("metadata-experts"))
def test_read_health_sidecar_missing_file_returns_none(self):
with mock.patch.object(Path, "exists", return_value=False):
self.assertIsNone(sfx._read_health_sidecar("metadata-experts"))
def test_probe_server_parses_json_line_from_stdout(self):
probe_json = json.dumps({"slug": "metadata-experts", "state": "auth",
"detail": "401", "httpStatus": 401, "org": "my-alias"})
with mock.patch.object(Path, "exists", return_value=True), \
mock.patch.object(sfx, "run_result",
return_value=sfx.RunResult(True, probe_json, 0, "")):
row = sfx._probe_server("metadata-experts")
self.assertEqual(row["status"], "warn")
self.assertEqual(row["name"], "Salesforce MCP (metadata-experts)")
def test_probe_server_uses_slug_arg_in_shell_out(self):
captured = {}
def fake_run_result(cmd, timeout=None):
captured["cmd"] = cmd
return sfx.RunResult(True, json.dumps({"state": "ok"}), 0, "")
with mock.patch.object(Path, "exists", return_value=True), \
mock.patch.object(sfx, "run_result", side_effect=fake_run_result):
sfx._probe_server("metadata-experts")
self.assertIn("--probe", captured["cmd"])
self.assertEqual(captured["cmd"][-1], "metadata-experts")
self.assertNotIn("salesforce-metadata-experts", captured["cmd"])
def test_probe_server_nonzero_exit_renders_warn_not_crash(self):
with mock.patch.object(Path, "exists", return_value=True), \
mock.patch.object(sfx, "run_result",
return_value=sfx.RunResult(False, "", 1, "nonzero")):
row = sfx._probe_server("metadata-experts")
self.assertEqual(row["status"], "warn")
def test_probe_server_unparseable_stdout_renders_warn_not_crash(self):
with mock.patch.object(Path, "exists", return_value=True), \
mock.patch.object(sfx, "run_result",
return_value=sfx.RunResult(True, "not json", 0, "")):
row = sfx._probe_server("metadata-experts")
self.assertEqual(row["status"], "warn")
def test_probe_server_missing_proxy_bundle_renders_warn_not_crash(self):
with mock.patch.object(Path, "exists", return_value=False):
row = sfx._probe_server("metadata-experts")
self.assertEqual(row["status"], "warn")
# --- _passive_mcp_summary (WIN-033 /status banner) -------------------
# The banner summary is network-free (reads only the sidecars) and MUST
# surface the worst observed state so an inactive server is never hidden
# behind a healthy one.
def _fake_sidecars(self, by_slug, org=None):
"""Return a _read_health_sidecar stand-in keyed by slug arg. A value may
be a bare state string, or a (state, org) tuple to model the sidecar's
`org` field; `org=` sets a default org for bare-string entries."""
def _reader(slug):
entry = by_slug.get(slug)
if entry is None:
return None
if isinstance(entry, tuple):
state, entry_org = entry
else:
state, entry_org = entry, org
return {"slug": slug, "state": state, "org": entry_org}
return _reader
def test_summary_all_ok_reports_both_active(self):
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"})):
summary = sfx._passive_mcp_summary()
self.assertIn("active", summary.lower())
self.assertNotIn("not activated", summary.lower())
def test_summary_inactive_surfaces_not_activated_even_if_other_ok(self):
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "inactive"})):
summary = sfx._passive_mcp_summary()
self.assertIn("NOT activated", summary)
self.assertIn("metadata-experts", summary)
def test_summary_no_sidecars_is_not_yet_observed_not_invented(self):
with mock.patch.object(sfx, "_read_health_sidecar", return_value=None):
summary = sfx._passive_mcp_summary()
self.assertIn("not yet observed", summary.lower())
def test_summary_mixed_degraded_points_at_check_tools(self):
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "auth"})):
summary = sfx._passive_mcp_summary()
self.assertIn("degraded", summary.lower())
self.assertIn("check-tools", summary.lower())
# --- partial observation is PENDING, not an outage (review P1 #1) ---------
# One server ok, the other not yet observed (no sidecar), none bad: this is
# still connecting, so the summary must read as pending ("not yet observed"),
# never "degraded" — otherwise _mcp_indicator paints a false ✗ unavailable.
def test_summary_partial_ok_and_unobserved_is_pending_not_degraded(self):
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok"})): # metadata-experts absent
summary = sfx._passive_mcp_summary()
self.assertIn("not yet observed", summary.lower())
self.assertNotIn("degraded", summary.lower())
self.assertNotIn("active", summary.lower()) # not a full-green claim either
# And the banner icon derives to connecting, not unavailable.
icon, style = sfx._mcp_indicator(summary)
self.assertIn("connecting", icon)
self.assertNotIn("unavailable", icon)
# --- org-scoped observations (review P1 #2) -------------------------------
# A sidecar written against a DIFFERENT org must not be shown as healthy for
# the org the user is currently on.
def test_summary_ignores_sidecar_from_a_different_org(self):
# Both servers ok, but recorded against "orgA"; active org is "orgB".
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"}, org="orgA")):
summary = sfx._passive_mcp_summary(active_org="orgB")
# No usable observation for orgB -> neutral not-yet-observed, NOT active.
self.assertIn("not yet observed", summary.lower())
self.assertNotIn("active", summary.lower())
def test_summary_accepts_sidecar_matching_active_org(self):
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"}, org="orgA")):
summary = sfx._passive_mcp_summary(active_org="orgA")
self.assertIn("active", summary.lower())
def test_summary_no_active_org_does_not_filter(self):
# When the active org is unknown, fall back to state-only (no over-filter).
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"}, org="orgA")):
summary = sfx._passive_mcp_summary() # no active_org
self.assertIn("active", summary.lower())
def test_summary_accepts_sidecar_by_username_when_resolved_by_alias(self):
# review P2 #2: the producer stamps the configured USERNAME while the
# consumer resolves the SAME org by ALIAS. Passing both identifiers must
# accept the username-stamped sidecar (not reject it as a foreign org).
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"},
org="user@example.com")):
summary = sfx._passive_mcp_summary(
active_org=("myAlias", "user@example.com"))
self.assertIn("active", summary.lower())
self.assertNotIn("not yet observed", summary.lower())
def test_summary_still_rejects_truly_foreign_org_with_both_ids(self):
# The alias/username tolerance must not defeat the org filter: a sidecar
# from a genuinely different org is still rejected when neither the alias
# nor the username matches.
with mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"}, org="otherOrg")):
summary = sfx._passive_mcp_summary(
active_org=("myAlias", "user@example.com"))
self.assertIn("not yet observed", summary.lower())
self.assertNotIn("active", summary.lower())
# --- _live_mcp_summary (WIN-040 live-probe banner) ------------------------
# The live summary actively probes each server so the banner reflects REAL
# current reachability. A fresh probe is authoritative for this session's org
# and OVERRIDES a stale sidecar (the activate-then-still-inactive demo gap);
# a probe that cannot run falls back to that server's last-known sidecar.
def _fake_probes(self, by_slug):
"""Return a _probe_server_raw stand-in keyed by slug arg. A value may be a
bare state string (-> {slug, state, org}) or None (probe could not run)."""
def _probe(slug, timeout=None):
state = by_slug.get(slug)
if state is None:
return None
return {"slug": slug, "state": state, "org": "liveOrg"}
return _probe
def test_live_summary_probe_overrides_stale_inactive_sidecar(self):
# Sidecars say inactive (stale); live probe says ok -> summary is active.
with mock.patch.object(sfx, "_probe_server_raw",
side_effect=self._fake_probes(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"})), \
mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "inactive",
"metadata-experts": "inactive"})):
summary = sfx._live_mcp_summary(active_org="liveOrg")
self.assertIn("active", summary.lower())
self.assertNotIn("not activated", summary.lower())
def test_live_summary_probe_surfaces_inactive_over_stale_ok(self):
# The reverse: sidecar says ok (stale), live probe says inactive.
with mock.patch.object(sfx, "_probe_server_raw",
side_effect=self._fake_probes(
{"salesforce-api-context": "ok",
"metadata-experts": "inactive"})), \
mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"})):
summary = sfx._live_mcp_summary(active_org="liveOrg")
self.assertIn("NOT activated", summary)
self.assertIn("metadata-experts", summary)
def test_live_summary_falls_back_to_sidecar_when_probe_cannot_run(self):
# Both probes fail to run (None); the last-known org-filtered sidecars are
# used so a transient/offline failure degrades to the cached reading.
with mock.patch.object(sfx, "_probe_server_raw",
side_effect=self._fake_probes({})), \
mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"}, org="cachedOrg")):
summary = sfx._live_mcp_summary(active_org="cachedOrg")
self.assertIn("active", summary.lower())
def test_live_summary_probe_failure_plus_foreign_sidecar_is_pending(self):
# Probe can't run AND the only sidecar is from another org -> no usable
# observation -> neutral not-yet-observed, never a false green.
with mock.patch.object(sfx, "_probe_server_raw",
side_effect=self._fake_probes({})), \
mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"salesforce-api-context": "ok",
"metadata-experts": "ok"}, org="otherOrg")):
summary = sfx._live_mcp_summary(active_org="thisOrg")
self.assertIn("not yet observed", summary.lower())
self.assertNotIn("active", summary.lower())
# --- partial: one tracked server healthy, one down (user-requested glyph) --
# A half-working feature is neither a full outage nor healthy: the summary
# reads "partial", names the down server, and the banner glyph is ⚠ partial —
# distinct from both ✓ connected (all ok) and ✗ unavailable (all down).
def test_summary_one_ok_one_inactive_is_partial(self):
summary = sfx._summarize_mcp_states(
{"salesforce-api-context": "ok", "metadata-experts": "inactive"})
self.assertIn("partial", summary.lower())
self.assertIn("metadata-experts", summary) # names the down server
icon, style = sfx._mcp_indicator(summary)
self.assertIn("partial", icon)
self.assertNotIn("connected", icon) # not a false green
self.assertNotIn("unavailable", icon) # not a full outage either
def test_summary_one_ok_one_auth_is_partial(self):
summary = sfx._summarize_mcp_states(
{"salesforce-api-context": "ok", "metadata-experts": "auth"})
self.assertIn("partial", summary.lower())
self.assertIn("partial", sfx._mcp_indicator(summary)[0])
def test_summary_both_inactive_is_full_unavailable_not_partial(self):
summary = sfx._summarize_mcp_states(
{"salesforce-api-context": "inactive", "metadata-experts": "inactive"})
self.assertNotIn("partial", summary.lower())
self.assertIn("unavailable", sfx._mcp_indicator(summary)[0])
def test_partial_summary_icon_precedence_over_active_substring(self):
# The partial summary contains the word "active" ("others active"); the
# indicator MUST test "partial" first so it never paints a false ✓.
summary = ("sf-mcp-proxy: partial — metadata-experts NOT activated in this "
"org (others active) — enable in Setup (check-tools for detail)")
icon, _ = sfx._mcp_indicator(summary)
self.assertIn("partial", icon)
self.assertNotIn("connected", icon)
def test_live_summary_mixes_live_probe_with_sidecar_fallback(self):
# One server probes live (ok); the other's probe fails but its sidecar is
# a fresh inactive -> the inactive must still surface (worst-of).
def _one_probe(slug, timeout=None):
if slug == "salesforce-api-context":
return {"slug": slug, "state": "ok", "org": "liveOrg"}
return None # metadata-experts probe could not run
with mock.patch.object(sfx, "_probe_server_raw", side_effect=_one_probe), \
mock.patch.object(sfx, "_read_health_sidecar",
side_effect=self._fake_sidecars(
{"metadata-experts": "inactive"}, org="liveOrg")):
summary = sfx._live_mcp_summary(active_org="liveOrg")
self.assertIn("NOT activated", summary)
self.assertIn("metadata-experts", summary)
# --- banner icon derivation (WIN-033 /status org-box "MCP" field) -----
# render_banner_message() derives the compact ✓/⟳/✗ icon from the health
# summary string. It MUST understand the _passive_mcp_summary() vocabulary,
# not only the legacy "connected/connecting/bridged" strings — otherwise a
# healthy "... active" summary falls through to "✗ unavailable" and the icon
# contradicts the Note (regression caught in live dry-run against DEorgFRI).
def _banner_for(self, summary):
org = {"alias": "x", "edition": "e", "apiVersion": "62.0",
"instanceUrl": "u", "username": "n"}
proj = {"name": "P", "source_api": "62.0", "package_dirs": "force-app"}
stats = {k: 0 for k in ("apex_src", "apex_test", "triggers", "lwc",
"aura", "objects", "permsets", "flows")}
return sfx.render_banner_message(org, proj, stats, "", summary)
def test_banner_icon_active_summary_is_connected_no_note(self):
out = self._banner_for("sf-mcp-proxy: api-context, metadata-experts active")
self.assertIn("✓ connected", out)
self.assertNotIn("✗ unavailable", out)
self.assertNotIn("Note:", out)
def test_banner_icon_inactive_summary_is_unavailable(self):
summary = ("sf-mcp-proxy: metadata-experts NOT activated in this org — "
"enable in Setup (check-tools for detail)")
out = self._banner_for(summary)
# "active" is a substring of "NOT activated" — the icon must NOT be fooled.
self.assertIn("✗ unavailable", out)
self.assertNotIn("✓ connected", out)
# The environment band shows only the tri-state icon; per-server detail
# lives in check-tools (WIN-040), so there is no verbose Note line here.
def test_banner_icon_not_yet_observed_is_connecting_no_note(self):
out = self._banner_for("sf-mcp-proxy: not yet observed — run check-tools to probe")
self.assertIn("⟳ connecting", out)
self.assertNotIn("Note:", out)
def test_banner_icon_degraded_summary_is_unavailable(self):
out = self._banner_for("sf-mcp-proxy: degraded — run check-tools for per-server detail")
self.assertIn("✗ unavailable", out)
# --- MCP names line is scoped to the servers the glyph covers -------------
# The names shown next to the single ✓/✗ glyph must be ONLY the health-tracked
# platform servers. salesforce-lsp is a local stdio process the glyph never
# reflects, so listing it beside the glyph misleads the viewer.
def test_mcp_server_names_excludes_local_lsp(self):
mcp_json = json.dumps({"mcpServers": {
"salesforce-api-context": {},
"salesforce-lsp": {},
"salesforce-metadata-experts": {},
}})
with mock.patch.object(Path, "read_text", return_value=mcp_json):
names = sfx._mcp_server_names(Path("/plugin"))
self.assertIn("api-context", names)
self.assertIn("metadata-experts", names)
self.assertNotIn("lsp", names)
def test_mcp_server_names_read_error_yields_empty(self):
with mock.patch.object(Path, "read_text", side_effect=OSError("boom")):
self.assertEqual(sfx._mcp_server_names(Path("/plugin")), [])
# --- cmd_status must not probe an unreachable org -------------------------
# The live probe runs on an executor thread that cannot be cancelled, so
# cmd_status resolves the org FIRST and only probes when it is reachable.
# Probing before the unreachable-org early return would leave a live thread
# that concurrent.futures joins at interpreter exit, hanging /status until the
# probe subprocesses time out (Prizm P2 on 94bab3b).
def test_cmd_status_unreachable_org_does_not_probe(self):
with mock.patch.object(sfx.Path, "exists", return_value=True), \
mock.patch.object(sfx, "resolve_executable", return_value="/usr/bin/sf"), \
mock.patch.object(sfx, "get_target_org_detailed",
return_value=("deadOrg", "")), \
mock.patch.object(sfx, "resolve_org_info", return_value=None), \
mock.patch.object(sfx, "_live_mcp_summary",
side_effect=AssertionError(
"must not probe an unreachable org")) as probe, \
mock.patch("builtins.print"):
rc = sfx.cmd_status()
self.assertEqual(rc, 0)
probe.assert_not_called()
class DiagnosticTests(unittest.TestCase):
def test_diagnostic_shape(self):
ctx = sfx.diagnostic_context(["sf", "npm"])
self.assertEqual(ctx["platform"], sfx.sys.platform)
self.assertIn("sf", ctx["resolvedExecutables"])
self.assertIn("npm", ctx["resolvedExecutables"])
def test_diagnostic_is_secret_free(self):
# The diagnostic must never carry tokens/secrets — only environment shape
# and resolved executable paths.
ctx = sfx.diagnostic_context()
blob = json.dumps(ctx).lower()
for forbidden in ("token", "jwt", "secret", "password", "authorization", "bearer"):
self.assertNotIn(forbidden, blob)
def test_render_diagnostic_lines_is_text(self):
text = sfx.render_diagnostic_lines(sfx.diagnostic_context(["sf"]))
self.assertIn("platform:", text)
self.assertIn("resolved executables:", text)
def test_render_diagnostic_lines_wraps_wide_paths_by_terminal_cells(self):
wide = "" * 80
text = sfx.render_diagnostic_lines({
"platform": "darwin", "shell": wide, "cwd": wide,
"pluginRoot": wide, "resolvedExecutables": {"sf": wide},
})
self.assertTrue(all(
sfx._terminal_cell_width(line) <= 80 for line in text.splitlines()
), text)
if __name__ == "__main__":
unittest.main(verbosity=2)