zcbot/tests/test_rename_working_dir.py

215 lines
7.6 KiB
Python

"""对话内 working_dir 延迟改名:工具登记 + worker 收尾时序。"""
from __future__ import annotations
import tempfile
import unittest
from contextlib import contextmanager
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from uuid import uuid4
from core.task_actions import DeferredTaskActions
from core.working_dirs import (
WorkingDirBusyError,
WorkingDirRenameResult,
rename_working_dir,
)
from tools.rename_working_dir import RenameWorkingDirTool
class RenameWorkingDirToolTests(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
self.wd = self.root / "旧目录"
self.wd.mkdir()
self.actions = DeferredTaskActions()
self.tool = RenameWorkingDirTool(
self.actions,
working_dir=self.wd,
base_dir=self.wd,
user_root=self.root,
)
def tearDown(self) -> None:
self.tmp.cleanup()
def test_registers_and_replaces_deferred_name(self) -> None:
out = self.tool.execute(" 新目录 ")
self.assertIn("[OK]", out)
self.assertEqual(self.actions.rename_working_dir_to, "新目录")
out = self.tool.execute("最终目录")
self.assertIn("更新为", out)
self.assertEqual(self.actions.rename_working_dir_to, "最终目录")
self.assertTrue(self.wd.is_dir(), "工具调用阶段不应立即改动文件系统")
def test_rejects_invalid_same_or_existing_name(self) -> None:
for bad in ("", ".memory", "a/b", "a\\b", "旧目录"):
with self.subTest(name=bad):
self.actions.rename_working_dir_to = ""
self.assertIn("[Error]", self.tool.execute(bad))
self.assertEqual(self.actions.rename_working_dir_to, "")
(self.root / "已存在").mkdir()
self.assertIn("[Error]", self.tool.execute("已存在"))
self.assertEqual(self.actions.rename_working_dir_to, "")
class DeferredRenameWorkerTests(unittest.TestCase):
def test_normal_run_renames_after_status_commit_before_done(self) -> None:
from web import runs
tid = uuid4()
uid = uuid4()
with tempfile.TemporaryDirectory() as tmp:
old = Path(tmp) / "旧目录"
old.mkdir()
new = old.parent / "新目录"
actions = DeferredTaskActions(rename_working_dir_to="新目录")
agent = SimpleNamespace(
run=MagicMock(return_value="ok"),
deferred_actions=actions,
sink=None,
)
order: list[str] = []
@contextmanager
def fake_scope():
yield SimpleNamespace(execute=MagicMock())
order.append("status_committed")
broker = MagicMock()
def fake_rename(**kwargs):
order.append("renamed")
self.assertEqual(kwargs["old_path"], old)
self.assertEqual(kwargs["new_path"], new)
return WorkingDirRenameResult(old, new, 1)
with (
patch("core.agent_builder.build_agent", return_value=(
agent, MagicMock(), str(tid), MagicMock(), old,
)),
patch("core.agent_builder.sync_task_tokens"),
patch.object(runs, "session_scope", fake_scope),
patch.object(runs, "broker", broker),
patch("core.working_dirs.rename_working_dir", side_effect=fake_rename),
):
runs.run_agent_bg(tid, uid, "把目录改名")
self.assertEqual(order, ["status_committed", "renamed"])
broker.clear_cancel.assert_called_once_with(tid)
broker.close.assert_called_once_with(tid)
broker.emit.assert_any_call(
tid,
{
"type": "warn",
"level": "info",
"msg": "工作目录已重命名为 新目录",
},
)
def test_cancelled_run_does_not_apply_deferred_rename(self) -> None:
from web import runs
tid = uuid4()
uid = uuid4()
actions = DeferredTaskActions(rename_working_dir_to="新目录")
agent = SimpleNamespace(
run=MagicMock(return_value="[cancelled]"),
deferred_actions=actions,
sink=None,
)
@contextmanager
def fake_scope():
yield SimpleNamespace(execute=MagicMock())
with tempfile.TemporaryDirectory() as tmp:
old = Path(tmp) / "旧目录"
old.mkdir()
with (
patch("core.agent_builder.build_agent", return_value=(
agent, MagicMock(), str(tid), MagicMock(), old,
)),
patch("core.agent_builder.sync_task_tokens"),
patch.object(runs, "session_scope", fake_scope),
patch.object(runs, "broker", MagicMock()),
patch("core.working_dirs.rename_working_dir") as rename,
):
runs.run_agent_bg(tid, uid, "停止")
rename.assert_not_called()
class WorkingDirServiceTests(unittest.TestCase):
def test_updates_all_associated_tasks_and_renames_fs(self) -> None:
uid = uuid4()
tids = [uuid4(), uuid4()]
rows = [
SimpleNamespace(task_id=tid, run_status="idle")
for tid in tids
]
session = MagicMock()
session.execute.side_effect = [
SimpleNamespace(all=lambda: rows),
MagicMock(),
]
@contextmanager
def fake_scope():
yield session
with tempfile.TemporaryDirectory() as tmp:
old = Path(tmp) / "旧目录"
new = Path(tmp) / "新目录"
old.mkdir()
(old / "产物.txt").write_text("ok", encoding="utf-8")
with (
patch("core.working_dirs.session_scope", fake_scope),
patch("core.working_dirs.check_no_subtask"),
patch("core.working_dirs.to_db_path", side_effect=["old-db", "new-db"]),
):
result = rename_working_dir(
user_id=uid,
old_path=old,
new_path=new,
)
self.assertEqual(result.tasks_updated, 2)
self.assertFalse(old.exists())
self.assertEqual((new / "产物.txt").read_text(encoding="utf-8"), "ok")
self.assertEqual(session.execute.call_count, 2)
def test_active_associated_task_blocks_before_fs_change(self) -> None:
uid = uuid4()
rows = [SimpleNamespace(task_id=uuid4(), run_status="running")]
session = MagicMock()
session.execute.return_value = SimpleNamespace(all=lambda: rows)
@contextmanager
def fake_scope():
yield session
with tempfile.TemporaryDirectory() as tmp:
old = Path(tmp) / "旧目录"
new = Path(tmp) / "新目录"
old.mkdir()
with (
patch("core.working_dirs.session_scope", fake_scope),
patch("core.working_dirs.to_db_path", side_effect=["old-db", "new-db"]),
):
with self.assertRaises(WorkingDirBusyError):
rename_working_dir(
user_id=uid,
old_path=old,
new_path=new,
)
self.assertTrue(old.is_dir())
self.assertFalse(new.exists())
if __name__ == "__main__":
unittest.main()