feat(wiki): sync upstream v2026.10.4 core maintenance

Selectively port source hashing, locked manifest/memory writes, path-aware
link lint and graph analysis, and revision-checked staging from upstream
4f457e154653756b04802d5aeb25c20f42b4770c. Record source hashes and exclusions.

Preserve local archive/catalog citations, curated manifest metadata, config
location, file-index and MCP behavior. Wire maintenance guidance into four
skills without migrating vault contents or changing running services.

Validation: 609 tests passed, including existing MCP/file-asset regressions
and imported upstream maintenance tests. Read-only vault status left shared
metadata hashes unchanged. Wiki MCP remains inactive; orders MCP is active.
This commit is contained in:
Codex
2026-10-31 12:39:48 +00:00
parent 1dd5d8d753
commit 82890606aa
35 changed files with 12632 additions and 44 deletions
+716
View File
@@ -0,0 +1,716 @@
"""Tests for vault graph analysis: community detection, god nodes, surprising connections."""
from __future__ import annotations
import json
import subprocess
import sys
import textwrap
from pathlib import Path
import pytest
from obsidian_wiki.graph_analysis import (
analyse_vault,
dead_ends,
detect_communities_greedy,
god_nodes,
isolated,
parse_vault_graph,
surprising_connections,
)
# ---------------------------------------------------------------------------
# Fixtures — synthetic vault
# ---------------------------------------------------------------------------
@pytest.fixture
def vault(tmp_path):
v = tmp_path / "vault"
v.mkdir()
return v
def _page(vault: Path, name: str, links: list[str], tags: list[str] | None = None) -> Path:
"""Write a minimal wiki page with wikilinks."""
lines = ["---", f"title: {name}"]
if tags:
lines.append(f"tags: [{', '.join(tags)}]")
lines += ["---", f"# {name}", ""]
lines += [f"[[{lnk}]]" for lnk in links]
p = vault / f"{name}.md"
p.write_text("\n".join(lines) + "\n")
return p
@pytest.fixture
def simple_vault(vault):
"""
A → B → C
A → C
D (isolated)
E → F (dead-end cluster)
"""
_page(vault, "a", ["b", "c"], tags=["concepts"])
_page(vault, "b", ["c"], tags=["concepts"])
_page(vault, "c", [], tags=["references"])
_page(vault, "d", [], tags=["entities"])
_page(vault, "e", ["f"])
_page(vault, "f", [])
return vault
# ---------------------------------------------------------------------------
# parse_vault_graph
# ---------------------------------------------------------------------------
class TestParseVaultGraph:
def test_reads_wikilinks(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert "b" in outgoing["a"]
assert "c" in outgoing["a"]
def test_all_pages_present_as_keys(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert set(outgoing.keys()) == {"a", "b", "c", "d", "e", "f"}
def test_reads_tags(self, simple_vault):
_, tags = parse_vault_graph(simple_vault)
assert "concepts" in tags.get("a", [])
def test_empty_vault(self, vault):
outgoing, tags = parse_vault_graph(vault)
assert outgoing == {}
def test_self_links_ignored(self, vault):
_page(vault, "selfref", ["selfref"])
outgoing, _ = parse_vault_graph(vault)
assert "selfref" not in outgoing.get("selfref", [])
def test_links_to_nonexistent_pages_excluded(self, vault):
_page(vault, "orphan", ["doesnotexist"])
outgoing, _ = parse_vault_graph(vault)
assert outgoing["orphan"] == []
def test_archive_wikilink_is_not_an_edge_to_same_stem(self, vault):
_page(vault, "attention", ["other"])
_page(vault, "other", ["attention"])
arch = vault / "_raw" / "_archived" / "attention.md"
arch.parent.mkdir(parents=True)
arch.write_text("clip\n", encoding="utf-8")
cited = vault / "cited.md"
cited.write_text(
"---\ntitle: cited\nsnapshots:\n - [[_raw/_archived/attention]]\n---\n# cited\n",
encoding="utf-8",
)
outgoing, _ = parse_vault_graph(vault)
assert "attention" not in outgoing.get("cited", [])
def test_archive_markdown_snapshot_is_not_an_edge_to_same_stem(self, vault):
_page(vault, "attention", ["other"])
_page(vault, "other", ["attention"])
arch = vault / "_raw" / "_archived" / "attention.md"
arch.parent.mkdir(parents=True)
arch.write_text("clip\n", encoding="utf-8")
cited = vault / "cited.md"
cited.write_text(
"---\ntitle: cited\nsnapshots:\n"
' - "[attention](_raw/_archived/attention.md)"\n---\n# cited\n',
encoding="utf-8",
)
outgoing, _ = parse_vault_graph(vault)
assert "attention" not in outgoing.get("cited", [])
# ---------------------------------------------------------------------------
# god_nodes
# ---------------------------------------------------------------------------
class TestGodNodes:
def test_c_is_top_hub(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
result = god_nodes(outgoing, top_n=3)
# c has 2 in-links (from a and b), so should be in top 3
top_pages = {r["page"] for r in result}
assert "c" in top_pages
def test_degree_sum(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
result = god_nodes(outgoing)
for node in result:
assert node["degree"] == node["in_degree"] + node["out_degree"]
def test_respects_top_n(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
result = god_nodes(outgoing, top_n=2)
assert len(result) <= 2
# ---------------------------------------------------------------------------
# dead_ends / isolated
# ---------------------------------------------------------------------------
class TestDeadEndsIsolated:
def test_dead_ends(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
de = dead_ends(outgoing)
assert "c" in de
assert "f" in de
assert "a" not in de
def test_isolated(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
iso = isolated(outgoing)
assert "d" in iso
assert "a" not in iso
assert "c" not in iso # c has incoming links so not isolated
# ---------------------------------------------------------------------------
# Community detection
# ---------------------------------------------------------------------------
class TestCommunityDetection:
def test_returns_list_of_sets(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
comms = detect_communities_greedy(outgoing)
assert isinstance(comms, list)
for c in comms:
assert isinstance(c, set)
def test_all_nodes_assigned(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
comms = detect_communities_greedy(outgoing)
all_nodes = set(outgoing.keys())
assigned = set()
for c in comms:
assigned |= c
assert assigned == all_nodes
def test_no_overlap(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
comms = detect_communities_greedy(outgoing)
seen = set()
for c in comms:
assert c.isdisjoint(seen), "Node appears in two communities"
seen |= c
def test_connected_cluster_grouped(self, vault):
# a-b-c tightly connected, x-y-z separate — should land in different communities
_page(vault, "a", ["b", "c"])
_page(vault, "b", ["a", "c"])
_page(vault, "c", ["a", "b"])
_page(vault, "x", ["y", "z"])
_page(vault, "y", ["x", "z"])
_page(vault, "z", ["x", "y"])
outgoing, _ = parse_vault_graph(vault)
comms = detect_communities_greedy(outgoing)
# At least 2 communities
assert len(comms) >= 2
def test_empty_graph(self, vault):
outgoing, _ = parse_vault_graph(vault)
comms = detect_communities_greedy(outgoing)
assert comms == []
# ---------------------------------------------------------------------------
# Surprising connections
# ---------------------------------------------------------------------------
class TestSurprisingConnections:
def test_cross_community_edge_found(self, vault):
# Build two tight clusters with one bridge
_page(vault, "a", ["b", "c", "x"])
_page(vault, "b", ["a", "c"])
_page(vault, "c", ["a", "b"])
_page(vault, "x", ["y", "z"])
_page(vault, "y", ["x", "z"])
_page(vault, "z", ["x", "y"])
outgoing, _ = parse_vault_graph(vault)
comms = detect_communities_greedy(outgoing)
sc = surprising_connections(outgoing, comms)
sources = {s["source"] for s in sc}
targets = {s["target"] for s in sc}
assert sources | targets # at least one cross-community edge found
def test_no_intra_community_edges(self, vault):
_page(vault, "a", ["b"])
_page(vault, "b", ["a"])
outgoing, _ = parse_vault_graph(vault)
comms = [{"a", "b"}] # one community
sc = surprising_connections(outgoing, comms)
assert sc == []
def test_scores_positive(self, vault):
_page(vault, "a", ["b", "x"])
_page(vault, "b", ["a"])
_page(vault, "x", ["y"])
_page(vault, "y", ["x"])
outgoing, _ = parse_vault_graph(vault)
comms = detect_communities_greedy(outgoing)
sc = surprising_connections(outgoing, comms)
for item in sc:
assert item["score"] > 0
# ---------------------------------------------------------------------------
# analyse_vault (integration)
# ---------------------------------------------------------------------------
class TestAnalyseVault:
def test_returns_all_keys(self, simple_vault):
result = analyse_vault(simple_vault)
assert set(result.keys()) == {
"god_nodes", "bridges", "communities", "surprising_connections",
"suggested_questions", "dead_ends", "isolated", "stats",
}
def test_stats_correct(self, simple_vault):
result = analyse_vault(simple_vault)
assert result["stats"]["pages"] == 6
assert result["stats"]["edges"] > 0
def test_empty_vault(self, vault):
result = analyse_vault(vault)
assert result["stats"]["pages"] == 0
def test_json_serialisable(self, simple_vault):
result = analyse_vault(simple_vault)
json.dumps(result) # must not raise
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
class TestGraphAnalyseCLI:
def test_outputs_json(self, simple_vault):
proc = subprocess.run(
[sys.executable, "-m", "obsidian_wiki.cli", "graph-analyse", str(simple_vault)],
capture_output=True, text=True,
)
assert proc.returncode == 0
data = json.loads(proc.stdout)
assert "god_nodes" in data
def test_pretty_flag(self, simple_vault):
proc = subprocess.run(
[sys.executable, "-m", "obsidian_wiki.cli", "graph-analyse", str(simple_vault), "--pretty"],
capture_output=True, text=True,
)
assert proc.returncode == 0
assert "\n " in proc.stdout
def test_top_flag(self, simple_vault):
proc = subprocess.run(
[sys.executable, "-m", "obsidian_wiki.cli", "graph-analyse", str(simple_vault), "--top", "3"],
capture_output=True, text=True,
)
assert proc.returncode == 0
data = json.loads(proc.stdout)
assert len(data["god_nodes"]) <= 3
def test_missing_vault_exits_nonzero(self, tmp_path):
proc = subprocess.run(
[sys.executable, "-m", "obsidian_wiki.cli", "graph-analyse", str(tmp_path / "nope")],
capture_output=True, text=True,
)
assert proc.returncode != 0
# ---------------------------------------------------------------------------
# graphify-derived algorithms: betweenness, cohesion, path, neighbourhood,
# diff, suggested questions
# ---------------------------------------------------------------------------
from obsidian_wiki.graph_analysis import ( # noqa: E402
betweenness_centrality,
bridge_pages,
cohesion_score,
graph_diff,
load_snapshot,
neighborhood,
shortest_path,
snapshot,
suggest_questions,
)
@pytest.fixture
def barbell_vault(vault):
"""Two triangles joined through a single bridge page `m`.
a-b-c clique — m — x-y-z clique
"""
_page(vault, "a", ["b", "c", "m"], tags=["left"])
_page(vault, "b", ["a", "c"], tags=["left"])
_page(vault, "c", ["a", "b"], tags=["left"])
_page(vault, "m", ["a", "x"], tags=["mid"])
_page(vault, "x", ["m", "y", "z"], tags=["right"])
_page(vault, "y", ["x", "z"], tags=["right"])
_page(vault, "z", ["x", "y"], tags=["right"])
return vault
class TestBetweenness:
def test_bridge_has_highest_betweenness(self, barbell_vault):
outgoing, _ = parse_vault_graph(barbell_vault)
bc = betweenness_centrality(outgoing)
assert max(bc, key=bc.get) == "m"
# leaf-of-clique nodes lie on no shortest paths
assert bc["b"] == 0.0 and bc["y"] == 0.0
def test_normalised_range(self, barbell_vault):
outgoing, _ = parse_vault_graph(barbell_vault)
for v in betweenness_centrality(outgoing).values():
assert 0.0 <= v <= 1.0
def test_matches_star_exact_value(self, vault):
# Star: hub h with 4 leaves — hub betweenness = 1.0 normalised
_page(vault, "h", ["p", "q", "r", "s"])
for leaf in "pqrs":
_page(vault, leaf, [])
outgoing, _ = parse_vault_graph(vault)
bc = betweenness_centrality(outgoing)
assert bc["h"] == pytest.approx(1.0)
def test_bridge_pages_reports_connected_communities(self, barbell_vault):
outgoing, _ = parse_vault_graph(barbell_vault)
comms = detect_communities_greedy(outgoing)
bridges = bridge_pages(outgoing, comms, top_n=3)
assert bridges[0]["page"] == "m"
assert bridges[0]["betweenness"] > 0
assert "connects" in bridges[0]
def test_empty(self):
assert betweenness_centrality({}) == {}
assert bridge_pages({}, [], 5) == []
class TestCohesion:
def test_clique_is_one(self, barbell_vault):
outgoing, _ = parse_vault_graph(barbell_vault)
assert cohesion_score(outgoing, {"a", "b", "c"}) == 1.0
def test_no_internal_links_is_zero(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert cohesion_score(outgoing, {"d", "f", "c"}) == 0.0
def test_singleton_is_one(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert cohesion_score(outgoing, {"d"}) == 1.0
def test_analyse_includes_cohesion(self, barbell_vault):
result = analyse_vault(barbell_vault)
for c in result["communities"]:
assert 0.0 <= c["cohesion"] <= 1.0
class TestShortestPath:
def test_finds_path_across_bridge(self, barbell_vault):
outgoing, _ = parse_vault_graph(barbell_vault)
path = shortest_path(outgoing, "b", "y")
assert path[0] == "b" and path[-1] == "y"
assert "m" in path
assert len(path) == 5 # b-a-m-x-y
def test_directed_respects_link_direction(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert shortest_path(outgoing, "a", "c", directed=True) == ["a", "c"]
assert shortest_path(outgoing, "c", "a", directed=True) is None
assert shortest_path(outgoing, "c", "a") == ["c", "a"]
def test_unreachable_and_unknown(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert shortest_path(outgoing, "a", "d") is None
assert shortest_path(outgoing, "a", "nope") is None
assert shortest_path(outgoing, "a", "a") == ["a"]
def test_slug_normalisation(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert shortest_path(outgoing, "A", "B") == ["a", "b"]
class TestNeighborhood:
def test_depth_layers(self, barbell_vault):
outgoing, _ = parse_vault_graph(barbell_vault)
hits = neighborhood(outgoing, "m", depth=1)
assert {h["page"] for h in hits} == {"a", "x"}
hits2 = neighborhood(outgoing, "m", depth=2)
assert {h["page"] for h in hits2} == {"a", "x", "b", "c", "y", "z"}
assert all(h["depth"] in (1, 2) for h in hits2)
def test_incoming_blast_radius(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
# who would break if `c` were renamed? a and b link to it
hits = neighborhood(outgoing, "c", depth=1, direction="in")
assert {h["page"] for h in hits} == {"a", "b"}
assert neighborhood(outgoing, "c", depth=1, direction="out") == []
def test_unknown_seed(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
assert neighborhood(outgoing, "nope") == []
class TestGraphDiff:
def test_added_removed(self):
old = {"nodes": ["a", "b", "c"], "edges": [["a", "b"]]}
new = {"nodes": ["a", "b", "d"], "edges": [["a", "b"], ["a", "d"], ["d", "b"]]}
d = graph_diff(old, new)
assert d["added_pages"] == ["d"]
assert d["removed_pages"] == ["c"]
assert ["a", "d"] in d["added_edges"]
assert d["removed_edges"] == []
assert d["summary"] == {"pages": 0, "edges": 2}
def test_newly_connected_and_lost_incoming(self):
old = {"nodes": ["a", "b", "c"], "edges": [["a", "b"]]}
new = {"nodes": ["a", "b", "c"], "edges": [["a", "c"]]}
d = graph_diff(old, new)
assert d["newly_connected"] == ["c"]
assert d["lost_incoming"] == ["b"]
def test_snapshot_roundtrip_via_insights_comment(self, simple_vault, tmp_path):
outgoing, _ = parse_vault_graph(simple_vault)
snap = snapshot(outgoing)
insights = tmp_path / "_insights.md"
insights.write_text("# Insights\n\nstuff\n\n<!-- GRAPH_SNAPSHOT: " + json.dumps(snap) + " -->\n")
loaded = load_snapshot(insights)
assert loaded == snap
assert graph_diff(loaded, snap)["summary"] == {"pages": 0, "edges": 0}
def test_load_snapshot_missing(self, tmp_path):
p = tmp_path / "x.md"
p.write_text("no snapshot here")
assert load_snapshot(p) is None
assert load_snapshot(tmp_path / "absent.md") is None
def test_analyse_with_previous(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
prev = snapshot(outgoing)
_page(simple_vault, "g", ["a"])
result = analyse_vault(simple_vault, previous_snapshot=prev, include_snapshot=True)
assert result["diff"]["added_pages"] == ["g"]
assert "snapshot" in result
class TestSuggestQuestions:
def test_bridge_question_first(self, barbell_vault):
outgoing, _ = parse_vault_graph(barbell_vault)
comms = detect_communities_greedy(outgoing)
labels = {i: f"c{i}" for i in range(len(comms))}
bridges = bridge_pages(outgoing, comms)
qs = suggest_questions(outgoing, comms, labels, bridges)
assert qs
assert qs[0]["type"] == "bridge_node" and "`m`" in qs[0]["question"]
def test_isolated_question(self, simple_vault):
outgoing, _ = parse_vault_graph(simple_vault)
qs = suggest_questions(outgoing, [], {}, [])
assert any(q["type"] == "isolated_nodes" for q in qs)
def test_no_signal(self):
qs = suggest_questions({"a": ["b"], "b": ["a"]}, [{"a", "b"}], {0: "x"}, [])
assert qs == [] or qs[0]["type"] in ("no_signal", "isolated_nodes")
def test_included_in_analyse(self, barbell_vault):
result = analyse_vault(barbell_vault)
assert "suggested_questions" in result and "bridges" in result
assert result["stats"]["density"] > 0
class TestSurprisingDedup:
def test_note_and_pair_dedup(self, vault):
# Hub h in community 0 links to 3 different pages of community 1;
# community 1 also has one link to community 2. Pair (1,2) must not
# be pushed out by the three (0,1) edges.
_page(vault, "h", ["p", "q", "r"])
_page(vault, "p", ["q"]); _page(vault, "q", ["p"]); _page(vault, "r", ["p", "s"])
_page(vault, "s", ["t"]); _page(vault, "t", ["s"])
outgoing, _ = parse_vault_graph(vault)
comms = [{"h"}, {"p", "q", "r"}, {"s", "t"}]
sc = surprising_connections(outgoing, comms, top_n=2)
assert all("note" in x for x in sc)
pairs = {tuple(sorted(x["note"].replace("bridges community ", "").split(" -> community "))) for x in sc}
assert pairs == {("0", "1"), ("1", "2")}
class TestGraphAnalyseCLIQueryModes:
def _run(self, *argv):
return subprocess.run(
[sys.executable, "-m", "obsidian_wiki.cli", "graph-analyse", *argv],
capture_output=True, text=True,
)
def test_path(self, barbell_vault):
proc = self._run(str(barbell_vault), "--path", "b", "y")
assert proc.returncode == 0, proc.stderr
data = json.loads(proc.stdout)
assert data["hops"] == 4 and "m" in data["path"]
def test_around(self, barbell_vault):
proc = self._run(str(barbell_vault), "--around", "m", "--depth", "1")
assert proc.returncode == 0, proc.stderr
data = json.loads(proc.stdout)
assert {p["page"] for p in data["pages"]} == {"a", "x"}
def test_diff_against_and_snapshot(self, simple_vault, tmp_path):
proc = self._run(str(simple_vault), "--snapshot")
snap = json.loads(proc.stdout)["snapshot"]
prev = tmp_path / "_insights.md"
prev.write_text("<!-- GRAPH_SNAPSHOT: " + json.dumps(snap) + " -->")
_page(simple_vault, "new", ["a"])
proc = self._run(str(simple_vault), "--diff-against", str(prev))
assert proc.returncode == 0, proc.stderr
assert json.loads(proc.stdout)["diff"]["added_pages"] == ["new"]
class TestUniqueCommunityLabels:
def test_labels_disambiguated(self, vault):
# two disconnected clusters both dominated by tag `x`
_page(vault, "a", ["b"], tags=["x", "left"]); _page(vault, "b", ["a"], tags=["x"])
_page(vault, "c", ["d"], tags=["x", "right"]); _page(vault, "d", ["c"], tags=["x"])
result = analyse_vault(vault)
labels = [c["label"] for c in result["communities"]]
assert len(labels) == len(set(labels))
assert "x" in labels and any(l.startswith("x/") for l in labels)
# ---------------------------------------------------------------------------
# Shared page selection + betweenness cache
# ---------------------------------------------------------------------------
from obsidian_wiki.graph_analysis import ( # noqa: E402
CACHE_FILENAME,
SKIP_ROOT_FILES,
betweenness_cached,
graph_fingerprint,
is_wiki_page,
iter_pages,
)
class TestPageSelection:
def test_root_bookkeeping_excluded(self, vault):
_page(vault, "a", ["b"]); _page(vault, "b", [])
for name in SKIP_ROOT_FILES:
(vault / f"{name}.md").write_text("---\ntitle: x\n---\n[[a]]\n")
out, _ = parse_vault_graph(vault)
assert set(out) == {"a", "b"}
def test_nested_index_is_a_real_page(self, vault):
"""Only ROOT index/log/hot are bookkeeping — concepts/index.md is content."""
(vault / "concepts").mkdir()
(vault / "concepts" / "index.md").write_text("---\ntitle: Concept Index\n---\n[[a]]\n")
_page(vault, "a", [])
out, _ = parse_vault_graph(vault)
assert "index" in out
assert is_wiki_page(vault / "concepts" / "index.md", vault)
assert not is_wiki_page(vault / "index.md", vault)
def test_skip_dirs_excluded(self, vault):
for d in ("_raw", "_meta", "_readouts", "_staging"):
(vault / d).mkdir()
(vault / d / "note.md").write_text("---\ntitle: x\n---\n")
_page(vault, "a", [])
assert [p.stem for p in iter_pages(vault)] == ["a"]
def test_tool_owned_directories_excluded(self, vault):
"""Dot-dirs are skipped outright (`.venv`, `.git`, `.trash`, …); the
non-hidden dependency trees batch.py skips are skipped here too."""
for d in (".venv/lib/site-packages/pkg", ".git/hooks", ".trash",
"node_modules/pkg", "venv", "__pycache__"):
(vault / d).mkdir(parents=True, exist_ok=True)
(vault / d / "note.md").write_text("---\ntitle: x\n---\n")
_page(vault, "a", [])
assert [p.stem for p in iter_pages(vault)] == ["a"]
assert not is_wiki_page(vault / ".venv" / "lib" / "site-packages" / "pkg" / "note.md", vault)
assert not is_wiki_page(vault / "node_modules" / "pkg" / "note.md", vault)
def test_iter_pages_is_sorted(self, vault):
for n in ("zebra", "alpha", "middle"):
_page(vault, n, [])
assert [p.stem for p in iter_pages(vault)] == ["alpha", "middle", "zebra"]
def test_path_outside_vault_is_not_a_page(self, vault, tmp_path):
assert not is_wiki_page(tmp_path / "elsewhere.md", vault)
class TestGraphFingerprint:
def test_stable_across_calls(self, simple_vault):
out, _ = parse_vault_graph(simple_vault)
assert graph_fingerprint(out) == graph_fingerprint(out)
def test_changes_when_an_edge_changes(self, simple_vault):
out, _ = parse_vault_graph(simple_vault)
before = graph_fingerprint(out)
_page(simple_vault, "e", ["f", "a"]) # add an edge
out2, _ = parse_vault_graph(simple_vault)
assert graph_fingerprint(out2) != before
def test_insensitive_to_key_order(self, simple_vault):
out, _ = parse_vault_graph(simple_vault)
shuffled = {k: out[k] for k in reversed(list(out))}
assert graph_fingerprint(shuffled) == graph_fingerprint(out)
class TestBetweennessCache:
def test_matches_uncached(self, barbell_vault):
out, _ = parse_vault_graph(barbell_vault)
exact = betweenness_centrality(out)
cached = betweenness_cached(out, barbell_vault)
assert all(abs(exact[k] - cached[k]) < 1e-12 for k in exact)
def test_no_cache_file_for_small_vault(self, barbell_vault):
"""Tiny graphs recompute in microseconds — don't litter the vault."""
out, _ = parse_vault_graph(barbell_vault)
betweenness_cached(out, barbell_vault)
assert not (barbell_vault / CACHE_FILENAME).exists()
def test_none_vault_still_works(self, barbell_vault):
out, _ = parse_vault_graph(barbell_vault)
assert betweenness_cached(out, None) == betweenness_centrality(out)
def test_corrupt_cache_fails_open(self, barbell_vault):
(barbell_vault / CACHE_FILENAME).write_text("{ not json")
out, _ = parse_vault_graph(barbell_vault)
assert betweenness_cached(out, barbell_vault) == betweenness_centrality(out)
def test_hit_is_used_when_present(self, barbell_vault):
"""A planted cache entry is returned verbatim — proves the key matches."""
out, _ = parse_vault_graph(barbell_vault)
key = f"{graph_fingerprint(out)}:1"
planted = {n: 0.5 for n in out}
(barbell_vault / CACHE_FILENAME).write_text(
json.dumps({"version": 1, "betweenness": {key: planted}})
)
assert betweenness_cached(out, barbell_vault) == planted
def test_stale_key_is_ignored(self, barbell_vault):
out, _ = parse_vault_graph(barbell_vault)
(barbell_vault / CACHE_FILENAME).write_text(
json.dumps({"version": 1, "betweenness": {"someotherkey:1": {n: 9.0 for n in out}}})
)
assert betweenness_cached(out, barbell_vault) == betweenness_centrality(out)
def test_unwritable_vault_does_not_raise(self, barbell_vault, tmp_path):
out, _ = parse_vault_graph(barbell_vault)
assert betweenness_cached(out, tmp_path / "does" / "not" / "exist")
class TestCommunityQuality:
def test_barbell_splits_into_two(self, barbell_vault):
"""A sequential label-propagation sweep collapses this into one group."""
out, _ = parse_vault_graph(barbell_vault)
comms = detect_communities_greedy(out)
assert len(comms) == 2
assert {"x", "y", "z"} in [set(c) for c in comms]
def test_deterministic_across_runs(self, barbell_vault):
out, _ = parse_vault_graph(barbell_vault)
sigs = {
json.dumps(sorted(sorted(c) for c in detect_communities_greedy(out)))
for _ in range(10)
}
assert len(sigs) == 1