aps-agent/tests/e2e/round87_masterdata_server.py

163 lines
7.8 KiB
Python

"""Isolated real APS HTTP backend for the Round 87 browser acceptance.
Only authentication is a test provider. Project files, database, approvals,
master-data edits and scheduling execute through production code.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
import tempfile
from pathlib import Path
from fastapi import HTTPException, Request
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=18787)
parser.add_argument("--source", default=os.environ.get("ROUND87_SOURCE"))
parser.add_argument("--expectations", type=Path)
args = parser.parse_args()
repo = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(repo))
from tests.workbook_acceptance import load_expectations, resolve_source
expectations = load_expectations(args.expectations)
source = resolve_source(args.source, required=True, expectations=expectations)
expected_sha = expectations["sourceSha256"]
if args.port in {8000, 8003, 5173}:
raise ValueError("Refusing a user development service port")
root = Path(tempfile.mkdtemp(prefix="aps-round87-browser-"))
data = root / "data"
data.mkdir()
path_env = {
"APS_HOME": root, "APS_DATA_DIR": data, "APS_DB_PATH": data / "master.db",
"APS_WORLD_PATH": data / "world.json", "APS_PROJECTS_PATH": root / "sessions/workspace.json",
"APS_KNOWLEDGE_PATH": data / "knowledge.json", "APS_CHECKPOINT_PATH": data / "checkpoints.json",
"APS_PREFERENCE_PATH": data / "preferences.json", "APS_EMBEDDINGS_PATH": data / "embeddings.json",
"APS_APPROVAL_PATH": data / "approvals.json", "APS_AUDIT_LEDGER_DIR": data / "audit-ledger",
"APS_AUDIT_MIRROR_DIR": data / "audit-mirror", "APS_AUTOMATION_STATE_PATH": data / "automation.json",
"APS_MCP_BUS_PATH": data / "mcp-bus.json", "APS_BRANCH_PATH": data / "branches.json",
"APS_BRANCH_DIR": data / "branches", "APS_GOLDEN_CACHE": data / "golden-tests.json",
"APS_MESH_PATH": data / "mesh.json", "APS_FALLBACK_ROOT": root / "fallback",
}
for key, value in path_env.items():
os.environ[key] = str(value)
for key in ("APS_DATABASE_URL", "APS_DB_DISABLED", "MES_HTTP_BASE_URL"):
os.environ.pop(key, None)
os.environ.update(APS_MODE="web", APS_AUTH_ENABLED="1", APS_SEED_DEMO="0",
APS_APPROVAL_BACKEND="file", APS_AUTH_COOKIE_SECURE="0",
APS_AUTOMATION_DRIVER="0", EMBEDDING_PROVIDER="off")
repo = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(repo))
import uvicorn
from fastapi import FastAPI
import server.auth.middleware as auth_middleware
import server.gateway.app as gateway_module
from tests.auth_provider import TestAuthProvider
provider = TestAuthProvider("round87-masterdata-acceptance")
auth_middleware.get_auth_provider = lambda: provider
gateway_module.get_auth_provider = lambda: provider
production_app = gateway_module.create_app()
if os.environ.get("APS_E2E_FAKE_PI") == "1":
# Browser acceptance must not depend on a live LLM: the suite sends one
# analysis command and then works through the UI. Real Pi runs stay
# covered by scripts/run-pi-real-workbook-e2e.ps1.
from tests.pi_primary_adapter import install_fake_pi_tool_adapter
class _ModulePatcher:
@staticmethod
def setattr(target, name, value):
setattr(target, name, value)
install_fake_pi_tool_adapter(_ModulePatcher)
app = FastAPI()
@app.get("/api/__e2e__/health")
@app.get("/__e2e__/health")
async def acceptance_health() -> dict:
return {"isolated": True, "runtimeRoot": str(root), "sourceSha256": expected_sha,
"sourceUnchanged": hashlib.sha256(source.read_bytes()).hexdigest() == expected_sha,
"productionCode": True}
@app.post("/api/__e2e__/seed-legacy-intake")
async def seed_legacy_intake_fixture(payload: dict, request: Request) -> dict:
from server.aps_domain.importers import preview_file
from server.auth.context import bind_identity, reset_identity
from server.state.projects import get_project_store
from server.state.store import get_store
from tests.intake_recovery_fixture import seed_legacy_intake
identity = await provider.authenticate(request)
token = bind_identity(identity)
try:
workspace = get_project_store().snapshot(include_messages=False)
if workspace["activeProjectId"] != payload.get("projectId") or not any(
row["id"] == payload.get("sessionId") and row["projectId"] == payload.get("projectId")
for row in workspace["sessions"]
):
raise HTTPException(status_code=409, detail="Fixture must target the active test project/session")
store = get_store()
if store.data.get("flexOrders"):
raise HTTPException(status_code=409, detail="Fixture requires an empty test project")
preview = preview_file(source.name, source.read_bytes(), store.data)
seed_legacy_intake(store.data, store.next_id, preview)
store.save()
return {"formalOrderNos": [row["orderNo"] for row in store.data["flexOrders"]],
"sourceSha256": expected_sha}
finally:
reset_identity(token)
@app.post("/api/__e2e__/supplement-scheduling-skill")
async def supplement_scheduling_skill(payload: dict, request: Request) -> dict:
"""Explicit test supplement, never installed on the production application."""
import copy
from server.auth.context import bind_identity, reset_identity
from server.state.projects import get_project_store
from server.state.store import get_store
identity = await provider.authenticate(request)
token = bind_identity(identity)
try:
workspace = get_project_store().snapshot(include_messages=False)
if workspace["activeProjectId"] != payload.get("projectId") or not any(
row["id"] == payload.get("sessionId") and row["projectId"] == payload.get("projectId")
for row in workspace["sessions"]
):
raise HTTPException(status_code=409, detail="Fixture must target the active test project/session")
store = get_store()
if not any(item.get("sha256") == expected_sha for item in store.data.get("intakeSources") or []):
raise HTTPException(status_code=409, detail="Fixture requires adoption of the designated workbook")
materials = copy.deepcopy(store.data["materials"])
orders = copy.deepcopy(store.data["flexOrders"])
person = copy.deepcopy(expectations["trial"]["supplementPerson"])
if not any(row.get("code") == person["code"] for row in store.data.get("flexPersonnel") or []):
store.data["flexPersonnel"].append({**person, "sourceRef": {"kind": "isolated_test_supplement"}})
store.save()
assert store.data["materials"] == materials and store.data["flexOrders"] == orders
return {"isolated": True, "personCode": person["code"],
"orderCount": len(orders), "stockUnchanged": True}
finally:
reset_identity(token)
app.mount("/", production_app)
print(json.dumps({"event": "round87-browser-server", "port": args.port,
"runtimeRoot": str(root)}, ensure_ascii=False), flush=True)
try:
uvicorn.run(app, host="127.0.0.1", port=args.port, log_level="warning")
finally:
from server.db.database import reset_engine
reset_engine()
print(json.dumps({"event": "round87-browser-server-stopped", "runtimeRoot": str(root)}), flush=True)
if __name__ == "__main__":
main()