mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 22:48:38 +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:
|
def _remove_projection_relative(root: Path, relative_path: Path) -> None:
|
||||||
"""Remove a projected package without following a drifted namespace symlink."""
|
"""Remove a projected package without following a drifted namespace symlink."""
|
||||||
current = root
|
current = root
|
||||||
for part in relative_path.parts:
|
parts = relative_path.parts
|
||||||
|
for index, part in enumerate(parts):
|
||||||
current /= part
|
current /= part
|
||||||
if current.is_symlink():
|
if current.is_symlink():
|
||||||
current.unlink()
|
current.unlink()
|
||||||
return
|
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)
|
_remove_projection_entry(current)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -283,6 +283,30 @@ def test_targeted_removal_failure_clears_drifted_projection_scope(projection_env
|
|||||||
assert not manifest.exists()
|
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:
|
def test_user_custom_skill_replaces_legacy_projection(projection_env) -> None:
|
||||||
env = projection_env
|
env = projection_env
|
||||||
_write_skill(env.skills_root / "custom", "legacy-skill")
|
_write_skill(env.skills_root / "custom", "legacy-skill")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user