modified: .gitlab-ci.yml modified: bench/qa_questions.txt modified: bench/qa_sweep.py modified: bench/run.sh modified: docs/TICKETS.md modified: docs/_source/README.md modified: docs/_source/_ext/makefile_targets.py modified: docs/_source/api/cli.rst modified: docs/_source/api/distill.rst modified: docs/_source/api/mesh.rst modified: docs/_source/api/qa.rst modified: docs/_source/api/retrieval.rst modified: docs/_source/api/storage.rst modified: docs/_source/api/substrate.rst modified: docs/_source/concepts.rst modified: docs/_source/conf.py modified: docs/_source/cookbook.rst modified: docs/_source/index.rst modified: docs/_source/license.rst modified: docs/_source/quickstart.rst modified: docs/bench-maxing.md modified: docs/benchmarks.md modified: docs/cti-architecture.md modified: docs/diagrams/aborist-modules.dot modified: docs/diagrams/aborist-modules.svg modified: docs/diagrams/mesh-data-flow.dot modified: docs/diagrams/mesh-epoch-lifecycle.dot modified: docs/diagrams/mesh-epoch-lifecycle.svg modified: docs/diagrams/mesh-group-decisions.dot modified: docs/diagrams/mesh-group-decisions.svg modified: docs/diagrams/mesh-identity-stack.dot modified: docs/diagrams/mesh-secret-envelope.dot modified: docs/mesh.md modified: docs/qa-modes-bench.md modified: docs/seven-point-program.md modified: docs/tickets/ticket-000001-retrieval-keywords-audit-gap.md modified: docs/tickets/ticket-000002-reference-frame-polarity-contract.md modified: docs/tickets/ticket-000003-anchor-class-warrant.md modified: docs/tickets/ticket-000005-label-ladder-migration.md modified: docs/tickets/ticket-000006-bench-emergent-findings.md modified: docs/tickets/ticket-000007-query-layer-hyphen-fold.md modified: docs/tickets/ticket-000008-broad-quantifier-preflight-guard.md modified: docs/tickets/ticket-000009-quantifier-preflight-dag-binding.md modified: docs/tickets/ticket-000010-metacognition-preflight-guard.md modified: docs/tickets/ticket-000011-soft-preflight-hint-sidecar.md modified: scripts/backfill_concepts.py modified: scripts/bench_emergent.py modified: tests/crawler/test_async_web_fetcher.py modified: tests/crawler/test_bridge.py modified: tests/crawler/test_web_fetch.py modified: tests/test_bench_qa_sweep.py modified: tests/test_burn.py modified: tests/test_burn_doc.py modified: tests/test_claim_lattice.py modified: tests/test_cli_render.py modified: tests/test_compress.py modified: tests/test_concepts.py modified: tests/test_dag.py modified: tests/test_directives.py modified: tests/test_distill.py modified: tests/test_distill_recursive.py modified: tests/test_evict.py modified: tests/test_frame.py modified: tests/test_grok_source.py modified: tests/test_html_source.py modified: tests/test_ingest.py modified: tests/test_inspect.py modified: tests/test_journal.py modified: tests/test_keys.py modified: tests/test_llm_context_base.py modified: tests/test_merkle.py modified: tests/test_mesh.py modified: tests/test_mesh_aead.py modified: tests/test_mesh_chain.py modified: tests/test_mesh_cli.py modified: tests/test_mesh_cli_pull.py modified: tests/test_mesh_wire.py modified: tests/test_mesh_wire_e2e.py modified: tests/test_metacognition.py modified: tests/test_migration_audit_mode.py modified: tests/test_providence_source.py modified: tests/test_qa.py modified: tests/test_qa_quality_live.py modified: tests/test_quantifier_caps.py modified: tests/test_quantifier_classifier.py modified: tests/test_quantifier_phase4.py modified: tests/test_quantifier_reminder.py modified: tests/test_query.py modified: tests/test_reclassify.py modified: tests/test_repair.py modified: tests/test_resume.py modified: tests/test_snapshot.py modified: tests/test_soft_preflight.py modified: tests/test_tfidf.py modified: tests/test_vcs_source.py modified: tests/test_verify.py modified: tests/test_verify_json.py modified: tests/test_versioned_ingest.py modified: tests/test_warrant.py modified: tests/test_wikipedia_old.py modified: tests/test_wikipedia_xml.py modified: tests/test_wikitext.py
185 lines
6.4 KiB
Python
185 lines
6.4 KiB
Python
"""Tests for the git and Mercurial repo sources.
|
|
|
|
Each test builds a tiny synthetic repo via the actual `git` / `hg` CLIs in
|
|
a tmp_path, ingests it, and asserts the round-trip and supersedes-chain
|
|
behavior. If the underlying VCS isn't on PATH, the test is skipped.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from arborist.ingest import ingest_source
|
|
from arborist.sources.vcs import GitRepoSource, MercurialRepoSource
|
|
from arborist.store import connect
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _git_available() -> bool:
|
|
return shutil.which("git") is not None
|
|
|
|
|
|
def _hg_available() -> bool:
|
|
return shutil.which("hg") is not None
|
|
|
|
|
|
def _mk_git_repo(path: Path) -> None:
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
env = {
|
|
**os.environ,
|
|
# Detached author so commits are reproducible and tests don't
|
|
# accidentally pick up the host's git identity.
|
|
"GIT_AUTHOR_NAME": "test",
|
|
"GIT_AUTHOR_EMAIL": "t@example.invalid",
|
|
"GIT_COMMITTER_NAME": "test",
|
|
"GIT_COMMITTER_EMAIL": "t@example.invalid",
|
|
"GIT_AUTHOR_DATE": "2026-01-01T00:00:00Z",
|
|
"GIT_COMMITTER_DATE": "2026-01-01T00:00:00Z",
|
|
}
|
|
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=path, check=True)
|
|
subprocess.run(["git", "config", "user.email", "t@example.invalid"], cwd=path, check=True)
|
|
subprocess.run(["git", "config", "user.name", "test"], cwd=path, check=True)
|
|
(path / "README.md").write_text("# Demo\n\nHello world.\n")
|
|
(path / "src.py").write_text("def hello():\n return 'hi'\n")
|
|
(path / "logo.bin").write_bytes(b"\x00\x01\x02\x03" * 64) # binary
|
|
subprocess.run(["git", "add", "-A"], cwd=path, check=True)
|
|
subprocess.run(
|
|
["git", "commit", "-q", "-m", "initial"],
|
|
cwd=path,
|
|
check=True,
|
|
env=env,
|
|
)
|
|
|
|
|
|
def _commit_file(path: Path, name: str, body: str, msg: str) -> None:
|
|
env = {
|
|
**os.environ,
|
|
"GIT_AUTHOR_NAME": "test",
|
|
"GIT_AUTHOR_EMAIL": "t@example.invalid",
|
|
"GIT_COMMITTER_NAME": "test",
|
|
"GIT_COMMITTER_EMAIL": "t@example.invalid",
|
|
"GIT_AUTHOR_DATE": "2026-02-01T00:00:00Z",
|
|
"GIT_COMMITTER_DATE": "2026-02-01T00:00:00Z",
|
|
}
|
|
(path / name).write_text(body)
|
|
subprocess.run(["git", "add", name], cwd=path, check=True)
|
|
subprocess.run(
|
|
["git", "commit", "-q", "-m", msg], cwd=path, check=True, env=env
|
|
)
|
|
|
|
|
|
def _mk_hg_repo(path: Path) -> None:
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
subprocess.run(["hg", "init", "-q"], cwd=path, check=True)
|
|
(path / ".hg" / "hgrc").write_text(
|
|
"[ui]\nusername = test <t@example.invalid>\n"
|
|
)
|
|
(path / "README.md").write_text("# Demo\n\nHello hg.\n")
|
|
(path / "src.py").write_text("def world():\n return 'world'\n")
|
|
subprocess.run(["hg", "add"], cwd=path, check=True)
|
|
subprocess.run(
|
|
["hg", "commit", "-m", "initial", "-d", "1735689600 0"],
|
|
cwd=path,
|
|
check=True,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GitRepoSource
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.skipif(not _git_available(), reason="git not on PATH")
|
|
def test_git_source_yields_text_files_and_skips_binary(tmp_path):
|
|
repo = tmp_path / "demo-repo"
|
|
_mk_git_repo(repo)
|
|
src = GitRepoSource(repo)
|
|
docs = list(src.iter_documents())
|
|
titles = sorted(d.title for d in docs)
|
|
# Binary file (logo.bin) should not appear.
|
|
assert titles == ["README.md", "src.py"]
|
|
readme = next(d for d in docs if d.title == "README.md")
|
|
assert readme.uri == "git://demo-repo/file/README.md"
|
|
assert "Hello world" in readme.content
|
|
assert readme.source_type == "git_repo"
|
|
# Commit metadata is captured (informational; not Merkle-bound).
|
|
assert "commit_hash" in readme.extra
|
|
assert len(readme.extra["commit_hash"]) == 40
|
|
|
|
|
|
@pytest.mark.skipif(not _git_available(), reason="git not on PATH")
|
|
def test_git_source_round_trip_and_supersedes_chain(tmp_path):
|
|
repo = tmp_path / "demo-repo"
|
|
_mk_git_repo(repo)
|
|
db = tmp_path / "arborist.db"
|
|
|
|
# First ingest: 2 docs (README.md, src.py).
|
|
conn = connect(db)
|
|
try:
|
|
first = ingest_source(conn, GitRepoSource(repo))
|
|
finally:
|
|
conn.close()
|
|
assert first.inserted == 2
|
|
|
|
# Modify src.py and re-ingest. Same URI, new content -> new document_root
|
|
# plus an automatic `supersedes` edge to the prior root.
|
|
_commit_file(repo, "src.py", "def hello():\n return 'updated'\n", "edit src")
|
|
conn = connect(db)
|
|
try:
|
|
second = ingest_source(conn, GitRepoSource(repo))
|
|
finally:
|
|
conn.close()
|
|
# README.md is unchanged so its content_root collides -> skipped.
|
|
# src.py changed -> 1 new doc inserted with a supersedes edge.
|
|
assert second.inserted == 1
|
|
assert second.skipped_duplicate >= 1
|
|
|
|
conn = connect(db)
|
|
try:
|
|
edges = conn.execute(
|
|
"SELECT src_root, dst_root FROM edges WHERE edge_type='supersedes'"
|
|
).fetchall()
|
|
finally:
|
|
conn.close()
|
|
assert len(edges) == 1
|
|
|
|
|
|
@pytest.mark.skipif(not _git_available(), reason="git not on PATH")
|
|
def test_git_source_rejects_non_git_path(tmp_path):
|
|
with pytest.raises(FileNotFoundError):
|
|
GitRepoSource(tmp_path) # tmp_path has no .git
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MercurialRepoSource
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.skipif(not _hg_available(), reason="hg not on PATH")
|
|
def test_hg_source_yields_text_files(tmp_path):
|
|
repo = tmp_path / "demo-hg"
|
|
_mk_hg_repo(repo)
|
|
src = MercurialRepoSource(repo)
|
|
docs = list(src.iter_documents())
|
|
titles = sorted(d.title for d in docs)
|
|
assert titles == ["README.md", "src.py"]
|
|
readme = next(d for d in docs if d.title == "README.md")
|
|
assert readme.uri == "hg://demo-hg/file/README.md"
|
|
assert "Hello hg" in readme.content
|
|
assert readme.source_type == "hg_repo"
|
|
assert "changeset_hash" in readme.extra
|
|
|
|
|
|
@pytest.mark.skipif(not _hg_available(), reason="hg not on PATH")
|
|
def test_hg_source_rejects_non_hg_path(tmp_path):
|
|
with pytest.raises(FileNotFoundError):
|
|
MercurialRepoSource(tmp_path)
|