mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-09 21:49:37 +00:00
fix(skills): fail closed on drifted projection namespace on all platforms (#4830)
* fix(skills): fail closed on drifted projection namespace on all platforms * test(skills): add regression test simulating swallowed unlink on drifted namespace
This commit is contained in:
parent
ae099c11ec
commit
adf6c422c2
@ -158,11 +158,18 @@ def _validate_projection_relative_path(relative_path: Path) -> None:
|
||||
def _remove_projection_relative(root: Path, relative_path: Path) -> None:
|
||||
"""Remove a projected package without following a drifted namespace symlink."""
|
||||
current = root
|
||||
for part in relative_path.parts:
|
||||
parts = relative_path.parts
|
||||
for index, part in enumerate(parts):
|
||||
current /= part
|
||||
if current.is_symlink():
|
||||
current.unlink()
|
||||
return
|
||||
# A namespace component that exists as a regular file means the
|
||||
# projection was externally replaced (drifted). Fail closed on every
|
||||
# platform: os.unlink() reports this as ENOTDIR on POSIX but ENOENT
|
||||
# on Windows, where unlink(missing_ok=True) swallows the ENOENT.
|
||||
if index < len(parts) - 1 and current.exists() and not current.is_dir():
|
||||
raise NotADirectoryError(errno.ENOTDIR, f"Projection namespace drifted to a file: {current}")
|
||||
_remove_projection_entry(current)
|
||||
|
||||
|
||||
|
||||
@ -283,6 +283,30 @@ def test_targeted_removal_failure_clears_drifted_projection_scope(projection_env
|
||||
assert not manifest.exists()
|
||||
|
||||
|
||||
def test_targeted_removal_drift_check_raises_when_underlying_unlink_swallows_error(projection_env, monkeypatch) -> None:
|
||||
"""Explicit drift check raises NotADirectoryError even if underlying unlink would swallow ENOENT (Windows semantics)."""
|
||||
env = projection_env
|
||||
_write_skill(env.skills_root / "public" / "team", "helper")
|
||||
projected = rebuild_skill_projections(env.storage)
|
||||
manifest = projected.public.parent / ".projection-manifest.json"
|
||||
namespace = projected.public / "team"
|
||||
shutil.rmtree(namespace)
|
||||
namespace.write_text("drifted file", encoding="utf-8")
|
||||
|
||||
from deerflow.skills import projection as projection_module
|
||||
|
||||
# Simulate Windows Path.unlink(missing_ok=True) semantics where file-in-path
|
||||
# returns ENOENT and is swallowed, so underlying removal would not raise ENOTDIR.
|
||||
monkeypatch.setattr(projection_module, "_remove_projection_entry", lambda _target: None)
|
||||
|
||||
with pytest.raises(NotADirectoryError, match="Projection namespace drifted to a file"):
|
||||
with skill_projection_mutation(env.storage, "public", remove_names=("helper",)):
|
||||
env.extensions.skills["helper"] = SkillStateConfig(enabled=False)
|
||||
|
||||
assert list(projected.public.iterdir()) == []
|
||||
assert not manifest.exists()
|
||||
|
||||
|
||||
def test_user_custom_skill_replaces_legacy_projection(projection_env) -> None:
|
||||
env = projection_env
|
||||
_write_skill(env.skills_root / "custom", "legacy-skill")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user