diff --git a/graphify/detect.py b/graphify/detect.py index 340b59576..287b0f431 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -1968,7 +1968,15 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: skipped_sensitive.append(str(p) + f" [Google Workspace export failed: {exc}]") continue if md_path: - if _ignored_for_scan(md_path): + # #3504: the sidecar lands under converted_dir, which the + # documented .gitignore advice puts inside a gitignored + # graphify-out/ -- an ignore check here would reject the + # tool's own output for the same reason it should be + # gitignored in the first place, silently dropping the + # source document from the corpus. The ignore check exists + # to keep USER files out of the scan, not to filter output + # this same pass just produced from an already-admitted file. + if _ignored_for_scan(md_path) and not md_path.is_relative_to(converted_dir): continue files[ftype].append(str(md_path)) total_words += _wc(md_path) @@ -1979,7 +1987,10 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: if p.suffix.lower() in OFFICE_EXTENSIONS: md_path = convert_office_file(p, converted_dir, root=root) if md_path: - if _ignored_for_scan(md_path): + # #3504: see the matching comment in the Google Workspace + # branch above -- same sidecar-under-a-gitignored-output-dir + # trap, same exemption. + if _ignored_for_scan(md_path) and not md_path.is_relative_to(converted_dir): continue files[ftype].append(str(md_path)) total_words += _wc(md_path) diff --git a/tests/test_detect.py b/tests/test_detect.py index 1bf6b056b..3a2635769 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -872,6 +872,58 @@ def test_detect_converts_google_workspace_shortcuts_when_enabled(tmp_path, monke assert result["total_words"] > 0 +def test_detect_office_sidecar_survives_a_gitignored_output_dir(tmp_path, monkeypatch): + """#3504: the documented .gitignore advice puts graphify-out/ (and so + graphify-out/converted/, where Office sidecars land) inside a gitignored + tree. The ignore check exists to keep USER files out of the scan, not to + filter output this same pass just produced from an already-admitted + source file -- so a sidecar landing under converted/ must survive it, + or every .docx/.xlsx silently vanishes from the corpus the moment a repo + follows that advice.""" + (tmp_path / ".gitignore").write_text("graphify-out/\n", encoding="utf-8") + src = tmp_path / "report.docx" + src.write_text("placeholder", encoding="utf-8") + + def fake_convert(path, out_dir, root=None): + out_dir.mkdir(parents=True, exist_ok=True) + out = out_dir / "report_converted.md" + out.write_text("# Report\n\nConverted content.", encoding="utf-8") + return out + + monkeypatch.setattr("graphify.detect.convert_office_file", fake_convert) + + result = detect(tmp_path) + + assert len(result["files"]["document"]) == 1, ( + "the Office sidecar was dropped by the gitignore check on the tool's own output dir" + ) + assert result["files"]["document"][0].endswith("report_converted.md") + assert result["total_words"] > 0 + + +def test_detect_google_workspace_sidecar_survives_a_gitignored_output_dir(tmp_path, monkeypatch): + """Same trap as the Office sidecar case (#3504), for the Google Workspace + conversion branch, which writes into the same converted/ directory.""" + (tmp_path / ".gitignore").write_text("graphify-out/\n", encoding="utf-8") + shortcut = tmp_path / "notes.gdoc" + shortcut.write_text('{"doc_id":"doc-1"}', encoding="utf-8") + + def fake_convert(path, out_dir, *, xlsx_to_markdown=None, root=None): + out_dir.mkdir(parents=True, exist_ok=True) + out = out_dir / "notes_converted.md" + out.write_text("# Notes\n\nA converted Google Doc.", encoding="utf-8") + return out + + monkeypatch.setattr("graphify.detect.convert_google_workspace_file", fake_convert) + + result = detect(tmp_path, google_workspace=True) + + assert len(result["files"]["document"]) == 1, ( + "the Google Workspace sidecar was dropped by the gitignore check on the tool's own output dir" + ) + assert result["files"]["document"][0].endswith("notes_converted.md") + + def test_detect_includes_video_key(tmp_path): """detect() result always includes a 'video' key even with no video files.""" (tmp_path / "main.py").write_text("x = 1")