vignette/scripts/test_deploy_preflight.py
Yun Chan 16e791e044 G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

215 lines
7.7 KiB
Python

from __future__ import annotations
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CHECKER_PATH = REPO_ROOT / "scripts" / "check-deploy-preflight.py"
def _load_checker():
spec = importlib.util.spec_from_file_location("check_deploy_preflight", CHECKER_PATH)
if spec is None or spec.loader is None:
raise RuntimeError(f"Cannot load {CHECKER_PATH}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
checker = _load_checker()
def _valid_values() -> dict[str, str]:
values = {
"ENVIRONMENT": "prod",
"POSTGRES_PASSWORD": "postgres-owner-secret",
"APP_DB_PASSWORD": "postgres-app-secret",
"OPENAI_API_KEY": "openai-secret",
"SESSION_SECRET": "session-secret-with-enough-entropy",
"OAUTH_GOOGLE_CLIENT_ID": "google-client",
"OAUTH_GOOGLE_CLIENT_SECRET": "google-secret",
"ENGINE_MODE": "openai",
"ENGINE_GATEWAY_SHARED_SECRET": "engine-gateway-secret-" + ("g" * 32),
"AUTH_DEV_LOGIN_ENABLED": "false",
"AUTO_SEED_PERSONAS": "false",
"ALLOW_SEED_PERSONA_FALLBACK": "false",
"VIGNETTE_VOICE_POC_SAMPLE_TTS": "false",
}
for index, key in enumerate(checker.INTERNAL_TOKEN_ENV_KEYS, start=1):
values[key] = f"token-{index}-" + chr(96 + index) * 40
return values
def _write_env(path: Path, values: dict[str, str]) -> None:
path.write_text(
"\n".join(f"{key}={value}" for key, value in values.items()) + "\n",
encoding="utf-8",
)
class DeployPreflightEnvTests(unittest.TestCase):
def _check(
self,
values: dict[str, str],
*,
allow_placeholders: bool = False,
deployment_profile: str = "compose",
):
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / ".env"
_write_env(path, values)
return checker.check_env_file(
path,
allow_placeholder_secrets=allow_placeholders,
deployment_profile=deployment_profile,
)
def test_valid_independent_internal_tokens_pass(self) -> None:
result = self._check(_valid_values())
self.assertTrue(result.ok, result.detail)
def test_gateway_shared_secret_is_optional(self) -> None:
values = _valid_values()
values.pop("ENGINE_GATEWAY_SHARED_SECRET")
result = self._check(values)
self.assertTrue(result.ok, result.detail)
def test_short_gateway_shared_secret_fails(self) -> None:
values = _valid_values()
values["ENGINE_GATEWAY_SHARED_SECRET"] = "too-short"
result = self._check(values)
self.assertFalse(result.ok)
self.assertIn("ENGINE_GATEWAY_SHARED_SECRET must contain", result.detail)
def test_placeholder_gateway_shared_secret_fails(self) -> None:
values = _valid_values()
values["ENGINE_GATEWAY_SHARED_SECRET"] = (
"change-me-engine-gateway-secret-with-32-characters"
)
result = self._check(values)
self.assertFalse(result.ok)
self.assertIn("ENGINE_GATEWAY_SHARED_SECRET is placeholder", result.detail)
def test_missing_internal_token_fails(self) -> None:
values = _valid_values()
values.pop("VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN")
result = self._check(values)
self.assertFalse(result.ok)
self.assertIn("VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN missing", result.detail)
def test_short_internal_token_fails(self) -> None:
values = _valid_values()
values["VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN"] = "too-short"
result = self._check(values)
self.assertFalse(result.ok)
self.assertIn("must contain at least 32 characters", result.detail)
def test_long_placeholder_internal_token_fails(self) -> None:
values = _valid_values()
values["VIGNETTE_RUPTURE_INTERNAL_TOKEN"] = "replace-with-a-random-token-of-32-chars"
result = self._check(values)
self.assertFalse(result.ok)
self.assertIn("is placeholder or empty", result.detail)
def test_shared_internal_token_fails(self) -> None:
values = _valid_values()
shared = values["VIGNETTE_RUPTURE_INTERNAL_TOKEN"]
values["VIGNETTE_PRACTICE_INTERNAL_TOKEN"] = shared
result = self._check(values)
self.assertFalse(result.ok)
self.assertIn("must be independently revocable", result.detail)
def test_example_env_allows_explicit_placeholders(self) -> None:
result = checker.check_env_file(
REPO_ROOT / "infra" / ".env.example",
allow_placeholder_secrets=True,
)
self.assertTrue(result.ok, result.detail)
def test_direct_runtime_uses_database_url_without_packaging_passwords(self) -> None:
values = _valid_values()
values.pop("POSTGRES_PASSWORD")
values.pop("APP_DB_PASSWORD")
values["DATABASE_URL"] = "postgresql://runtime:secret@127.0.0.1/vignette"
result = self._check(values, deployment_profile="direct-runtime")
self.assertTrue(result.ok, result.detail)
def test_direct_runtime_requires_database_url(self) -> None:
values = _valid_values()
values.pop("POSTGRES_PASSWORD")
values.pop("APP_DB_PASSWORD")
result = self._check(values, deployment_profile="direct-runtime")
self.assertFalse(result.ok)
self.assertIn("DATABASE_URL missing", result.detail)
def test_root_dockerignore_uses_ascii_buildkit_patterns(self) -> None:
active_patterns = [
line.strip()
for line in (REPO_ROOT / ".dockerignore").read_text(
encoding="utf-8"
).splitlines()
if line.strip() and not line.lstrip().startswith("#")
]
self.assertTrue(
all(pattern.isascii() for pattern in active_patterns),
"BuildKit metadata와 호환되도록 .dockerignore 패턴은 ASCII여야 한다.",
)
def test_compose_requires_frontend_origin_map_without_nested_json_default(self) -> None:
compose = (REPO_ROOT / "infra" / "docker-compose.yml").read_text(
encoding="utf-8"
)
self.assertIn(
"FRONTEND_ORIGIN_MAP: '${FRONTEND_ORIGIN_MAP:?set FRONTEND_ORIGIN_MAP in .env}'",
compose,
)
self.assertNotIn("${FRONTEND_ORIGIN_MAP:-{", compose)
def test_compose_healthchecks_require_durable_runtime_readiness(self) -> None:
compose = (REPO_ROOT / "infra" / "docker-compose.yml").read_text(
encoding="utf-8"
)
self.assertIn("app.ci_regression_dag_node", compose)
self.assertIn("PGPASSWORD=$${APP_DB_PASSWORD}", compose)
self.assertIn("start_period: 5m", compose)
self.assertIn("payload.get('status') == 'ok'", compose)
self.assertIn("payload.get('db') is True", compose)
self.assertIn("payload.get('engine') is True", compose)
def test_compose_exposes_g8_scheduler_and_drift_trigger_as_separate_opt_ins(self) -> None:
compose = (REPO_ROOT / "infra" / "docker-compose.yml").read_text(
encoding="utf-8"
)
self.assertIn(
"VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED: "
"${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED:-false}",
compose,
)
self.assertIn(
"VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED: "
"${VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED:-false}",
compose,
)
if __name__ == "__main__":
unittest.main()