#!/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. 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)