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

338 lines
12 KiB
Python
Raw Permalink 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.

"""已完成业务动作的计划员语言说明必须走只读 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"