mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-20 20:46:15 +00:00
fix(build): relativize source_file across a symlinked root (#1571 follow-up)
Rigorous smoke testing surfaced an edge case the canonical-tmp unit tests couldn't reach: when the scan root is under a symlink (macOS /var -> /private/var, a symlinked home or git worktree), the absolute prune path and the resolved root differ by prefix, so _norm_source_file's lexical relative_to fails and the prune/replace match silently misses — deleted files' ghost nodes survive. Latent in the pre-existing #1007 path too, now that build_merge resolves the root. Fix: when lexical relative_to fails, retry with both sides fully resolved. Only the failure path resolves, so the common lexical match stays filesystem-free (no per-node stat on the hot replace-per-source loop). Adds a symlinked-root prune regression test (POSIX-only). Full suite 2768, and the full end-to-end smoke battery (indirect_call all contexts, JS, Ruby/Groovy inherits, hyperedge preservation, symlinked-root ghost prune, corrupt-json errors, dedup collision warning) is green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6eb7c014f4
commit
de7d362537
+10
-1
@@ -117,7 +117,16 @@ def _norm_source_file(p: str | None, root: str | None = None) -> str | None:
|
||||
try:
|
||||
p = Path(p).relative_to(root).as_posix()
|
||||
except ValueError:
|
||||
pass
|
||||
# Lexical relative_to failed. Retry with both sides fully resolved:
|
||||
# a symlinked scan root (macOS /var -> /private/var, or a symlinked
|
||||
# home/worktree) makes the raw prefixes differ even though they point
|
||||
# at the same dir, which otherwise silently defeats prune/replace
|
||||
# matching. Only the slow path resolves, so the common lexical match
|
||||
# stays filesystem-free.
|
||||
try:
|
||||
p = Path(p).resolve().relative_to(Path(root).resolve()).as_posix()
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return p
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,11 @@ build_merge backs `graphify --update`. Two regressions covered here:
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from graphify.build import build_merge, _infer_merge_root
|
||||
|
||||
|
||||
@@ -124,3 +127,25 @@ def test_prune_without_root_uses_graphify_root_marker(tmp_path):
|
||||
assert _infer_merge_root(graph_path) == str(real_root.resolve())
|
||||
G = build_merge([], graph_path, prune_sources=[str(real_root / "HANDOFF.md")], dedup=False)
|
||||
assert "handoff" not in {d["label"] for _, d in G.nodes(data=True)}
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX symlink semantics")
|
||||
def test_prune_matches_across_symlinked_root(tmp_path):
|
||||
"""A symlinked scan root (macOS /var -> /private/var, symlinked home/worktree)
|
||||
makes the absolute prune path and the resolved root differ by prefix. The prune
|
||||
must still match — lexical relative_to fails, so normalization resolves both
|
||||
sides. Regression for the edge case a canonical-tmp unit test can't reach."""
|
||||
real = tmp_path / "real"
|
||||
(real / "graphify-out").mkdir(parents=True)
|
||||
link = tmp_path / "link"
|
||||
os.symlink(real, link)
|
||||
graph_path = real / "graphify-out" / "graph.json"
|
||||
_write_graph(graph_path, [
|
||||
{"id": "h1", "label": "handoff", "file_type": "document", "source_file": "HANDOFF.md"},
|
||||
{"id": "k1", "label": "keep", "file_type": "document", "source_file": "KEEP.md"},
|
||||
], [], [])
|
||||
# prune path addressed via the SYMLINK, root resolved to the real dir
|
||||
G = build_merge([], graph_path=graph_path,
|
||||
prune_sources=[str(link / "HANDOFF.md")], root=str(real), dedup=False)
|
||||
labels = {d["label"] for _, d in G.nodes(data=True)}
|
||||
assert "handoff" not in labels and "keep" in labels
|
||||
|
||||
Reference in New Issue
Block a user