feat(docs): save chat answers as docs — edit screen, commit + push to the .env docs branch
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""Unit tests: docs-push service (phase 59, task 03).
|
||||
|
||||
``push_document`` is exercised against a **real local git repo** — a
|
||||
bare origin in ``tmp_path`` plus the working clones the service creates
|
||||
itself — and every result assertion reads the bare repo's state
|
||||
directly (``git show <branch>:<path>``, ``git rev-list``), not the
|
||||
return value alone. The remote is a plain local path, so no network is
|
||||
ever involved.
|
||||
|
||||
The module skips (``pytest.skip``) when ``git --version`` fails — a
|
||||
machine without git must not see hard failures.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.docs_push import DocsPushError, push_document
|
||||
|
||||
BASE = "main"
|
||||
BRANCH = "bor-docs"
|
||||
REL = "docs/note.md"
|
||||
IDENTITY = ("-c", "commit.gpgsign=false", "-c", "user.name=Test", "-c", "user.email=t@example.com")
|
||||
|
||||
|
||||
def _git_available() -> bool:
|
||||
try:
|
||||
proc = subprocess.run(["git", "--version"], capture_output=True, check=False)
|
||||
return proc.returncode == 0
|
||||
except (FileNotFoundError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def _require_git() -> None:
|
||||
"""Skip the whole module when the git CLI is missing."""
|
||||
if not _git_available():
|
||||
pytest.skip("git is not available on this machine")
|
||||
|
||||
|
||||
def _git(cwd: Path, *argv: str) -> str:
|
||||
"""Run git for the tests themselves (setup + assertions); loud on failure."""
|
||||
proc = subprocess.run(["git", *argv], cwd=cwd, capture_output=True, text=True, check=False)
|
||||
assert proc.returncode == 0, f"git {' '.join(argv)} failed: {proc.stderr}"
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def _push(bare: Path, work: Path, content: str, message: str = "docs: note") -> tuple[str, str]:
|
||||
"""push_document against the fixture bare repo (plain local path)."""
|
||||
return push_document(
|
||||
repo=str(bare),
|
||||
base_branch=BASE,
|
||||
branch=BRANCH,
|
||||
work_dir=str(work),
|
||||
rel_path=REL,
|
||||
content=content,
|
||||
commit_message=message,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def bare_repo(tmp_path: Path) -> Path:
|
||||
"""A bare origin seeded with one commit on ``main`` (``README.md``)."""
|
||||
bare = tmp_path / "bare.git"
|
||||
_git(tmp_path, "init", "--bare", str(bare))
|
||||
seed = tmp_path / "seed"
|
||||
_git(tmp_path, "clone", str(bare), str(seed))
|
||||
(seed / "README.md").write_text("# docs\n", encoding="utf-8")
|
||||
_git(seed, "checkout", "-B", BASE)
|
||||
_git(seed, *IDENTITY, "add", "README.md")
|
||||
_git(seed, *IDENTITY, "commit", "-m", "seed README")
|
||||
_git(seed, "push", "origin", BASE)
|
||||
return bare
|
||||
|
||||
|
||||
def test_first_push_creates_branch_and_returns_sha(bare_repo: Path, tmp_path: Path) -> None:
|
||||
"""First push: clones the base, creates the branch, lands the file."""
|
||||
work = tmp_path / "work" # absent — push_document clones it
|
||||
branch, sha = _push(bare_repo, work, "# Note\n\nbody one\n")
|
||||
|
||||
assert branch == BRANCH
|
||||
assert (work / ".git").is_dir()
|
||||
assert (work / REL).read_text(encoding="utf-8") == "# Note\n\nbody one\n"
|
||||
# The file lands on the branch of the BARE repo, at the returned sha.
|
||||
assert _git(bare_repo, "show", f"{BRANCH}:{REL}") == "# Note\n\nbody one\n"
|
||||
assert _git(bare_repo, "rev-parse", BRANCH).strip() == sha
|
||||
assert len(sha) == 40
|
||||
# Exactly one commit beyond main.
|
||||
assert _git(bare_repo, "rev-list", "--count", f"main..{BRANCH}").strip() == "1"
|
||||
# Fixed per-invocation identity + message (no global git config reliance).
|
||||
ident = _git(bare_repo, "log", "-1", BRANCH, "--format=%an <%ae>").strip()
|
||||
assert ident == "Brain of Reese <bor@local>"
|
||||
assert _git(bare_repo, "log", "-1", BRANCH, "--format=%s").strip() == "docs: note"
|
||||
|
||||
|
||||
def test_second_push_fast_forwards_same_branch(bare_repo: Path, tmp_path: Path) -> None:
|
||||
"""Second push (edited content, same path): fast-forward, 2 commits."""
|
||||
work = tmp_path / "work"
|
||||
sha1 = _push(bare_repo, work, "v1\n")[1]
|
||||
sha2 = _push(bare_repo, work, "v2 edited\n")[1]
|
||||
|
||||
assert sha1 != sha2
|
||||
assert _git(bare_repo, "show", f"{BRANCH}:{REL}") == "v2 edited\n"
|
||||
assert _git(bare_repo, "rev-list", "--count", f"main..{BRANCH}").strip() == "2"
|
||||
# Fast-forward, no force: the first commit is still an ancestor.
|
||||
_git(bare_repo, "merge-base", "--is-ancestor", sha1, sha2)
|
||||
|
||||
|
||||
def test_fresh_checkout_reattaches_onto_remote_branch(bare_repo: Path, tmp_path: Path) -> None:
|
||||
"""An absent checkout re-attaches onto the existing remote branch
|
||||
(its history) so the push still fast-forwards."""
|
||||
work1 = tmp_path / "work1"
|
||||
sha1 = _push(bare_repo, work1, "v1\n")[1]
|
||||
work2 = tmp_path / "work2" # different dir — push_document clones anew
|
||||
branch, sha2 = _push(bare_repo, work2, "v2\n")
|
||||
|
||||
assert branch == BRANCH
|
||||
assert _git(bare_repo, "rev-list", "--count", f"main..{BRANCH}").strip() == "2"
|
||||
assert _git(bare_repo, "rev-parse", BRANCH).strip() == sha2
|
||||
# work2's commit sits on work1's commit (re-attach, not a fork).
|
||||
_git(bare_repo, "merge-base", "--is-ancestor", sha1, sha2)
|
||||
|
||||
|
||||
def test_concurrently_advanced_remote_fails_loudly(bare_repo: Path, tmp_path: Path) -> None:
|
||||
"""Remote advanced by a second clone → the first clone's push is a
|
||||
non-fast-forward: DocsPushError carrying git's stderr, remote kept."""
|
||||
work_a = tmp_path / "work_a"
|
||||
_push(bare_repo, work_a, "from A\n")
|
||||
|
||||
# A second clone advances the branch on the bare repo.
|
||||
work_b = tmp_path / "work_b"
|
||||
_git(tmp_path, "clone", "--depth", "1", "--branch", BRANCH, str(bare_repo), str(work_b))
|
||||
(work_b / "docs" / "other.md").write_text("from B\n", encoding="utf-8")
|
||||
_git(work_b, *IDENTITY, "add", "docs/other.md")
|
||||
_git(work_b, *IDENTITY, "commit", "-m", "docs: other")
|
||||
_git(work_b, "push", "origin", BRANCH)
|
||||
remote_tip_before = _git(bare_repo, "rev-parse", BRANCH).strip()
|
||||
|
||||
with pytest.raises(DocsPushError) as excinfo:
|
||||
_push(bare_repo, work_a, "from A again\n")
|
||||
|
||||
msg = str(excinfo.value)
|
||||
# git's stderr is surfaced (the non-fast-forward refusal).
|
||||
assert "non-fast-forward" in msg
|
||||
assert "rejected" in msg
|
||||
# The remote branch was NOT touched (no force-push, no merge).
|
||||
assert _git(bare_repo, "rev-parse", BRANCH).strip() == remote_tip_before
|
||||
assert _git(bare_repo, "show", f"{BRANCH}:{REL}") == "from A\n"
|
||||
|
||||
|
||||
def test_missing_repo_path_fails_loudly(tmp_path: Path) -> None:
|
||||
"""No such repo → DocsPushError naming the failed git step."""
|
||||
with pytest.raises(DocsPushError, match="git clone .* failed"):
|
||||
push_document(
|
||||
repo=str(tmp_path / "no-such-repo"),
|
||||
base_branch=BASE,
|
||||
branch=BRANCH,
|
||||
work_dir=str(tmp_path / "w"),
|
||||
rel_path=REL,
|
||||
content="x\n",
|
||||
commit_message="docs: x",
|
||||
)
|
||||
# No fake checkout is left behind.
|
||||
assert not (tmp_path / "w" / ".git").exists()
|
||||
|
||||
|
||||
def test_non_repo_dir_fails_loudly(tmp_path: Path) -> None:
|
||||
"""A plain directory (not a git repo) as the remote → DocsPushError."""
|
||||
plain = tmp_path / "plain"
|
||||
plain.mkdir()
|
||||
(plain / "file.txt").write_text("not a repo\n", encoding="utf-8")
|
||||
with pytest.raises(DocsPushError, match="failed"):
|
||||
push_document(
|
||||
repo=str(plain),
|
||||
base_branch=BASE,
|
||||
branch=BRANCH,
|
||||
work_dir=str(tmp_path / "w"),
|
||||
rel_path=REL,
|
||||
content="x\n",
|
||||
commit_message="docs: x",
|
||||
)
|
||||
|
||||
|
||||
def test_unsafe_rel_path_is_refused_before_any_git(bare_repo: Path, tmp_path: Path) -> None:
|
||||
"""The defensive parts re-assertion refuses traversal paths."""
|
||||
for bad in ("../evil.md", "/etc/passwd", "a/b/../c.md"):
|
||||
with pytest.raises(DocsPushError, match="unsafe rel_path"):
|
||||
push_document(
|
||||
repo=str(bare_repo),
|
||||
base_branch=BASE,
|
||||
branch=BRANCH,
|
||||
work_dir=str(tmp_path / "w"),
|
||||
rel_path=bad,
|
||||
content="x\n",
|
||||
commit_message="docs: x",
|
||||
)
|
||||
# No checkout was even attempted.
|
||||
assert not (tmp_path / "w").exists()
|
||||
Reference in New Issue
Block a user