mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-13 06:31:47 +00:00
* 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
92 lines
3.7 KiB
Python
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()) == []
|