Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion tests/test_ingest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Tests for graphify.ingest.save_query_result"""
from __future__ import annotations
import re
import urllib.error
from pathlib import Path
import pytest
from graphify.ingest import save_query_result
from graphify.ingest import ingest, save_query_result


def test_file_created(tmp_path):
Expand Down Expand Up @@ -98,3 +99,88 @@ def test_no_outcome_means_no_outcome_section(tmp_path):
def test_invalid_outcome_rejected(tmp_path):
with pytest.raises(ValueError):
save_query_result("q", "a", tmp_path / "memory", outcome="great")


def test_ingest_webpage_writes_markdown(tmp_path, monkeypatch):
target = tmp_path / "raw"
monkeypatch.setattr("graphify.ingest.validate_url", lambda _url: None)
monkeypatch.setattr(
"graphify.ingest._fetch_webpage",
lambda _url, _author, _contributor: ("# Example", "example.md"),
)

out = ingest("https://example.com/docs", target)

assert out == target / "example.md"
assert out.read_text(encoding="utf-8") == "# Example"


def test_ingest_avoids_overwrite_with_counter(tmp_path, monkeypatch):
target = tmp_path / "raw"
target.mkdir(parents=True)
(target / "example.md").write_text("existing", encoding="utf-8")
monkeypatch.setattr("graphify.ingest.validate_url", lambda _url: None)
monkeypatch.setattr(
"graphify.ingest._fetch_webpage",
lambda _url, _author, _contributor: ("# New", "example.md"),
)

out = ingest("https://example.com/docs", target)

assert out.name == "example_1.md"
assert out.read_text(encoding="utf-8") == "# New"


def test_ingest_pdf_path_uses_binary_downloader(tmp_path, monkeypatch):
target = tmp_path / "raw"
downloaded = target / "paper.pdf"
monkeypatch.setattr("graphify.ingest.validate_url", lambda _url: None)
monkeypatch.setattr("graphify.ingest._download_binary", lambda *_a, **_k: downloaded)

out = ingest("https://example.com/paper.pdf", target)

assert out == downloaded


def test_ingest_image_without_suffix_defaults_to_jpg(tmp_path, monkeypatch):
target = tmp_path / "raw"
monkeypatch.setattr("graphify.ingest.validate_url", lambda _url: None)
monkeypatch.setattr("graphify.ingest._detect_url_type", lambda _url: "image")
captured = {}

def _fake_download(url: str, suffix: str, _target):
captured["url"] = url
captured["suffix"] = suffix
return target / "img.jpg"

monkeypatch.setattr("graphify.ingest._download_binary", _fake_download)

out = ingest("https://example.com/image", target)

assert out.name == "img.jpg"
assert captured["suffix"] == ".jpg"


def test_ingest_invalid_url_is_wrapped(tmp_path, monkeypatch):
target = tmp_path / "raw"

def _raise_invalid(_url: str) -> None:
raise ValueError("blocked")

monkeypatch.setattr("graphify.ingest.validate_url", _raise_invalid)

with pytest.raises(ValueError, match=r"^ingest: blocked$"):
ingest("javascript:alert(1)", target)


def test_ingest_fetch_errors_are_wrapped(tmp_path, monkeypatch):
target = tmp_path / "raw"
monkeypatch.setattr("graphify.ingest.validate_url", lambda _url: None)

def _raise_fetch(_url, _author, _contributor):
raise urllib.error.URLError("timeout")

monkeypatch.setattr("graphify.ingest._fetch_webpage", _raise_fetch)

with pytest.raises(RuntimeError, match=r"^ingest: failed to fetch "):
ingest("https://example.com", target)
67 changes: 67 additions & 0 deletions tests/test_prs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
_path_match,
build_community_labels,
compute_pr_impact,
fetch_prs,
fetch_pr_files,
fetch_worktrees,
format_prs_text,
attach_graph_impact,
_detect_default_branch,
)

Expand Down Expand Up @@ -331,6 +333,71 @@ def test_empty_pr_list(self):
assert "(0 on wrong base, not shown)" in out


class TestFetchPrs:
def test_fetch_prs_parses_gh_payload(self):
raw = [
{
"number": 7,
"title": "Fix auth race",
"headRefName": "fix-auth",
"baseRefName": "v8",
"author": {"login": "alice"},
"isDraft": False,
"reviewDecision": "APPROVED",
"statusCheckRollup": [{"conclusion": "SUCCESS", "status": "COMPLETED"}],
"updatedAt": "2026-07-20T10:00:00Z",
}
Comment thread
chronicgiardia marked this conversation as resolved.
]
with patch("graphify.prs._gh", return_value=raw):
prs = fetch_prs(base="v8")
assert len(prs) == 1
pr = prs[0]
assert pr.number == 7
assert pr.branch == "fix-auth"
assert pr.author == "alice"
assert pr.ci_status == "SUCCESS"
assert pr.expected_base == "v8"
assert pr.status == "APPROVED"

def test_fetch_prs_raises_when_gh_unavailable(self):
with patch("graphify.prs._gh", return_value=None):
with pytest.raises(RuntimeError, match="gh CLI not found or not authenticated"):
fetch_prs()


class TestAttachGraphImpact:
def test_attach_graph_impact_populates_actionable_prs_only(self, tmp_path):
graph = {
"nodes": [
{"id": "n1", "label": "AuthAPI", "source_file": "src/auth/api.py", "community": 2},
{"id": "n2", "label": "AuthHelper", "source_file": "src/auth/api.py", "community": 2},
{"id": "n3", "label": "Billing", "source_file": "src/billing/pay.py", "community": 7},
],
"edges": [],
}
graph_path = tmp_path / "graph.json"
graph_path.write_text(json.dumps(graph), encoding="utf-8")

actionable = make_pr(number=1, base_branch="v8", expected_base="v8")
wrong_base = make_pr(number=2, base_branch="main", expected_base="v8")
prs = [actionable, wrong_base]

def _fake_files(number: int, _repo=None) -> list[str]:
if number == 1:
return ["src/auth/api.py"]
raise AssertionError("wrong-base PR should not request diff files")

with patch("graphify.prs.fetch_pr_files", side_effect=_fake_files):
labels = attach_graph_impact(prs, graph_path)

assert labels[2] == ["AuthAPI", "AuthHelper"]
assert actionable.communities_touched == [2]
assert actionable.nodes_affected == 2
assert actionable.files_changed == ["src/auth/api.py"]
assert wrong_base.communities_touched == []
assert wrong_base.nodes_affected == 0


# ── _detect_default_branch ────────────────────────────────────────────────────

class TestDetectDefaultBranch:
Expand Down
63 changes: 63 additions & 0 deletions tests/test_serve.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for serve.py - MCP graph query helpers (no mcp package required)."""
import asyncio
import json
import pytest
import networkx as nx
Expand Down Expand Up @@ -28,6 +29,7 @@
_cut_lines_to_budget,
_load_graph,
_community_header,
_ApiKeyMiddleware,
_search_tokens,
)

