import json import re import tempfile from pathlib import Path from graphify.build import build_from_json from graphify.cluster import cluster from graphify.export import to_json, to_cypher, to_graphml, to_html, to_canvas, to_obsidian FIXTURES = Path(__file__).parent / "fixtures" def make_graph(): return build_from_json(json.loads((FIXTURES / "extraction.json").read_text())) def test_to_json_creates_file(): G = make_graph() communities = cluster(G) with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "graph.json" to_json(G, communities, str(out)) assert out.exists() def test_to_json_valid_json(): G = make_graph() communities = cluster(G) with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "graph.json" to_json(G, communities, str(out)) data = json.loads(out.read_text()) assert "nodes" in data assert "links" in data def test_to_json_nodes_have_community(): G = make_graph() communities = cluster(G) with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "graph.json" to_json(G, communities, str(out)) data = json.loads(out.read_text()) for node in data["nodes"]: assert "community" in node def test_to_cypher_creates_file(): G = make_graph() with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "cypher.txt" to_cypher(G, str(out)) assert out.exists() def test_to_cypher_contains_merge_statements(): G = make_graph() with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "cypher.txt" to_cypher(G, str(out)) content = out.read_text() assert "MERGE" in content def test_to_graphml_creates_file(): G = make_graph() communities = cluster(G) with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "graph.graphml" to_graphml(G, communities, str(out)) assert out.exists() def test_to_graphml_valid_xml(): G = make_graph() communities = cluster(G) with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "graph.graphml" to_graphml(G, communities, str(out)) content = out.read_text() assert "= G.number_of_nodes() assert len(data["edges"]) >= 1 assert out.stat().st_size > 32 # ── Issue #1409: punctuation-only Obsidian/Canvas filenames ─────────────────── def _punct_graph(label: str): """A 2-node graph where one node's label is all-punctuation (e.g. a `@/*` tsconfig paths key) and the other is a normal symbol.""" return build_from_json({ "nodes": [ {"id": "n1", "label": label, "file_type": "code", "source_file": "tsconfig.json"}, {"id": "n2", "label": "AuthHandler", "file_type": "code", "source_file": "auth.ts"}, ], "edges": [], }) def test_to_obsidian_never_emits_punctuation_only_filenames(): """#1409: an all-punctuation label (e.g. `@/*`) must not produce a `@.md`-style filename — valid on disk but empty once a downstream tool re-slugs on word chars (crashes `qmd update`). It falls back to `unnamed`.""" G = _punct_graph("@/*") communities = cluster(G) with tempfile.TemporaryDirectory() as tmp: to_obsidian(G, communities, tmp) stems = [p.stem for p in Path(tmp).rglob("*.md")] assert stems, "to_obsidian wrote no notes" bad = [s for s in stems if not re.search(r"\w", s, flags=re.UNICODE)] assert not bad, f"punctuation-only filenames emitted: {bad}" assert any(s == "unnamed" or s.startswith("unnamed") for s in stems), stems def test_to_canvas_never_emits_punctuation_only_filenames(): """#1409: same guard on the canvas exporter's file-node names.""" G = _punct_graph("@") communities = cluster(G) with tempfile.TemporaryDirectory() as tmp: out = Path(tmp) / "graph.canvas" to_canvas(G, communities, str(out)) data = json.loads(out.read_text()) file_nodes = [n for n in data["nodes"] if n.get("type") == "file"] assert file_nodes, "canvas has no file nodes" bad = [n["file"] for n in file_nodes if not re.search(r"\w", Path(n["file"]).stem, flags=re.UNICODE)] assert not bad, f"punctuation-only canvas filenames: {bad}" # ── Issue #834: backup_if_protected ────────────────────────────────────────── def test_backup_no_graph_json(tmp_path): """No graph.json → no backup.""" from graphify.export import backup_if_protected assert backup_if_protected(tmp_path) is None def test_backup_no_markers(tmp_path): """graph.json present but no sentinel and no curated labels → no backup.""" from graphify.export import backup_if_protected (tmp_path / "graph.json").write_text('{"nodes":[],"links":[]}') assert backup_if_protected(tmp_path) is None def test_backup_semantic_marker(tmp_path): """graph.json + .graphify_semantic_marker → backup taken.""" from graphify.export import backup_if_protected (tmp_path / "graph.json").write_text('{"nodes":[],"links":[]}') (tmp_path / "GRAPH_REPORT.md").write_text("# Report") (tmp_path / ".graphify_semantic_marker").write_text('{"output_tokens": 1234}') result = backup_if_protected(tmp_path) assert result is not None assert result.is_dir() assert (result / "graph.json").exists() assert (result / "GRAPH_REPORT.md").exists() assert (result / ".graphify_semantic_marker").exists() def test_backup_curated_labels(tmp_path): """graph.json + non-default label in .graphify_labels.json → backup taken.""" import json from graphify.export import backup_if_protected (tmp_path / "graph.json").write_text('{"nodes":[],"links":[]}') (tmp_path / ".graphify_labels.json").write_text(json.dumps({"0": "Auth Pipeline", "1": "Community 1"})) result = backup_if_protected(tmp_path) assert result is not None def test_backup_default_labels_only(tmp_path): """All-default labels → no backup (not curated).""" import json from graphify.export import backup_if_protected (tmp_path / "graph.json").write_text('{"nodes":[],"links":[]}') (tmp_path / ".graphify_labels.json").write_text(json.dumps({"0": "Community 0", "1": "Community 1"})) assert backup_if_protected(tmp_path) is None def test_backup_same_day_no_accumulation(tmp_path): """Same content on same day returns existing backup dir without re-copying.""" from graphify.export import backup_if_protected from datetime import date (tmp_path / "graph.json").write_text('{"nodes":[],"links":[]}') (tmp_path / ".graphify_semantic_marker").write_text("{}") b1 = backup_if_protected(tmp_path) b2 = backup_if_protected(tmp_path) assert b1 is not None and b2 is not None assert b1 == b2 # same dir, no _2 accumulation assert b1.name == date.today().isoformat() def test_backup_same_day_changed_content(tmp_path): """Changed graph.json on same day overwrites the existing backup in place.""" from graphify.export import backup_if_protected from datetime import date (tmp_path / "graph.json").write_text('{"nodes":[],"links":[]}') (tmp_path / ".graphify_semantic_marker").write_text("{}") b1 = backup_if_protected(tmp_path) (tmp_path / "graph.json").write_text('{"nodes":[{"id":"x"}],"links":[]}') b2 = backup_if_protected(tmp_path) assert b1 == b2 # still one folder per day assert (b2 / "graph.json").read_text() == '{"nodes":[{"id":"x"}],"links":[]}' def test_backup_env_disable(tmp_path, monkeypatch): """GRAPHIFY_NO_BACKUP=1 disables backup entirely.""" from graphify.export import backup_if_protected monkeypatch.setenv("GRAPHIFY_NO_BACKUP", "1") (tmp_path / "graph.json").write_text('{"nodes":[],"links":[]}') (tmp_path / ".graphify_semantic_marker").write_text("{}") assert backup_if_protected(tmp_path) is None