deer-flow/backend/tests/test_managed_subagent_create_race.py
paer 2dc68b242f
fix(subagents): prevent concurrent file-backed creates from overwriting definitions (#8132)
* docs(subagents): describe create-only file publication

* fix(subagents): prevent concurrent create overwrites

* test(subagents): cover independent create races

* docs(persistence): record create-only publication

* docs(config): explain managed subagent file claims

* docs(persistence): retain create-only publication rule

* docs(subagents): retain shared file create semantics

* fix(gateway): probe managed subagent hard links on startup

* test(gateway): cover managed subagent volume probes

* test(gateway): keep volume probes off the event loop

* docs(config): describe startup hard-link warnings

* docs(changelog): reference managed subagent fix #8132

* docs(changelog): keep the fix entry clear of append conflicts
2026-11-12 14:39:25 +08:00

92 lines
3.7 KiB
Python

"""Create-only publication across independent file-store processes."""
from __future__ import annotations
import errno
import multiprocessing
import pytest
from deerflow.config.paths import Paths
from deerflow.persistence.managed_subagents import ManagedSubagentDefinition, ManagedSubagentExistsError
from deerflow.persistence.managed_subagents import file as file_module
def _definition(name, writer):
return ManagedSubagentDefinition(name=name, description=f"Writer {writer}", system_prompt=f"Instructions from writer {writer}.")
def _create_in_process(home, name, writer, staged, results):
# Spawn gives each writer its own module-level RLock and path registry.
paths = Paths(base_dir=home)
definition = _definition(name, writer)
original_fsync = file_module.os.fsync
def fsync_then_wait(fd):
original_fsync(fd)
# Both complete files are staged after both writers passed exists().
# Synchronize the real I/O boundary, without fabricating a collision.
staged.wait(timeout=30)
with pytest.MonkeyPatch.context() as patch:
patch.setattr(file_module, "get_paths", lambda: paths)
patch.setattr(file_module.os, "fsync", fsync_then_wait)
try:
file_module.FileManagedSubagentStore().create(definition)
except ManagedSubagentExistsError:
outcome = "conflict"
else:
outcome = "created"
results.put((writer, outcome))
@pytest.mark.parametrize("same_name", [True, False], ids=["same-name", "different-names"])
def test_independent_process_creates_never_overwrite_a_winner(tmp_path, monkeypatch, same_name):
paths = Paths(base_dir=tmp_path)
monkeypatch.setattr(file_module, "get_paths", lambda: paths)
names = ["researcher", "researcher" if same_name else "reviewer"]
context = multiprocessing.get_context("spawn")
staged = context.Barrier(2)
results = context.Queue()
workers = [context.Process(target=_create_in_process, args=(tmp_path, name, writer, staged, results)) for writer, name in enumerate(names)]
try:
for worker in workers:
worker.start()
outcomes = [results.get(timeout=45) for _ in workers]
for worker in workers:
worker.join(10)
assert worker.exitcode == 0
assert sorted(outcome for _, outcome in outcomes) == (["conflict", "created"] if same_name else ["created", "created"])
store = file_module.FileManagedSubagentStore()
for writer, outcome in outcomes:
if outcome == "created":
assert store.get(names[writer]) == _definition(names[writer], writer)
assert len(store.list()) == (1 if same_name else 2)
assert sorted(path.name for path in paths.managed_subagents_dir.iterdir()) == sorted({f"{name}.json" for name in names})
finally:
staged.abort()
for worker in workers:
if worker.pid is not None:
worker.join(5)
if worker.is_alive():
worker.terminate()
worker.join(5)
worker.close()
results.close()
results.join_thread()
def test_create_propagates_publication_errors_and_discards_staging(tmp_path, monkeypatch):
paths = Paths(base_dir=tmp_path)
monkeypatch.setattr(file_module, "get_paths", lambda: paths)
def denied_link(*args, **kwargs):
raise PermissionError(errno.EPERM, "hard links denied")
monkeypatch.setattr(file_module.os, "link", denied_link)
with pytest.raises(PermissionError, match="hard links denied"):
file_module.FileManagedSubagentStore().create(_definition("researcher", 0))
assert list(paths.managed_subagents_dir.iterdir()) == []