Expand Down Expand Up @@ -1386,3 +1388,64 @@ def test_subgraph_to_text_honors_valid_src_tgt_direction():
out = _subgraph_to_text(G, {"caller", "callee"}, [("callee", "caller")])
edge_line = next(l for l in out.splitlines() if l.startswith("EDGE"))
assert "caller --calls" in edge_line and "--> callee" in edge_line


def _run_asgi(app, scope: dict) -> tuple[list[dict], int]:
Comment thread
chronicgiardia marked this conversation as resolved.
Outdated
sent: list[dict] = []
app_calls = 0

async def _receive() -> dict:
return {"type": "http.request", "body": b"", "more_body": False}

async def _send(message: dict) -> None:
sent.append(message)

async def _inner() -> int:
nonlocal app_calls

async def _downstream(_scope, _receive, send):
nonlocal app_calls
app_calls += 1
await send({"type": "http.response.start", "status": 204, "headers": []})
await send({"type": "http.response.body", "body": b""})

wrapped = _ApiKeyMiddleware(_downstream, api_key="s3cret")
await wrapped(scope, _receive, _send)
return app_calls

calls = asyncio.run(_inner())
return sent, calls


def test_api_key_middleware_rejects_missing_key():
scope = {"type": "http", "headers": []}
sent, calls = _run_asgi(None, scope)

assert calls == 0
assert sent[0]["type"] == "http.response.start"
assert sent[0]["status"] == 401
assert sent[1]["body"] == b'{"error": "unauthorized"}'


def test_api_key_middleware_accepts_x_api_key():
scope = {"type": "http", "headers": [(b"x-api-key", b"s3cret")]}
sent, calls = _run_asgi(None, scope)

assert calls == 1
assert sent[0]["status"] == 204


def test_api_key_middleware_accepts_case_insensitive_bearer():
scope = {"type": "http", "headers": [(b"authorization", b"bEaReR s3cret")]}
sent, calls = _run_asgi(None, scope)

assert calls == 1
assert sent[0]["status"] == 204


def test_api_key_middleware_skips_non_http_scopes():
scope = {"type": "lifespan", "headers": []}
sent, calls = _run_asgi(None, scope)

assert calls == 1
assert sent[0]["status"] == 204
Loading