aps-agent/tests/golden/test_external_engine.py

174 lines
6.1 KiB
Python
Raw Normal View History

# ============================================================
# 外部算法排产 Skill 黄金测试
# ============================================================
from __future__ import annotations
import pytest
from server.agent_core.skills import SkillRegistry
from server.aps_domain.scheduling_dto import (
apply_flex_solution, world_to_flex_problem,
)
from server.engines import get_engine
from server.engines.external_engine import ExternalEngine, run_external_flex
from server.integrations.algo_skill_stub import solve_problem
from server.state.seed import ensure_flex_seed, seed_world
class _MemStore:
def __init__(self, data):
self.data = data
self._c = {}
def next_id(self, kind: str) -> int:
self._c[kind] = self._c.get(kind, 0) + 1
return self._c[kind]
def save(self) -> None:
pass
def test_stub_solves_flex_problem():
world = seed_world()
ensure_flex_seed(world)
problem = world_to_flex_problem(world)
assert problem.orders
assert problem.routings
assert problem.resources
sol = solve_problem(problem)
assert sol.status in ("FEASIBLE", "OPTIMAL")
assert sol.operations
assert sol.runId
# 写回
counters = {}
def nid(k):
counters[k] = counters.get(k, 0) + 1
return counters[k]
summary = apply_flex_solution(world, sol, nid, skill_id="algo.stub")
assert summary["woCount"] >= 1
assert summary["skillId"] == "algo.stub"
assert any(v.get("engineType") == "EXTERNAL" for v in world["flexScheduleVersions"])
def test_external_engine_local_stub(tmp_path, monkeypatch):
monkeypatch.setenv("APS_SKILLS_PATH", str(tmp_path / "skills.json"))
# 重置单例
import server.agent_core.skills as sk
sk._registry = None
world = seed_world()
ensure_flex_seed(world)
eng = get_engine("EXTERNAL:algo.stub")
assert isinstance(eng, ExternalEngine)
counters = {}
def nid(k):
counters[k] = counters.get(k, 0) + 1
return counters[k]
from server.engines.base import EngineParams
from server.timeutil import add_minutes, fmt_date, today0
params = EngineParams(
orderIds=[], engineType="EXTERNAL", strategyTemplate="EXTERNAL",
planningHorizonDays=14, startDate=fmt_date(add_minutes(today0(), 24 * 60)),
)
world["_externalSkillId"] = "algo.stub"
result = eng.solve(world, params, nid)
assert result.engineType == "EXTERNAL"
assert result.woCount >= 1
assert any(r.startswith("skill:") for r in result.evidenceRefs)
assert any(r.startswith("run:") for r in result.evidenceRefs)
def test_run_external_flex_and_registry_health(tmp_path, monkeypatch):
monkeypatch.setenv("APS_SKILLS_PATH", str(tmp_path / "skills.json"))
import server.agent_core.skills as sk
sk._registry = None
reg = SkillRegistry(path=str(tmp_path / "skills.json"))
health = reg.health("algo.stub")
assert health and health[0]["ok"] is True
world = seed_world()
ensure_flex_seed(world)
store = _MemStore(world)
summary = run_external_flex(store, skill_id="algo.stub")
assert summary["woCount"] >= 1
assert summary["runId"]
def test_real_world_external_skill_is_v2_validated_before_materialization(tmp_path, monkeypatch):
monkeypatch.setenv("APS_SKILLS_PATH", str(tmp_path / "skills-v2.json"))
import server.agent_core.skills as sk
from tests.golden.test_closed_loop_runtime import _ready_world
sk._registry = None
world = _ready_world()
world["businessDate"] = "2026-08-03"
world["scheduleVersions"] = []
store = _MemStore(world)
summary = run_external_flex(store, skill_id="algo.stub")
assert summary["executionMode"] == "CLOSED_LOOP_V2_VALIDATED"
assert summary["solveStatus"] == "FEASIBLE"
assert summary["validation"]["valid"] is True
assert summary["woCount"] == 1
version = next(
row for row in world["flexScheduleVersions"]
if row["id"] == summary["versionId"]
)
assert version["validationReport"]["hardViolations"] == []
assert version["schedulingSolutionV2"]["problemId"] == summary["planningProblemId"]
assert world["flexWorkOrders"][0]["activityId"].startswith("ACT:")
def test_real_world_external_skill_invalid_resource_fails_closed(tmp_path, monkeypatch):
monkeypatch.setenv("APS_SKILLS_PATH", str(tmp_path / "skills-invalid.json"))
import server.agent_core.skills as sk
from tests.golden.test_closed_loop_runtime import _ready_world
sk._registry = None
world = _ready_world()
world["businessDate"] = "2026-08-03"
world["scheduleVersions"] = []
store = _MemStore(world)
def invalid_solution(self, skill, problem):
solution = solve_problem(problem)
operations = tuple(
op.model_copy(update={"resourceCode": "EQ-NOT-ALLOWED"})
for op in solution.operations
)
return solution.model_copy(update={"operations": operations})
monkeypatch.setattr(ExternalEngine, "_call_skill", invalid_solution)
with pytest.raises(ValueError, match="SchedulingProblemV2 validator"):
run_external_flex(store, skill_id="algo.stub")
assert world.get("flexScheduleVersions") in (None, [])
assert world.get("flexVirtualLines") in (None, [])
assert world.get("flexWorkOrders") in (None, [])
assert world["closedLoopProblems"]
def test_external_skill_version_metadata_uses_closed_loop_business_date(tmp_path, monkeypatch):
monkeypatch.setenv("APS_SKILLS_PATH", str(tmp_path / "skills-business-date.json"))
import server.agent_core.skills as sk
from tests.golden.test_closed_loop_runtime import _ready_world
sk._registry = None
world = _ready_world()
world["businessDate"] = "2026-08-02"
world["scheduleVersions"] = []
store = _MemStore(world)
summary = run_external_flex(store, skill_id="algo.stub")
version = next(
row for row in world["flexScheduleVersions"]
if row["id"] == summary["versionId"]
)
assert summary["versionNo"].startswith("EXT20260802-")
assert version["versionNo"] == summary["versionNo"]
assert version["createdAt"] == "2026-08-02 00:00"