- 预设 12 个本机工具并默认启用,装好即是可操作电脑的 Agent、无需总控配置或安装: 文件与文件夹的查看/读取/写入/建目录/移动/删除(默认不覆盖、删目录需 recursive、 拒绝删除系统目录与用户根目录)、用默认程序打开网址或文件、时间与时区换算 (tzdata 已随包内置)、查看进程、屏幕截图、读写剪贴板。 - 工具下发改为通用:任意 HTTP MCP 服务与 stdio 本机命令(Agent 拉起子进程, 容忍 stdout 噪声,命令缺失或进程崩溃会明确回报),支持多工具并存、 定义未变更时复用连接、替换/删除/退出时回收子进程。 - 多轮上下文:task_dispatch.history 作为此前的 user/assistant 文本拼接到本次请求前, OpenAI 兼容、Anthropic、Gemini 分别适配;Agent 侧独立校验与限幅。 - 工具级实时进度以 progress 事件回传;使用预设工具的任务标记为 builtin 执行来源。 - Skill 与历史内容上限与总控对齐,多个已批准 Skill 不再被静默截断。 - 修复 build.ps1 缺 UTF-8 BOM 导致 build.bat 在 PowerShell 5.1 下语法报错。
77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
import pytest
|
|
|
|
from mulit_agent_app.config import AgentConfig
|
|
from mulit_agent_app.infrastructure.controller_session import ControllerSession
|
|
|
|
|
|
def test_https_address_is_converted_to_wss():
|
|
assert ControllerSession._websocket_url("https://192.168.1.10:8443") == "wss://192.168.1.10:8443"
|
|
|
|
|
|
def test_insecure_address_is_rejected():
|
|
with pytest.raises(ValueError):
|
|
ControllerSession._websocket_url("http://192.168.1.10:8443")
|
|
|
|
|
|
def test_current_socket_close_schedules_a_reconnect(tmp_path):
|
|
config = AgentConfig(
|
|
tmp_path,
|
|
tmp_path / "settings.json",
|
|
tmp_path / "agent.sqlite3",
|
|
tmp_path / "agent.log",
|
|
controller_url="wss://10.0.0.9:8443",
|
|
agent_id="agent-id",
|
|
certificate_fingerprint="fingerprint",
|
|
)
|
|
session = ControllerSession(config, object()) # type: ignore[arg-type]
|
|
socket = object()
|
|
scheduled: list[bool] = []
|
|
session._socket = socket # type: ignore[assignment]
|
|
session._schedule_reconnect = lambda: scheduled.append(True) # type: ignore[method-assign]
|
|
|
|
session._on_close(socket, None, None) # type: ignore[arg-type]
|
|
|
|
assert scheduled == [True]
|
|
|
|
|
|
def test_replaced_socket_close_does_not_schedule_an_extra_reconnect(tmp_path):
|
|
config = AgentConfig(tmp_path, tmp_path / "settings.json", tmp_path / "agent.sqlite3", tmp_path / "agent.log")
|
|
session = ControllerSession(config, object()) # type: ignore[arg-type]
|
|
scheduled: list[bool] = []
|
|
session._socket = object() # type: ignore[assignment]
|
|
session._schedule_reconnect = lambda: scheduled.append(True) # type: ignore[method-assign]
|
|
|
|
session._on_close(object(), None, None) # type: ignore[arg-type]
|
|
|
|
assert scheduled == []
|
|
|
|
|
|
def test_conversation_history_is_validated_and_kept_in_order():
|
|
history = ControllerSession._validated_history(
|
|
[
|
|
{"role": "user", "content": "下载到 D:\\demo"},
|
|
{"role": "assistant", "content": "已下载到 D:\\demo\\a.zip"},
|
|
{"role": "system", "content": "不该被接受"},
|
|
{"role": "assistant", "content": " "},
|
|
"not-a-mapping",
|
|
]
|
|
)
|
|
|
|
assert history == (("user", "下载到 D:\\demo"), ("assistant", "已下载到 D:\\demo\\a.zip"))
|
|
|
|
|
|
def test_conversation_history_is_bounded_by_budget_and_drops_the_oldest_turns():
|
|
entries = [{"role": "user", "content": "x" * 4_000}, {"role": "assistant", "content": "y" * 4_000}] * 10
|
|
|
|
history = ControllerSession._validated_history(entries)
|
|
|
|
assert history, "至少应保留最近的一轮"
|
|
assert len(history) * 4_000 <= 60_000
|
|
assert history[-1][1].startswith("y"), "保留的应是最新的轮次"
|
|
|
|
|
|
def test_malformed_history_never_raises():
|
|
assert ControllerSession._validated_history(None) == ()
|
|
assert ControllerSession._validated_history({"role": "user"}) == ()
|
|
assert ControllerSession._validated_history([{"role": "user", "content": 42}]) == ()
|