mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 10:27:11 +00:00
a16b3e1b45
Harden incremental no-cluster updates: fixes empty-write graph wipeout on no-op update --no-cluster (#1347) and git-hook subdir path resolution (#1348). Complementary to #1317. Validated: full suite 2107 passed, no-op re-run no longer wipes graph. Thanks @pkudinov.
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
"""Integration tests for incremental graphify extract behavior."""
|
|
from __future__ import annotations
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PYTHON = sys.executable
|
|
|
|
# Backend-selecting env vars. These tests assume no working LLM backend (a docs
|
|
# corpus should fail without one); strip them so a developer who has a real
|
|
# ANTHROPIC_API_KEY / OPENAI_API_KEY / etc. exported does not make a docs extract
|
|
# succeed and break the "no backend" path. CI has none of these set anyway.
|
|
_LLM_ENV_KEYS = (
|
|
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY",
|
|
"MOONSHOT_API_KEY", "DEEPSEEK_API_KEY", "OLLAMA_BASE_URL",
|
|
"AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_ACCESS_KEY_ID",
|
|
)
|
|
|
|
|
|
def _run(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
|
|
env = {k: v for k, v in os.environ.items() if k not in _LLM_ENV_KEYS}
|
|
return subprocess.run(
|
|
[PYTHON, "-m", "graphify"] + args,
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
|
|
|
|
def _make_docs_corpus(tmp_path: Path) -> Path:
|
|
docs = tmp_path / "docs"
|
|
docs.mkdir()
|
|
(docs / "intro.md").write_text("# Introduction\nThis doc introduces the system.")
|
|
(docs / "api.md").write_text("# API Reference\nThe API has endpoints.")
|
|
return docs
|
|
|
|
|
|
def test_manifest_written_after_extract(tmp_path):
|
|
"""After a full extract run, manifest.json must exist (or run fails before writing it)."""
|
|
docs = _make_docs_corpus(tmp_path)
|
|
r = _run(["extract", str(docs)], tmp_path)
|
|
# Should fail with no API key — but NOT with a path error
|
|
assert "no LLM API key" in r.stderr or r.returncode != 0
|
|
# manifest should NOT exist (run failed before writing)
|
|
manifest = docs / "graphify-out" / "manifest.json"
|
|
assert not manifest.exists()
|
|
|
|
|
|
def test_incremental_mode_detected_via_manifest(tmp_path):
|
|
"""If manifest.json + graph.json exist, incremental mode message is shown."""
|
|
docs = _make_docs_corpus(tmp_path)
|
|
out = docs / "graphify-out"
|
|
out.mkdir()
|
|
(out / "graph.json").write_text(json.dumps({"nodes": [], "links": []}))
|
|
(out / "manifest.json").write_text(json.dumps({"document": [str(docs / "intro.md")]}))
|
|
r = _run(["extract", str(docs)], tmp_path)
|
|
combined = r.stdout + r.stderr
|
|
assert "incremental" in combined.lower() or r.returncode != 0
|
|
|
|
|
|
def test_no_incremental_without_manifest(tmp_path):
|
|
"""Without manifest.json, full scan message is shown (not incremental)."""
|
|
docs = _make_docs_corpus(tmp_path)
|
|
r = _run(["extract", str(docs)], tmp_path)
|
|
# Check combined output doesn't contain incremental-mode phrasing.
|
|
# Use a phrase rather than a bare word to avoid matching the tmp_path,
|
|
# which pytest derives from the test name and contains "incremental".
|
|
assert "incremental update" not in r.stdout.lower()
|
|
assert "incremental scan" not in r.stdout.lower()
|
|
|
|
|
|
def test_extract_no_cluster_incremental_noop_preserves_existing_graph(tmp_path):
|
|
"""#1347: no-op incremental no-cluster extract must not overwrite graph.json."""
|
|
project = tmp_path / "project"
|
|
project.mkdir()
|
|
(project / "app.py").write_text(
|
|
"def alpha():\n return 1\n", encoding="utf-8"
|
|
)
|
|
|
|
first = _run(["extract", str(project), "--no-cluster"], tmp_path)
|
|
assert first.returncode == 0, first.stderr
|
|
graph_path = project / "graphify-out" / "graph.json"
|
|
before_text = graph_path.read_text(encoding="utf-8")
|
|
before = json.loads(before_text)
|
|
assert before.get("nodes"), "first run should produce a non-empty code graph"
|
|
|
|
second = _run(["extract", str(project), "--no-cluster"], tmp_path)
|
|
assert second.returncode == 0, second.stderr
|
|
|
|
after_text = graph_path.read_text(encoding="utf-8")
|
|
after = json.loads(after_text)
|
|
assert after.get("nodes"), "no-op incremental run must not empty the graph"
|
|
assert after_text == before_text
|