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"
|
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):
|
def test_archive_collision_gets_suffix(skills_home):
|
||||||
from tools.skill_usage import archive_skill
|
from tools.skill_usage import archive_skill
|
||||||
skills_dir = skills_home / "skills"
|
skills_dir = skills_home / "skills"
|
||||||
|
|||||||
@@ -385,11 +385,13 @@ def restore_skill(skill_name: str) -> Tuple[bool, str]:
|
|||||||
if not archive_root.exists():
|
if not archive_root.exists():
|
||||||
return False, "no archive directory"
|
return False, "no archive directory"
|
||||||
|
|
||||||
# Try exact name match first, then any prefix match (for timestamped dupes)
|
# 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]
|
# 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:
|
if not candidates:
|
||||||
candidates = sorted(
|
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}-")],
|
if p.is_dir() and p.name.startswith(f"{skill_name}-")],
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user