mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(gateway,integrations): keep temp cleanup from replacing the caller's outcome * fix(gateway): adopt the family helper so a published edit stays silent
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""Staged-temp cleanup in the artifact and Lark writers must keep the real outcome.
|
|
|
|
Both writers publish through a same-directory temporary and discard it on exit.
|
|
A failing discard used to replace the error the caller needed — the artifact
|
|
editor returned a cleanup ``OSError`` instead of the failed write, and the Lark
|
|
flow-state writer did the same.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def real_unlink():
|
|
return Path.unlink
|
|
|
|
|
|
def _fail_unlink_on(monkeypatch, predicate, real_unlink):
|
|
"""Make ``Path.unlink`` raise for the paths a test owns."""
|
|
|
|
def unlink(self, *args, **kwargs):
|
|
if predicate(self):
|
|
raise PermissionError("temporary unlink failed")
|
|
return real_unlink(self, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(Path, "unlink", unlink)
|
|
|
|
|
|
def test_artifact_edit_keeps_the_publish_error_when_cleanup_fails(tmp_path, monkeypatch, real_unlink):
|
|
from app.gateway.routers import artifacts
|
|
|
|
target = tmp_path / "note.txt"
|
|
target.write_text("original", encoding="utf-8")
|
|
file_stat = os.stat(target)
|
|
|
|
def _failing_replace(*_args, **_kwargs):
|
|
raise OSError("replace failed")
|
|
|
|
monkeypatch.setattr(artifacts.os, "replace", _failing_replace)
|
|
_fail_unlink_on(monkeypatch, lambda path: path.name.startswith(".") and path.suffix == "", real_unlink)
|
|
|
|
with pytest.raises(OSError, match="replace failed"):
|
|
artifacts._replace_artifact_atomically(target, b"replacement", file_stat)
|
|
|
|
assert target.read_text(encoding="utf-8") == "original"
|
|
|
|
|
|
def test_artifact_edit_success_is_quiet(tmp_path, caplog):
|
|
"""The published path must not warn: its temp name is already gone."""
|
|
from app.gateway.routers import artifacts
|
|
|
|
target = tmp_path / "note.txt"
|
|
target.write_text("original", encoding="utf-8")
|
|
file_stat = os.stat(target)
|
|
caplog.clear()
|
|
|
|
with caplog.at_level("WARNING", logger="deerflow.utils.atomic_write"):
|
|
artifacts._replace_artifact_atomically(target, b"replacement", file_stat)
|
|
|
|
assert target.read_bytes() == b"replacement"
|
|
assert [p.name for p in tmp_path.iterdir() if p.name.startswith(artifacts._ARTIFACT_EDIT_TEMP_PREFIX)] == []
|
|
assert caplog.text == ""
|
|
|
|
|
|
def test_lark_flow_state_write_keeps_the_write_error_when_cleanup_fails(tmp_path, monkeypatch, real_unlink):
|
|
from deerflow.integrations import lark_cli
|
|
|
|
target = tmp_path / "flow-state.json"
|
|
|
|
def _failing_replace(*_args, **_kwargs):
|
|
raise OSError("replace failed")
|
|
|
|
monkeypatch.setattr(lark_cli.os, "replace", _failing_replace)
|
|
_fail_unlink_on(monkeypatch, lambda path: path.name.startswith(".flow-state.json"), real_unlink)
|
|
|
|
with pytest.raises(OSError, match="replace failed"):
|
|
lark_cli._write_json_file_atomic(target, {"state": "pending"})
|
|
|
|
assert not target.exists()
|
|
|
|
|
|
def test_lark_flow_state_write_still_succeeds(monkeypatch, tmp_path):
|
|
"""The success path keeps its behaviour: payload written, no temp left."""
|
|
from deerflow.integrations import lark_cli
|
|
|
|
target = tmp_path / "flow-state.json"
|
|
lark_cli._write_json_file_atomic(target, {"state": "pending"})
|
|
|
|
assert target.read_text(encoding="utf-8") == '{"state": "pending"}\n'
|
|
assert [p.name for p in tmp_path.iterdir() if p.name.startswith(".flow-state")] == []
|