aps-agent/tests/golden/test_scheduling_entry_workf...

190 lines
9.2 KiB
Python

"""Scheduling entry must reach a real nonempty plan, not only a blocked response.
The real source is selected externally. A positive case adds only the explicitly
configured test worker; source inventory, orders and execution facts stay intact.
"""
from __future__ import annotations
import copy
import pytest
from server.auth.context import bind_identity, reset_identity
from tests.auth_provider import TestAuthProvider
from tests.golden.test_ruiyang_intake_workflow import (
_chat,
_confirm,
)
from tests.golden.test_ruiyang_intake_workflow import (
intake_client as intake_client, # noqa: PLC0414 -- explicit pytest fixture re-export
)
from tests.workbook_acceptance import load_expectations
def _review(client, project_id, session_id):
blocks, text = _chat(client, project_id, session_id, "分析一下数据文件")
review = next(block for block in blocks if block["type"] == "folder-pack")
return review, blocks, text
def _entry(review, action_id="start_schedule"):
actions = review["props"].get("nextActions") or []
assert actions, "资料检查后必须保留可操作的排产入口"
action = next(action for action in actions if action["id"] == action_id)
if action_id == "start_schedule":
assert action["label"] == "开始排产"
assert action["command"] == "立即排产"
elif action_id == "trial_schedule":
assert action["label"] == "试排"
assert action["command"] == "立即排产"
else:
assert action["label"] == "查看缺少资料"
assert action["command"] == "排产还缺什么"
assert action["enabled"] is True
return action
def _adopt(client, project_id, session_id):
_review_block, blocks, _text = _review(client, project_id, session_id)
card = next(block for block in blocks if block["type"] == "confirm-card")
result = _confirm(client, session_id, card)
assert result["refresh"] is True, result
def _supply_snapshot(world):
return {row["code"]: {key: row.get(key) for key in ("stock", "inTransit", "expectedArrivalDate")}
for row in world.get("flexMaterials", [])}
def _add_only_configured_test_worker(project_id):
"""Seed one test fact in the authenticated isolated project, never production."""
from server.state.store import get_store
expected = load_expectations()
identity = TestAuthProvider("round87-workflow")._identity("planner")
token = bind_identity(identity)
try:
store = get_store()
assert store.world_key == project_id
before = {"inventory": _supply_snapshot(store.data),
"orders": copy.deepcopy(store.data["flexOrders"]),
"wip": copy.deepcopy(store.data["flexWip"]),
"peopleCount": len(store.data["flexPersonnel"])}
worker = copy.deepcopy(expected["trial"]["supplementPerson"])
worker["sourceRef"] = {"kind": "isolated_test_supplement"}
assert not any(person["code"] == worker["code"] for person in store.data["flexPersonnel"])
store.data["flexPersonnel"].append(worker)
store.save()
return before
finally:
reset_identity(token)
def test_entry_before_adoption_reaches_review_without_scheduling(intake_client):
client, project_id, session_id = intake_client
review, _blocks, _text = _review(client, project_id, session_id)
assert review["props"]["adopted"] is False
assert review["props"]["canSchedule"] is False
action = _entry(review, "review_missing")
blocks, text = _chat(client, project_id, session_id, action["command"])
assert any(block["type"] == "confirm-card" and block["props"]["action"] == "import.commit" for block in blocks), text
assert not any(block["type"] == "flex-schedule" for block in blocks)
assert client.get("/api/master").json()["materials"] == []
assert client.get("/api/flex/world").json()["workOrders"] == []
def test_original_source_keeps_enabled_entry_and_reports_real_blockers(intake_client):
client, project_id, session_id = intake_client
_adopt(client, project_id, session_id)
review, blocks, _text = _review(client, project_id, session_id)
assert review["props"]["adopted"] is True
assert review["props"]["canSchedule"] is False
assert review["props"]["trialAvailable"] is True
_entry(review, "trial_schedule")
assert not any(block["type"] == "confirm-card" for block in blocks)
action = _entry(review, "review_missing")
planned, text = _chat(client, project_id, session_id, action["command"])
refreshed = next(block for block in planned if block["type"] == "folder-pack")
assert refreshed["props"]["canSchedule"] is False
assert refreshed["props"]["nextActions"][0]["id"] == "review_missing"
assert not any(block["type"] == "flex-schedule" for block in planned), text
assert not any(block["type"] == "confirm-card" for block in planned)
@pytest.mark.parametrize("command", ["立即排产", "在 APS 系统中手动触发排产"])
def test_real_chat_entry_generates_nonempty_plan_with_only_explicit_worker_supplement(intake_client, command):
client, project_id, session_id = intake_client
expected = load_expectations()
_adopt(client, project_id, session_id)
before = _add_only_configured_test_worker(project_id)
review, _blocks, _text = _review(client, project_id, session_id)
assert review["props"]["canSchedule"] is True
_entry(review)
planned, text = _chat(client, project_id, session_id, command)
result = next(block["props"] for block in planned if block["type"] == "flex-schedule")
assert result["trialOnly"] is True and result["productionReady"] is False
assert result["stats"]["woCount"] > 0, text
assert 0 < result["stats"]["vlCount"] < expected["entityCounts"]["orders"]
assert result["stats"]["blockedOrderCount"] > 0, "测试补充不能掩盖其他订单的真实缺项"
assert not any(block["type"] == "confirm-card" for block in planned), text
flex = client.get("/api/flex/world").json()
assert len(flex["workOrders"]) == result["stats"]["woCount"]
assert all(row["equipmentCode"] and row["plannedStartTime"] and row["plannedEndTime"]
for row in flex["workOrders"])
assert not set(expected["sandboxOrderNos"]) & {row["flexOrderNo"] for row in flex["workOrders"]}
assert result["conflicts"], "未解决的在制、库存缺项必须继续展示"
from server.state.store import get_store
token = bind_identity(TestAuthProvider("round87-workflow")._identity("planner"))
try:
world = get_store().data
assert _supply_snapshot(world) == before["inventory"]
assert world["flexOrders"] == before["orders"]
assert world["flexWip"] == before["wip"]
assert len(world["flexPersonnel"]) == before["peopleCount"] + 1
version = world["flexScheduleVersions"][-1]
assert version["woCount"] == result["stats"]["woCount"]
assert version["inputSnapshot"]["planningContext"]["sourceSha256"] == expected["sourceSha256"]
finally:
reset_identity(token)
def test_review_action_survives_actual_message_persistence_and_reload(intake_client):
client, project_id, session_id = intake_client
_adopt(client, project_id, session_id)
review, _blocks, _text = _review(client, project_id, session_id)
_entry(review, "trial_schedule")
messages = [{"role": "agent", "text": "资料核对结果", "blocks": [review]}]
saved = client.put(f"/api/sessions/{session_id}/messages", json={"messages": messages})
assert saved.status_code == 200, saved.text
loaded = client.get(f"/api/sessions/{session_id}/messages")
assert loaded.status_code == 200, loaded.text
recovered = loaded.json()["messages"][0]["blocks"][0]
assert recovered == review
action = _entry(recovered, "trial_schedule")
blocks, text = _chat(client, project_id, session_id, action["command"])
assert any(block["type"] == "flex-schedule" for block in blocks), text
def test_adopted_source_with_real_blockers_exposes_and_runs_trial(intake_client):
"""现场回归:有真实阻断时仍能从卡片发起草稿试排;假设项逐条留痕,真实缺料订单保持未排出。"""
client, project_id, session_id = intake_client
expected = load_expectations()
plan = expected["trialPlan"]
_adopt(client, project_id, session_id)
review, _blocks, _text = _review(client, project_id, session_id)
action = _entry(review, "trial_schedule")
planned, text = _chat(client, project_id, session_id, action["command"])
result = next(block["props"] for block in planned if block["type"] == "flex-schedule")
assert result["trialOnly"] is True
assert result["productionReady"] is False
assert result["stats"]["orderCount"] == expected["entityCounts"]["orders"]
assert result["stats"]["blockedOrderCount"] == len(plan["blockedOrderNos"])
assert result["stats"]["woCount"] > 0
assert {row["orderNo"] for row in result["lines"] if row.get("status") == "scheduled"} == set(plan["scheduledOrderNos"])
assert {row["orderNo"] for row in result["lines"] if row.get("status") == "blocked"} == set(plan["blockedOrderNos"])
assert {c["type"] for c in result["conflicts"] if c.get("severity") == plan["assumptionSeverity"]} == set(plan["assumptionTypes"])
assert result["conflicts"], text
assert not result["downloadUrl"]