test: add local demo readiness smoke guard

This commit is contained in:
2026-05-02 03:07:31 -04:00
parent a2247faa19
commit 39bcb6b3ef
4 changed files with 230 additions and 0 deletions
+6
View File
@@ -56,6 +56,12 @@ test "$(docker ps --filter name=^/ai-ict$ --format '{{.Names}}' | wc -l | tr -d
test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy"
```
- Offline local demo readiness check, which does not invoke Docker:
```bash
.venv/bin/python scripts/check_local_demo_readiness.py
```
## Runtime utility scripts
- `python3 scripts/run_dev.py` - run the scaffold entrypoint
@@ -45,6 +45,12 @@ test "$(docker ps --filter name=^/ai-ict$ --format '{{.Names}}' | wc -l | tr -d
test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy"
```
Offline local demo readiness check, which does not invoke Docker:
```bash
.venv/bin/python scripts/check_local_demo_readiness.py
```
## API Surface
All endpoints use JSON responses. List endpoints accept `limit` with an upper bound of `100`.
+129
View File
@@ -0,0 +1,129 @@
from dataclasses import dataclass
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[1]
LOCAL_DEMO_DEPLOY_DIR = "/root/deploy/ai-ict"
LOCAL_DEMO_COMPOSE_PROJECT = "ai-ict"
LOCAL_DEMO_CONTAINER = "ai-ict"
LOCAL_DEMO_COMPOSE_FILE = f"{LOCAL_DEMO_DEPLOY_DIR}/docker-compose.yml"
LOCAL_DEMO_COMPOSE_PREFIX = (
f"docker compose -p {LOCAL_DEMO_COMPOSE_PROJECT} -f {LOCAL_DEMO_COMPOSE_FILE}"
)
LOCAL_DEMO_DOCS = (
ROOT / "README.md",
ROOT / "docs" / "training_case_engine_m1_m6_product_readme.md",
)
LOCAL_DEMO_REBUILD_COMMANDS = (
f"{LOCAL_DEMO_COMPOSE_PREFIX} down --remove-orphans",
f"{LOCAL_DEMO_COMPOSE_PREFIX} up -d --build",
)
LOCAL_DEMO_SMOKE_COMMANDS = (
f"{LOCAL_DEMO_COMPOSE_PREFIX} 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\"",
)
LOCAL_DEMO_REQUIRED_SNIPPETS = (
"single-set local Docker deployment",
f"deploy directory `{LOCAL_DEMO_DEPLOY_DIR}`",
f"compose project `{LOCAL_DEMO_COMPOSE_PROJECT}`",
f"container `{LOCAL_DEMO_CONTAINER}`",
"exactly one active deployment set",
f"one active `{LOCAL_DEMO_CONTAINER}` container",
"do not create a second local stack",
)
@dataclass(frozen=True)
class LocalDemoReadinessReport:
checked_docs: tuple[Path, ...]
command_lines_by_doc: tuple[tuple[Path, tuple[str, ...]], ...]
errors: tuple[str, ...]
def check_local_demo_readiness(
docs: tuple[Path, ...] = LOCAL_DEMO_DOCS,
) -> LocalDemoReadinessReport:
expected_commands = LOCAL_DEMO_REBUILD_COMMANDS + LOCAL_DEMO_SMOKE_COMMANDS
command_lines_by_doc = []
errors = []
for path in docs:
if not path.is_file():
command_lines_by_doc.append((path, ()))
errors.append(f"{_display_path(path)} is missing")
continue
text = path.read_text(encoding="utf-8")
try:
section = _local_docker_deployment_section(text)
except ValueError as exc:
command_lines_by_doc.append((path, ()))
errors.append(f"{_display_path(path)} {exc}")
continue
command_lines = _local_docker_command_lines(section)
command_lines_by_doc.append((path, command_lines))
for snippet in LOCAL_DEMO_REQUIRED_SNIPPETS:
if snippet not in section:
errors.append(f"{_display_path(path)} missing local demo snippet: {snippet}")
if command_lines != expected_commands:
errors.append(
f"{_display_path(path)} local demo commands drifted from the locked single-set shape"
)
return LocalDemoReadinessReport(
checked_docs=tuple(docs),
command_lines_by_doc=tuple(command_lines_by_doc),
errors=tuple(errors),
)
def main() -> int:
report = check_local_demo_readiness()
if report.errors:
print("Local demo readiness check failed")
for error in report.errors:
print(f"- {error}")
return 1
command_count = len(LOCAL_DEMO_REBUILD_COMMANDS) + len(LOCAL_DEMO_SMOKE_COMMANDS)
print(
"Local demo readiness check passed: "
f"{len(report.checked_docs)} docs, {command_count} locked commands."
)
return 0
def _local_docker_deployment_section(text: str) -> str:
match = re.search(
r"^## Local Docker [Dd]eployment\n(?P<section>.*?)(?:\n## |\Z)",
text,
re.DOTALL | re.MULTILINE,
)
if match is None:
raise ValueError("is missing the Local Docker deployment section")
return match.group("section")
def _local_docker_command_lines(text: str) -> tuple[str, ...]:
return tuple(
line.strip()
for line in text.splitlines()
if line.strip().startswith(("docker ", "test "))
)
def _display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return str(path)
if __name__ == "__main__":
raise SystemExit(main())
+89
View File
@@ -0,0 +1,89 @@
import ast
import subprocess
import sys
import unittest
from pathlib import Path
from scripts import check_local_demo_readiness as readiness
from scripts import run_offline_checks
ROOT = Path(__file__).resolve().parents[2]
CHECK_COMMAND = ".venv/bin/python scripts/check_local_demo_readiness.py"
EXPECTED_DOC_PATHS = (
ROOT / "README.md",
ROOT / "docs" / "training_case_engine_m1_m6_product_readme.md",
)
EXPECTED_REBUILD_COMMANDS = (
"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",
)
EXPECTED_SMOKE_COMMANDS = (
"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\"",
)
class LocalDemoReadinessTests(unittest.TestCase):
def test_checker_locks_single_set_local_demo_constants(self) -> None:
self.assertEqual(readiness.LOCAL_DEMO_DEPLOY_DIR, "/root/deploy/ai-ict")
self.assertEqual(readiness.LOCAL_DEMO_COMPOSE_PROJECT, "ai-ict")
self.assertEqual(readiness.LOCAL_DEMO_CONTAINER, "ai-ict")
self.assertEqual(readiness.LOCAL_DEMO_DOCS, EXPECTED_DOC_PATHS)
self.assertEqual(readiness.LOCAL_DEMO_REBUILD_COMMANDS, EXPECTED_REBUILD_COMMANDS)
self.assertEqual(readiness.LOCAL_DEMO_SMOKE_COMMANDS, EXPECTED_SMOKE_COMMANDS)
def test_checker_verifies_documented_demo_smoke_commands(self) -> None:
report = readiness.check_local_demo_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_REBUILD_COMMANDS + EXPECTED_SMOKE_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("does not invoke Docker", text)
process = subprocess.run(
[sys.executable, "scripts/check_local_demo_readiness.py"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(process.returncode, 0, process.stderr)
self.assertIn("Local demo 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_discover_local_demo_readiness_tests(self) -> None:
check_names = tuple(name for name, _command in run_offline_checks.build_check_commands(env={}))
self.assertIn("unit_tests", check_names)
self.assertTrue((ROOT / "tests" / "unit" / "test_local_demo_readiness.py").is_file())
if __name__ == "__main__":
unittest.main()