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 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
215
scripts/test_deploy_preflight.py
Normal file
215
scripts/test_deploy_preflight.py
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue