++
This commit is contained in:
@@ -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 == ""
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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], ...]] = []
|
||||
|
||||
Reference in New Issue
Block a user