#!/usr/bin/env python3 """Focused offline tests for on-demand cached org-feature detection.""" from __future__ import annotations import io import json import os import stat import tempfile import unittest from contextlib import redirect_stderr, redirect_stdout from pathlib import Path from _test_support import load_module SCRIPTS = Path(__file__).resolve().parent.parent MODULE_PATH = SCRIPTS / "feature_detection.py" PLUGIN_ROOT = SCRIPTS.parent feature = load_module(MODULE_PATH, "feature_detection_under_test") class FakeRunner: def __init__(self, responses): self.responses = list(responses) self.calls = [] def __call__(self, argv, timeout=None): self.calls.append(list(argv)) if not self.responses: raise AssertionError(f"unexpected command: {argv}") response = self.responses.pop(0) if callable(response): return response(argv) return response def ok(payload): return feature.CommandResult(True, json.dumps(payload), 0, "") def fail(reason="nonzero"): return feature.CommandResult(False, "sensitive raw output", 1, reason) def org(api="67.0", org_id="00D-stable", url="https://trial.example.salesforce.com", username="must-not-leak@example.com"): result = {"id": org_id, "instanceUrl": url, "apiVersion": api, "accessToken": "secret"} if username is not None: result["username"] = username return ok({"result": result}) def describe(queryable=True, relationship="SubscriberPackage"): return ok({"result": {"queryable": queryable, "fields": [ {"name": "SubscriberPackageId", "relationshipName": relationship} ]}}) def query(names): return ok({"result": {"records": [ {"Id": f"raw-{index}", "SubscriberPackage": {"Name": name, "NamespacePrefix": f"raw_ns_{index}"}} for index, name in enumerate(names) ]}}) def config(alias="configured-alias"): return ok({"result": [{"name": "target-org", "value": alias}]}) class FeatureDetectionTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.cache = Path(self.tmp.name) / "cache" def tearDown(self): self.tmp.cleanup() def run_features(self, args, runner, now=1000.0): out, err = io.StringIO(), io.StringIO() ticks = iter([0.0, 0.010, 0.020, 0.030, 0.040, 0.050, 0.060, 0.070, 0.080, 0.090, 0.100, 0.110, 0.120]) with redirect_stdout(out), redirect_stderr(err): code = feature.run_features( args, plugin_root=PLUGIN_ROOT, runner=runner, cache_root=self.cache, monotonic=lambda: next(ticks), wall_clock=lambda: now, ) return code, out.getvalue(), err.getvalue() def refresh_responses(self, names=(), spaces=None): return [org(), ok({"dataSpaces": [] if spaces is None else spaces}), describe(), query(names)] def test_static_versioned_mapping_is_exact_and_separate_from_cache(self): mapping = feature.load_feature_domains(PLUGIN_ROOT) self.assertEqual(mapping, { "data360": "data360", "devops-center": "dx", "omnistudio": "omnistudio" }) catalog = json.loads((PLUGIN_ROOT / "catalog/feature-domains.json").read_text()) self.assertEqual(catalog["schemaVersion"], "1.0") self.assertEqual(feature.SCHEMA_VERSION, "1.0") self.assertEqual(feature.CACHE_SCHEMA_VERSION, "2.0") def test_explicit_target_skips_config_and_every_downstream_org_call_is_explicit(self): runner = FakeRunner(self.refresh_responses()) code, out, err = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) self.assertEqual((code, err), (0, "")) self.assertEqual(runner.calls[0], ["sf", "org", "display", "--target-org", "trial", "--json"]) self.assertFalse(any(call[1:4] == ["config", "get", "target-org"] for call in runner.calls)) for call in runner.calls[1:]: self.assertTrue( ("-o" in call and call[call.index("-o") + 1] == "trial") or ("--target-org" in call and call[call.index("--target-org") + 1] == "trial"), call, ) self.assertNotIn("trial", out) def test_omitted_target_uses_config_then_explicit_downstream_target(self): runner = FakeRunner([config("from-config"), *self.refresh_responses()]) code, out, err = self.run_features(["--refresh", "--json"], runner) self.assertEqual((code, err), (0, "")) self.assertEqual(runner.calls[0], ["sf", "config", "get", "target-org", "--json"]) self.assertEqual(runner.calls[1], ["sf", "org", "display", "--target-org", "from-config", "--json"]) self.assertNotIn("from-config", out) def test_api_version_is_dynamic_and_nonempty_data_spaces_array_means_present(self): runner = FakeRunner([ org(api="66.0"), ok({"dataSpaces": [{"name": "raw space"}]}), describe(), query([]) ]) code, out, _ = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) data = json.loads(out) self.assertEqual(code, 0) self.assertEqual(data["apiVersion"], "66.0") self.assertIn("/services/data/v66.0/ssot/data-spaces", runner.calls[1]) d360 = next(item for item in data["features"] if item["feature"] == "data360") self.assertEqual(d360["status"], "present") self.assertEqual(d360["evidence"], {"kind": "core-rest-reachable", "matchedCount": 1}) self.assertNotIn("raw space", out) def test_empty_data_spaces_is_unknown_and_cache_preserves_empty_limitation(self): runner = FakeRunner(self.refresh_responses([], spaces=[])) code, out, _ = self.run_features( ["--target-org", "trial", "--refresh", "--json"], runner ) item = next(x for x in json.loads(out)["features"] if x["feature"] == "data360") self.assertEqual(code, 0) self.assertEqual(item["status"], "unknown") self.assertEqual( item["evidence"], {"kind": "core-rest-reachable-empty", "matchedCount": 0} ) self.assertIn("endpoint reachable", item["limitation"].lower()) self.assertIn("readiness not established", item["limitation"].lower()) hit = FakeRunner([org()]) _, cached, _ = self.run_features(["--target-org", "trial", "--json"], hit, now=1001.0) cached_item = next( x for x in json.loads(cached)["features"] if x["feature"] == "data360" ) self.assertEqual(cached_item, item) def test_cache_reconstruction_matches_fresh_feature_for_every_evidence_kind(self): # Fresh probes and cache-hit reconstruction both build rows through _feature, # which derives the limitation from the single _LIMITATION_BY_EVIDENCE map — # so a cache-hit row is byte-identical to a fresh row for the same evidence, # and limitation presence tracks the map (never an independent re-derivation # that could drift or drop the text). domains = {"data360": "data-cloud", "omnistudio": "omnistudio", "devops-center": "devops-center"} samples = [ ("data360", "present", "core-rest-reachable", 3), ("data360", "unknown", "core-rest-reachable-empty", 0), ("data360", "unknown", "core-rest-unavailable", 0), ("omnistudio", "present", "package-name-match", 2), ("omnistudio", "unknown", "package-name-not-detected", 0), ("omnistudio", "unknown", "package-inventory-unavailable", 0), ("devops-center", "present", "package-name-match", 1), ("devops-center", "unknown", "package-name-not-detected", 0), ("devops-center", "unknown", "package-inventory-unavailable", 0), ] for name, status, kind, count in samples: with self.subTest(feature=name, kind=kind): fresh = feature._feature(name, domains[name], status, kind, count) cached_shape = {"feature": name, "status": status, "evidence": {"kind": kind, "matchedCount": count}} reconstructed = feature._cache_rows_to_output([cached_shape], domains)[0] self.assertEqual(reconstructed, fresh) self.assertEqual("limitation" in fresh, feature._limitation(name, kind) is not None) def test_data_failure_or_bad_shape_is_unknown_never_absent(self): for response in (fail("timeout"), ok({"totalSize": 0})): with self.subTest(response=response): runner = FakeRunner([org(), response, describe(), query([])]) code, out, _ = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) item = next(x for x in json.loads(out)["features"] if x["feature"] == "data360") self.assertEqual(code, 0) self.assertEqual(item["status"], "unknown") self.assertNotEqual(item["status"], "absent") self.assertIn("permission", item["limitation"].lower()) self.assertNotIn("sensitive raw output", out) def test_describe_precedes_one_query_and_validates_relationship(self): runner = FakeRunner(self.refresh_responses(["Other", "Another"])) code, out, _ = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) self.assertEqual(code, 0) describe_call, query_call = runner.calls[2], runner.calls[3] self.assertEqual(describe_call[:4], ["sf", "sobject", "describe", "-s"]) self.assertIn("--use-tooling-api", describe_call) self.assertEqual(sum("query" in call[1:3] for call in runner.calls), 1) self.assertIn("LIMIT 100", query_call[query_call.index("--query") + 1]) for bad_describe in (fail(), describe(queryable=False), describe(relationship="Wrong")): with self.subTest(bad_describe=bad_describe): blocked = FakeRunner([org(), ok({"dataSpaces": []}), bad_describe]) code, raw, _ = self.run_features(["--target-org", "trial", "--refresh", "--json"], blocked) self.assertEqual(code, 0) self.assertEqual(len(blocked.calls), 3, "query must not run after failed validation") package = [x for x in json.loads(raw)["features"] if x["feature"] != "data360"] self.assertTrue(all(x["status"] == "unknown" for x in package)) def test_package_name_matches_are_normalized_limited_and_raw_inventory_is_not_retained(self): names = ["OmniStudio", "Vlocity Communications", "Salesforce DevOps Center", "Unrelated"] runner = FakeRunner(self.refresh_responses(names)) code, out, _ = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) data = json.loads(out) self.assertEqual(code, 0) omni = next(x for x in data["features"] if x["feature"] == "omnistudio") devops = next(x for x in data["features"] if x["feature"] == "devops-center") self.assertEqual((omni["status"], omni["evidence"]["matchedCount"]), ("present", 2)) self.assertEqual((devops["status"], devops["evidence"]["matchedCount"]), ("present", 1)) self.assertEqual(omni["evidence"]["kind"], "package-name-match") self.assertEqual(devops["evidence"]["kind"], "package-name-match") for row in (omni, devops): self.assertIn("name", row["limitation"].lower()) self.assertIn("identity", row["limitation"].lower()) self.assertIn("legacy status", row["limitation"].lower()) self.assertIn("vlocity", omni["limitation"].lower()) cache_blob = "".join(p.read_text() for p in self.cache.glob("*.json")) for forbidden in names + ["raw_ns_0", "00D-stable", "must-not-leak", "secret", "trial"]: self.assertNotIn(forbidden, out + cache_blob) def test_package_name_matching_rejects_helpers_and_unrelated_names(self): names = [ "DevOps Center Helper", "OmniStudio Helper", "Acme DevOps Center Toolkit", "Vlocity Helper", "Unrelated Package", ] runner = FakeRunner(self.refresh_responses(names)) code, out, _ = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) rows = {x["feature"]: x for x in json.loads(out)["features"]} self.assertEqual(code, 0) self.assertEqual(rows["omnistudio"]["status"], "unknown") self.assertEqual(rows["omnistudio"]["evidence"]["matchedCount"], 0) self.assertEqual(rows["devops-center"]["status"], "unknown") self.assertEqual(rows["devops-center"]["evidence"]["matchedCount"], 0) self.assertEqual(rows["omnistudio"]["evidence"]["kind"], "package-name-not-detected") self.assertEqual(rows["devops-center"]["evidence"]["kind"], "package-name-not-detected") def test_no_package_match_is_unknown_with_product_specific_blind_spots(self): runner = FakeRunner(self.refresh_responses(["Unrelated Package"])) _, out, _ = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) rows = {x["feature"]: x for x in json.loads(out)["features"]} self.assertEqual(rows["omnistudio"]["status"], "unknown") self.assertEqual(rows["omnistudio"]["evidence"]["kind"], "package-name-not-detected") self.assertIn("limit 100", rows["omnistudio"]["limitation"].lower()) self.assertIn("standard", rows["omnistudio"]["limitation"].lower()) self.assertEqual(rows["devops-center"]["status"], "unknown") self.assertEqual(rows["devops-center"]["evidence"]["kind"], "package-name-not-detected") self.assertIn("limit 100", rows["devops-center"]["limitation"].lower()) self.assertIn("next-generation", rows["devops-center"]["limitation"].lower()) self.assertTrue(all(x["status"] != "absent" for x in rows.values())) def test_cache_hit_skips_feature_probes_and_refresh_bypasses_cache(self): first = FakeRunner(self.refresh_responses(["OmniStudio", "DevOps Center"])) _, refreshed, _ = self.run_features(["--target-org", "alias-one", "--refresh", "--json"], first) self.assertEqual(json.loads(refreshed)["cacheState"], "refresh") hit = FakeRunner([org()]) _, cached, _ = self.run_features(["--target-org", "renamed-alias", "--json"], hit, now=1001.0) cached_data = json.loads(cached) self.assertEqual(cached_data["cacheState"], "cache-hit") self.assertEqual(len(hit.calls), 1, "cache identity still requires explicit org display") self.assertEqual(cached_data["elapsedMs"]["data360"], 0) self.assertEqual(cached_data["elapsedMs"]["packageInventory"], 0) bypass = FakeRunner(self.refresh_responses()) _, rerun, _ = self.run_features(["--target-org", "alias-one", "--refresh", "--json"], bypass, now=1002.0) self.assertEqual(json.loads(rerun)["cacheState"], "refresh") self.assertEqual(len(bypass.calls), 4) def test_cache_identity_includes_canonical_principal_without_persisting_it(self): first = FakeRunner([ org(username="first-principal@example.com"), ok({"dataSpaces": []}), describe(), query([]), ]) self.run_features(["--target-org", "first-alias", "--refresh", "--json"], first) second = FakeRunner([ org(username="second-principal@example.com"), ok({"dataSpaces": []}), describe(), query([]), ]) code, out, err = self.run_features(["--target-org", "second-alias", "--json"], second, now=1001.0) self.assertEqual((code, err), (0, "")) self.assertEqual(json.loads(out)["cacheState"], "refresh") self.assertEqual(len(second.calls), 4, "a different principal must not share a cache hit") files = list(self.cache.glob("*.json")) self.assertEqual(len(files), 2) cache_blob = "".join(path.read_text(encoding="utf-8") for path in files) self.assertNotIn("principal", cache_blob) self.assertNotIn("@example.com", cache_blob) def test_missing_canonical_principal_fails_bounded_and_does_not_cache(self): runner = FakeRunner([org(username=None)]) code, out, err = self.run_features(["--target-org", "private-alias", "--json"], runner) self.assertEqual(code, 2) self.assertEqual(err, "") self.assertIn("stable identity", out) self.assertNotIn("private-alias", out) self.assertEqual(len(runner.calls), 1) self.assertFalse(self.cache.exists()) def test_unusable_cache_parent_is_nonfatal_to_normalized_refresh(self): self.cache.parent.mkdir(parents=True, exist_ok=True) self.cache.write_text("regular file blocks cache directory", encoding="utf-8") runner = FakeRunner(self.refresh_responses(["OmniStudio"])) code, out, err = self.run_features( ["--target-org", "trial", "--refresh", "--json"], runner ) self.assertEqual((code, err), (0, "")) data = json.loads(out) self.assertEqual(data["cacheState"], "refresh") self.assertEqual([row["feature"] for row in data["features"]], list(feature.FEATURE_ORDER)) self.assertTrue(self.cache.is_file()) def test_cache_ttl_corruption_atomic_shape_and_owner_only_mode(self): unknown = FakeRunner(self.refresh_responses([])) self.run_features(["--target-org", "trial", "--refresh", "--json"], unknown) files = list(self.cache.glob("*.json")) self.assertEqual(len(files), 1) cache_data = json.loads(files[0].read_text()) self.assertEqual(cache_data["schemaVersion"], "2.0") self.assertEqual(cache_data["ttlSeconds"], 300) if os.name != "nt": self.assertEqual(stat.S_IMODE(files[0].stat().st_mode), 0o600) self.assertEqual(stat.S_IMODE(self.cache.stat().st_mode), 0o700) self.assertNotIn("featureDomains", cache_data) self.assertFalse(list(self.cache.glob("*.tmp"))) files[0].write_text("{corrupt", encoding="utf-8") miss = FakeRunner(self.refresh_responses([])) _, out, _ = self.run_features(["--target-org", "trial", "--json"], miss, now=1001.0) self.assertEqual(json.loads(out)["cacheState"], "refresh") self.assertEqual(len(miss.calls), 4) present_cache = Path(self.tmp.name) / "present-cache" self.cache = present_cache present = FakeRunner(self.refresh_responses( ["OmniStudio", "DevOps Center"], spaces=[{"name": "raw space"}] )) self.run_features(["--target-org", "trial", "--refresh", "--json"], present) body = json.loads(next(present_cache.glob("*.json")).read_text()) self.assertEqual(body["ttlSeconds"], 1800) stale = json.loads(json.dumps(body["features"])) stale[0] = { "feature": "data360", "status": "present", "evidence": {"kind": "core-rest-reachable", "matchedCount": 0}, } self.assertIsNone(feature._safe_cache_features(stale)) stale = json.loads(json.dumps(body["features"])) stale[1]["evidence"]["kind"] = "legacy-package" self.assertIsNone(feature._safe_cache_features(stale)) def test_expired_unknown_cache_is_a_miss(self): first = FakeRunner(self.refresh_responses([])) self.run_features(["--target-org", "trial", "--refresh", "--json"], first, now=1000.0) expired = FakeRunner(self.refresh_responses([])) _, out, _ = self.run_features(["--target-org", "trial", "--json"], expired, now=1301.0) self.assertEqual(json.loads(out)["cacheState"], "refresh") self.assertEqual(len(expired.calls), 4) def test_json_is_bounded_and_human_lists_present_domains_before_unknowns(self): runner = FakeRunner(self.refresh_responses([])) code, raw, err = self.run_features(["--target-org", "trial", "--refresh", "--json"], runner) data = json.loads(raw) self.assertEqual((code, err), (0, "")) self.assertLess(len(raw), 3000) self.assertEqual(data["mode"], "features") self.assertEqual(data["featureDomains"], feature.load_feature_domains(PLUGIN_ROOT)) self.assertEqual(set(data["elapsedMs"]), {"data360", "packageInventory", "total"}) human_runner = FakeRunner(self.refresh_responses( ["OmniStudio"], spaces=[{"name": "raw space"}] )) code, human, _ = self.run_features(["--target-org", "trial", "--refresh"], human_runner) self.assertEqual(code, 0) self.assertLess(human.index("Present domains"), human.index("Unknown / limitations")) self.assertIn("package-name-match", human) self.assertIn("identity", human.lower()) self.assertIn("legacy status", human.lower()) self.assertIn("not authoritative absence", human) self.assertNotIn("trial", human) def test_missing_identity_or_target_and_invalid_flags_fail_bounded_without_echoing_identity(self): cases = [ ([], FakeRunner([ok({"result": []})])), (["--target-org", "private-alias", "--json"], FakeRunner([ok({"result": {"apiVersion": "67.0"}})])), (["--wat", "private-value", "--json"], FakeRunner([])), ] for args, runner in cases: with self.subTest(args=args): code, out, err = self.run_features(args, runner) self.assertEqual(code, 2) blob = out + err self.assertLess(len(blob), 500) self.assertNotIn("private", blob) if __name__ == "__main__": unittest.main(verbosity=2)