文档刷新: - CLAUDE.md/README.md/DEVELOP.md/RELEASE.md 同步 org v2 架构 (Boss/GroupLeader/SeniorDev/Ops 组织层、EventBus/DailyReport/OpsHub/ AgentScorer/VersionPipeline 工具层、nrf-dev/esp32-dev/market-* 新角色) - 清除所有 /data/rockchip 路径引用 → /data/company(源码+测试+前端) - 保留 "Rockchip" 芯片厂商名作为技术描述(hw_engineer/algo_vision 等) - docs/plans/ 历史文档保持原样(反映当时路径状态) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
5.5 KiB
Python
148 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
from nmfs_agents.config import load_config, AgentsConfig
|
|
|
|
|
|
def _write_base_yamls(tmp_path: Path, projects_extra: str = "") -> None:
|
|
(tmp_path / "projects.yaml").write_text(
|
|
"projects:\n yolo:\n path: /data/company/yolo\n mode: confirm\n"
|
|
+ projects_extra
|
|
)
|
|
(tmp_path / "agents.yaml").write_text(
|
|
"scheduler:\n interval_hours: 1\n max_concurrent: 3\n"
|
|
"claude:\n model: claude-sonnet-4-6\n"
|
|
"feishu:\n callback_port: 8765\n"
|
|
)
|
|
(tmp_path / "devices.yaml").write_text(
|
|
"devices:\n rk3588:\n host: 192.168.0.127\n user: pi\n password: '123123'\n"
|
|
" workspace: /home/pi/Desktop\n pre_cmd: 'echo'\n"
|
|
)
|
|
|
|
|
|
def test_load_config_returns_typed_config(tmp_path):
|
|
_write_base_yamls(tmp_path)
|
|
cfg = load_config(tmp_path)
|
|
assert "yolo" in cfg.projects
|
|
assert cfg.projects["yolo"].mode == "confirm"
|
|
assert cfg.projects["yolo"].path == Path("/data/company/yolo")
|
|
assert cfg.scheduler.interval_hours == 1
|
|
assert cfg.claude.model == "claude-sonnet-4-6"
|
|
assert "rk3588" in cfg.devices
|
|
assert cfg.devices["rk3588"].host == "192.168.0.127"
|
|
|
|
|
|
def test_load_config_scan_dirs_resolves_path_for_explicit_project(tmp_path):
|
|
"""scan_dirs 仅用于为显式列出的项目解析路径,不自动引入未列出的项目。"""
|
|
# Create a parent dir with two sub-projects
|
|
parent = tmp_path / "repos"
|
|
(parent / "proj_a").mkdir(parents=True)
|
|
(parent / "proj_a" / "README.md").write_text("# Proj A\nsome text\n")
|
|
(parent / "proj_b").mkdir()
|
|
(parent / "proj_b" / "CLAUDE.md").write_text("# Proj B\n")
|
|
|
|
(tmp_path / "projects.yaml").write_text(
|
|
f"scan_dirs:\n - {parent}\n"
|
|
"projects:\n proj_a:\n mode: auto\n" # 只显式列出 proj_a
|
|
)
|
|
(tmp_path / "agents.yaml").write_text(
|
|
"scheduler:\n interval_hours: 1\n max_concurrent: 3\n"
|
|
"claude:\n model: claude-sonnet-4-6\n"
|
|
"feishu:\n callback_port: 8765\n"
|
|
)
|
|
(tmp_path / "devices.yaml").write_text("devices: {}\n")
|
|
|
|
cfg = load_config(tmp_path)
|
|
# 只有显式列出的 proj_a 应该存在
|
|
assert "proj_a" in cfg.projects
|
|
# proj_b 未在 projects: 中列出,不应自动引入
|
|
assert "proj_b" not in cfg.projects
|
|
# scan_dirs 帮助解析了 proj_a 的路径
|
|
assert cfg.projects["proj_a"].path == parent / "proj_a"
|
|
assert cfg.projects["proj_a"].mode == "auto"
|
|
|
|
|
|
def test_load_config_kind_field_present(tmp_path):
|
|
"""ProjectConfig must have a 'kind' field after loading."""
|
|
_write_base_yamls(tmp_path)
|
|
cfg = load_config(tmp_path)
|
|
assert hasattr(cfg.projects["yolo"], "kind")
|
|
assert cfg.projects["yolo"].kind == "host" # default when not specified
|
|
|
|
|
|
def test_claude_wx_project_in_config(tmp_path):
|
|
"""claude-wx 应在 projects 配置中,mode=auto"""
|
|
(tmp_path / "projects.yaml").write_text(
|
|
"projects:\n"
|
|
" claude-wx:\n"
|
|
" path: /data/test-platform/claude-wx\n"
|
|
" mode: auto\n"
|
|
" description: \"Claude Code 飞书监控控制系统\"\n"
|
|
)
|
|
(tmp_path / "agents.yaml").write_text(
|
|
"scheduler:\n interval_hours: 1\n max_concurrent: 3\n"
|
|
"claude:\n model: claude-sonnet-4-6\n"
|
|
"feishu:\n callback_port: 8765\n"
|
|
)
|
|
(tmp_path / "devices.yaml").write_text("devices: {}\n")
|
|
cfg = load_config(tmp_path)
|
|
assert "claude-wx" in cfg.projects
|
|
assert cfg.projects["claude-wx"].mode == "auto"
|
|
|
|
|
|
def test_load_mcp_tools_yaml():
|
|
"""load_mcp_tools() 应返回 registry 和 roles 两个 dict"""
|
|
from nmfs_agents.config import load_mcp_tools
|
|
tools = load_mcp_tools()
|
|
assert "registry" in tools
|
|
assert "roles" in tools
|
|
assert "sequential-thinking" in tools["registry"]
|
|
assert "dev-embedded" in tools["roles"]
|
|
|
|
def test_load_role_routing_yaml():
|
|
"""load_role_routing() 返回 role → keywords 映射"""
|
|
from nmfs_agents.config import load_role_routing, route_role
|
|
routing = load_role_routing()
|
|
assert "dev-kernel" in routing
|
|
assert "kernel" in routing["dev-kernel"]
|
|
role = route_role("修复 RKNN NPU 推理崩溃", routing)
|
|
assert role == "dev-embedded"
|
|
|
|
def test_route_role_fallback():
|
|
"""无匹配时返回 dev-embedded(嵌入式项目默认)"""
|
|
from nmfs_agents.config import load_role_routing, route_role
|
|
routing = load_role_routing()
|
|
role = route_role("随机任务无关键词", routing)
|
|
assert role == "dev-embedded"
|
|
|
|
|
|
def test_queue_configs_loaded():
|
|
"""load_config 能解析三组队列配置"""
|
|
from nmfs_agents.config import load_config, QueueConfig
|
|
cfg = load_config()
|
|
assert hasattr(cfg, "queues"), "AgentsConfig 缺少 queues 字段"
|
|
assert "base_opt" in cfg.queues, "缺少 base_opt 队列"
|
|
assert "project_delivery" in cfg.queues, "缺少 project_delivery 队列"
|
|
assert "insight" in cfg.queues, "缺少 insight 队列"
|
|
base = cfg.queues["base_opt"]
|
|
assert base.max_concurrent == 5
|
|
assert base.scan_interval_hours == 4
|
|
assert "hw-engineer" in base.roles
|
|
assert "algo-antishake" in base.roles
|
|
assert "algo-position" in base.roles
|
|
assert "algo-nav" in base.roles
|
|
|
|
|
|
def test_queue_config_dataclass():
|
|
"""QueueConfig dataclass 字段正确"""
|
|
from nmfs_agents.config import QueueConfig
|
|
q = QueueConfig(
|
|
db="data/test.db",
|
|
max_concurrent=2,
|
|
scan_interval_hours=6,
|
|
roles=frozenset({"developer", "tester"}),
|
|
)
|
|
assert q.max_concurrent == 2
|
|
assert "developer" in q.roles
|