Files
ai-exchange/tests/unit/test_trading_delivery_readiness.py
T
2026-05-18 22:30:34 +08:00

136 lines
5.6 KiB
Python

import ast
import subprocess
import sys
import unittest
from pathlib import Path
from scripts import check_trading_delivery_readiness as readiness
from scripts import run_offline_checks
ROOT = Path(__file__).resolve().parents[2]
CHECK_COMMAND = ".venv/bin/python scripts/check_trading_delivery_readiness.py"
EXPECTED_DOC_PATHS = (
ROOT / "README.md",
ROOT / "RUNBOOK.md",
)
EXPECTED_COMMAND_GROUPS = (
(
"pre_deploy_offline",
(
'.venv/bin/python -m unittest discover -s tests/unit -p "test_*.py"',
".venv/bin/python scripts/run_offline_checks.py",
".venv/bin/python scripts/check_local_demo_readiness.py",
".venv/bin/python scripts/run_secret_static_checks.py",
".venv/bin/python scripts/check_trading_delivery_readiness.py",
),
),
(
"database_gate",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
".venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
"PIPELINE_OUTPUT_JSON=1 BACKTEST_REQUIRE_QUALITY_GATE=1 .venv/bin/python scripts/run_pipeline.py",
),
),
(
"deploy_and_health",
(
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml down --remove-orphans",
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml up -d --build",
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml ps",
'test "$(docker ps --filter label=com.docker.compose.project=ai-ict --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker ps --filter name=^/ai-ict$ --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker inspect -f \'{{.State.Health.Status}}\' ai-ict)" = "healthy"',
"curl -fsS http://127.0.0.1:8000/health",
'curl -fsS "http://127.0.0.1:8000/trading/candidates?instrument_id=1&limit=1"',
),
),
(
"post_deploy_observe",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"REPORTS_OUTPUT_JSON=1 .venv/bin/python scripts/run_reports.py",
),
),
(
"rollback_boundary",
(
"git rev-parse HEAD",
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml logs --tail=100 ai-ict",
"git worktree add /tmp/ai-ict-rollback <last-known-good-sha>",
"cd /tmp/ai-ict-rollback && AI_ICT_DEPLOY_PASSWORD=<set-in-shell> AI_ICT_DEPLOY_USE_SUDO=1 .venv/bin/python scripts/deploy_with_password.py --host 192.168.3.90 --user yuqei --port 22 --remote-path /home/yuqei/project/ai-exchange --remote-archive /home/yuqei/ai-ict-deploy.tgz",
"rm -rf /tmp/ai-ict-rollback",
),
),
)
EXPECTED_COMMANDS = tuple(
command
for _group_name, commands in EXPECTED_COMMAND_GROUPS
for command in commands
)
class TradingDeliveryReadinessTests(unittest.TestCase):
def test_checker_locks_trading_delivery_constants(self) -> None:
self.assertEqual(readiness.TRADING_DELIVERY_DOCS, EXPECTED_DOC_PATHS)
self.assertEqual(readiness.TRADING_DELIVERY_COMMAND_GROUPS, EXPECTED_COMMAND_GROUPS)
self.assertEqual(readiness.TRADING_DELIVERY_COMMANDS, EXPECTED_COMMANDS)
def test_checker_verifies_documented_trading_delivery_commands(self) -> None:
report = readiness.check_trading_delivery_readiness()
self.assertEqual(report.errors, ())
self.assertEqual(report.checked_docs, EXPECTED_DOC_PATHS)
for path, command_lines in report.command_lines_by_doc:
with self.subTest(path=path.name):
self.assertEqual(command_lines, EXPECTED_COMMANDS)
def test_checker_command_is_documented_and_offline(self) -> None:
for path in EXPECTED_DOC_PATHS:
text = path.read_text(encoding="utf-8")
with self.subTest(path=path.name):
self.assertIn(CHECK_COMMAND, text)
self.assertIn("without relying on chat memory", text)
process = subprocess.run(
[sys.executable, "scripts/check_trading_delivery_readiness.py"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(process.returncode, 0, process.stderr)
self.assertIn("Trading delivery readiness check passed", process.stdout)
def test_checker_source_has_no_process_launches(self) -> None:
source = Path(readiness.__file__).read_text(encoding="utf-8")
tree = ast.parse(source)
imported_modules = {
alias.name
for node in ast.walk(tree)
if isinstance(node, ast.Import)
for alias in node.names
}
called_attributes = {
node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute)
}
self.assertNotIn("subprocess", imported_modules)
self.assertNotIn("system", called_attributes)
def test_offline_checks_include_trading_delivery_readiness(self) -> None:
commands = dict(run_offline_checks.build_check_commands(env={}))
self.assertEqual(
commands["trading_delivery_readiness"],
[sys.executable, "scripts/check_trading_delivery_readiness.py"],
)
if __name__ == "__main__":
unittest.main()