Add P24 remote ops smoke

This commit is contained in:
Codex
2026-05-12 09:18:12 +08:00
parent ee7eefc2b2
commit da3c456b95
4 changed files with 439 additions and 0 deletions
+5
View File
@@ -136,6 +136,11 @@ def run_http_smoke(base_url: str, *, attempts: int = 10, sleep_seconds: float =
"/trading/research-council/optimization-cycles?instrument_id=1&limit=1",
"/trading/research-council/optimization-review?instrument_id=1",
"/trading/review-packet?instrument_id=1",
"/trading/alerts/quality-summary?instrument_id=1",
"/trading/alerts/operator-digest?instrument_id=1",
"/trading/runtime-incidents?limit=20&refresh=0",
"/trading/maintenance-summary?instrument_id=1",
"/trading/unattended-harness?instrument_id=1",
]
results = []
for endpoint in endpoints:
+330
View File
@@ -0,0 +1,330 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Callable, Optional
import argparse
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
DEFAULT_BASE_URL = "http://192.168.3.90:8000"
DEFAULT_TIMEOUT_SECONDS = 20.0
DEFAULT_ATTEMPTS = 2
DEFAULT_SLEEP_SECONDS = 2.0
ENDPOINTS = (
{
"name": "health",
"method": "GET",
"path": "/health",
"expected_statuses": (200,),
},
{
"name": "runtime_status",
"method": "GET",
"path": "/trading/runtime-status",
"expected_statuses": (200,),
},
{
"name": "review_packet",
"method": "GET",
"path": "/trading/review-packet?instrument_id={instrument_id}",
"expected_statuses": (200,),
},
{
"name": "alert_quality",
"method": "GET",
"path": "/trading/alerts/quality-summary?instrument_id={instrument_id}",
"expected_statuses": (200,),
},
{
"name": "operator_digest",
"method": "GET",
"path": "/trading/alerts/operator-digest?instrument_id={instrument_id}",
"expected_statuses": (200,),
},
{
"name": "runtime_incidents",
"method": "GET",
"path": "/trading/runtime-incidents?limit=20&refresh=0",
"expected_statuses": (200,),
},
{
"name": "maintenance_summary",
"method": "GET",
"path": "/trading/maintenance-summary?instrument_id={instrument_id}",
"expected_statuses": (200,),
},
{
"name": "visual_watch_cleanup_dry_run",
"method": "POST",
"path": "/trading/visual-watch/cleanup-captures",
"expected_statuses": (200,),
"body": {
"dry_run": True,
"retention_hours": 168,
"max_delete_files": 0,
},
},
{
"name": "unattended_harness",
"method": "GET",
"path": "/trading/unattended-harness?instrument_id={instrument_id}",
"expected_statuses": (200,),
"payload_status_must_pass": True,
},
)
def run_remote_ops_smoke(
*,
base_url: str = DEFAULT_BASE_URL,
instrument_id: int = 1,
attempts: int = DEFAULT_ATTEMPTS,
sleep_seconds: float = DEFAULT_SLEEP_SECONDS,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
opener: Optional[Callable[[urllib.request.Request, float], object]] = None,
sleeper: Optional[Callable[[float], None]] = None,
) -> dict:
opener = opener or _urlopen
sleeper = sleeper or time.sleep
endpoint_results = []
blockers = []
warnings = []
passed_checks = []
for endpoint in ENDPOINTS:
result = _call_endpoint(
endpoint=endpoint,
base_url=base_url,
instrument_id=instrument_id,
attempts=max(attempts, 1),
sleep_seconds=max(sleep_seconds, 0.0),
timeout_seconds=max(timeout_seconds, 0.1),
opener=opener,
sleeper=sleeper,
)
endpoint_results.append(result)
if result["status"] == "passed":
passed_checks.append(result["name"])
elif result["status"] == "warning":
warnings.append(result["name"])
else:
blockers.append(result["name"])
return {
"status": "blocked" if blockers else ("warning" if warnings else "passed"),
"mode": "remote_ops_smoke",
"generated_ts": datetime.now(timezone.utc).isoformat(),
"base_url": base_url.rstrip("/"),
"instrument_id": instrument_id,
"external_channels_used": False,
"real_trade_required": False,
"endpoints": endpoint_results,
"passed_checks": passed_checks,
"warnings": warnings,
"blockers": blockers,
}
def _call_endpoint(
*,
endpoint: dict,
base_url: str,
instrument_id: int,
attempts: int,
sleep_seconds: float,
timeout_seconds: float,
opener,
sleeper,
) -> dict:
name = endpoint["name"]
method = endpoint["method"]
path = endpoint["path"].format(instrument_id=urllib.parse.quote(str(instrument_id)))
url = base_url.rstrip("/") + path
body = endpoint.get("body")
request = _build_request(url=url, method=method, body=body)
expected_statuses = tuple(endpoint.get("expected_statuses") or (200,))
last_error = None
for attempt in range(1, attempts + 1):
try:
with opener(request, timeout_seconds) as response:
raw_body = response.read()
payload = _decode_payload(raw_body)
status_code = int(getattr(response, "status", 0) or 0)
return _classify_response(
name=name,
method=method,
path=path,
status_code=status_code,
expected_statuses=expected_statuses,
body_size=len(raw_body),
payload=payload,
payload_status_must_pass=bool(endpoint.get("payload_status_must_pass")),
attempt=attempt,
)
except urllib.error.HTTPError as exc:
raw_body = exc.read()
payload = _decode_payload(raw_body)
status_code = int(exc.code)
return _classify_response(
name=name,
method=method,
path=path,
status_code=status_code,
expected_statuses=expected_statuses,
body_size=len(raw_body),
payload=payload,
payload_status_must_pass=bool(endpoint.get("payload_status_must_pass")),
attempt=attempt,
)
except Exception as exc:
last_error = exc
if attempt < attempts:
sleeper(sleep_seconds)
return {
"name": name,
"method": method,
"path": path,
"status": "blocked",
"blocker_type": "environment",
"reason": "endpoint_unreachable",
"attempts": attempts,
"error": str(last_error),
}
def _build_request(*, url: str, method: str, body) -> urllib.request.Request:
headers = {"Accept": "application/json"}
data = None
if body is not None:
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
return urllib.request.Request(url, data=data, headers=headers, method=method)
def _classify_response(
*,
name: str,
method: str,
path: str,
status_code: int,
expected_statuses: tuple,
body_size: int,
payload,
payload_status_must_pass: bool,
attempt: int,
) -> dict:
result = {
"name": name,
"method": method,
"path": path,
"status_code": status_code,
"body_size": body_size,
"attempt": attempt,
}
if status_code not in expected_statuses:
result.update(
{
"status": "blocked",
"reason": "unexpected_http_status",
"expected_statuses": list(expected_statuses),
"payload_excerpt": _payload_excerpt(payload),
}
)
return result
if payload_status_must_pass and isinstance(payload, dict) and payload.get("status") != "passed":
blocker_type = "environment" if _payload_has_environment_blocker(payload) else "remote_runtime"
result.update(
{
"status": "blocked",
"blocker_type": blocker_type,
"reason": "payload_status_not_passed",
"payload_status": payload.get("status"),
"payload_blockers": list(payload.get("blockers") or []),
"payload_warnings": list(payload.get("warnings") or []),
}
)
return result
result["status"] = "passed"
return result
def _decode_payload(raw_body: bytes):
if not raw_body:
return None
try:
return json.loads(raw_body.decode("utf-8"))
except Exception:
return raw_body[:256].decode("utf-8", errors="replace")
def _payload_excerpt(payload):
if isinstance(payload, dict):
keys = sorted(payload.keys())[:8]
return {key: payload.get(key) for key in keys}
if isinstance(payload, str):
return payload[:256]
return payload
def _payload_has_environment_blocker(payload: dict) -> bool:
for check in payload.get("checks") or []:
if isinstance(check, dict) and check.get("status") == "blocked" and check.get("blocker_type") == "environment":
return True
return False
def _urlopen(request: urllib.request.Request, timeout_seconds: float):
return urllib.request.urlopen(request, timeout=timeout_seconds)
def _get_bool(env, key: str, default: bool) -> bool:
value = env.get(key)
if value is None or value == "":
return default
return str(value).strip().lower() in {"1", "true", "yes", "on"}
def _parse_args(argv: Optional[list[str]], env) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run P24 remote ops smoke against ai-exchange API.")
parser.add_argument("--base-url", default=env.get("REMOTE_OPS_SMOKE_BASE_URL", DEFAULT_BASE_URL))
parser.add_argument("--instrument-id", type=int, default=int(env.get("REMOTE_OPS_SMOKE_INSTRUMENT_ID", "1")))
parser.add_argument("--attempts", type=int, default=int(env.get("REMOTE_OPS_SMOKE_ATTEMPTS", str(DEFAULT_ATTEMPTS))))
parser.add_argument("--sleep-seconds", type=float, default=float(env.get("REMOTE_OPS_SMOKE_SLEEP_SECONDS", str(DEFAULT_SLEEP_SECONDS))))
parser.add_argument("--timeout-seconds", type=float, default=float(env.get("REMOTE_OPS_SMOKE_TIMEOUT_SECONDS", str(DEFAULT_TIMEOUT_SECONDS))))
return parser.parse_args(argv)
def main(argv: Optional[list[str]] = None, env=None) -> int:
env = os.environ if env is None else env
args = _parse_args(argv, env)
payload = run_remote_ops_smoke(
base_url=args.base_url,
instrument_id=args.instrument_id,
attempts=args.attempts,
sleep_seconds=args.sleep_seconds,
timeout_seconds=args.timeout_seconds,
)
if _get_bool(env, "REMOTE_OPS_SMOKE_OUTPUT_JSON", False):
print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
else:
print(f"Remote ops smoke status: {payload['status']}")
print(f"Base URL: {payload['base_url']}")
print(f"Passed checks: {', '.join(payload['passed_checks']) or 'none'}")
print(f"Warnings: {', '.join(payload['warnings']) or 'none'}")
print(f"Blockers: {', '.join(payload['blockers']) or 'none'}")
return 0 if payload["status"] == "passed" else 2
if __name__ == "__main__":
raise SystemExit(main())
+7
View File
@@ -28,6 +28,13 @@ class DeployWithPasswordTests(unittest.TestCase):
self.assertIn("./.claude:/app/.claude", compose)
def test_deploy_smoke_includes_p24_execution_layer_endpoints(self) -> None:
smoke_source = Path("scripts/deploy_with_password.py").read_text(encoding="utf-8")
self.assertIn("/trading/alerts/operator-digest?instrument_id=1", smoke_source)
self.assertIn("/trading/maintenance-summary?instrument_id=1", smoke_source)
self.assertIn("/trading/unattended-harness?instrument_id=1", smoke_source)
if __name__ == "__main__":
unittest.main()
+97
View File
@@ -0,0 +1,97 @@
import json
import unittest
import urllib.error
from scripts.run_remote_ops_smoke import ENDPOINTS, run_remote_ops_smoke
class FakeResponse:
def __init__(self, status, payload):
self.status = status
self.payload = payload
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
return False
def read(self):
return json.dumps(self.payload).encode("utf-8")
def close(self):
return None
class RemoteOpsSmokeTests(unittest.TestCase):
def test_passes_all_required_remote_ops_endpoints(self) -> None:
seen = []
def opener(request, timeout_seconds):
seen.append((request.get_method(), request.full_url))
if request.full_url.endswith("/trading/unattended-harness?instrument_id=1"):
return FakeResponse(200, {"status": "passed", "blockers": [], "checks": []})
return FakeResponse(200, {"status": "ok"})
payload = run_remote_ops_smoke(base_url="http://remote.test", opener=opener, sleeper=lambda _: None)
self.assertEqual(payload["status"], "passed")
self.assertEqual(len(payload["passed_checks"]), len(ENDPOINTS))
self.assertIn(("POST", "http://remote.test/trading/visual-watch/cleanup-captures"), seen)
self.assertFalse(payload["external_channels_used"])
self.assertFalse(payload["real_trade_required"])
def test_http_404_blocks_stale_remote(self) -> None:
def opener(request, timeout_seconds):
raise urllib.error.HTTPError(
request.full_url,
404,
"not found",
hdrs=None,
fp=FakeResponse(404, {"error": "not_found"}),
)
payload = run_remote_ops_smoke(base_url="http://remote.test", opener=opener, sleeper=lambda _: None)
self.assertEqual(payload["status"], "blocked")
self.assertEqual(payload["blockers"][0], "health")
self.assertEqual(payload["endpoints"][0]["reason"], "unexpected_http_status")
def test_unreachable_endpoint_is_environment_blocker(self) -> None:
def opener(request, timeout_seconds):
raise TimeoutError("timed out")
payload = run_remote_ops_smoke(base_url="http://remote.test", attempts=1, opener=opener, sleeper=lambda _: None)
self.assertEqual(payload["status"], "blocked")
self.assertEqual(payload["endpoints"][0]["blocker_type"], "environment")
self.assertEqual(payload["endpoints"][0]["reason"], "endpoint_unreachable")
def test_harness_environment_blocker_is_preserved(self) -> None:
def opener(request, timeout_seconds):
if request.full_url.endswith("/trading/unattended-harness?instrument_id=1"):
return FakeResponse(
200,
{
"status": "blocked",
"blockers": ["runtime_environment"],
"checks": [
{
"name": "runtime_environment",
"status": "blocked",
"blocker_type": "environment",
}
],
},
)
return FakeResponse(200, {"status": "ok"})
payload = run_remote_ops_smoke(base_url="http://remote.test", opener=opener, sleeper=lambda _: None)
self.assertEqual(payload["status"], "blocked")
self.assertEqual(payload["blockers"], ["unattended_harness"])
self.assertEqual(payload["endpoints"][-1]["blocker_type"], "environment")
if __name__ == "__main__":
unittest.main()