diff --git a/graphify/detect.py b/graphify/detect.py index f76a4259f..32fdead47 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -1644,7 +1644,8 @@ def _on_walk_error(err: OSError) -> None: all_files.sort(key=lambda p: str(p)) - converted_dir = root / GRAPHIFY_OUT / "converted" + out_base = Path(cache_root).resolve() if cache_root is not None else root + converted_dir = out_base / GRAPHIFY_OUT / "converted" for p in all_files: # For memory dir files, skip hidden/noise filtering diff --git a/tests/test_detect.py b/tests/test_detect.py index 972955297..36e6a4811 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -2557,6 +2557,49 @@ def test_convert_office_file_outside_root_falls_back(tmp_path, monkeypatch): assert out1 is not None and out1.name == out2.name +def test_detect_office_conversion_respects_cache_root(tmp_path, monkeypatch): + """#2787: detect() with cache_root must write converted sidecars under + cache_root/GRAPHIFY_OUT/converted, leaving the scanned corpus untouched, while + keeping the sidecar filename hash anchored to the scan root.""" + monkeypatch.setattr(detect_mod, "docx_to_markdown", lambda p: "# Spec\nConverted specification text.") + + corpus = tmp_path / "corpus" + corpus.mkdir(parents=True) + cache_out = tmp_path / "cache_out" + cache_out.mkdir(parents=True) + + doc_path = corpus / "spec.docx" + doc_path.write_bytes(b"placeholder") + + result = detect(corpus, cache_root=cache_out) + + # 1. Scanned corpus tree must not be mutated + assert not (corpus / detect_mod.GRAPHIFY_OUT).exists(), ( + "detect() must not write graphify-out into the scanned corpus tree when cache_root is provided (#2787)" + ) + + # 2. Converted sidecar must exist under cache_root + converted_dir = cache_out / detect_mod.GRAPHIFY_OUT / "converted" + assert converted_dir.is_dir(), "converted directory must be created under cache_root" + + # 3. Detection result must point to the redirected sidecar + doc_files = result["files"]["document"] + assert len(doc_files) == 1 + sidecar_path = Path(doc_files[0]) + assert sidecar_path.is_file() + assert sidecar_path.parent.resolve() == converted_dir.resolve() + + # 4. Content must match expected conversion + content = sidecar_path.read_text(encoding="utf-8") + assert "" in content + assert "# Spec" in content + + # 5. Sidecar filename must remain anchored to the corpus scan root + import hashlib + expected_hash = hashlib.sha256(unicodedata.normalize("NFC", "spec.docx").encode()).hexdigest()[:8] + assert sidecar_path.name == f"spec_{expected_hash}.md" + + def test_detect_keeps_env_source_dirs(tmp_path): """#2058: a real source directory named env/ or *_env/ with no virtualenv markers must be indexed, not silently pruned as a false-positive venv."""