diff --git a/tests/python/test_findings.py b/tests/python/test_findings.py index b68e86f7..569f9d3c 100644 --- a/tests/python/test_findings.py +++ b/tests/python/test_findings.py @@ -2,7 +2,9 @@ Every test runs `command()` on a ledger and a search record in `tmp_path`, so the repository's ledger and the live record under the git common directory are -never read or written. The commit a test names is this checkout's HEAD. +never read or written. The commit a test names is this checkout's HEAD, which +the ledger's own repository (a `git init` in `tmp_path`) borrows through git's +alternates file, as a private repository holding the same commits would. """ from __future__ import annotations @@ -43,9 +45,18 @@ def paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, Path]: (transcripts / f"{SESSION}.jsonl").write_text(json.dumps(human(AGREED, STAMP)) + "\n") monkeypatch.setattr(importlib.import_module("session_state"), "TRANSCRIPTS", transcripts) monkeypatch.setenv("CLAUDE_CODE_SESSION_ID", SESSION) + git(tmp_path, "init", "-q") + common = git(harness_fixtures.REAL, "rev-parse", "--path-format=absolute", "--git-common-dir") + (tmp_path / ".git" / "objects" / "info" / "alternates").write_text(f"{common}/objects\n") return tmp_path / "ledger.jsonl", tmp_path / "common" / "findings-searches.jsonl" +def git(where: Path, *argv: str) -> str: + return subprocess.run(["git", "-C", str(where), "-c", "user.name=t", "-c", "user.email=t@t", + "-c", "commit.gpgsign=false", *argv], + capture_output=True, text=True, check=True).stdout.strip() # fmt: skip + + def run(paths: tuple[Path, Path], *argv: str, now: datetime = NOW) -> list[str]: args = findings.parser().parse_args(list(argv)) return list(findings.command(args, *paths, now)) @@ -176,3 +187,51 @@ def test_questions_act_and_taken_need_no_search(tmp_path: Path) -> None: for argv in (["add", "May I push?", "--default", "no", "--act"], ["taken", "Kept the old name.", "--default", "keep"]): # fmt: skip questions.command(questions.parser().parse_args(argv), store, NOW) + + +def main(capsys: pytest.CaptureFixture[str], *argv: str) -> list[str]: + assert findings.main(list(argv)) == 0 + return capsys.readouterr().out.splitlines() + + +@pytest.fixture +def live(paths: tuple[Path, Path], monkeypatch: pytest.MonkeyPatch) -> tuple[Path, Path]: + """`main()` with its search record moved to `tmp_path`, and the default ledger too; + patched in the loaded modules themselves, since `Tool` only forwards reads.""" + record = paths[1].with_name("q.jsonl") + monkeypatch.setattr(importlib.import_module("questions"), "store_of", lambda _root: record) + monkeypatch.setattr(importlib.import_module("u1_findings"), "LEDGER", paths[0].with_name("d")) + monkeypatch.delenv("FINDINGS_LEDGER", raising=False) + return paths + + +def test_the_variable_redirects_add_search_and_list( + live: tuple[Path, Path], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setenv("FINDINGS_LEDGER", str(live[0])) + main(capsys, "add", "A private result.", "--commit", HEAD, "--topic=x", "--ola", AGREED) + assert [e["text"] for e in entries(live)] == ["A private result."] + assert main(capsys, "search", "private")[0].startswith("1. A private result.") + assert main(capsys, "list")[0].startswith("1. A private result.") + assert live[1].exists() and not findings.LEDGER.exists() + + +def test_unset_the_ledger_stays_the_default( + live: tuple[Path, Path], capsys: pytest.CaptureFixture[str] +) -> None: + main(capsys, "add", "A public result.", "--commit", HEAD, "--topic=x", "--ola", AGREED) + assert main(capsys, "list")[0].startswith("1. A public result.") + assert findings.LEDGER.exists() and not live[0].exists() + + +def test_the_commit_is_checked_in_the_ledgers_own_repository(paths: tuple[Path, Path]) -> None: + private = paths[0].parent / "private" + git(private.parent, "init", "-q", str(private)) + git(private, "commit", "-q", "--allow-empty", "-m", "a private commit") + own = git(private, "rev-parse", "HEAD") + ledger = (private / "docs" / "findings.jsonl", paths[1]) + assert "no commit" in refused( + ledger, "add", "A.", "--commit", HEAD, "--topic=x", "--ola", AGREED + ) + run(ledger, "add", "A.", "--commit", own, "--topic=x", "--ola", AGREED) + assert [e["commit"] for e in entries(ledger)] == [own] diff --git a/tools/findings.py b/tools/findings.py index d18974bd..e0fc5ef5 100644 --- a/tools/findings.py +++ b/tools/findings.py @@ -7,8 +7,10 @@ python3 tools/findings.py replace "" --ola "" | drop -The ledger is data, `docs/findings.jsonl`, one JSON entry per fact; a missing -file is an empty ledger, and git history keeps what `replace` and `drop` remove. +The ledger is data, `docs/findings.jsonl` or the path in `FINDINGS_LEDGER`, one +JSON entry per fact; `--commit` names a commit of the repository holding the +ledger, which may be private. A missing file is an empty ledger, and git +history keeps what `replace` and `drop` remove. `add` and `replace` need `--ola`: Ola's agreement to the wording, found in this session's transcript as `brief.py --ola` finds it. Each `search` appends its time and words to `/findings-searches.jsonl`, untracked; `questions.py @@ -20,6 +22,7 @@ import argparse import json +import os import subprocess import sys from datetime import UTC, datetime, timedelta @@ -89,7 +92,8 @@ def command(args: argparse.Namespace, ledger: Path, searches: Path, now: datetim raise RefusedError(f"the ledger has no entry {args.n}") if args.cmd == "add": ola = _worded(args) - shown = subprocess.run(["git", "-C", str(ROOT), "show", "-s", "--format=%H %cs", + home = next(d for d in ledger.parents if d.is_dir()) # the ledger's own repository + shown = subprocess.run(["git", "-C", str(home), "show", "-s", "--format=%H %cs", f"{args.commit}^{{commit}}"], capture_output=True, text=True, check=False).stdout.split() # fmt: skip if len(shown) != 2: @@ -142,7 +146,8 @@ def main(argv: list[str] | None = None) -> int: try: searches = questions.store_of(ROOT).with_name(SEARCHES) - lines = command(parser().parse_args(argv), LEDGER, searches, datetime.now(UTC)) + ledger = Path(os.environ.get("FINDINGS_LEDGER", LEDGER)) + lines = command(parser().parse_args(argv), ledger, searches, datetime.now(UTC)) except RefusedError as error: print(error, file=sys.stderr) return 1