| """Config-contract tests for the dedicated install flags. |
| |
| Drives the real glob/manager_core config reader/writer through a |
| subprocess-isolated harness and pins: missing keys read False |
| (secure-by-default), only case-insensitive "true" is truthy, write |
| round-trips losslessly, edits need a restart (cached_config), the |
| exception-fallback path supplies False, no auto-migration seeds the |
| flags from a legacy security_level, and the get_bool missing->False |
| quirk the flags rely on stays frozen. |
| |
| Harness: the child process injects a stub `folder_paths` (routing |
| import-time side effects into a tmpdir, and making has_system_user_api() |
| True so force_security_level_if_needed does not force 'strong'), prepends |
| `glob/` to ITS OWN sys.path (shadowing of stdlib `glob` confined to the |
| child), points manager_core.manager_config_path at a tmp config.ini, |
| resets cached_config, runs the scenario, and prints one JSON line for the |
| parent to assert. |
| """ |
| import json |
| import subprocess |
| import sys |
| import textwrap |
| import unittest |
| from pathlib import Path |
|
|
| REPO_ROOT = Path(__file__).resolve().parent.parent |
|
|
| _CHILD_PREAMBLE = textwrap.dedent( |
| """ |
| import sys, types, tempfile, os, json |
| tmp = tempfile.mkdtemp(prefix="cm_flags_cfg_") |
| stub = types.ModuleType("folder_paths") |
| stub.get_user_directory = lambda: tmp |
| stub.get_system_user_directory = lambda *a, **k: os.path.join(tmp, "sysuser") |
| sys.modules["folder_paths"] = stub |
| sys.path.insert(0, {glob_path!r}) |
| import manager_core |
| CONFIG_PATH = os.path.join(tmp, "config.ini") |
| manager_core.manager_config_path = CONFIG_PATH |
| manager_core.cached_config = None |
| |
| def write_ini(text): |
| with open(CONFIG_PATH, "w") as f: |
| f.write(text) |
| |
| def fresh_read(): |
| manager_core.cached_config = None |
| return manager_core.get_config() |
| |
| def flag_view(cfg): |
| return {{ |
| "git": cfg.get("allow_git_url_install", "<ABSENT>"), |
| "pip": cfg.get("allow_pip_install", "<ABSENT>"), |
| }} |
| """ |
| ) |
|
|
|
|
| def _run_child(body): |
| """Run a scenario body in the isolated child; return its JSON payload.""" |
| script = _CHILD_PREAMBLE.format(glob_path=str(REPO_ROOT / "glob")) + textwrap.dedent(body) |
| proc = subprocess.run( |
| [sys.executable, "-c", script], |
| capture_output=True, |
| text=True, |
| timeout=180, |
| cwd=str(REPO_ROOT), |
| ) |
| if proc.returncode != 0: |
| raise AssertionError( |
| "config-harness child failed (rc=%d). stderr tail:\n%s" |
| % (proc.returncode, "\n".join(proc.stderr.strip().splitlines()[-8:])) |
| ) |
| lines = proc.stdout.strip().splitlines() |
| if not lines: |
| raise AssertionError( |
| "config-harness child exited 0 but produced no stdout. stderr tail:\n%s" |
| % "\n".join(proc.stderr.strip().splitlines()[-8:]) |
| ) |
| last_line = lines[-1] |
| try: |
| return json.loads(last_line) |
| except json.JSONDecodeError as e: |
| raise AssertionError( |
| "config-harness child emitted a non-JSON last line: %r\nfull stdout:\n%s" |
| % (last_line, proc.stdout) |
| ) from e |
|
|
|
|
| class InstallFlagsConfigContractTest(unittest.TestCase): |
| def test_sc17_missing_keys_read_false(self): |
| """Both keys absent from config.ini -> both flags read False |
| (secure-by-default).""" |
| payload = _run_child( |
| """ |
| write_ini("[default]\\nsecurity_level = normal\\n") |
| print(json.dumps(flag_view(fresh_read()))) |
| """ |
| ) |
| self.assertIs(payload["git"], False) |
| self.assertIs(payload["pip"], False) |
|
|
| def test_sc18_malformed_and_case_matrix(self): |
| """Only case-insensitive "true" is truthy; malformed -> False.""" |
| payload = _run_child( |
| """ |
| out = {} |
| for raw in ["1", "yes", "TRUE", "true ", "true"]: |
| write_ini("[default]\\nallow_git_url_install = %s\\nallow_pip_install = %s\\n" % (raw, raw)) |
| cfg = fresh_read() |
| out[raw] = flag_view(cfg) |
| print(json.dumps(out)) |
| """ |
| ) |
| expected = { |
| "1": False, |
| "yes": False, |
| "TRUE": True, |
| "true ": True, |
| "true": True, |
| } |
| for raw, want in expected.items(): |
| with self.subTest(value=raw): |
| self.assertIs(payload[raw]["git"], want) |
| self.assertIs(payload[raw]["pip"], want) |
|
|
| def test_sc19_write_round_trip(self): |
| """write_config persists str(bool); round-trip is lossless.""" |
| payload = _run_child( |
| """ |
| write_ini("[default]\\nsecurity_level = normal\\n") |
| cfg = fresh_read() |
| cfg["allow_git_url_install"] = True |
| cfg["allow_pip_install"] = False |
| manager_core.write_config() |
| raw = open(CONFIG_PATH).read() |
| reread = flag_view(fresh_read()) |
| print(json.dumps({ |
| "raw_has_git_true": "allow_git_url_install = True" in raw, |
| "raw_has_pip_false": "allow_pip_install = False" in raw, |
| "reread": reread, |
| })) |
| """ |
| ) |
| self.assertTrue( |
| payload["raw_has_git_true"], |
| "write_config must persist allow_git_url_install = True in [default]", |
| ) |
| self.assertTrue( |
| payload["raw_has_pip_false"], |
| "write_config must persist allow_pip_install = False in [default]", |
| ) |
| self.assertIs(payload["reread"]["git"], True) |
| self.assertIs(payload["reread"]["pip"], False) |
|
|
| def test_sc20_restart_only_activation(self): |
| """Editing config.ini without restart has NO effect (cache wins); |
| a reset (== restart) picks up the change.""" |
| payload = _run_child( |
| """ |
| write_ini("[default]\\nallow_git_url_install = false\\n") |
| first = manager_core.get_config() # populates cached_config |
| before_edit = flag_view(first) |
| write_ini("[default]\\nallow_git_url_install = true\\n") |
| cached = flag_view(manager_core.get_config()) # NO reset: cache must win |
| after_restart = flag_view(fresh_read()) # reset == restart |
| print(json.dumps({ |
| "before_edit": before_edit, |
| "cached_after_edit": cached, |
| "after_restart": after_restart, |
| })) |
| """ |
| ) |
| self.assertIs(payload["before_edit"]["git"], False) |
| self.assertIs( |
| payload["cached_after_edit"]["git"], |
| False, |
| "cached_config must NOT hot-reload the edited flag", |
| ) |
| self.assertIs(payload["after_restart"]["git"], True) |
|
|
| def test_sc21_exception_fallback_supplies_false(self): |
| """Corrupted config.ini -> exception-fallback dict supplies flags False.""" |
| payload = _run_child( |
| """ |
| # No [default] section header -> read_config raises inside try, |
| # lands in the exception-fallback dict. |
| write_ini("allow_git_url_install = true\\ngarbage without section\\n") |
| cfg = fresh_read() |
| print(json.dumps({ |
| "flags": flag_view(cfg), |
| "fallback_marker_file_logging": cfg.get("file_logging"), |
| })) |
| """ |
| ) |
| |
| |
| self.assertIs( |
| payload["fallback_marker_file_logging"], |
| True, |
| "corrupted ini must route through the exception-fallback dict", |
| ) |
| self.assertIs(payload["flags"]["git"], False) |
| self.assertIs(payload["flags"]["pip"], False) |
|
|
| def test_sc28_no_auto_migration_from_weak(self): |
| """Legacy `security_level=weak` does NOT seed the flags (no auto-migration).""" |
| payload = _run_child( |
| """ |
| write_ini("[default]\\nsecurity_level = weak\\n") |
| cfg = fresh_read() |
| print(json.dumps({ |
| "flags": flag_view(cfg), |
| "security_level": cfg.get("security_level"), |
| })) |
| """ |
| ) |
| self.assertEqual(payload["security_level"], "weak") |
| self.assertIs(payload["flags"]["git"], False, "no auto-seed from weak") |
| self.assertIs(payload["flags"]["pip"], False, "no auto-seed from weak") |
|
|
| def test_sc42_get_bool_quirk_guard(self): |
| """get_bool ignores its default param: missing `file_logging` reads |
| False despite a True default. The flags rely on this missing->False |
| quirk; this guard pins it.""" |
| payload = _run_child( |
| """ |
| write_ini("[default]\\nsecurity_level = normal\\n") |
| cfg = fresh_read() |
| print(json.dumps({"file_logging": cfg.get("file_logging", "<ABSENT>")})) |
| """ |
| ) |
| self.assertIs( |
| payload["file_logging"], |
| False, |
| "get_bool quirk changed: missing key no longer reads False — " |
| "new flags rely on missing->False", |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main(verbosity=2) |
|
|