163 lines
7.8 KiB
Python
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()
|