From ab82e603b6c92f2eb777c43d40a15f00f3a1bff0 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Sat, 24 Oct 2026 02:17:48 +0800 Subject: [PATCH] emrg: one directory is one directory, decided by the volume's identity --- emrg/tools/shell_env.py | 29 +++++++++++++++---- tests/test_bash_v2_boundary.py | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/emrg/tools/shell_env.py b/emrg/tools/shell_env.py index 708d66de4..e18c0ef90 100644 --- a/emrg/tools/shell_env.py +++ b/emrg/tools/shell_env.py @@ -47,6 +47,7 @@ from emrg.sandbox.policy import SandboxPolicy from emrg.sandbox.roots import canonical_path, writable_roots +from emrg.tools.file_policy import same_file #: The directory the relocated caches live in, under the granted temp root. CACHE_DIR_NAME = "emrg-confined-cache" @@ -108,16 +109,32 @@ def _running_package_root() -> str: def _same_directory(left: str, right: str) -> bool: """Whether two spellings name one directory. - Canonical and case-folded, because ``PYTHONPATH`` is a deployer-editable - string: ``C:\\EMRG`` and ``c:\\emrg\\`` are one root, and treating them as two - would prepend a duplicate rather than leave a correct declaration alone. + ``PYTHONPATH`` is a deployer-editable string, so ``C:\\EMRG`` and ``c:\\emrg\\`` + have to read as one root — treating them as two would prepend a duplicate + rather than leave a correct declaration alone. + + Answered by the filesystem's own identity — ``st_dev`` + ``st_ino``, the pair + ``os.path.samefile`` compares, asked through the one home + (:func:`emrg.tools.file_policy.same_file`) — and **not** by a transform of the + two strings. The transform it used to be, ``os.path.normcase``, folds case + only where ``os.name`` is ``nt`` and is the *identity function* on POSIX: on a + volume that folds case while the platform is POSIX — APFS's default, which is + what macOS runs — it answered "two directories" for one directory and the root + was prepended a second time (#2557). Identity also covers the platform the old + spelling got right, and does it without asking which platform is running: NTFS + answers through the same pair. + + A directory that is not there has no identity to compare, so it is not the + other one: ``False``, and the root is prepended. That is the fail-safe + direction — a redundant entry naming a directory that does not exist changes + nothing a run can see, while withholding an entry the runner needs breaks its + import. :param left: one spelling of a directory. :param right: the other spelling. - :returns: whether both resolve to the same directory. + :returns: whether both name one directory on disk. """ - canonical = (canonical_path(left), canonical_path(right)) - return os.path.normcase(canonical[0]) == os.path.normcase(canonical[1]) + return same_file(canonical_path(left), canonical_path(right)) def runner_import_env(environ: dict | None = None) -> dict[str, str]: diff --git a/tests/test_bash_v2_boundary.py b/tests/test_bash_v2_boundary.py index 674189328..adcd13506 100644 --- a/tests/test_bash_v2_boundary.py +++ b/tests/test_bash_v2_boundary.py @@ -526,6 +526,59 @@ def test_a_declaration_that_already_names_the_root_is_left_alone(monkeypatch): assert runner_import_env() == {} +def _same_name_other_case(path: str) -> str: + """One directory's other spelling: the same name, case swapped.""" + head, tail = os.path.split(path.rstrip(os.sep)) + return os.path.join(head, tail.swapcase()) + + +def test_the_case_a_folding_volume_folds_is_one_directory(): + """The answer comes from the volume, not from ``os.name`` (issue #2557). + + ``os.path.normcase`` folds case only where ``os.name`` is ``nt`` — it is the + *identity function* on POSIX — so a volume that folds case while the platform + is POSIX (APFS's default, which is what macOS runs) was read as two + directories, and the root was prepended a second time. The test above cannot + see that: its case half is armed only on ``nt``, and on POSIX it asserts + nothing about a fold. + + The probe comes first and the skip states its reason. On a volume that keeps + two case spellings apart there is no fold to measure, and passing without + asking would make this test evidence for a property it never measured — which + is exactly how the defect survived: the fold was asserted where it happened + to work and nowhere else. + """ + from emrg.tools.shell_env import runner_import_env + + root = _running_root() + variant = _same_name_other_case(root) + if not os.path.exists(variant) or not os.path.samefile(variant, root): + pytest.skip( + "this volume keeps two case spellings apart, so there is no fold to " + "measure; the arm runs on a case-folding volume (APFS's default, NTFS)" + ) + assert runner_import_env({"PYTHONPATH": variant}) == {} + + +def test_a_declaration_naming_a_missing_directory_is_not_the_root(tmp_path): + """A path with no inode is not the root, so the root is prepended (issue #2557). + + Prepending is the fail-safe direction: a redundant entry naming a directory + that does not exist changes nothing a run can see, while withholding the entry + the runner needs breaks its import. This is also the arm that keeps an + identity test from turning a missing path into an ``OSError`` raised out of a + function whose whole contract is "add a variable, or add nothing". + + ``tmp_path`` is used and its subdirectory deliberately never created, so the + subject is a path that cannot exist rather than one that happens not to. + """ + from emrg.tools.shell_env import runner_import_env + + missing = str(tmp_path / "not-created" / "emrg") + env = runner_import_env({"PYTHONPATH": missing}) + assert env["PYTHONPATH"].split(os.pathsep) == [_running_root(), missing] + + def test_the_unconfined_path_relocates_nothing(tmp_path, monkeypatch): """``danger-full-access`` runs bare, so its caches belong where they always were.""" import emrg.tools.bash_tool_v2 as v2