fix(curator): scan nested archive subdirs in restore_skill
restore_skill() in tools/skill_usage.py used archive_root.iterdir(), which
only walked the top level of .archive/. Skills archived under nested layouts
(e.g. .archive/openclaw-imports/<skill>/ from older archive paths or
external imports) were invisible to both the exact-match and prefix-match
candidate scans, surfacing as a misleading "skill '<name>' not found in
archive" error even though the directory existed on disk.
Switch both candidate scans to archive_root.rglob('*') so the lookup
descends into category subdirectories.
Fixes #17942
This commit is contained in:
@ -315,6 +315,41 @@ def test_restore_skill_moves_back(skills_home):
|
||||
assert get_record("temp-skill")["state"] == "active"
|
||||
|
||||
|
||||
def test_restore_skill_finds_nested_archive_subdir(skills_home):
|
||||
"""Skills archived under nested category subdirs (e.g.
|
||||
.archive/<category>/<skill>/) — left behind by older archive layouts or
|
||||
external imports — must still be restorable by name."""
|
||||
from tools.skill_usage import restore_skill, get_record
|
||||
skills_dir = skills_home / "skills"
|
||||
nested = skills_dir / ".archive" / "openclaw-imports" / "nested-skill"
|
||||
nested.mkdir(parents=True)
|
||||
(nested / "SKILL.md").write_text(
|
||||
"---\nname: nested-skill\ndescription: x\n---\n", encoding="utf-8",
|
||||
)
|
||||
|
||||
ok, msg = restore_skill("nested-skill")
|
||||
assert ok, msg
|
||||
assert (skills_dir / "nested-skill" / "SKILL.md").exists()
|
||||
assert not nested.exists()
|
||||
assert get_record("nested-skill")["state"] == "active"
|
||||
|
||||
|
||||
def test_restore_skill_finds_nested_timestamped_prefix(skills_home):
|
||||
"""Prefix-match path (timestamped dupes) must also descend into nested
|
||||
archive subdirs, not just .archive/ top-level."""
|
||||
from tools.skill_usage import restore_skill
|
||||
skills_dir = skills_home / "skills"
|
||||
nested = skills_dir / ".archive" / "imports" / "dup-skill-20260101000000"
|
||||
nested.mkdir(parents=True)
|
||||
(nested / "SKILL.md").write_text(
|
||||
"---\nname: dup-skill\ndescription: x\n---\n", encoding="utf-8",
|
||||
)
|
||||
|
||||
ok, msg = restore_skill("dup-skill")
|
||||
assert ok, msg
|
||||
assert (skills_dir / "dup-skill" / "SKILL.md").exists()
|
||||
|
||||
|
||||
def test_archive_collision_gets_suffix(skills_home):
|
||||
from tools.skill_usage import archive_skill
|
||||
skills_dir = skills_home / "skills"
|
||||
|
||||
@ -385,11 +385,13 @@ def restore_skill(skill_name: str) -> Tuple[bool, str]:
|
||||
if not archive_root.exists():
|
||||
return False, "no archive directory"
|
||||
|
||||
# Try exact name match first, then any prefix match (for timestamped dupes)
|
||||
candidates = [p for p in archive_root.iterdir() if p.is_dir() and p.name == skill_name]
|
||||
# Try exact name match first, then any prefix match (for timestamped dupes).
|
||||
# Recursive walk handles nested archive layouts (e.g. .archive/<category>/<skill>/)
|
||||
# left behind by older archive paths or external imports.
|
||||
candidates = [p for p in archive_root.rglob("*") if p.is_dir() and p.name == skill_name]
|
||||
if not candidates:
|
||||
candidates = sorted(
|
||||
[p for p in archive_root.iterdir()
|
||||
[p for p in archive_root.rglob("*")
|
||||
if p.is_dir() and p.name.startswith(f"{skill_name}-")],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user