ouroboros/tests/test_repo_diff_capture.py

904 lines
41 KiB
Python

"""#1222 — ONE acceptance repository capture, and the two identities it feeds.
The acceptance packet used to read the repository twice (so the preview and the
exact source could describe two different trees) through ``text=True`` (so a
text-classified non-UTF-8 file raised ``UnicodeDecodeError`` outside the guard
and killed the whole panel), and reported every failure as an EMPTY diff — which
reads as a clean tree.
These pin the replacement: one file-backed bytes capture per round under a real
subprocess timeout, an explicit gap for every nonzero exit/timeout/bounded
cut/undecodable run, readable text retained beside those gaps, a REDACTED text
projection (redacted WHOLE before any cut) for the reviewer, and the EXACT bytes
— including the part past the memory ceiling — streamed privately and never
published.
Every credential literal here is synthetic and assembled at runtime.
"""
from __future__ import annotations
import os
import stat
import subprocess as sp
from types import SimpleNamespace as NS
import pytest
pytestmark = pytest.mark.serial # Real Git fixtures and capture subprocesses.
def _repo(tmp_path, name="r"):
repo = tmp_path / name
repo.mkdir()
sp.run(["git", "init"], cwd=repo, check=True, capture_output=True)
return repo
def _commit(repo, message="i"):
sp.run(["git", "add", "-A"], cwd=repo, check=True, capture_output=True)
sp.run(["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", message],
cwd=repo, check=True, capture_output=True)
def _secret():
# Synthetic, assembled at runtime so this FILE holds no contiguous literal.
return "sk-" + "or-" + "v1-" + "abcdef1234567890" * 2 + "deadbeef"
def test_non_utf8_tracked_file_keeps_readable_text_and_locates_the_gap(tmp_path):
"""A latin-1 source file is text to git and bytes to us: the readable hunk
survives, the undecodable run is LOCATED, and nothing raises."""
from ouroboros.repo_diff_capture import capture_repo_diff, repo_diff_projection
repo = _repo(tmp_path)
(repo / "notes.txt").write_bytes(b"first line\nsecond line\n")
_commit(repo)
# Valid latin-1, invalid UTF-8 — git still classifies the file as text.
(repo / "notes.txt").write_bytes(b"first line\nCaf\xe9 na\xefve\nsecond line\n")
capture = capture_repo_diff(repo)
assert capture.available is True and capture.spool == ()
text, decode_gaps = repo_diff_projection(capture)
assert "notes.txt" in text and "first line" in text # readable text retained
assert decode_gaps and decode_gaps[0]["status"] == "undecodable_bytes"
assert decode_gaps[0]["byte_offset"] > 0 and decode_gaps[0]["bytes"] >= 1
assert "REPOSITORY DIFF CAPTURE GAPS" in text # the gap is stated, not implied
assert b"\xe9" in b"".join(capture.iter_raw()) # the exact bytes are still exact
assert capture.raw_size == len(b"".join(capture.iter_raw()))
def test_binary_and_mixed_diffs_keep_the_text_hunk_and_name_the_binary_file(tmp_path):
"""A mixed patch must not lose its text hunk to its binary sibling, and the
binary change is named without claiming its content."""
from ouroboros.artifacts import materialize_repo_diff_evidence
repo = _repo(tmp_path)
(repo / "code.py").write_text("x = 1\n", encoding="utf-8")
(repo / "blob.bin").write_bytes(bytes(range(256)) * 4)
_commit(repo)
(repo / "code.py").write_text("x = 2 # changed\n", encoding="utf-8")
(repo / "blob.bin").write_bytes(bytes(range(255, -1, -1)) * 4)
text, meta = materialize_repo_diff_evidence(repo, tmp_path, "binary-mixed")
assert "code.py" in text and "changed" in text # the text hunk survives
assert "blob.bin" in text # the binary change is not invisible
assert meta["complete"] is False
assert meta["capture_disclosure"]["complete"] is False
assert any(gap["status"] == "binary_content_omitted" for gap in meta["issue"]["gaps"])
assert "REPOSITORY DIFF CAPTURE GAPS" in text
def _corrupt(tmp_path, name="corrupt"):
"""A Git-REQUIRED root that Git cannot read: a broken gitfile, not a plain folder."""
folder = tmp_path / name
folder.mkdir()
(folder / "a.txt").write_text("hello\n", encoding="utf-8")
(folder / ".git").write_text("gitdir: /nonexistent/gitdir\n", encoding="utf-8")
return folder
def test_an_unreadable_repository_is_a_stated_gap_not_a_clean_tree(tmp_path):
"""`git diff HEAD` failing is UNKNOWN. Reporting "" would tell the reviewer
the tree is clean — the exact false-green #1222 is about. (A folder that is
proven plain is a different, stated fact: see the plain-folder tests.)"""
from ouroboros.artifacts import materialize_repo_diff_evidence
from ouroboros.repo_diff_capture import capture_repo_diff, proven_plain_folder
not_a_repo = _corrupt(tmp_path)
assert proven_plain_folder(not_a_repo) is False
capture = capture_repo_diff(not_a_repo)
assert capture.applicable is True and capture.available is False
assert any(gap["status"] in {"git_exit_nonzero", "git_unavailable"} for gap in capture.gaps)
text, meta = materialize_repo_diff_evidence(not_a_repo, tmp_path, "task-1")
assert meta["complete"] is False
assert meta["issue"]["status"] == "source_unavailable"
assert meta["issue"]["reason"] == "repo_diff_capture_unavailable"
assert "REPOSITORY DIFF CAPTURE GAPS" in text
assert text.strip() != ""
def test_a_missing_git_binary_is_a_stated_gap_under_the_real_subprocess_path(tmp_path, monkeypatch):
from ouroboros.repo_diff_capture import capture_repo_diff
repo = _repo(tmp_path)
monkeypatch.setenv("PATH", str(tmp_path / "nowhere"))
capture = capture_repo_diff(repo)
assert capture.available is False
assert {gap["status"] for gap in capture.gaps} == {"git_unavailable"}
def test_collect_turn_diff_reports_the_capture_gap_as_a_typed_partial_source(tmp_path):
"""The packet-level view of the same fact: an unavailable capture rides the
evidence's partial-source list instead of vanishing."""
from ouroboros.review_evidence import collect_turn_diff
state: dict = {}
text = collect_turn_diff(NS(repo_dir=_corrupt(tmp_path)), capture_meta=state)
assert state["issue"]["reason"] == "repo_diff_capture_unavailable"
assert state["capture_disclosure"]["available"] is False
assert state["capture_disclosure"]["complete"] is False
assert "REPOSITORY DIFF CAPTURE GAPS" in text
def test_a_bounded_capture_keeps_the_bytes_it_could_not_hold_in_memory(tmp_path):
"""Bounded MEMORY is not permission to shorten the truth: the section past
the ceiling stays on the private spool, the cut is disclosed with the real
total, and the retention streams the WHOLE source."""
from ouroboros.observability import posix_private_modes_supported
from ouroboros.repo_diff_capture import (
capture_repo_diff, read_private_capture, retain_private_capture,
)
repo = _repo(tmp_path)
(repo / "big.py").write_text("x = 0\n", encoding="utf-8")
_commit(repo)
(repo / "big.py").write_text("\n".join(f"v{i} = {i}" for i in range(40000)), encoding="utf-8")
capture = capture_repo_diff(repo, limit=4096)
truncation = [gap for gap in capture.gaps if gap["status"] == "capture_bytes_truncated"]
assert truncation, capture.gaps
assert truncation[0]["captured_bytes"] <= 4096 < truncation[0]["total_bytes"]
assert capture.available is True # observed the tree; partial, not unavailable
assert len(capture.section("tracked")) <= 4096
assert capture.raw_size >= truncation[0]["total_bytes"]
spooled = dict(capture.spool)["tracked"]
assert os.path.isfile(spooled)
# Windows stat modes do not describe ACL privacy; retain the POSIX check
# where it is meaningful without skipping byte retention on Windows.
if posix_private_modes_supported():
assert (os.stat(spooled).st_mode & 0o077) == 0
private = retain_private_capture(tmp_path, capture)
assert private["access"] == "host_private" and private["raw_bytes"] == capture.raw_size
assert not os.path.exists(spooled) # released once retained
raw = read_private_capture(tmp_path, private)
assert len(raw) == capture.raw_size and raw.count(b"v39999 = 39999") == 1
if posix_private_modes_supported():
assert (os.stat(private["blob_ref"]["path"]).st_mode & 0o077) == 0
def test_a_retention_that_cannot_happen_is_disclosed_never_implied(tmp_path, monkeypatch):
import ouroboros.repo_diff_capture as cap
from ouroboros.artifacts import materialize_repo_diff_evidence
from ouroboros.repo_diff_capture import capture_repo_diff
repo = _repo(tmp_path)
(repo / "big.py").write_text("x = 0\n", encoding="utf-8")
_commit(repo)
(repo / "big.py").write_text("\n".join(f"v{i} = {i}" for i in range(6000)), encoding="utf-8")
monkeypatch.setattr(cap, "write_blob_stream",
lambda *_a, **_k: (_ for _ in ()).throw(OSError("disk full")))
capture = capture_repo_diff(repo, limit=2048)
spooled = dict(capture.spool)["tracked"]
_text, meta = materialize_repo_diff_evidence(repo, tmp_path, "task-1", capture=capture)
assert meta["capture_disclosure"]["raw_retained"] is False
assert meta["capture_disclosure"]["raw_retention"]["status"] == "unavailable"
assert "private_source_ref" not in meta
assert not os.path.exists(spooled) # the spool never outlives its use
def test_the_raw_bytes_stay_private_and_the_reviewer_gets_the_masked_projection(tmp_path):
"""Two identities, never interchangeable: the exact source is retained in the
private observability CAS, and only the REDACTED text is published."""
import json
from ouroboros.artifacts import materialize_repo_diff_evidence
from ouroboros.repo_diff_capture import capture_repo_diff, read_private_capture
repo = _repo(tmp_path)
(repo / "conf.py").write_text('API_KEY = "placeholder"\n', encoding="utf-8")
_commit(repo)
secret = _secret()
(repo / "conf.py").write_text(f'API_KEY = "{secret}"\n', encoding="utf-8")
capture = capture_repo_diff(repo)
published, meta = materialize_repo_diff_evidence(repo, tmp_path, "task-1", capture=capture)
assert secret not in published and "REDACTED" in published
assert secret not in json.dumps(meta.get("capture_disclosure") or {})
private = meta["private_source_ref"]
assert private["access"] == "host_private"
assert meta["capture_disclosure"]["raw_retained"] is True
assert read_private_capture(tmp_path, private).decode("utf-8", "replace").count(secret) == 1
def test_redaction_happens_before_any_cut_so_a_secret_straddling_the_bound_never_leaks(tmp_path):
"""A presentation bound applied BEFORE redaction would split the credential
and hand the redactor an unrecognizable fragment. The projection redacts
each section whole first, so no bound can expose part of it."""
from ouroboros.repo_diff_capture import capture_repo_diff, repo_diff_projection
repo = _repo(tmp_path)
(repo / "conf.py").write_text("PREFIX = 1\n", encoding="utf-8")
_commit(repo)
secret = _secret()
(repo / "conf.py").write_text(f'PREFIX = 1\nAPI_KEY = "{secret}"\n', encoding="utf-8")
capture = capture_repo_diff(repo)
full, _ = repo_diff_projection(capture)
assert "REDACTED" in full
unredacted_offset = capture.section("tracked").decode("utf-8").index(secret)
for limit in range(unredacted_offset - 8, unredacted_offset + len(secret) + 8, 7):
text, _ = repo_diff_projection(capture, section_limits={"tracked": limit})
assert secret not in text
assert secret[:12] not in text and secret[-12:] not in text, limit
def _evidence_ctx(repo, tmp_path):
"""The real tool context the host builder reads, bound to this repo."""
from ouroboros.tools.registry import ToolRegistry
registry = ToolRegistry(repo_dir=repo, drive_root=tmp_path)
registry._ctx.task_id = "task-1"
registry._ctx.root_task_id = "task-1"
registry._ctx.task_metadata = {"root_task_id": "task-1"}
registry._ctx.task_contract = {}
return registry._ctx
def test_the_evidence_packet_publishes_the_digest_but_never_the_raw_source_ref(tmp_path):
"""The packet may say WHAT the source was (digest, size, gaps); it may not
carry a handle a reviewer, an export or a download could resolve."""
import json
from ouroboros.review_evidence import build_task_acceptance_evidence
repo = _repo(tmp_path)
(repo / "code.py").write_text("x = 1\n", encoding="utf-8")
_commit(repo)
(repo / "code.py").write_bytes(b"x = 2\n# caf\xe9\n")
evidence = build_task_acceptance_evidence(
_evidence_ctx(repo, tmp_path), drive_root=tmp_path, task_id="task-1", agent_evidence={},
)
assert evidence["repo_diff_capture"]["raw_sha256"]
assert evidence["repo_diff_capture"]["projection"] == "redacted_text"
serialized = json.dumps(evidence, ensure_ascii=False, default=str)
assert "private_source_ref" not in serialized
assert "observability_blob" not in serialized
def test_one_round_reads_the_repository_exactly_once_and_leaves_no_spool(tmp_path, monkeypatch):
"""The preview and the exact source must describe the SAME tree: two
independent reads could straddle a concurrent write."""
from ouroboros import repo_diff_capture as capture_mod
from ouroboros.review_evidence import build_task_acceptance_evidence
repo = _repo(tmp_path)
(repo / "big.py").write_text("x = 0\n", encoding="utf-8")
_commit(repo)
# Large enough that the bounded preview is cut and the exact path is owed.
(repo / "big.py").write_text("\n".join(f"v{i} = {i}" for i in range(6000)), encoding="utf-8")
captures = []
real = capture_mod.capture_repo_diff
def _counted(repo_dir, **kwargs):
capture = real(repo_dir, limit=2048, **kwargs) # force a spool so its release is observable
captures.append(capture)
return capture
monkeypatch.setattr(capture_mod, "capture_repo_diff", _counted)
evidence = build_task_acceptance_evidence(
_evidence_ctx(repo, tmp_path), drive_root=tmp_path, task_id="task-1", agent_evidence={},
)
assert len(captures) == 1, captures
assert "Section withheld" in evidence["repo_diff"]
assert evidence["repo_diff_capture"]["complete"] is False
assert captures[0].spool and not any(os.path.exists(path) for _n, path in captures[0].spool)
def test_decode_gaps_stay_bounded_on_a_pathologically_binary_source():
"""Many undecodable runs must not cost quadratic work or unbounded rows."""
from ouroboros.repo_diff_capture import _MAX_DECODE_GAPS, decode_capture_text
data = b"ok\n" + b"\xff\xfe" * 5000 + b"\ntail\n"
text, gaps = decode_capture_text(data)
assert "ok" in text and "tail" in text
assert len(gaps) <= _MAX_DECODE_GAPS + 1
assert gaps[-1]["located"] is False # the remainder is summarized, not dropped
@pytest.mark.parametrize("include_commit", [False, True])
def test_capture_sections_match_the_historical_evidence_layout(tmp_path, include_commit):
"""Existing readers find untracked files and this turn's commit by these
exact header lines; the capture owns them so both projections agree."""
from ouroboros.repo_diff_capture import capture_repo_diff, repo_diff_projection_text
repo = _repo(tmp_path)
(repo / "a.py").write_text("x = 1\n", encoding="utf-8")
_commit(repo, "base")
(repo / "feature.py").write_text("def f():\n return 1\n", encoding="utf-8")
_commit(repo, "feature")
(repo / "a.py").write_text("x = 2 # tweaked\n", encoding="utf-8")
(repo / "scratch_test.py").write_text("def test_x(): pass\n", encoding="utf-8")
text = repo_diff_projection_text(
capture_repo_diff(repo, include_recent_commit=include_commit),
)
assert "tweaked" in text
assert "Untracked working-tree files" in text and "scratch_test.py" in text
assert ("committed this turn" in text) is include_commit
assert ("feature.py" in text) is include_commit
# ── the SOURCE identity the local preparation binds to ─────────────────────
def test_repo_source_identity_is_stable_over_an_unchanged_tree_and_moves_with_its_bytes(tmp_path):
from ouroboros.repo_diff_capture import repo_source_identity
repo = _repo(tmp_path)
(repo / "a.py").write_text("x = 1\n", encoding="utf-8")
_commit(repo)
(repo / "a.py").write_bytes(b"x = 2\n# caf\xe9\n")
first = repo_source_identity(repo)
assert first == repo_source_identity(repo)
(repo / "a.py").write_text("x = 2\n# cafe\n", encoding="utf-8") # same size, new bytes
assert repo_source_identity(repo) != first
with pytest.raises(RuntimeError):
repo_source_identity(_corrupt(tmp_path)) # unreadable raises; the caller says "unknown"
def test_repo_source_identity_stays_bounded_past_its_byte_budget(tmp_path):
from ouroboros.repo_diff_capture import repo_source_identity
repo = _repo(tmp_path)
(repo / "a.py").write_text("x = 1\n", encoding="utf-8")
_commit(repo)
(repo / "huge.bin").write_bytes(b"\x00" * 3000)
(repo / "a.py").write_text("x = 3\n", encoding="utf-8")
with pytest.raises(RuntimeError, match="identity incomplete"):
repo_source_identity(repo, max_bytes=1000)
(repo / "huge.bin").write_bytes(b"\x00" * 3001)
with pytest.raises(RuntimeError, match="identity incomplete"):
repo_source_identity(repo, max_bytes=1000)
def test_staging_and_index_only_edits_do_not_change_preparation_material(tmp_path):
"""Only the source Git diff HEAD would show can reopen preparation."""
from ouroboros.repo_diff_capture import repo_source_identity
repo = _repo(tmp_path)
source = repo / "a.py"
source.write_text("x = 1\n")
_commit(repo)
clean = repo_source_identity(repo)
source.write_text("x = 2\n")
changed = repo_source_identity(repo)
assert changed != clean
sp.run(["git", "add", "a.py"], cwd=repo, check=True, capture_output=True)
assert repo_source_identity(repo) == changed
# Index still holds x=2, but the working-tree-vs-HEAD source is clean.
source.write_text("x = 1\n")
assert repo_source_identity(repo) == clean
def test_head_read_failure_is_unknown_instead_of_an_unborn_identity(tmp_path, monkeypatch):
from ouroboros import repo_diff_capture as cap
repo = _repo(tmp_path)
(repo / "a.py").write_text("x = 1\n")
_commit(repo)
original = cap._run_git_to_file
def fail_head(root, args, **kwargs):
path, gap = original(root, args, **kwargs)
if args[0] == "rev-parse":
gap = {"status": "git_timeout"}
return path, gap
monkeypatch.setattr(cap, "_run_git_to_file", fail_head)
with pytest.raises(RuntimeError, match="HEAD identity unavailable"):
cap.repo_source_identity(repo)
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
def test_unborn_repository_uses_empty_tree_for_capture_and_identity(tmp_path, object_format):
from ouroboros.repo_diff_capture import capture_repo_diff, repo_source_identity
repo = tmp_path / object_format
repo.mkdir()
sp.run(["git", "init", f"--object-format={object_format}"], cwd=repo, check=True, capture_output=True)
empty = capture_repo_diff(repo, include_recent_commit=True)
assert empty.complete and empty.raw_size == 0
initial = repo_source_identity(repo)
source = repo / "new.txt"
source.write_bytes(b"uncommitted caf\xe9\n")
changed = repo_source_identity(repo)
assert changed != initial
sp.run(["git", "add", "new.txt"], cwd=repo, check=True, capture_output=True)
assert repo_source_identity(repo) == changed # Index status is still not material.
source.write_bytes(b"working tree caf\xe9\n")
capture = capture_repo_diff(repo, include_recent_commit=True)
try:
assert capture.complete
assert b"+working tree caf\xe9" in capture.section("tracked")
assert not capture.section("commit") # An unborn repository has no recent commit.
assert repo_source_identity(repo) != changed
finally:
capture.release()
@pytest.mark.parametrize("failure", ["corrupt_ref", "missing_object", "detached_missing", "head_read_error"])
def test_git_failure_is_never_guessed_to_be_unborn(tmp_path, monkeypatch, failure):
from ouroboros import repo_diff_capture as cap
repo = _repo(tmp_path)
(repo / "tracked.txt").write_text("baseline\n")
_commit(repo)
branch = sp.check_output(["git", "symbolic-ref", "HEAD"], cwd=repo).decode().strip()
if failure == "corrupt_ref":
(repo / ".git" / branch).write_text("not an object id\n")
elif failure == "missing_object":
head = sp.check_output(["git", "rev-parse", "HEAD"], cwd=repo).decode().strip()
obj = repo / ".git" / "objects" / head[:2] / head[2:]
# Git's loose objects are read-only; Windows requires clearing that
# attribute before this fixture can deliberately remove its own object.
obj.chmod(obj.stat().st_mode | stat.S_IWRITE)
obj.unlink()
assert not obj.exists()
elif failure == "detached_missing":
(repo / ".git" / "HEAD").write_text("f" * 40 + "\n")
else:
original = cap._run_git_to_file
def fail_head(root, args, **kwargs):
path, gap = original(root, args, **kwargs)
return path, ({"status": "git_exit_nonzero", "returncode": 128}
if args[0] == "rev-parse" else gap)
monkeypatch.setattr(cap, "_run_git_to_file", fail_head)
capture = cap.capture_repo_diff(repo)
try:
assert not capture.available and capture.gaps
with pytest.raises(RuntimeError, match="HEAD identity unavailable"):
cap.repo_source_identity(repo)
finally:
capture.release()
def test_memory_cut_inside_secret_withholds_section_and_retains_full_raw(tmp_path):
from ouroboros.repo_diff_capture import capture_repo_diff, repo_diff_projection, retain_private_capture, read_private_capture
repo = _repo(tmp_path)
(repo / 'a.py').write_text('x = 1\n')
_commit(repo)
secret = _secret()
(repo / 'a.py').write_text('x = 2\n' + secret + '\n')
full = capture_repo_diff(repo)
offset = full.section('tracked').index(secret.encode())
capture = capture_repo_diff(repo, limit=offset + 12)
text, _ = repo_diff_projection(capture)
assert secret[:12] not in text
assert 'Section withheld' in text
retained = retain_private_capture(tmp_path, capture)
assert secret.encode() in read_private_capture(tmp_path, retained)
@pytest.mark.parametrize("failure", ["exit", "timeout", "unavailable"])
@pytest.mark.parametrize("secret_offset", [490, 2040])
def test_failed_git_output_never_publishes_diagnostic_or_stdout_fragments(tmp_path, monkeypatch, failure, secret_offset):
import json
from ouroboros import repo_diff_capture as cap
from ouroboros.artifacts import materialize_repo_diff_evidence
secret = _secret()
prefix = secret[:18]
diagnostic = "x" * secret_offset + secret + "\n-----BEGIN PRIVATE KEY-----\nprivate-fragment"
def failed_run(argv, **kwargs):
kwargs["stdout"].write(prefix.encode()) # interrupted in the middle of a credential
kwargs["stderr"].write(diagnostic.encode())
if failure == "timeout":
raise sp.TimeoutExpired(argv, 1, output=prefix, stderr=diagnostic)
if failure == "unavailable":
raise OSError(diagnostic)
return NS(returncode=128)
(tmp_path / ".git").mkdir() # Git-required fixture, not a proven plain folder.
monkeypatch.setattr(cap.subprocess, "run", failed_run)
capture = cap.capture_repo_diff(tmp_path)
text, meta = materialize_repo_diff_evidence(tmp_path, tmp_path, "diagnostics", capture=capture)
published = text + json.dumps(meta["capture_disclosure"])
assert prefix not in published and secret[-12:] not in published
assert "private-fragment" not in published
assert "Section withheld" in text and "CAPTURE GAPS" in text
assert not meta["complete"] and not meta["capture_disclosure"]["available"]
assert prefix.encode() in cap.read_private_capture(tmp_path, meta["private_source_ref"])
def test_gap_disclosure_is_redacted_before_presentation(tmp_path):
import json
from ouroboros.repo_diff_capture import RepoDiffCapture, capture_disclosure, repo_diff_projection
capture = RepoDiffCapture(gaps=({"section": "tracked", "status": "git_exit_nonzero",
"detail": _secret()},))
text, _ = repo_diff_projection(capture)
assert _secret() not in text + json.dumps(capture_disclosure(capture))
def test_small_complete_capture_is_retained_privately_once(tmp_path, monkeypatch):
from ouroboros import repo_diff_capture as cap
from ouroboros.review_evidence import collect_turn_diff
repo = _repo(tmp_path)
(repo / "small.txt").write_text("old\n")
_commit(repo)
(repo / "small.txt").write_text(_secret() + "\n")
writes = []
original = cap.write_blob_stream
def retain(*args, **kwargs):
ref = original(*args, **kwargs)
writes.append(ref)
return ref
monkeypatch.setattr(cap, "write_blob_stream", retain)
meta = {}
text = collect_turn_diff(NS(repo_dir=repo, drive_root=tmp_path), capture_meta=meta)
assert meta["exact_required"] is False
assert meta["capture_disclosure"]["raw_retained"] is True and len(writes) == 1
assert _secret() not in text
assert _secret().encode() in cap.read_private_capture(tmp_path, {
"blob_ref": writes[0], "raw_sha256": meta["capture_disclosure"]["raw_sha256"]})
assert not any(os.path.exists(path) for _, path in meta["capture"].spool)
@pytest.mark.parametrize("consumer", ["preview", "exact"])
def test_projection_exception_releases_spool(tmp_path, monkeypatch, consumer):
from ouroboros import repo_diff_capture as cap
from ouroboros.artifacts import materialize_repo_diff_evidence
from ouroboros.review_evidence import collect_turn_diff
repo = _repo(tmp_path)
(repo / "a.txt").write_text("old\n")
_commit(repo)
(repo / "a.txt").write_text("new\n")
capture = cap.capture_repo_diff(repo, limit=1)
assert capture.spool
monkeypatch.setattr(cap, "capture_repo_diff", lambda *_a, **_k: capture)
monkeypatch.setattr(cap, "repo_diff_projection",
lambda *_a, **_k: (_ for _ in ()).throw(ValueError("projection unavailable")))
with pytest.raises(ValueError, match="projection unavailable"):
if consumer == "preview":
collect_turn_diff(NS(repo_dir=repo, drive_root=tmp_path), capture_meta={})
else:
materialize_repo_diff_evidence(repo, tmp_path, "cleanup", capture=capture)
assert not any(os.path.exists(path) for _, path in capture.spool)
def test_capture_without_exact_consumer_retains_then_releases_large_source(tmp_path, monkeypatch):
from ouroboros import repo_diff_capture as cap
from ouroboros.review_evidence import collect_turn_diff
repo = _repo(tmp_path)
(repo / "a.txt").write_text("old\n")
_commit(repo)
(repo / "a.txt").write_text("new\n")
capture = cap.capture_repo_diff(repo, limit=1)
calls = []
original = cap.retain_private_capture
monkeypatch.setattr(cap, "capture_repo_diff", lambda *_a, **_k: capture)
def retained(*args, **kwargs):
ref = original(*args, **kwargs)
calls.append(ref)
return ref
monkeypatch.setattr(cap, "retain_private_capture", retained)
collect_turn_diff(NS(repo_dir=repo, drive_root=tmp_path))
assert len(calls) == 1 and calls[0]["blob_ref"]
assert not any(os.path.exists(path) for _, path in capture.spool)
# ── diff-marked assignments and private-key blocks (Astra findings, #1222 follow-up) ──
def _opaque(tag):
# Synthetic and WITHOUT a provider prefix: only the assignment rule can catch it.
return f"opaque-{tag}-" + "0123456789abcdef" * 2
def _pem():
# Synthetic key material, assembled at runtime so this FILE holds no contiguous block.
body = "\n".join(["MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC"] * 3)
return "-----BEGIN " + "PRIVATE KEY-----\n" + body + "\n-----END " + "PRIVATE KEY-----\n"
def test_diff_marked_assignments_are_redacted_while_the_raw_bytes_keep_both_values(tmp_path):
"""A changed line starts with its diff marker, not with the key: ``+API_KEY = ...``
and ``-API_KEY = ...`` used to pass the assignment rule untouched."""
from ouroboros.repo_diff_capture import (
capture_repo_diff, read_private_capture, repo_diff_projection, retain_private_capture,
)
repo = _repo(tmp_path)
old, new = _opaque("old"), _opaque("new")
(repo / "conf.py").write_text(f'API_KEY = "{old}"\n', encoding="utf-8")
_commit(repo)
(repo / "conf.py").write_text(f'API_KEY = "{new}"\n', encoding="utf-8")
capture = capture_repo_diff(repo)
raw = capture.section("tracked")
assert f'-API_KEY = "{old}"'.encode() in raw and f'+API_KEY = "{new}"'.encode() in raw
text, _ = repo_diff_projection(capture)
assert old not in text and new not in text
assert '-API_KEY = "***REDACTED***"' in text and '+API_KEY = "***REDACTED***"' in text
assert "+++ b/conf.py" in text and "--- a/conf.py" in text # file headers untouched
private = read_private_capture(tmp_path, retain_private_capture(tmp_path, capture))
assert old.encode() in private and new.encode() in private # exact bytes stay private
def test_an_unmarked_context_line_and_a_marked_line_redact_alike(tmp_path):
from ouroboros.repo_diff_capture import capture_repo_diff, repo_diff_projection
repo = _repo(tmp_path)
kept, added = _opaque("kept"), _opaque("added")
(repo / "conf.py").write_text(f'TOKEN = "{kept}"\nx = 1\n', encoding="utf-8")
_commit(repo)
(repo / "conf.py").write_text(f'TOKEN = "{kept}"\nx = 1\nSECRET = "{added}"\n', encoding="utf-8")
text, _ = repo_diff_projection(capture_repo_diff(repo))
assert kept not in text and added not in text
assert text.count("***REDACTED***") == 2
def test_a_private_key_added_in_a_diff_is_masked_whole_before_any_cut(tmp_path):
from ouroboros.repo_diff_capture import (
capture_repo_diff, read_private_capture, repo_diff_projection, retain_private_capture,
)
repo = _repo(tmp_path)
(repo / "conf.py").write_text("x = 1\n", encoding="utf-8")
_commit(repo)
pem = _pem()
(repo / "conf.py").write_text("x = 1\n" + pem, encoding="utf-8")
capture = capture_repo_diff(repo)
material = pem.splitlines()[1]
assert material.encode() in capture.section("tracked")
text, _ = repo_diff_projection(capture)
assert material not in text and "PRIVATE KEY" not in text and "***REDACTED***" in text
offset = capture.section("tracked").decode("utf-8").index(material)
for limit in range(offset - 8, offset + len(material) + 8, 7):
cut, _ = repo_diff_projection(capture, section_limits={"tracked": limit})
assert material not in cut and material[:12] not in cut and "PRIVATE KEY" not in cut, limit
private = read_private_capture(tmp_path, retain_private_capture(tmp_path, capture))
assert all(line.encode() in private for line in pem.splitlines())
# ── private temp custody on construction failure ──
def test_a_failed_second_spool_file_closes_and_removes_the_first(tmp_path, monkeypatch):
import time
from ouroboros import repo_diff_capture as cap
created = []
real = cap.tempfile.mkstemp
def one_then_fail(*args, **kwargs):
if created:
raise OSError("no more temp files")
fd, path = real(*args, **kwargs)
created.append((fd, path))
return fd, path
monkeypatch.setattr(cap.tempfile, "mkstemp", one_then_fail)
with pytest.raises(OSError, match="no more temp files"):
cap._run_git_to_file(tmp_path, ["status"], deadline=time.monotonic() + 5)
fd, path = created[0]
assert not os.path.exists(path)
with pytest.raises(OSError):
os.fstat(fd) # the descriptor was closed, not leaked
def test_a_later_section_failure_releases_the_spool_of_an_earlier_section(tmp_path, monkeypatch):
from ouroboros import repo_diff_capture as cap
repo = _repo(tmp_path)
(repo / "a.txt").write_text("old\n")
_commit(repo)
(repo / "a.txt").write_text("new " * 64 + "\n")
original = cap._run_git_to_file
paths = []
def fail_untracked(root, args, **kwargs):
if args[0] == "ls-files":
raise OSError("temp files exhausted")
path, gap = original(root, args, **kwargs)
paths.append(path)
return path, gap
monkeypatch.setattr(cap, "_run_git_to_file", fail_untracked)
with pytest.raises(OSError, match="temp files exhausted"):
cap.capture_repo_diff(repo, limit=1) # the tracked section exceeds the ceiling and is spooled
assert paths and not any(os.path.exists(path) for path in paths)
# ── a proven plain folder: no baseline (never a clean tree), never a parent repository ──
def _plain(root, name="plain"):
folder = root / name
folder.mkdir()
(folder / "a.txt").write_bytes(b"hello caf\xe9\n")
(folder / "sub").mkdir()
(folder / "sub" / "b.py").write_text("x = 1\n", encoding="utf-8")
return folder
def test_a_proven_plain_folder_is_not_applicable_complete_and_never_calls_git(tmp_path, monkeypatch):
"""`.git` and `HEAD` both absent by lstat: no baseline exists, so the capture is
applicable=False and complete — a stated non-applicability, never an empty diff,
never a gap and never a `source_unavailable` partial — and no Git process runs."""
from ouroboros import repo_diff_capture as cap
from ouroboros.artifacts import materialize_repo_diff_evidence
from ouroboros.review_evidence import collect_turn_diff
plain = _plain(tmp_path)
monkeypatch.setattr(cap, "_run_git_to_file", lambda *_a, **_k: pytest.fail("git was run on a plain folder"))
assert cap.proven_plain_folder(plain) is True
capture = cap.capture_repo_diff(plain, include_recent_commit=True)
assert capture.applicable is False and capture.available and capture.complete and not capture.gaps
text, gaps = cap.repo_diff_projection(capture)
assert "NOT APPLICABLE" in text and "NOT a clean-tree claim" in text and gaps == []
disclosure = cap.capture_disclosure(capture)
assert disclosure["applicable"] is False and disclosure["complete"] is True and disclosure["gaps"] == []
state: dict = {}
preview = collect_turn_diff(NS(repo_dir=plain), capture_meta=state)
assert "NOT APPLICABLE" in preview and "issue" not in state and state["exact_required"] is False
assert state["capture_disclosure"]["applicable"] is False
exact, meta = materialize_repo_diff_evidence(plain, tmp_path, "task-1")
assert meta["complete"] is True and "issue" not in meta and "NOT APPLICABLE" in exact
assert meta["capture_disclosure"]["applicable"] is False
def test_a_plain_folder_packet_refuses_nothing_and_states_no_baseline(tmp_path):
"""The full evidence over a plain workspace carries no `source_unavailable`
partial, so the zero-physical refusal is empty — acceptance may run — while the
packet states that no baseline exists rather than presenting a clean tree."""
from ouroboros.review_dispatch import task_acceptance_zero_physical_refusal
from ouroboros.review_evidence import build_task_acceptance_evidence
plain = _plain(tmp_path)
evidence = build_task_acceptance_evidence(
_evidence_ctx(plain, tmp_path), drive_root=tmp_path, task_id="task-1", agent_evidence={},
)
assert evidence["repo_diff_capture"]["applicable"] is False
assert evidence["repo_diff_capture"]["complete"] is True
assert "NOT APPLICABLE" in evidence["repo_diff"]
assert not [row for row in evidence.get("__unresolved_partial_artifacts__") or []
if row.get("status") == "source_unavailable"]
assert task_acceptance_zero_physical_refusal(evidence) == {}
def test_a_plain_folder_inside_a_repository_never_leaks_its_parent(tmp_path, monkeypatch):
"""The selected root alone decides: a plain subfolder of a checkout has no
`.git`/`HEAD` of its own, so it is captured and identified as a plain folder;
the parent repository is neither discovered by Git nor part of the identity."""
from ouroboros import repo_diff_capture as cap
parent = _repo(tmp_path)
(parent / "outside.txt").write_text("parent\n", encoding="utf-8")
_commit(parent)
plain = _plain(parent)
monkeypatch.setattr(cap, "_run_git_to_file", lambda *_a, **_k: pytest.fail("git was run on a plain folder"))
assert cap.capture_repo_diff(plain).applicable is False
first = cap.repo_source_identity(plain)
(parent / "outside.txt").write_text("parent moved\n", encoding="utf-8") # not this folder's material
assert cap.repo_source_identity(plain) == first
(plain / "a.txt").write_bytes(b"hello cafe\n") # same size, new bytes
assert cap.repo_source_identity(plain) != first
def test_a_plain_folder_identity_is_content_bounded_and_symlink_safe(tmp_path):
"""Deterministic over the same bytes; moved by a same-size edit, an added or a
removed file; a symlink (a directory one included) is its target and is never
traversed; a nested repository's `.git` internals are skipped; a special file
or an inventory past its bounds RAISES (unknown), never a size/mtime proxy."""
from ouroboros.repo_diff_capture import repo_source_identity
plain = _plain(tmp_path)
first = repo_source_identity(plain)
assert first == repo_source_identity(plain)
(plain / "sub" / "b.py").write_text("x = 2\n", encoding="utf-8") # same size, new bytes
edited = repo_source_identity(plain)
assert edited != first
(plain / "c.txt").write_text("new\n", encoding="utf-8")
added = repo_source_identity(plain)
assert added != edited
(plain / "c.txt").unlink()
assert repo_source_identity(plain) == edited
target = tmp_path / "elsewhere"
target.mkdir()
(target / "t.txt").write_text("t\n", encoding="utf-8")
os.symlink(target, plain / "link") # a directory symlink
linked = repo_source_identity(plain)
assert linked != edited
(target / "t.txt").write_text("changed behind the link\n", encoding="utf-8")
assert repo_source_identity(plain) == linked # never traversed
nested = _repo(plain, "nested")
(nested / "n.txt").write_text("n\n", encoding="utf-8")
with_nested = repo_source_identity(plain)
assert with_nested != linked
(nested / ".git" / "touched").write_text("internals moved\n", encoding="utf-8")
assert repo_source_identity(plain) == with_nested # `.git` internals are not content
with pytest.raises(RuntimeError, match="identity incomplete"):
repo_source_identity(plain, max_files=2)
with pytest.raises(RuntimeError, match="identity incomplete"):
repo_source_identity(plain, max_bytes=4)
if hasattr(os, "mkfifo"):
os.mkfifo(plain / "pipe")
with pytest.raises(RuntimeError, match="identity incomplete"):
repo_source_identity(plain)
def test_any_git_marker_keeps_a_folder_git_required(tmp_path):
"""Any `.git` or `HEAD` entry — an empty directory, a broken gitfile, a bare
HEAD — is NOT a plain folder: the capture stays unavailable with its typed gap
and the identity raises (unknown); so do a missing root and a file."""
from ouroboros.repo_diff_capture import capture_repo_diff, proven_plain_folder, repo_source_identity
for name, marker, content in (("gitfile", ".git", "gitdir: /nonexistent\n"),
("gitdir", ".git", None), ("bare", "HEAD", "ref: refs/heads/main\n")):
folder = tmp_path / name
folder.mkdir()
(folder / "a.txt").write_text("hello\n", encoding="utf-8")
if content is None:
(folder / marker).mkdir()
else:
(folder / marker).write_text(content, encoding="utf-8")
assert proven_plain_folder(folder) is False, name
capture = capture_repo_diff(folder)
assert capture.applicable is True and capture.available is False, name
with pytest.raises(RuntimeError):
repo_source_identity(folder)
assert proven_plain_folder(tmp_path / "missing") is False
assert proven_plain_folder(tmp_path / "gitfile" / "a.txt") is False
def test_git_retargeting_environment_never_captures_a_foreign_repository(tmp_path, monkeypatch):
"""`GIT_DIR`/`GIT_WORK_TREE` in the host environment would make git read a
FOREIGN repository from the selected root (or turn a plain folder into one): the
capture scrubs them, so the selected root alone is captured and identified."""
from ouroboros.repo_diff_capture import capture_repo_diff, repo_source_identity
selected = _repo(tmp_path, "selected")
(selected / "mine.txt").write_text("mine\n", encoding="utf-8")
_commit(selected)
(selected / "mine.txt").write_text("mine changed\n", encoding="utf-8")
identity = repo_source_identity(selected)
foreign = _repo(tmp_path, "foreign")
(foreign / "theirs.txt").write_text("theirs\n", encoding="utf-8")
_commit(foreign)
(foreign / "theirs.txt").write_text("theirs changed\n", encoding="utf-8")
monkeypatch.setenv("GIT_DIR", str(foreign / ".git"))
monkeypatch.setenv("GIT_WORK_TREE", str(foreign))
capture = capture_repo_diff(selected)
try:
assert capture.complete
assert b"+mine changed" in capture.section("tracked") and b"theirs" not in capture.section("tracked")
finally:
capture.release()
assert repo_source_identity(selected) == identity
assert capture_repo_diff(_plain(tmp_path)).applicable is False # still plain, not the foreign tree