Files
FabledGrimoire/tests/test_scribe_marker.py
T
bvandeusenandClaude Opus 5.5 e99f1169f7
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
rename(547): what people read says Grimoire
Step 1 of the Scribe -> Grimoire rename: every capitalised "Scribe" in prose
becomes "Grimoire" ("Fabled Scribe" -> "Fabled Grimoire") across the UI,
the PWA manifest and page title, the MCP instructions and tool docstrings,
hook and skill text, docs and README, with the tests that assert on that
prose moving with it. Old migrations keep their wording.

Identifiers are untouched here: the package, plugin name, MCP server key,
hook file names, env vars, the .scribe marker and FabledScribe repo/image
names move in steps 2 and 3, along with prose that quotes them.

The sweep matched "Scribe" only as a whole word, so describe/subscribe and
friends are untouched. The longer name took the MCP instructions one
character over their 1,600 budget; two phrases are shortened to fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:46:37 -04:00

150 lines
6.6 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 Grimoire 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 Grimoire 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):
"""The reason cannot be a global the function sets.
Every caller reads the function through `$( )` — a subshell, so a global
assignment dies with it and the caller sees an UNSET variable. The hooks all
run `set -u`, where reading one aborts the script: a directory with a
misaddressed marker would 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) == ""