"""`.scribe` — the second key a directory can be scoped by (#4085). Every hook in `plugin/hooks/` scopes its request to a project, and until this existed all six turned on one key: `git remote get-url origin`. That key does not exist outside a git repo, so a session in a plain directory was unscoped everywhere at once — silently, because a missing remote is indistinguishable from a remote nobody bound. `scribe_scope_query` is the shared answer, so these run the real shell rather than a reimplementation of it. What they pin: * the marker is found from a subdirectory, the way git finds its root; * a bare integer works, because that is what a person writes by hand; * the marker BEATS a git remote, since someone put the file there on purpose; * an `instance` naming a different Scribe is refused — the case the field exists for. A project id means nothing on its own: id 2 is a different project on every instance, so a marker that travels would otherwise scope the session to the wrong project, confidently and without a word. That last one is the reason the file is JSON and not a number, so it is tested from both directions: the matching host is honoured, the mismatched one is not. """ from __future__ import annotations import json import os import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] DEFS = ROOT / "plugin" / "hooks" / "scribe_defs.sh" INSTANCE = "https://scribe.example.com" def _sh(script: str, cwd: Path, env_extra: dict | None = None) -> str: for tool in ("bash", "jq"): if shutil.which(tool) is None: pytest.skip(f"hook runtime tool {tool!r} not installed") env = {"PATH": os.environ["PATH"], "HOME": str(cwd)} env.update(env_extra or {}) body = f'set -uo pipefail\n. "{DEFS}"\nurl="{INSTANCE}"\n{script}\n' out = subprocess.run(["bash", "-c", body], cwd=cwd, capture_output=True, text=True, env=env, timeout=30) assert out.returncode == 0, out.stderr return out.stdout.strip() def _marker(d: Path, **fields) -> None: (d / ".scribe").write_text(json.dumps(fields)) def _git_repo(d: Path, remote: str) -> None: env = {"PATH": os.environ["PATH"], "HOME": str(d)} subprocess.run(["git", "init", "-q"], cwd=d, check=True, env=env) subprocess.run(["git", "remote", "add", "origin", remote], cwd=d, check=True, env=env) def test_a_bare_integer_is_a_valid_marker(tmp_path): """What someone writes by hand. It parses as JSON already, so one filter reads it and the written form alike.""" (tmp_path / ".scribe").write_text("7\n") assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) == "project_id=7" def test_the_marker_is_found_from_a_subdirectory(tmp_path): _marker(tmp_path, instance=INSTANCE, project_id=2, project="FabledScribe") deep = tmp_path / "src" / "scribe" / "services" deep.mkdir(parents=True) assert _sh(f'scribe_scope_query "{deep}"', tmp_path) == "project_id=2" def test_a_matching_instance_is_honoured(tmp_path): """Host-only comparison, so http/https and a trailing slash are not a mismatch — only a genuinely different Scribe is.""" _marker(tmp_path, instance="http://scribe.example.com/", project_id=2) assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) == "project_id=2" def test_a_marker_for_another_instance_is_refused_with_a_reason(tmp_path): """The case the `instance` field exists for. No project beats the wrong project, and the reason is carried so the session can say which it was.""" _marker(tmp_path, instance="https://someone-elses.example.org", project_id=2) assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) == "" said = _sh(f'scribe_marker_read "{tmp_path}/.scribe"', tmp_path) assert "someone-elses.example.org" in said and "scribe.example.com" in said def test_the_marker_beats_a_git_remote(tmp_path): """Someone put the file there deliberately; a remote is only where the code happens to be pushed. This is also how a directory overrides its binding.""" _git_repo(tmp_path, "git@git.example.com:someone/thing.git") assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path).startswith("repo=") _marker(tmp_path, instance=INSTANCE, project_id=2) assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) == "project_id=2" def test_without_a_marker_the_git_remote_still_answers(tmp_path): """The path every existing install is on — it must not have moved.""" _git_repo(tmp_path, "git@git.example.com:someone/thing.git") got = _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) assert got.startswith("repo=") and "git.example.com" in got.replace("%2F", "/") def test_a_plain_directory_with_no_marker_scopes_to_nothing(tmp_path): """Not an error — the caller sends no scope and the server says so.""" assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) == "" @pytest.mark.parametrize("body", ["", "not json at all", "{}", '{"project_id": 0}', '{"project_id": "../../etc"}', "[1,2,3]"]) def test_an_unusable_marker_is_refused_rather_than_sent(tmp_path, body): """A marker is operator-written and can say anything. Nothing that is not a positive integer may reach the query string.""" (tmp_path / ".scribe").write_text(body) assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) == "" def test_the_reason_survives_the_command_substitution_that_fetches_it(tmp_path): """Regression on a bug this nearly shipped with. The reason used to be a global the function set, which every caller reads through `$( )` — a subshell, so the assignment died with it and the caller saw an UNSET variable. The hooks all run `set -u`, where reading one aborts the script: a directory with a misaddressed marker would have cost the session its entire SessionStart context, to fetch a warning about a file. So the reason comes back through stdout with the id, and this asserts on the shape the caller actually uses: read in a subshell, split, and used under `set -u` without the shell dying. """ _marker(tmp_path, instance="https://elsewhere.example.org", project_id=2) got = _sh( f'read_out=$(scribe_marker_read "$(scribe_marker_file "{tmp_path}")")\n' 'printf "id=[%s] why=[%s]" "${read_out%%$\'\\t\'*}" "${read_out#*$\'\\t\'}"', tmp_path, ) assert got.startswith("id=[]") assert "elsewhere.example.org" in got def test_the_walk_up_terminates_at_the_root(tmp_path): """No marker anywhere above: the loop must end rather than spin. tmp_path is several levels down from /, so this really does walk.""" deep = tmp_path / "a" / "b" / "c" deep.mkdir(parents=True) assert _sh(f'scribe_scope_query "{deep}"', tmp_path) == ""