136 lines
5.6 KiB
Python
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()
|