Files
awoooi/apps/api/tests/agents/test_solver_agent.py
Your Name 7d1c85eb86
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
fix(hermes): ANTHROPIC_API_KEY 注入 + solver 信心度修法 A + 12-Agent 治理文件
- nl_gateway.py: ClaudeAgentOptions 透過 env= 注入 ANTHROPIC_API_KEY(CLAUDE_API_KEY alias),
  修復 SDK 找不到 API key 的問題(SDK 讀 ANTHROPIC_API_KEY,K8s secret 名稱是 CLAUDE_API_KEY)
- solver_agent.py: 修法 A — kubectl_command 欄位優先路徑,OpenClaw Nemo 回傳完整指令時
  不再被語意合成壓縮 confidence(0.9 → min(0.5) 的 bug),9 tests pass
- AGENTS.md: Codex CLI 對應版 CLAUDE.md(Codex Session 啟動用)
- docs/12-agent-game-rules.md: 12-Agent 任務判型 + 主責/協作派工 + 9 skills 對照(v1.0)
- .agents/skills/06-awoooi-monorepo-master.md: v1.6,新增 12-agent 協作治理章節

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 02:33:43 +08:00

160 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
solver_agent._extract_candidates 單元測試
2026-04-24 ogt + Claude Sonnet 4.6: 修法 A — kubectl_command 優先路徑驗證
"""
from __future__ import annotations
import sys
import os
# 確保 src 可找到
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../"))
import pytest
from src.agents.solver_agent import _extract_candidates
class TestExtractCandidatesNemoFormat:
"""OpenClaw Nemo 格式解析測試"""
def test_kubectl_command_field_preserves_confidence(self):
"""修法 A 核心kubectl_command 存在時confidence 不被壓縮"""
parsed = {
"action_title": "重啟 Crash Looping Pod",
"kubectl_command": "kubectl rollout restart deployment/awoooi-api -n awoooi-prod",
"confidence": 0.9,
"risk_level": "medium",
}
result = _extract_candidates(parsed)
assert len(result) == 1
c = result[0]
assert c.confidence == 0.9, f"期望 0.9,實際 {c.confidence}"
assert c.action == "kubectl rollout restart deployment/awoooi-api -n awoooi-prod"
assert "OpenClaw Nemo" in c.rationale
assert "重啟 Crash Looping Pod" in c.rationale
def test_kubectl_command_field_high_confidence(self):
"""kubectl_command 存在confidence 0.85 仍完整保留"""
parsed = {
"action_title": "Scale Down Pod Count",
"kubectl_command": "kubectl scale deployment/awoooi-api --replicas=2 -n awoooi-prod",
"confidence": 0.85,
"risk_level": "low",
}
result = _extract_candidates(parsed)
assert len(result) == 1
assert result[0].confidence == 0.85
assert result[0].blast_radius == 10 # risk_level=low → blast=10
def test_kubectl_command_field_takes_priority_over_synthesis(self):
"""kubectl_command 存在時,不走語意合成(不被 min(0.5) 壓)"""
parsed = {
"action_title": "重啟服務", # 若無 kubectl_command會走語意合成被壓到 0.5
"kubectl_command": "kubectl rollout restart deployment/api -n awoooi-prod",
"confidence": 0.9,
"risk_level": "medium",
}
result = _extract_candidates(parsed)
assert len(result) == 1
assert result[0].confidence == 0.9, "kubectl_command 路徑不應觸發 min(confidence, 0.5)"
def test_no_kubectl_command_action_title_has_kubectl(self):
"""舊路徑向後相容action_title 本身含 kubectl無 kubectl_command"""
parsed = {
"action_title": "kubectl rollout restart deployment -n awoooi-prod",
"confidence": 0.8,
"risk_level": "medium",
}
result = _extract_candidates(parsed)
assert len(result) == 1
assert result[0].confidence == 0.8
assert "kubectl rollout restart" in result[0].action
def test_no_kubectl_command_synthesis_caps_confidence(self):
"""語意合成備援路徑confidence 仍被 min(0.5) 壓制(預期行為)"""
parsed = {
"action_title": "重啟服務", # 無 kubectl_command觸發語意合成
"confidence": 0.9,
"risk_level": "medium",
}
result = _extract_candidates(parsed)
assert len(result) == 1
assert result[0].confidence == 0.5, "語意合成路徑 confidence 必須被壓到 0.5"
assert "[語意合成]" in result[0].rationale
def test_kubectl_command_empty_string_falls_through(self):
"""kubectl_command 為空字串時,回落到既有邏輯"""
parsed = {
"action_title": "重啟服務",
"kubectl_command": "",
"confidence": 0.9,
"risk_level": "medium",
}
result = _extract_candidates(parsed)
# 空 kubectl_command → 走語意合成 → confidence 被壓
assert len(result) == 1
assert result[0].confidence == 0.5
def test_kubectl_command_not_starting_with_kubectl_falls_through(self):
"""kubectl_command 非 kubectl 開頭(可能是雜訊),回落到既有邏輯"""
parsed = {
"action_title": "重啟服務",
"kubectl_command": "helm rollback awoooi-api",
"confidence": 0.9,
"risk_level": "medium",
}
result = _extract_candidates(parsed)
# helm 指令不被採用 → 語意合成 → confidence 被壓
assert len(result) == 1
assert result[0].confidence == 0.5
def test_no_action_title_no_candidates_uses_standard_path(self):
"""標準 candidates 陣列格式不受影響"""
parsed = {
"candidates": [
{
"action": "kubectl rollout restart deployment/api -n awoooi-prod",
"blast_radius": 25,
"rollback_cost": 20,
"confidence": 0.85,
"rationale": "重啟修復 OOM",
}
]
}
result = _extract_candidates(parsed)
assert len(result) == 1
assert result[0].confidence == 0.85
assert result[0].blast_radius == 25
def test_risk_level_blast_radius_mapping(self):
"""risk_level → blast_radius 映射正確"""
cases = [
("critical", 60),
("high", 40),
("medium", 25),
("low", 10),
("unknown", 30), # 預設
]
for risk_level, expected_blast in cases:
parsed = {
"action_title": "fix",
"kubectl_command": "kubectl get pods -n awoooi-prod",
"confidence": 0.9,
"risk_level": risk_level,
}
result = _extract_candidates(parsed)
assert result[0].blast_radius == expected_blast, (
f"risk_level={risk_level} → 期望 blast={expected_blast},實際 {result[0].blast_radius}"
)