CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 15s
Docstrings, section headers and assertion messages drop the record number, milestone or step that introduced the behaviour, and incident accounts are restated as the present-tense failure shape. A dead pointer to a design doc that is not in the repo is removed. Renamed to the property they check: test_the_old_shape_would_be_caught -> test_a_rank_then_scope_query_is_caught test_a_ledger_repeat_no_longer_produces_that_case_at_all -> test_a_ledger_repeat_is_counted_not_withheld test_the_header_no_longer_promises_once_per_session -> test_the_header_explains_the_seen_marker test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file -> test_one_canon_is_not_stamped_onto_a_whole_file test_the_repos_own_prose_no_longer_defines_with -> test_the_repos_own_prose_does_not_define_with test_the_original_regression_create_note_with_content -> test_the_real_server_refuses_content_on_create_note Fixture and example ids stay: "note 5" in a supersession chain, "rule 156" in an illustrative question, `#2275` as the hex-versus-citation case, `#3244` as the example of a bare number, fixed dates in date-filter tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
6.7 KiB
Python
150 lines
6.7 KiB
Python
"""`.scribe` — the second key a directory can be scoped by.
|
|
|
|
Every hook in `plugin/hooks/` scopes its request to a project. With only one
|
|
key, `git remote get-url origin`, a session outside a git repo has no key at
|
|
all, so a session in a plain directory is 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",):
|
|
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) == ""
|