fix(#4783): mtime probe follows symlinked skill dirs to match compute path (gate finding 3)
Codex round-3 reproduced: _skill_tree_max_mtime_ns used os.walk(followlinks=False) while the compute path iter_skill_index_files uses followlinks=True, so an edit to a symlinked skill's target SKILL.md was invisible to the probe (stale up to TTL). Added followlinks=True to the probe walk so it descends symlinked skill dirs and sees target mtime changes. Regression test added + proven non-vacuous. Co-authored-by: Rod Boev <rod.boev@gmail.com>
This commit is contained in:
parent
8a46d88daf
commit
e4e3b46830
2 changed files with 40 additions and 1 deletions
|
|
@ -1597,7 +1597,10 @@ def _skill_tree_max_mtime_ns(skills_dir: Path, config_path: Path) -> int:
|
|||
return max_ns
|
||||
try:
|
||||
# Directory mtimes catch nested out-of-band deletes that leave file mtimes unchanged.
|
||||
for root, dirnames, filenames in os.walk(skills_dir):
|
||||
# followlinks=True mirrors agent.skill_utils.iter_skill_index_files (the compute
|
||||
# path), so a symlinked skill directory is descended into and edits to its target
|
||||
# SKILL.md change the probe value — otherwise such edits would stay stale up to the TTL.
|
||||
for root, dirnames, filenames in os.walk(skills_dir, followlinks=True):
|
||||
root_path = Path(root)
|
||||
try:
|
||||
max_ns = max(max_ns, root_path.stat().st_mtime_ns)
|
||||
|
|
|
|||
|
|
@ -308,6 +308,42 @@ class TestNestedDeletionDetected:
|
|||
assert result == (1, 1)
|
||||
|
||||
|
||||
class TestSymlinkedSkillProbeFollowsLinks:
|
||||
"""The mtime probe must follow symlinked skill directories the same way the
|
||||
compute path (iter_skill_index_files, followlinks=True) does — otherwise an
|
||||
edit to a symlinked skill's target SKILL.md is invisible to the probe and
|
||||
the counts stay stale up to the TTL (gate finding)."""
|
||||
|
||||
def test_symlinked_skill_target_edit_changes_probe(self, profiles_mod, tmp_path):
|
||||
mod, profile_dir = profiles_mod
|
||||
skills_dir = profile_dir / "skills"
|
||||
skills_dir.mkdir(parents=True, exist_ok=True)
|
||||
config_path = profile_dir / "config.yaml"
|
||||
|
||||
# Real skill living OUTSIDE the profile, linked in via a symlinked dir.
|
||||
target = tmp_path / "external-skill"
|
||||
target.mkdir()
|
||||
skill_md = target / "SKILL.md"
|
||||
skill_md.write_text("---\nname: linked\n---\n# linked\n", encoding="utf-8")
|
||||
link = skills_dir / "linked"
|
||||
try:
|
||||
link.symlink_to(target, target_is_directory=True)
|
||||
except (OSError, NotImplementedError):
|
||||
import pytest
|
||||
pytest.skip("symlinks not supported on this platform")
|
||||
|
||||
before = mod._skill_tree_max_mtime_ns(skills_dir, config_path)
|
||||
# Edit the target SKILL.md (bump its mtime well past granularity).
|
||||
import os as _os
|
||||
future = time.time() + 100
|
||||
_os.utime(skill_md, (future, future))
|
||||
after = mod._skill_tree_max_mtime_ns(skills_dir, config_path)
|
||||
|
||||
assert after > before, (
|
||||
"probe must follow the symlinked skill dir and see the target SKILL.md mtime change"
|
||||
)
|
||||
|
||||
|
||||
class TestReturnSignature:
|
||||
"""Proof matrix row 6: return signature is tuple[int, int]."""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue