- 修复脚本配置文件路径处理问题,避免打包后 ZIP_FILE_PATH 等参数未生效并回退默认值 - 在 chat 模式执行前增加参数归一化和预检,提前检查 ZIP_FILE_PATH、脚本入口和 MCP 配置 - 优化 chat 交互体验,问候语不再触发结构化分析,分析前增加提示,执行中播报每步 action 状态 - 修复 action 失败被误判为 LangGraph 不可用的问题,失败后保留 checkpoint 并给出明确续跑提示 - 补齐 MCP 参数传递,支持向 action 传入 hashCode、nodeUrl、targetIp 等上下文 - 增强全局 action、单 IP action、回滚和日志下载的异常处理与进度回调 - 同步 README、打包 README 和 run.sh 帮助文案,更新打包后 chat 的实际使用说明 - 补充回归测试,覆盖 chat 预检、进度播报、问候处理、MCP 传参与配置路径修复
218 lines
7.3 KiB
Python
218 lines
7.3 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pam_deploy_graph.agent import PamDeployAgent
|
|
from pam_deploy_graph.checkpoint_store import load_agent_state
|
|
from pam_deploy_graph.constants import GLOBAL_ACTION_SEQUENCE
|
|
from pam_deploy_graph.fake_runner import FakeActionRunner
|
|
|
|
|
|
PARAMS = {
|
|
"HOME_BASE_URL": "https://pam.home.example.com",
|
|
"CLIENT_ID": "client",
|
|
"CLIENT_SECRET": "secret",
|
|
"AIRPORT_CODE": "HET",
|
|
"APP_NAME": "PAM",
|
|
"MODULE_NAME": "Node",
|
|
"VERSION_NUMBER": "2.0.5",
|
|
"ZIP_FILE_PATH": "C:/pkg.zip",
|
|
}
|
|
|
|
|
|
def test_run_deploy_flow_success(tmp_path: Path):
|
|
agent = PamDeployAgent(fake_runner=FakeActionRunner())
|
|
state = agent.create_state(
|
|
params=PARAMS,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "config.txt"),
|
|
)
|
|
|
|
agent.run_deploy_flow(state)
|
|
|
|
assert state.pending_confirmation == ""
|
|
assert set(state.ip_states) == {"192.168.1.10", "192.168.1.11"}
|
|
assert all(item["status"] == "SUCCESS" for item in state.ip_states.values())
|
|
|
|
|
|
def test_create_state_writes_absolute_script_config_path_and_normalized_zip(tmp_path: Path):
|
|
package_path = tmp_path / "pkg.zip"
|
|
params = {**PARAMS, "ZIP_FILE_PATH": str(package_path)}
|
|
agent = PamDeployAgent(fake_runner=FakeActionRunner())
|
|
|
|
state = agent.create_state(
|
|
params=params,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "runtime" / "config.txt"),
|
|
trace_file_path=str(tmp_path / "logs" / "trace.log"),
|
|
)
|
|
|
|
assert Path(state.config_path).is_absolute()
|
|
assert Path(state.trace_file_path).is_absolute()
|
|
config_text = Path(state.config_path).read_text(encoding="utf-8")
|
|
assert f"ZIP_FILE_PATH={package_path.resolve()}" in config_text
|
|
|
|
|
|
def test_global_action_requires_hash_code_from_upload_package(tmp_path: Path):
|
|
fake = FakeActionRunner({"upload-package": {"ACTION": "upload-package"}})
|
|
agent = PamDeployAgent(fake_runner=fake)
|
|
state = agent.create_state(
|
|
params=PARAMS,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "config.txt"),
|
|
checkpoint_path=str(tmp_path / "checkpoint.json"),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="缺少必要字段: HASH_CODE"):
|
|
agent.run_deploy_flow(state)
|
|
|
|
assert state.last_failed_step == "upload-package"
|
|
assert "upload-package" not in state.completed_global_steps
|
|
|
|
|
|
def test_run_deploy_flow_stops_on_verify_failure(tmp_path: Path):
|
|
fake = FakeActionRunner(
|
|
{
|
|
"verify-ip:192.168.1.10": {
|
|
"ACTION": "verify-ip",
|
|
"IP": "192.168.1.10",
|
|
"SUCCESS": "false",
|
|
"MESSAGE": "health check failed",
|
|
}
|
|
}
|
|
)
|
|
agent = PamDeployAgent(fake_runner=fake)
|
|
state = agent.create_state(
|
|
params=PARAMS,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "config.txt"),
|
|
)
|
|
|
|
agent.run_deploy_flow(state)
|
|
|
|
assert state.pending_confirmation == "rollback-ip:192.168.1.10"
|
|
assert state.ip_states["192.168.1.10"]["status"] == "FAILED"
|
|
assert state.ip_states["192.168.1.10"]["rollback_status"] == "PENDING_AGENT_CONFIRMATION"
|
|
assert "192.168.1.11" not in state.ip_states
|
|
assert any(event["type"] == "CONFIRMATION_REQUIRED" for event in state.events)
|
|
|
|
|
|
def test_action_analysis_event_is_recorded_when_enabled(tmp_path: Path):
|
|
fake = FakeActionRunner(
|
|
{
|
|
"verify-ip:192.168.1.10": {
|
|
"ACTION": "verify-ip",
|
|
"IP": "192.168.1.10",
|
|
"SUCCESS": "false",
|
|
"MESSAGE": "health check failed",
|
|
}
|
|
}
|
|
)
|
|
agent = PamDeployAgent(fake_runner=fake, action_analysis_enabled=True)
|
|
state = agent.create_state(
|
|
params=PARAMS,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "config.txt"),
|
|
)
|
|
|
|
agent.run_deploy_flow(state)
|
|
|
|
analyses = [event for event in state.events if event["type"] == "ACTION_ANALYSIS"]
|
|
verify_analysis = [event for event in analyses if event["stage"] == "verify-ip"][0]
|
|
assert verify_analysis["has_anomaly"] is True
|
|
assert verify_analysis["severity"] == "high"
|
|
assert verify_analysis["requires_confirmation"] is True
|
|
|
|
|
|
def test_confirm_pending_rollback_runs_rollback_and_resume_continues(tmp_path: Path):
|
|
fake = FakeActionRunner(
|
|
{
|
|
"verify-ip:192.168.1.10": {
|
|
"ACTION": "verify-ip",
|
|
"IP": "192.168.1.10",
|
|
"SUCCESS": "false",
|
|
"MESSAGE": "health check failed",
|
|
}
|
|
}
|
|
)
|
|
agent = PamDeployAgent(fake_runner=fake)
|
|
state = agent.create_state(
|
|
params=PARAMS,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "config.txt"),
|
|
)
|
|
|
|
agent.run_deploy_flow(state)
|
|
request = agent.build_confirmation_request(state)
|
|
agent.confirm_pending(state, approved=True)
|
|
agent.run_deploy_flow(state)
|
|
|
|
assert request["type"] == "rollback-ip"
|
|
assert state.pending_confirmation == ""
|
|
assert state.ip_states["192.168.1.10"]["rollback_status"] == "ROLLBACK_DONE"
|
|
assert state.ip_states["192.168.1.11"]["status"] == "SUCCESS"
|
|
assert any(call[0] == "rollback-ip" for call in fake.calls)
|
|
|
|
|
|
def test_failed_rollback_keeps_confirmation_pending(tmp_path: Path):
|
|
fake = FakeActionRunner(
|
|
{
|
|
"verify-ip:192.168.1.10": {
|
|
"ACTION": "verify-ip",
|
|
"IP": "192.168.1.10",
|
|
"SUCCESS": "false",
|
|
"MESSAGE": "health check failed",
|
|
},
|
|
"rollback-ip:192.168.1.10": {
|
|
"_fail": True,
|
|
"ACTION": "rollback-ip",
|
|
"IP": "192.168.1.10",
|
|
"MESSAGE": "rollback failed",
|
|
},
|
|
}
|
|
)
|
|
agent = PamDeployAgent(fake_runner=fake)
|
|
state = agent.create_state(
|
|
params=PARAMS,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "config.txt"),
|
|
)
|
|
|
|
agent.run_deploy_flow(state)
|
|
agent.confirm_pending(state, approved=True)
|
|
|
|
assert state.pending_confirmation == "rollback-ip:192.168.1.10"
|
|
assert state.ip_states["192.168.1.10"]["rollback_status"] == "ROLLBACK_FAILED"
|
|
|
|
|
|
def test_checkpoint_resume_skips_completed_global_and_success_ip(tmp_path: Path):
|
|
checkpoint = tmp_path / "checkpoint.json"
|
|
fake = FakeActionRunner()
|
|
agent = PamDeployAgent(fake_runner=fake)
|
|
state = agent.create_state(
|
|
params=PARAMS,
|
|
execution_strategy="fake",
|
|
config_path=str(tmp_path / "config.txt"),
|
|
checkpoint_path=str(checkpoint),
|
|
)
|
|
state.completed_global_steps = list(GLOBAL_ACTION_SEQUENCE)
|
|
state.online_ips = ["192.168.1.10", "192.168.1.11"]
|
|
state.target_ips = ["192.168.1.10", "192.168.1.11"]
|
|
state.ip_states["192.168.1.10"] = {
|
|
"status": "SUCCESS",
|
|
"completed_steps": ["upgrade-ip", "poll-upgrade-progress", "start-ip", "verify-ip", "download-log"],
|
|
"failed_stage": "",
|
|
"failure_reason": "",
|
|
"rollback_status": "ROLLBACK_NOT_RUN",
|
|
"rollback_stop_first": False,
|
|
"log_file": "logs/fake.zip",
|
|
}
|
|
|
|
agent.run_deploy_flow(state)
|
|
loaded = load_agent_state(checkpoint)
|
|
|
|
called_actions = [call[0] for call in fake.calls]
|
|
assert "get-token" not in called_actions
|
|
assert all(call[1].get("ip") != "192.168.1.10" for call in fake.calls)
|
|
assert loaded.ip_states["192.168.1.11"]["status"] == "SUCCESS"
|