"""ORCH-057 D2/D4/D6: ownership-detect leaf (src/fs_normalize.py) unit tests. TC-03..TC-09 (04-test-plan.yaml). All FS-dependent tests use ``tmp_path`` and vary ``target_uid`` (a uid no tmp file actually has -> mismatch; the runner's own uid -> clean) so NO real chown / privilege is needed. ``os.geteuid`` is monkeypatched for the privilege-gated normalize test (TC-08). Never touches /repos. """ import os import tempfile import pytest _test_db = os.path.join(tempfile.gettempdir(), "test_orchestrator_fsn.db") os.environ["ORCH_DB_PATH"] = _test_db os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir() os.environ["ORCH_GITEA_TOKEN"] = "test-token" os.environ["ORCH_PLANE_API_TOKEN"] = "test-token" from src import fs_normalize _NONEXISTENT_UID = 999999 # no tmp file is owned by this uid -> deterministic mismatch @pytest.fixture(autouse=True) def _reset(monkeypatch): fs_normalize.reset_cache() monkeypatch.setattr(fs_normalize.settings, "fs_normalize_enabled", True) monkeypatch.setattr(fs_normalize.settings, "fs_normalize_repos", "") monkeypatch.setattr(fs_normalize.settings, "fs_normalize_auto", False) monkeypatch.setattr(fs_normalize.settings, "fs_scan_cache_ttl_s", 300) yield fs_normalize.reset_cache() @pytest.fixture def tree(tmp_path): """A small dir tree with a file, owned by the test runner's own uid.""" d = tmp_path / "root" (d / "sub").mkdir(parents=True) (d / "a.txt").write_text("a") (d / "sub" / "b.txt").write_text("b") return d # --------------------------------------------------------------------------- # TC-03 / TC-04 — scan verdict # --------------------------------------------------------------------------- def test_tc03_scan_detects_mismatch(tree): """TC-03: a tree whose files are not owned by target_uid -> mismatch=True with the affected root listed and a sample path set.""" scan = fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID) assert scan.mismatch is True assert str(tree) in scan.roots_mismatch assert scan.sample_path is not None assert scan.target_uid == _NONEXISTENT_UID def test_tc04_clean_tree_no_mismatch(tree): """TC-04: a clean tree (all files owned by target_uid == the runner) -> idempotent mismatch=False no-op.""" scan = fs_normalize.scan_ownership(roots=[str(tree)], target_uid=os.getuid()) assert scan.mismatch is False assert scan.roots_mismatch == [] assert scan.sample_path is None # --------------------------------------------------------------------------- # TC-05 — never-raise on bad/missing root # --------------------------------------------------------------------------- def test_tc05_never_raise_on_missing_root(tmp_path): """TC-05: a non-existent root degrades to mismatch=False, never raises.""" missing = str(tmp_path / "does-not-exist") scan = fs_normalize.scan_ownership(roots=[missing], target_uid=_NONEXISTENT_UID) assert scan.mismatch is False assert scan.roots_checked == [] # the missing root is skipped def test_tc05_never_raise_on_walk_error(tree, monkeypatch): """TC-05: an os.walk explosion mid-scan degrades to a conservative verdict.""" def boom(*a, **k): raise OSError("simulated walk failure") monkeypatch.setattr(fs_normalize.os, "walk", boom) scan = fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID) # The root dir itself is owned by the runner (not _NONEXISTENT_UID was checked via # lstat which still works) -> walk error swallowed, no exception escapes. assert isinstance(scan, fs_normalize.OwnershipScan) # --------------------------------------------------------------------------- # TC-06 — applies() scope # --------------------------------------------------------------------------- def test_tc06_applies_empty_csv_self_hosting_only(monkeypatch): """TC-06: empty ORCH_FS_NORMALIZE_REPOS -> True only for the self-hosting repo (orchestrator), False for enduro-trails.""" monkeypatch.setattr(fs_normalize.settings, "fs_normalize_repos", "") assert fs_normalize.applies("orchestrator") is True assert fs_normalize.applies("enduro-trails") is False def test_tc06_applies_explicit_csv(monkeypatch): """TC-06: a non-empty CSV scopes by list (case-insensitive).""" monkeypatch.setattr(fs_normalize.settings, "fs_normalize_repos", "enduro-trails") assert fs_normalize.applies("enduro-trails") is True assert fs_normalize.applies("orchestrator") is False # --------------------------------------------------------------------------- # TC-07 — kill-switch # --------------------------------------------------------------------------- def test_tc07_killswitch_off_scan_inert(tree, monkeypatch): """TC-07: fs_normalize_enabled=False -> scan is inert (mismatch=False, enabled flag exposes the off state); applies() False for everyone.""" monkeypatch.setattr(fs_normalize.settings, "fs_normalize_enabled", False) scan = fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID) assert scan.mismatch is False assert scan.enabled is False assert fs_normalize.applies("orchestrator") is False def test_tc07_killswitch_off_normalize_inert(tree, monkeypatch): """TC-07: normalize is a documented no-op when the kill-switch is off.""" monkeypatch.setattr(fs_normalize.settings, "fs_normalize_enabled", False) res = fs_normalize.normalize(roots=[str(tree)], target_uid=_NONEXISTENT_UID) assert res["attempted"] is False assert res["changed"] == 0 assert "disabled" in res["note"] # --------------------------------------------------------------------------- # TC-08 — normalize without privilege # --------------------------------------------------------------------------- def test_tc08_normalize_without_rights_is_noop_not_error(tree, monkeypatch): """TC-08: under a non-root euid with auto=True and foreign files, normalize is a no-op + honest log ('operator procedure required'), NOT an exception.""" monkeypatch.setattr(fs_normalize.settings, "fs_normalize_auto", True) monkeypatch.setattr(fs_normalize.os, "geteuid", lambda: 1000) # non-root res = fs_normalize.normalize(roots=[str(tree)], target_uid=_NONEXISTENT_UID) assert res["privileged"] is False assert res["attempted"] is False assert res["changed"] == 0 assert "INFRA.md" in res["note"] # --------------------------------------------------------------------------- # TC-09 — TTL cache # --------------------------------------------------------------------------- def test_tc09_ttl_cache_avoids_rescan(tree, monkeypatch): """TC-09: a repeat call inside the TTL window does NOT re-walk; force/reset invalidates (mirrors preflight._cache).""" calls = {"n": 0} real_scan = fs_normalize._scan def counting_scan(roots, target_uid): calls["n"] += 1 return real_scan(roots, target_uid) monkeypatch.setattr(fs_normalize, "_scan", counting_scan) fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID) fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID) assert calls["n"] == 1 # second call served from cache fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID, force=True) assert calls["n"] == 2 # force bypasses the cache fs_normalize.reset_cache() fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID) assert calls["n"] == 3 # reset invalidates def test_tc09_cache_keyed_by_roots_and_uid(tree, monkeypatch): """A different (roots, target_uid) key is not served from another key's cache.""" calls = {"n": 0} real_scan = fs_normalize._scan def counting_scan(roots, target_uid): calls["n"] += 1 return real_scan(roots, target_uid) monkeypatch.setattr(fs_normalize, "_scan", counting_scan) fs_normalize.scan_ownership(roots=[str(tree)], target_uid=_NONEXISTENT_UID) fs_normalize.scan_ownership(roots=[str(tree)], target_uid=os.getuid()) # different uid assert calls["n"] == 2 # --------------------------------------------------------------------------- # classifier (pure) + snapshot # --------------------------------------------------------------------------- def test_classify_worktree_error_markers(): assert fs_normalize.classify_worktree_error("fatal: ...: Permission denied") is True assert fs_normalize.classify_worktree_error("could not create leading directories") is True assert fs_normalize.classify_worktree_error("insufficient permission for adding an object") is True assert fs_normalize.classify_worktree_error("fatal: branch already checked out") is False assert fs_normalize.classify_worktree_error("") is False assert fs_normalize.classify_worktree_error(None) is False def test_is_permission_failure_from_exc(): assert fs_normalize.is_permission_failure(exc=PermissionError(13, "denied")) is True import errno as _errno assert fs_normalize.is_permission_failure(exc=OSError(_errno.EACCES, "x")) is True assert fs_normalize.is_permission_failure(exc=OSError(_errno.ENOENT, "x")) is False def test_snapshot_shape(tree, monkeypatch): """snapshot() returns the additive fs_ownership block and never raises.""" monkeypatch.setattr(fs_normalize.settings, "fs_scan_roots", str(tree)) snap = fs_normalize.snapshot() for k in ("enabled", "auto", "repos", "target_uid", "mismatch", "roots_checked", "roots_mismatch", "sample_path", "checked_at"): assert k in snap assert snap["enabled"] is True