aps-agent/tests/golden/test_action_result_narrativ...

338 lines
12 KiB
Python
Raw Permalink Normal View History

"""已完成业务动作的计划员语言说明必须走只读 Pi 叙事段。"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from server.agent_core import fallback_lane, harness
from server.auth.context import bind_identity, reset_identity
from server.contracts import AgentReply
from server.state.seed import seed_world
from tests.auth_provider import install_test_auth
class _Store:
def __init__(self) -> None:
self.data = seed_world()
self._next = 0
def next_id(self, _kind: str) -> int:
self._next += 1
return self._next
def save(self) -> None:
pass
@pytest.fixture(autouse=True)
def _clear_pending_confirmations():
harness._approval_store.clear()
yield
harness._approval_store.clear()
def test_action_result_uses_readonly_pi_without_business_tools(tmp_path, monkeypatch):
monkeypatch.setenv("APS_FALLBACK_DIR", str(tmp_path / "fallback"))
captured: dict[str, object] = {}
def runner(task: str, work_dir: Path):
captured["task"] = task
facts_path = work_dir.parent / "inbox" / "completed-action.json"
raw = facts_path.read_text(encoding="utf-8")
captured["payload"] = json.loads(raw.split("\n", 1)[1])
yield {
"type": "message_end",
"message": {
"role": "assistant",
"stopReason": "stop",
"content": [{
"type": "text",
"text": (
"status: success\n\n"
"资料已经采用完成。本次保存了物料和设备资料,"
"下一步可以生成试排方案。"
),
}],
},
}
yield {"type": "agent_end", "messages": []}
reply = fallback_lane.propose_action_result(
_Store(),
"s-action-result",
action="采用排产资料",
fallback_text=(
"排产资料已采用完成。本次写入:物料 45 项、设备 15 台。\n\n"
"这次操作只保存资料,没有生成排产方案。"
),
facts={
"summary": "排产资料已采用完成。",
"items": [
{"label": "物料", "count": 45, "unit": "项"},
{"label": "设备", "count": 15, "unit": "台"},
],
"nextStep": "下一步可以生成试排方案。",
},
runner=runner,
config=fallback_lane.FallbackConfig(pi_home=str(tmp_path / "pi-home")),
)
assert reply.text == (
"资料已经采用完成。本次保存了物料和设备资料,下一步可以生成试排方案。"
)
payload = captured["payload"]
assert payload["本次写入"] == [
{"label": "物料", "count": 45, "unit": "项"},
{"label": "设备", "count": 15, "unit": "台"},
]
assert "materials×45" not in captured["task"]
assert "APS 业务工具" not in captured["task"]
assert "aps_invoke" not in captured["task"]
assert "completed-action.json" in captured["task"]
def test_action_result_keeps_success_when_pi_is_unavailable(tmp_path, monkeypatch):
monkeypatch.setenv("APS_FALLBACK_DIR", str(tmp_path / "fallback"))
def unavailable(*_args, **_kwargs):
raise fallback_lane.FallbackUnavailable("模型服务未配置")
monkeypatch.setattr(fallback_lane, "build_pi_runner", unavailable)
fallback_text = (
"排产资料已采用完成。本次写入:物料 45 项、设备 15 台。\n\n"
"这次操作只保存资料,没有生成排产方案。"
)
reply = fallback_lane.propose_action_result(
_Store(),
"s-action-result",
action="采用排产资料",
fallback_text=fallback_text,
facts={
"summary": "排产资料已采用完成。",
"items": [
{"label": "物料", "count": 45, "unit": "项"},
{"label": "设备", "count": 15, "unit": "台"},
],
"nextStep": "下一步可以生成试排方案。",
},
config=fallback_lane.FallbackConfig(pi_home=str(tmp_path / "pi-home")),
)
assert reply.text == fallback_text
def test_action_result_rejects_internal_field_names_from_pi(tmp_path, monkeypatch):
monkeypatch.setenv("APS_FALLBACK_DIR", str(tmp_path / "fallback"))
def runner(_task: str, _work_dir: Path):
yield {
"type": "message_end",
"message": {
"role": "assistant",
"stopReason": "stop",
"content": [{
"type": "text",
"text": "status: success\n\n已采用 materials×45、equipment×15。",
}],
},
}
yield {"type": "agent_end", "messages": []}
fallback_text = "排产资料已采用完成。本次写入:物料 45 项、设备 15 台。"
reply = fallback_lane.propose_action_result(
_Store(),
"s-action-result",
action="采用排产资料",
fallback_text=fallback_text,
facts={
"summary": "排产资料已采用完成。",
"items": [
{"label": "物料", "count": 45, "unit": "项"},
{"label": "设备", "count": 15, "unit": "台"},
],
"nextStep": "下一步可以生成试排方案。",
},
runner=runner,
config=fallback_lane.FallbackConfig(pi_home=str(tmp_path / "pi-home")),
)
assert reply.text == fallback_text
def test_import_commit_confirmation_route_returns_pi_explanation(tmp_path, monkeypatch):
monkeypatch.setenv("APS_SEED_DEMO", "0")
monkeypatch.setenv("APS_HOME", str(tmp_path))
monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "master.db"))
monkeypatch.setenv("APS_WORLD_PATH", str(tmp_path / "world.json"))
monkeypatch.setenv("APS_DATA_DIR", str(tmp_path / "data"))
provider = install_test_auth(monkeypatch, "action-result-test")
from server.gateway.app import create_app
from server.state.store import get_store
with TestClient(create_app()) as client:
login = client.post(
"/api/auth/login",
json={"username": "planner", "password": "test"},
)
assert login.status_code == 200, login.text
store = get_store()
token = bind_identity(provider._identity("planner"))
try:
block = harness.stage_confirmation(
"s-action-result",
"import.commit",
{
"filename": "测试资料.xlsx",
"analysisIngest": {
"projectName": "测试项目",
"sources": ["测试资料.xlsx"],
},
"batches": [{
"kind": "materials",
"sheet": "物料",
"okCount": 1,
"errorCount": 0,
"okRows": [{
"code": "M-100",
"name": "测试物料",
"type": "RAW_MATERIAL",
"unit": "kg",
"stock": 0,
}],
}],
},
title="核对并采用本次排产资料",
summary_lines=["确认后保存资料,本步不会排产。"],
)
finally:
reset_identity(token)
store.save()
captured: dict[str, object] = {}
def fake_result(
store, session_id, *, action, fallback_text, facts,
actor="planner", **_kwargs,
):
captured.update({
"session_id": session_id,
"action": action,
"fallback_text": fallback_text,
"facts": facts,
"actor": actor,
})
return AgentReply(text="资料已经采用,下一步可以生成试排方案。")
monkeypatch.setattr(fallback_lane, "propose_action_result", fake_result)
response = client.post(
"/api/actions/confirm",
json={
"sessionId": "s-action-result",
"confirmId": block.props["confirmId"],
"approve": True,
},
)
assert response.status_code == 200, response.text
assert response.json()["message"] == "资料已经采用,下一步可以生成试排方案。"
assert captured["session_id"] == "s-action-result"
assert captured["action"] == "采用排产资料"
assert str(captured["fallback_text"]).startswith("排产资料已采用完成。")
assert captured["facts"]["items"] == [
{"label": "物料", "count": 1, "unit": "项"},
]
assert "materials×" not in json.dumps(captured["facts"], ensure_ascii=False)
assert captured["actor"] == "planner"
def test_import_commit_batch_route_returns_pi_explanation(tmp_path, monkeypatch):
monkeypatch.setenv("APS_SEED_DEMO", "0")
monkeypatch.setenv("APS_HOME", str(tmp_path))
monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "master.db"))
monkeypatch.setenv("APS_WORLD_PATH", str(tmp_path / "world.json"))
monkeypatch.setenv("APS_DATA_DIR", str(tmp_path / "data"))
provider = install_test_auth(monkeypatch, "action-result-batch-test")
from server.gateway.app import create_app
from server.state.store import get_store
with TestClient(create_app()) as client:
login = client.post(
"/api/auth/login",
json={"username": "planner", "password": "test"},
)
assert login.status_code == 200, login.text
store = get_store()
token = bind_identity(provider._identity("planner"))
try:
block = harness.stage_confirmation(
"s-action-result-batch",
"import.commit",
{
"filename": "批量测试资料.xlsx",
"analysisIngest": {
"projectName": "批量测试项目",
"sources": ["批量测试资料.xlsx"],
},
"batches": [{
"kind": "equipment",
"sheet": "设备",
"okCount": 1,
"errorCount": 0,
"okRows": [{
"code": "EQ-100",
"name": "测试设备",
"type": "MACHINE",
}],
}],
},
title="核对并采用本次排产资料",
summary_lines=["确认后保存资料,本步不会排产。"],
)
finally:
reset_identity(token)
store.save()
captured: list[dict[str, object]] = []
def fake_result(
store, session_id, *, action, fallback_text, facts,
actor="planner", **_kwargs,
):
captured.append({
"session_id": session_id,
"action": action,
"fallback_text": fallback_text,
"facts": facts,
"actor": actor,
})
return AgentReply(text="资料已经采用,下一步可以生成试排方案。")
monkeypatch.setattr(fallback_lane, "propose_action_result", fake_result)
response = client.post(
"/api/actions/confirm-batch",
json={
"sessionId": "s-action-result-batch",
"confirmIds": [block.props["confirmId"]],
"approve": True,
},
)
assert response.status_code == 200, response.text
results = response.json()["results"]
assert len(results) == 1
assert results[0]["ok"] is True
assert results[0]["message"] == "资料已经采用,下一步可以生成试排方案。"
assert len(captured) == 1
assert captured[0]["session_id"] == "s-action-result-batch"
assert captured[0]["action"] == "采用排产资料"
assert captured[0]["facts"]["items"] == [
{"label": "设备", "count": 1, "unit": "台"},
]
assert "equipment×" not in json.dumps(captured[0]["facts"], ensure_ascii=False)
assert captured[0]["actor"] == "planner"