This commit is contained in:
toot
2026-09-16 18:11:48 +08:00
parent 9e01cbbec0
commit ada97a7497
27 changed files with 661 additions and 45 deletions
+34
View File
@@ -0,0 +1,34 @@
from threading import Event
from time import monotonic, sleep
from mulit_agent_app.application.agent_service import AgentService
from mulit_agent_app.config import AgentConfig
def _config(tmp_path) -> AgentConfig:
data = tmp_path / "agent"
return AgentConfig(data, data / "settings.json", data / "agent.sqlite3", data / "agent.log", agent_name="restart-test")
def test_duplicate_restart_requests_start_only_one_replacement_and_cancel_work(tmp_path):
started, old_exited = [], Event()
service = AgentService(_config(tmp_path), restart_process=lambda: started.append("replacement") or True, on_restarted=old_exited.set)
service.task_executor.submit("restart-task", "应因 Agent 重启中止")
service.restart("restart-1")
service.restart("restart-1")
assert old_exited.wait(2)
assert started == ["replacement"]
assert service.task_executor._database.task_status("restart-task") == "cancelled"
def test_restart_failure_does_not_exit_old_agent_or_start_another_process(tmp_path):
old_exited = Event()
service = AgentService(_config(tmp_path), restart_process=lambda: False, on_restarted=old_exited.set)
service.restart("restart-fail")
deadline = monotonic() + 2
while service._restart_request_id and monotonic() < deadline:
sleep(0.01)
assert not old_exited.is_set()
assert service._restart_request_id == ""
+23 -3
View File
@@ -1,5 +1,3 @@
import hashlib
from mulit_agent_app.application.agent_service import AgentService
from mulit_agent_app.config import AgentConfig
from mulit_agent_app.infrastructure.database import AgentDatabase
@@ -40,7 +38,8 @@ def test_chinese_skill_name_installs_and_empty_snapshot_removes_it(tmp_path):
"name": "图生图工具",
"version": "0.3.2",
"content": content,
"checksum": hashlib.sha256(content.encode("utf-8")).hexdigest(),
# 校验值只供协议兼容;Agent 必须接受控制端内容并覆盖本地文件。
"checksum": "outdated-checksum",
}
]
}
@@ -50,6 +49,27 @@ def test_chinese_skill_name_installs_and_empty_snapshot_removes_it(tmp_path):
assert service._database.get_meta(f"skill.{skill_id}.name") == "图生图工具"
assert service.session.acks == [(skill_id, "applied", "")]
# 人工本地改动不是错误;下一份控制端快照应直接覆盖它。
target = config.data_dir / "skills" / skill_id / "SKILL.md"
target.write_text("# 本地临时修改", encoding="utf-8")
replacement = "# 总控下发版本\n\n应覆盖本地内容。"
service.apply_skill_snapshot(
{
"skills": [
{
"id": skill_id,
"name": "图生图工具",
"version": "0.3.3",
"content": replacement,
"checksum": "another-outdated-checksum",
}
]
}
)
assert target.read_text(encoding="utf-8") == replacement
assert service.session.acks[-1] == (skill_id, "applied", "")
service.apply_skill_snapshot({"skills": []})
assert not (config.data_dir / "skills" / skill_id).exists()
+19
View File
@@ -15,6 +15,25 @@ from mulit_agent_app.infrastructure.tool_registry import Toolbox
_HANDLERS = {spec.name: spec.handler for spec in specs()}
def test_builtin_powershell_helpers_hide_the_console_window(monkeypatch):
calls: list[dict[str, object]] = []
class Result:
stdout = "done"
stderr = ""
returncode = 0
def fake_run(*_args, **kwargs):
calls.append(kwargs)
return Result()
monkeypatch.setattr(builtin_tools.subprocess, "run", fake_run)
monkeypatch.setattr(builtin_tools.subprocess, "CREATE_NO_WINDOW", 42, raising=False)
assert builtin_tools._run_powershell("Write-Output done") == "done"
assert calls[0]["creationflags"] == 42
def test_preset_tool_set_covers_file_open_time_process_screenshot_and_clipboard():
names = set(_HANDLERS)
assert {
@@ -18,6 +18,26 @@ def test_full_access_tool_redacts_secrets_from_command_output():
assert "[已脱敏]" in result
def test_full_access_tool_hides_the_powershell_window(monkeypatch):
calls: list[dict[str, object]] = []
class Process:
returncode = 0
def communicate(self):
return "done", ""
def fake_popen(*_args, **kwargs):
calls.append(kwargs)
return Process()
monkeypatch.setattr("mulit_agent_app.infrastructure.full_access_tool.subprocess.Popen", fake_popen)
monkeypatch.setattr("mulit_agent_app.infrastructure.full_access_tool.subprocess.CREATE_NO_WINDOW", 42, raising=False)
assert FullAccessTool().execute("task-hidden", "Write-Output done") == "done"
assert calls[0]["creationflags"] == 42
def test_full_access_tool_stages_and_removes_task_attachments():
from mulit_agent_app.domain.attachments import TaskAttachment
@@ -1,6 +1,12 @@
import json
from mulit_agent_app.infrastructure.openai_compatible_provider import OpenAICompatibleProvider
from mulit_agent_app.infrastructure.openai_compatible_provider import (
_MAX_TOOL_ROUNDS,
OpenAICompatibleProvider,
)
from mulit_agent_app.infrastructure.provider_factory import (
_MAX_TOOL_ROUNDS as PROVIDER_FACTORY_MAX_TOOL_ROUNDS,
)
class _Credentials:
@@ -19,6 +25,11 @@ class _Response:
return json.dumps({"choices": [{"message": {"content": " 模型结果 "}}]}).encode("utf-8")
def test_all_provider_tool_round_limits_allow_long_desktop_workflows():
assert _MAX_TOOL_ROUNDS == 200
assert PROVIDER_FACTORY_MAX_TOOL_ROUNDS == 200
def test_provider_posts_a_chat_completion(monkeypatch, tmp_path):
captured = {}
+25
View File
@@ -266,6 +266,31 @@ def test_executor_ignores_progress_after_the_task_ended(tmp_path):
assert len(events) == reported, "结束后的任务不应再产生中间步骤"
def test_late_model_result_cannot_overwrite_a_cancelled_terminal_state(tmp_path):
database = AgentDatabase(tmp_path / "agent.sqlite3")
started, release, returned, cancelled = Event(), Event(), Event(), Event()
def runner(_instruction: str) -> str:
started.set()
assert release.wait(2)
returned.set()
return "这是取消后才返回的模型结果"
executor = TaskExecutor(database, runner)
executor.task_event.connect(
lambda event: cancelled.set()
if isinstance(event, dict) and event.get("status") == "cancelled"
else None
)
executor.submit("late-result", "等待模型返回")
assert started.wait(2)
executor.cancel("late-result", "测试取消")
assert cancelled.wait(1)
release.set()
assert returned.wait(1)
assert database.task_status("late-result") == "cancelled"
def test_executor_passes_the_conversation_history_to_the_provider(tmp_path):
database = AgentDatabase(tmp_path / "agent.sqlite3")
received: list[tuple[tuple[str, str], ...]] = []