From 074f8d40504e52eebfc2fc810e5a0a07ffed6c6b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 22:09:35 +0000 Subject: [PATCH 1/2] fix: probe compiled RIMAPI DLL before Workshop Restore run-metadata pins to the compiled fork checkout. Probe RIMAPI_DLL_PATH, then RIMAPI_FORK_PATH, then the sibling of the RLE git toplevel, and only then the Workshop overlay. Fork commit uses the same env plus git toplevel sibling so site-packages installs still record the pin. Co-authored-by: Jason --- .env.example | 4 + CLAUDE.md | 11 ++ CONTRIBUTING.md | 2 + README.md | 2 +- docs/2026-09-07-rimapi-path-probe.md | 32 +++++ src/rle/tracking/metadata.py | 139 +++++++++++++++---- tests/unit/test_metadata.py | 200 +++++++++++++++++++++++++++ 7 files changed, 364 insertions(+), 26 deletions(-) create mode 100644 docs/2026-09-07-rimapi-path-probe.md diff --git a/.env.example b/.env.example index 5e52c6a..7c8a925 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,10 @@ OPENROUTER_API_KEY= # --- RIMAPI (RimWorld mod) --- RIMAPI_URL=http://localhost:8765 +# AppSprout runs: point these at the compiled fork checkout (not Workshop). +# Summaries record rimapi_dll_path, rimapi_dll_sha256, and rimapi_fork_commit. +# RIMAPI_DLL_PATH=../RIMAPI/1.6/Assemblies/RIMAPI.dll +# RIMAPI_FORK_PATH=../RIMAPI # --- MCP HTTP host (coding-agent harnesses: grok-build, opencode, ...) --- # Default: bind 127.0.0.1, ephemeral port, advertise http://127.0.0.1:/mcp diff --git a/CLAUDE.md b/CLAUDE.md index ad1a3f9..d94e686 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,15 @@ cp ../../1.6/Assemblies/RIMAPI.dll \ The upstream Workshop DLL is backed up as `RIMAPI.dll.upstream-backup` in the same folder. +**AppSprout runs pin the compiled checkout, not Workshop.** Set both: + +```bash +RIMAPI_DLL_PATH=/path/to/RIMAPI/1.6/Assemblies/RIMAPI.dll +RIMAPI_FORK_PATH=/path/to/RIMAPI +``` + +`collect_metadata()` probes `RIMAPI_DLL_PATH` → `{RIMAPI_FORK_PATH}/1.6/Assemblies/RIMAPI.dll` → sibling `../RIMAPI/1.6/Assemblies/RIMAPI.dll` → Workshop last. Workshop is not source of truth. Run summaries record `rimapi_dll_path`, `rimapi_dll_sha256`, and `rimapi_fork_commit` (compiled pin: `BFC9DD53…` @ `b6c5003`; Workshop `73E659E8…` is Flash drift). See [docs/2026-09-07-rimapi-path-probe.md](docs/2026-09-07-rimapi-path-probe.md). + ### RIMAPI gotchas - RIMAPI only starts serving **after the map loads** (not on the main menu) @@ -81,6 +90,8 @@ The `.env` file controls which LLM provider is used. Key fields: | `MODEL` | Model name as the provider expects it | `unsloth/nvidia-nemotron-3-nano-4b` | | `PROVIDER_BASE_URL` | API base URL (required for LM Studio and OpenRouter) | `http://localhost:1234/v1` | | `RIMAPI_URL` | RIMAPI mod URL | `http://localhost:8765` | +| `RIMAPI_DLL_PATH` | Compiled `RIMAPI.dll` for run metadata (preferred over Workshop) | `../RIMAPI/1.6/Assemblies/RIMAPI.dll` | +| `RIMAPI_FORK_PATH` | RIMAPI fork checkout (DLL probe + `rimapi_fork_commit`) | `../RIMAPI` | | `MCP_CONTAINER_REACHABLE` | Bind MCP on `0.0.0.0:8766`, advertise `http://host.docker.internal:8766/mcp` for Docker agents (host RimWorld). Not `--docker`. | `true` | | `MCP_BIND_HOST` / `MCP_ADVERTISE_HOST` / `MCP_PORT` | Optional MCP listen overrides (also `--harness-opt mcp_*`) | `0.0.0.0` / `host.docker.internal` / `8766` | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d8e74b..eedb6fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,6 +88,8 @@ uv pip install git+https://github.com/AppSprout-dev/rle-harness-opencode python scripts/run_scenario.py crashlanded_survival --harness opencode --model openai/gpt-4o --ticks 10 ``` +AppSprout live runs set `RIMAPI_DLL_PATH` and `RIMAPI_FORK_PATH` to the compiled RIMAPI checkout (not Workshop). Summaries record that path, the DLL SHA, and the fork commit — see [CLAUDE.md](CLAUDE.md) and [docs/2026-09-07-rimapi-path-probe.md](docs/2026-09-07-rimapi-path-probe.md). + ### Testing a harness plugin External plugins depend on RLE core and use `rle.testing`: diff --git a/README.md b/README.md index 46e9e4b..7697d92 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ You need four things set up: 3. **Python 3.14+** with [uv](https://docs.astral.sh/uv/) 4. **Save file** — `rle_crashlanded_v1` in RimWorld's save folder (the scenario auto-loads it) -> **RIMAPI note:** The Workshop version may not have our contributed endpoints yet. See [CLAUDE.md](CLAUDE.md) for instructions on building and deploying our fork DLL. +> **RIMAPI note:** The Workshop version may not have our contributed endpoints yet. See [CLAUDE.md](CLAUDE.md) for instructions on building and deploying our fork DLL. AppSprout runs set `RIMAPI_DLL_PATH` and `RIMAPI_FORK_PATH` to the compiled checkout; summaries record that path, the DLL SHA, and the fork commit. Workshop is not source of truth. ### Verify diff --git a/docs/2026-09-07-rimapi-path-probe.md b/docs/2026-09-07-rimapi-path-probe.md new file mode 100644 index 0000000..d0fe237 --- /dev/null +++ b/docs/2026-09-07-rimapi-path-probe.md @@ -0,0 +1,32 @@ +# RIMAPI path probe + +Run metadata must pin the **compiled** AppSprout-dev/RIMAPI checkout, not the Steam Workshop DLL. + +## Known pins + +| Source | DLL SHA-256 prefix | Fork commit | Role | +|--------|--------------------|-------------|------| +| Compiled checkout (`../RIMAPI/1.6/Assemblies/RIMAPI.dll`) | `BFC9DD53…` | `b6c5003` | Source of truth | +| Steam Workshop overlay | `73E659E8…` | — | Flash drift; not SoT | + +Do not re-hash these pins to "discover" them. AppSprout runs set `RIMAPI_DLL_PATH` and `RIMAPI_FORK_PATH` to the compiled checkout. + +## Probe order + +`collect_metadata()` in `src/rle/tracking/metadata.py` records `rimapi_dll_path`, `rimapi_dll_sha256`, and `rimapi_fork_commit`. + +DLL (first existing file wins): + +1. `$RIMAPI_DLL_PATH` +2. `$RIMAPI_FORK_PATH/1.6/Assemblies/RIMAPI.dll` +3. Sibling of the RLE git toplevel: `../RIMAPI/1.6/Assemblies/RIMAPI.dll` +4. Workshop path (optional last fallback) + +Fork commit (first checkout with `.git` wins; empty string if none): + +1. `$RIMAPI_FORK_PATH` +2. Sibling of `git rev-parse --show-toplevel` (not only `Path(__file__).parents[3]`, which is empty from site-packages / `.venv`) + +## Box draft + +The requested operator memo path `/workspace/drafts/2026-09-07-rimapi-path-probe.md` is outside this repository and will be written separately. diff --git a/src/rle/tracking/metadata.py b/src/rle/tracking/metadata.py index 337f186..9b01861 100644 --- a/src/rle/tracking/metadata.py +++ b/src/rle/tracking/metadata.py @@ -27,9 +27,12 @@ # baseline no longer banks free process points; weights redistributed. SCORING_VERSION = "1.2" -# Conventional install path for the RIMAPI Workshop mod we deploy our fork DLL -# over. Best-effort — if Steam lives elsewhere set the RIMAPI_DLL_PATH env var. -_RIMAPI_DLL_DEFAULT_PATH = Path( +# Compiled fork layout relative to a RIMAPI checkout root. +_RIMAPI_DLL_RELATIVE = Path("1.6") / "Assemblies" / "RIMAPI.dll" + +# Steam Workshop install we sometimes overlay. Last-resort fallback only — +# Workshop is not source of truth (Flash can drift the Workshop DLL). +_RIMAPI_DLL_WORKSHOP_FALLBACK = Path( "C:/Steam/steamapps/workshop/content/294100/3593423732/1.6/Assemblies/RIMAPI.dll", ) @@ -83,36 +86,122 @@ def file_sha256(path: Path | None) -> str | None: return None +def _is_packaged_install(path: Path) -> bool: + """True when *path* lives in a venv/site-packages install, not a checkout.""" + return "site-packages" in path.parts or ".venv" in path.parts + + +def _checkout_root_from_source_file(file_path: Path) -> Path | None: + """RLE checkout root inferred from ``src/rle/tracking/metadata.py``. + + Returns None for packaged installs (site-packages / .venv): ``parents[3]`` + is not the RLE repo in those layouts. + """ + if _is_packaged_install(file_path): + return None + try: + candidate = file_path.resolve().parents[3] + except IndexError: + return None + if (candidate / ".git").exists() or (candidate / "src" / "rle").is_dir(): + return candidate + return None + + +def _rle_checkout_root() -> Path | None: + """Git toplevel of the RLE checkout, if findable. + + Prefers ``git rev-parse --show-toplevel`` from the process cwd (works when + RLE is imported from site-packages/.venv but launched from the checkout). + Falls back to ``__file__`` only when that path still looks like a source + tree — not a packaged install. + """ + raw = _git("rev-parse", "--show-toplevel") + if raw: + return Path(raw) + return _checkout_root_from_source_file(Path(__file__)) + + +def _sibling_rimapi_root() -> Path | None: + """``../RIMAPI`` next to the RLE checkout, or None if the checkout is unknown.""" + checkout = _rle_checkout_root() + if checkout is None: + return None + return checkout.parent / "RIMAPI" + + +def _first_existing_file(candidates: list[Path]) -> Path | None: + seen: set[str] = set() + for candidate in candidates: + key = os.fspath(candidate) + if key in seen: + continue + seen.add(key) + if candidate.is_file(): + return candidate + return None + + +def _rimapi_dll_candidates() -> list[Path]: + """Deterministic DLL probe order. First existing file wins. + + 1. ``$RIMAPI_DLL_PATH`` + 2. ``$RIMAPI_FORK_PATH/1.6/Assemblies/RIMAPI.dll`` + 3. sibling checkout ``../RIMAPI/1.6/Assemblies/RIMAPI.dll`` + 4. Workshop path (optional last fallback; not source of truth) + """ + candidates: list[Path] = [] + dll_override = os.environ.get("RIMAPI_DLL_PATH") + if dll_override: + candidates.append(Path(dll_override)) + fork_override = os.environ.get("RIMAPI_FORK_PATH") + if fork_override: + candidates.append(Path(fork_override) / _RIMAPI_DLL_RELATIVE) + sibling = _sibling_rimapi_root() + if sibling is not None: + candidates.append(sibling / _RIMAPI_DLL_RELATIVE) + candidates.append(_RIMAPI_DLL_WORKSHOP_FALLBACK) + return candidates + + def _rimapi_dll_path() -> Path | None: - """Resolve the deployed RIMAPI DLL path (env override → Workshop default).""" - override = os.environ.get("RIMAPI_DLL_PATH") + """Resolve the RIMAPI DLL, preferring a compiled fork over Workshop.""" + return _first_existing_file(_rimapi_dll_candidates()) + + +def _rimapi_fork_candidates() -> list[Path]: + """Fork checkout probe order. First path with a ``.git`` dir wins. + + 1. ``$RIMAPI_FORK_PATH`` + 2. sibling of the RLE git toplevel (``../RIMAPI``), not only ``__file__`` + """ + candidates: list[Path] = [] + override = os.environ.get("RIMAPI_FORK_PATH") if override: - candidate = Path(override) - return candidate if candidate.is_file() else None - return ( - _RIMAPI_DLL_DEFAULT_PATH if _RIMAPI_DLL_DEFAULT_PATH.is_file() else None - ) + candidates.append(Path(override)) + sibling = _sibling_rimapi_root() + if sibling is not None: + candidates.append(sibling) + return candidates def _rimapi_fork_commit() -> str: """HEAD short SHA of the local RIMAPI fork checkout, if findable. - Honors $RIMAPI_FORK_PATH; otherwise checks the conventional sibling repo - location (../RIMAPI relative to this RLE checkout). Empty string when the - fork isn't reachable from the runtime environment. + Honors ``$RIMAPI_FORK_PATH`` and the sibling of the RLE git toplevel. + Empty string when the fork isn't reachable from the runtime environment. """ - override = os.environ.get("RIMAPI_FORK_PATH") - candidates = [Path(override)] if override else [] - candidates.append(Path(__file__).resolve().parents[3] / "RIMAPI") - for fork_path in candidates: - if (fork_path / ".git").exists(): - try: - return subprocess.check_output( - ["git", "-C", str(fork_path), "rev-parse", "--short", "HEAD"], - stderr=subprocess.DEVNULL, text=True, - ).strip() - except (subprocess.CalledProcessError, FileNotFoundError): - return "" + for fork_path in _rimapi_fork_candidates(): + if not (fork_path / ".git").exists(): + continue + try: + return subprocess.check_output( + ["git", "-C", str(fork_path), "rev-parse", "--short", "HEAD"], + stderr=subprocess.DEVNULL, + text=True, + ).strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return "" return "" diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index 1a19672..7947306 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -3,10 +3,16 @@ from __future__ import annotations import hashlib +import os +import subprocess from pathlib import Path +import pytest + +from rle.tracking import metadata as metadata_mod from rle.tracking.metadata import ( SCORING_VERSION, + _RIMAPI_DLL_RELATIVE, collect_metadata, file_sha256, ) @@ -79,3 +85,197 @@ def test_collect_metadata_dll_path_and_hash_pair_consistently() -> None: assert isinstance(digest, str) assert len(digest) == 64 int(digest, 16) # raises if not hex + + +def _write_compiled_dll(fork_root: Path, payload: bytes) -> Path: + dll = fork_root / _RIMAPI_DLL_RELATIVE + dll.parent.mkdir(parents=True, exist_ok=True) + dll.write_bytes(payload) + return dll + + +def _init_git_repo(path: Path) -> str: + path.mkdir(parents=True, exist_ok=True) + env = { + **os.environ, + "GIT_AUTHOR_NAME": "rle-test", + "GIT_AUTHOR_EMAIL": "rle-test@example.com", + "GIT_COMMITTER_NAME": "rle-test", + "GIT_COMMITTER_EMAIL": "rle-test@example.com", + } + git_ident = [ + "-c", + "user.name=rle-test", + "-c", + "user.email=rle-test@example.com", + "-c", + "commit.gpgsign=false", + ] + subprocess.run( + ["git", *git_ident, "init", "-b", "main"], + cwd=path, + check=True, + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + marker = path / "marker.txt" + marker.write_text("ok\n", encoding="utf-8") + subprocess.run( + ["git", *git_ident, "add", "marker.txt"], + cwd=path, + check=True, + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + subprocess.run( + ["git", *git_ident, "commit", "-m", "init"], + cwd=path, + check=True, + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return subprocess.check_output( + ["git", "-C", str(path), "rev-parse", "--short", "HEAD"], + text=True, + ).strip() + + +@pytest.fixture +def isolated_rimapi_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + """No inherited pin env; workshop fallback and checkout root are temp-local.""" + monkeypatch.delenv("RIMAPI_DLL_PATH", raising=False) + monkeypatch.delenv("RIMAPI_FORK_PATH", raising=False) + workshop = tmp_path / "workshop" / "RIMAPI.dll" + monkeypatch.setattr(metadata_mod, "_RIMAPI_DLL_WORKSHOP_FALLBACK", workshop) + monkeypatch.setattr(metadata_mod, "_rle_checkout_root", lambda: tmp_path / "Projects" / "RLE") + return tmp_path + + +def test_dll_probe_order_env_then_fork_then_sibling_then_workshop( + isolated_rimapi_env: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + tmp_path = isolated_rimapi_env + env_dll = tmp_path / "explicit" / "RIMAPI.dll" + env_dll.parent.mkdir(parents=True) + env_dll.write_bytes(b"env-dll") + fork_root = tmp_path / "fork-env" + fork_dll = _write_compiled_dll(fork_root, b"fork-dll") + sibling_root = tmp_path / "Projects" / "RIMAPI" + sibling_dll = _write_compiled_dll(sibling_root, b"sibling-dll") + workshop = tmp_path / "workshop" / "RIMAPI.dll" + workshop.parent.mkdir(parents=True) + workshop.write_bytes(b"workshop-dll") + + monkeypatch.setenv("RIMAPI_DLL_PATH", str(env_dll)) + monkeypatch.setenv("RIMAPI_FORK_PATH", str(fork_root)) + assert metadata_mod._rimapi_dll_path() == env_dll + + monkeypatch.delenv("RIMAPI_DLL_PATH") + assert metadata_mod._rimapi_dll_path() == fork_dll + + monkeypatch.delenv("RIMAPI_FORK_PATH") + assert metadata_mod._rimapi_dll_path() == sibling_dll + + sibling_dll.unlink() + assert metadata_mod._rimapi_dll_path() == workshop + + +def test_dll_probe_prefers_compiled_sibling_over_workshop( + isolated_rimapi_env: Path, +) -> None: + tmp_path = isolated_rimapi_env + sibling_dll = _write_compiled_dll(tmp_path / "Projects" / "RIMAPI", b"compiled") + workshop = tmp_path / "workshop" / "RIMAPI.dll" + workshop.parent.mkdir(parents=True) + workshop.write_bytes(b"flash-drift") + + assert metadata_mod._rimapi_dll_path() == sibling_dll + + +def test_dll_probe_falls_through_when_env_path_missing( + isolated_rimapi_env: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + tmp_path = isolated_rimapi_env + fork_dll = _write_compiled_dll(tmp_path / "fork-env", b"fork") + monkeypatch.setenv("RIMAPI_DLL_PATH", str(tmp_path / "missing.dll")) + monkeypatch.setenv("RIMAPI_FORK_PATH", str(tmp_path / "fork-env")) + assert metadata_mod._rimapi_dll_path() == fork_dll + + +def test_collect_metadata_records_compiled_dll_path_and_hash( + isolated_rimapi_env: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + tmp_path = isolated_rimapi_env + dll = _write_compiled_dll(tmp_path / "compiled", b"compiled-bytes") + monkeypatch.setenv("RIMAPI_DLL_PATH", str(dll)) + md = collect_metadata() + assert md["rimapi_dll_path"] == str(dll) + assert md["rimapi_dll_sha256"] == file_sha256(dll) + + +def test_collect_metadata_records_fork_commit_from_env( + isolated_rimapi_env: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + tmp_path = isolated_rimapi_env + fork = tmp_path / "compiled" + commit = _init_git_repo(fork) + dll = _write_compiled_dll(fork, b"compiled-bytes") + monkeypatch.setenv("RIMAPI_DLL_PATH", str(dll)) + monkeypatch.setenv("RIMAPI_FORK_PATH", str(fork)) + md = collect_metadata() + assert md["rimapi_dll_path"] == str(dll) + assert md["rimapi_dll_sha256"] == file_sha256(dll) + assert md["rimapi_fork_commit"] == commit + + +def test_fork_commit_honors_env_over_sibling( + isolated_rimapi_env: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + tmp_path = isolated_rimapi_env + env_commit = _init_git_repo(tmp_path / "fork-env") + sibling_commit = _init_git_repo(tmp_path / "Projects" / "RIMAPI") + assert env_commit != sibling_commit + monkeypatch.setenv("RIMAPI_FORK_PATH", str(tmp_path / "fork-env")) + assert metadata_mod._rimapi_fork_commit() == env_commit + + +def test_fork_commit_falls_back_to_git_toplevel_sibling( + isolated_rimapi_env: Path, +) -> None: + tmp_path = isolated_rimapi_env + sibling_commit = _init_git_repo(tmp_path / "Projects" / "RIMAPI") + assert metadata_mod._rimapi_fork_commit() == sibling_commit + + +def test_fork_commit_empty_when_missing(isolated_rimapi_env: Path) -> None: + assert metadata_mod._rimapi_fork_commit() == "" + + +def test_checkout_root_from_source_file_skips_site_packages(tmp_path: Path) -> None: + packaged = ( + tmp_path + / ".venv" + / "lib" + / "python3.14" + / "site-packages" + / "rle" + / "tracking" + / "metadata.py" + ) + assert metadata_mod._checkout_root_from_source_file(packaged) is None + + +def test_checkout_root_from_source_file_uses_editable_tree(tmp_path: Path) -> None: + tracking = tmp_path / "RLE" / "src" / "rle" / "tracking" + tracking.mkdir(parents=True) + source = tracking / "metadata.py" + source.write_text("# test\n", encoding="utf-8") + assert metadata_mod._checkout_root_from_source_file(source) == tmp_path / "RLE" From 6250606b031f1a583d7dc01fca6e0b5262f9cfe6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 22:10:19 +0000 Subject: [PATCH 2/2] test: give fork-commit fixtures distinct SHAs Identical marker files produced the same short hash, so the env-over-sibling assertion could not distinguish the two repos. Co-authored-by: Jason --- tests/unit/test_metadata.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index 7947306..15d29f5 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -11,8 +11,8 @@ from rle.tracking import metadata as metadata_mod from rle.tracking.metadata import ( - SCORING_VERSION, _RIMAPI_DLL_RELATIVE, + SCORING_VERSION, collect_metadata, file_sha256, ) @@ -94,7 +94,7 @@ def _write_compiled_dll(fork_root: Path, payload: bytes) -> Path: return dll -def _init_git_repo(path: Path) -> str: +def _init_git_repo(path: Path, marker: str = "ok") -> str: path.mkdir(parents=True, exist_ok=True) env = { **os.environ, @@ -119,8 +119,8 @@ def _init_git_repo(path: Path) -> str: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - marker = path / "marker.txt" - marker.write_text("ok\n", encoding="utf-8") + marker_file = path / "marker.txt" + marker_file.write_text(f"{marker}\n", encoding="utf-8") subprocess.run( ["git", *git_ident, "add", "marker.txt"], cwd=path, @@ -240,8 +240,8 @@ def test_fork_commit_honors_env_over_sibling( monkeypatch: pytest.MonkeyPatch, ) -> None: tmp_path = isolated_rimapi_env - env_commit = _init_git_repo(tmp_path / "fork-env") - sibling_commit = _init_git_repo(tmp_path / "Projects" / "RIMAPI") + env_commit = _init_git_repo(tmp_path / "fork-env", marker="env-fork") + sibling_commit = _init_git_repo(tmp_path / "Projects" / "RIMAPI", marker="sibling-fork") assert env_commit != sibling_commit monkeypatch.setenv("RIMAPI_FORK_PATH", str(tmp_path / "fork-env")) assert metadata_mod._rimapi_fork_commit() == env_commit