Files
graphify/tests/test_skill_semantic_all_cached.py
T
brunovima83 031c58ee18 fix(skill): run Step B3 when every semantic file is cached
The split-host runbook told the agent to skip to Part C when every doc,
paper and image hit the semantic cache. Part C reads
.graphify_semantic.json unconditionally and only the Step B3 merge writes
it, so a second run on an unchanged mixed corpus failed with
FileNotFoundError; writing an empty file instead dropped every cached node.

The all-cached case now skips B1/B2 but still runs B3. Step B0 also clears
.graphify_chunk_*.json before dispatch: B3 merges every chunk on disk, and a
leftover from an interrupted run would otherwise be merged as fresh.

The aider and devin monoliths keep the old sentence; they are pinned to the
v8 baseline by --monolith-roundtrip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit bab1bb2775)
2026-10-05 20:12:34 +01:00

93 lines
3.8 KiB
Python

"""A semantic run where every doc/paper/image hits the cache must still reach
Part C with its cached nodes.
Part C reads ``graphify-out/.graphify_semantic.json`` unconditionally, and the
only Part B step that writes it is the Step B3 merge. A body that routes the
all-cached case straight to Part C crashes there with ``FileNotFoundError``;
working around that with an empty file drops every cached node. Running B3 with
no new chunks also means a ``.graphify_chunk_*.json`` left by an interrupted run
would be merged as if it were fresh, so Step B0 must clear those before dispatch.
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from graphify.cache import save_semantic_cache # noqa: E402
from tools.skillgen import gen # noqa: E402
_ROUTING_PREFIX = "Only dispatch subagents for files listed in"
def _split_host_bodies():
arts = gen.render_all(gen.load_platforms())
bodies = [a for a in arts
if "check_semantic_cache(" in a.content
and "references/extraction-spec.md" in a.content]
assert bodies, "no rendered split-host skill body runs the semantic cache check"
return bodies
def test_all_cached_run_is_routed_through_the_b3_merge():
for a in _split_host_bodies():
routing = next(ln for ln in a.content.splitlines() if ln.startswith(_ROUTING_PREFIX))
assert "skip to Part C directly" not in routing, a.path
assert "still run Step B3" in routing, a.path
def _python_blocks(body: str, start: str, end: str) -> list[str]:
section = body.split(start, 1)[1].split(end, 1)[0]
sources = []
for block in re.findall(r"```bash\n(.*?)```", section, re.S):
m = re.search(r' -c "\n(.*)\n"\s*$', block, re.S)
if m:
sources.append(m.group(1).replace('\\"', '"'))
return sources
def test_all_cached_run_reaches_part_c_with_cached_nodes_and_no_stale_chunks(tmp_path):
platforms = gen.load_platforms()
body = next(a.content for a in gen.render_all(platforms, only="claude")
if a.path == "graphify/skill.md")
b0 = _python_blocks(body, "**Step B0", "**Step B1")
b3 = _python_blocks(body, "**Step B3", "#### Part C")
part_c = _python_blocks(body, "#### Part C", "\n### ")[:1]
assert len(b0) == 1 and len(b3) == 3 and len(part_c) == 1
corpus = tmp_path / "corpus"
corpus.mkdir()
doc = corpus / "notes.md"
doc.write_text("# Notes\nCached content.\n", encoding="utf-8")
spec = tmp_path / "extraction-spec.md"
spec.write_text("# spec\n", encoding="utf-8")
out = tmp_path / "graphify-out"
out.mkdir()
(out / ".graphify_detect.json").write_text(
json.dumps({"files": {"document": [doc.as_posix()]}}), encoding="utf-8")
(out / ".graphify_ast.json").write_text(
json.dumps({"nodes": [{"id": "ast_node", "label": "a", "source_file": "a.py"}],
"edges": []}), encoding="utf-8")
save_semantic_cache(
[{"id": "cached_node", "label": "Notes", "source_file": doc.as_posix()}], [], [],
root=corpus, prompt_file=spec)
(out / ".graphify_chunk_01.json").write_text(
json.dumps({"nodes": [{"id": "stale_node", "label": "old", "source_file": "x.md"}],
"edges": []}), encoding="utf-8")
for src in b0 + b3 + part_c:
src = src.replace("INPUT_PATH", corpus.as_posix()).replace("SPEC_PATH", spec.as_posix())
subprocess.run([sys.executable, "-c", src], cwd=tmp_path, check=True,
capture_output=True, text=True)
extract = json.loads((out / ".graphify_extract.json").read_text(encoding="utf-8"))
ids = {n["id"] for n in extract["nodes"]}
assert {"ast_node", "cached_node"} <= ids
assert "stale_node" not in ids