전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -17,6 +17,7 @@ from .routes import admin as admin_routes
from .routes import eval as eval_routes
from .routes import kb as kb_routes
from .routes import users as users_routes
from .services import notifications
@contextmanager
@ -48,15 +49,21 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
self.assertFalse(durable)
self.assertEqual(users, [])
async def test_staging_blocks_auth_registry_fallback_when_db_pool_missing(self) -> None:
async def test_staging_blocks_auth_registry_fallback_when_db_pool_missing(
self,
) -> None:
with environment("staging"):
with self.assertRaises(HTTPException) as caught:
await auth_sessions.list_managed_users()
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("runtime fallback is disabled in staging", caught.exception.detail)
self.assertIn(
"runtime fallback is disabled in staging", caught.exception.detail
)
async def test_prod_blocks_browser_session_creation_when_db_pool_missing(self) -> None:
async def test_prod_blocks_browser_session_creation_when_db_pool_missing(
self,
) -> None:
with environment("prod"):
with self.assertRaises(HTTPException) as caught:
await auth_sessions.create_session(
@ -68,7 +75,9 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("runtime fallback is disabled in prod", caught.exception.detail)
async def test_staging_blocks_session_store_fallback_when_db_pool_missing(self) -> None:
async def test_staging_blocks_session_store_fallback_when_db_pool_missing(
self,
) -> None:
principal = Principal(
user_id="00000000-0000-0000-0000-000000000001",
role=Role.LEARNER,
@ -81,9 +90,13 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
await session_persistence.list_recent_sessions(principal)
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("runtime fallback is disabled in staging", caught.exception.detail)
self.assertIn(
"runtime fallback is disabled in staging", caught.exception.detail
)
async def test_staging_blocks_eval_store_cache_fallback_when_db_pool_missing(self) -> None:
async def test_staging_blocks_eval_store_cache_fallback_when_db_pool_missing(
self,
) -> None:
principal = Principal(
user_id="00000000-0000-0000-0000-000000000002",
role=Role.TEACHER,
@ -93,10 +106,14 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
)
with environment("staging"):
with self.assertRaises(HTTPException) as caught:
await eval_routes.get_session_evaluation("missing-session-id", principal)
await eval_routes.get_session_evaluation(
"missing-session-id", principal
)
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("runtime fallback is disabled in staging", caught.exception.detail)
self.assertIn(
"runtime fallback is disabled in staging", caught.exception.detail
)
async def test_staging_kb_search_fails_closed_when_db_pool_missing(self) -> None:
with environment("staging"):
@ -106,7 +123,9 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("DB not ready", caught.exception.detail)
async def test_staging_blocks_user_preferences_fallback_when_db_pool_missing(self) -> None:
async def test_staging_blocks_user_preferences_fallback_when_db_pool_missing(
self,
) -> None:
principal = Principal(
user_id="00000000-0000-0000-0000-000000000003",
role=Role.LEARNER,
@ -119,9 +138,13 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
await users_routes.get_preferences(principal)
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("runtime fallback is disabled in staging", caught.exception.detail)
self.assertIn(
"runtime fallback is disabled in staging", caught.exception.detail
)
async def test_dev_allows_user_preferences_fallback_when_db_pool_missing(self) -> None:
async def test_dev_allows_user_preferences_fallback_when_db_pool_missing(
self,
) -> None:
principal = Principal(
user_id="00000000-0000-0000-0000-000000000004",
role=Role.LEARNER,
@ -134,7 +157,9 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(prefs.voice_preset_id, "soft-young-fem")
async def test_prod_blocks_runtime_schema_bootstrap_ddl_when_schema_incomplete(self) -> None:
async def test_prod_blocks_runtime_schema_bootstrap_ddl_when_schema_incomplete(
self,
) -> None:
class IncompleteConn:
async def fetchrow(self, *args, **kwargs):
return {
@ -179,13 +204,80 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
def acquire(self):
return IncompleteAcquire()
with environment("prod"), patch.object(auth_sessions, "get_pool", return_value=IncompletePool()):
with (
environment("prod"),
patch.object(auth_sessions, "get_pool", return_value=IncompletePool()),
):
with self.assertRaises(RuntimeError) as caught:
await auth_sessions.ensure_runtime_tables()
self.assertIn("runtime DB schema is incomplete", str(caught.exception))
async def test_runtime_readiness_requires_all_turn_voice_metadata_columns(self) -> None:
async def test_prod_blocks_review_schema_bootstrap_ddl_when_schema_incomplete(
self,
) -> None:
class IncompleteConn:
async def fetchrow(self, *args, **kwargs):
return {"ready": False}
async def execute(self, *args, **kwargs):
raise AssertionError("prod startup must not run review schema DDL")
class IncompleteAcquire:
async def __aenter__(self):
return IncompleteConn()
async def __aexit__(self, exc_type, exc, tb):
return None
with (
environment("prod"),
patch.object(session_persistence, "get_pool", return_value=object()),
patch.object(
session_persistence, "acquire", return_value=IncompleteAcquire()
),
):
with self.assertRaises(RuntimeError) as caught:
await session_persistence.ensure_review_tables()
self.assertIn(
"review/evaluation runtime DB schema is incomplete", str(caught.exception)
)
async def test_prod_blocks_notification_schema_bootstrap_ddl_when_schema_incomplete(
self,
) -> None:
class IncompleteConn:
async def fetchrow(self, *args, **kwargs):
return {"ready": False}
async def execute(self, *args, **kwargs):
raise AssertionError(
"prod startup must not run notification schema DDL"
)
class IncompleteAcquire:
async def __aenter__(self):
return IncompleteConn()
async def __aexit__(self, exc_type, exc, tb):
return None
with (
environment("prod"),
patch.object(notifications, "get_pool", return_value=object()),
patch.object(notifications, "acquire", return_value=IncompleteAcquire()),
):
with self.assertRaises(RuntimeError) as caught:
await notifications.ensure_notification_tables()
self.assertIn(
"notification runtime DB schema is incomplete", str(caught.exception)
)
async def test_runtime_readiness_requires_all_turn_voice_metadata_columns(
self,
) -> None:
class VoiceMetadataDriftConn:
def __init__(self) -> None:
self.query = ""
@ -225,7 +317,13 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
ready = await auth_sessions._runtime_tables_ready(conn)
self.assertFalse(ready)
for column in ("audio_ref", "silence_ms", "speech_rate", "barge_in", "provider_events"):
for column in (
"audio_ref",
"silence_ms",
"speech_rate",
"barge_in",
"provider_events",
):
self.assertIn(column, conn.query)
self.assertIn("HAVING count(*) = 5", conn.query)
@ -296,7 +394,10 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
def acquire(self):
return EmptyConfigAcquire()
with environment("staging"), patch.object(admin_routes, "get_pool", return_value=EmptyConfigPool()):
with (
environment("staging"),
patch.object(admin_routes, "get_pool", return_value=EmptyConfigPool()),
):
config = await admin_routes._current_engine_config()
self.assertFalse(config.durable)
@ -309,14 +410,21 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
def acquire(self):
raise RuntimeError("db unavailable")
with environment("staging"), patch.object(admin_routes, "get_pool", return_value=BrokenConfigPool()):
with (
environment("staging"),
patch.object(admin_routes, "get_pool", return_value=BrokenConfigPool()),
):
with self.assertRaises(HTTPException) as caught:
await admin_routes._current_engine_config()
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("runtime fallback is disabled in staging", caught.exception.detail)
self.assertIn(
"runtime fallback is disabled in staging", caught.exception.detail
)
async def test_prod_admin_health_marks_db_down_when_persistence_unavailable(self) -> None:
async def test_prod_admin_health_marks_db_down_when_persistence_unavailable(
self,
) -> None:
principal = Principal(
user_id="00000000-0000-0000-0000-000000000005",
role=Role.ADMIN,
@ -334,10 +442,20 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
with (
environment("prod"),
patch.object(admin_routes, "_current_engine_config", AsyncMock(return_value=engine_config)),
patch.object(
admin_routes,
"_current_engine_config",
AsyncMock(return_value=engine_config),
),
patch.object(admin_routes, "healthcheck", AsyncMock(return_value=False)),
patch.object(admin_routes.engine_client, "health_detail", AsyncMock(return_value={"ok": True})),
patch.object(admin_routes.voice_service, "is_available", return_value=False),
patch.object(
admin_routes.engine_client,
"health_detail",
AsyncMock(return_value={"ok": True}),
),
patch.object(
admin_routes.voice_service, "is_available", return_value=False
),
):
health = await admin_routes.admin_health(principal)
@ -346,7 +464,9 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(db.metric, "저장소 중단")
self.assertIn("DB 저장소", db.detail)
async def test_dev_admin_health_labels_db_fallback_as_non_durable_runtime(self) -> None:
async def test_dev_admin_health_labels_db_fallback_as_non_durable_runtime(
self,
) -> None:
principal = Principal(
user_id="00000000-0000-0000-0000-000000000006",
role=Role.ADMIN,
@ -364,10 +484,20 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
with (
environment("dev"),
patch.object(admin_routes, "_current_engine_config", AsyncMock(return_value=engine_config)),
patch.object(
admin_routes,
"_current_engine_config",
AsyncMock(return_value=engine_config),
),
patch.object(admin_routes, "healthcheck", AsyncMock(return_value=False)),
patch.object(admin_routes.engine_client, "health_detail", AsyncMock(return_value={"ok": True})),
patch.object(admin_routes.voice_service, "is_available", return_value=False),
patch.object(
admin_routes.engine_client,
"health_detail",
AsyncMock(return_value={"ok": True}),
),
patch.object(
admin_routes.voice_service, "is_available", return_value=False
),
):
health = await admin_routes.admin_health(principal)
@ -535,7 +665,9 @@ class RuntimeFallbackPolicyTest(unittest.IsolatedAsyncioTestCase):
voice_poc_sample_tts_enabled=False,
)
self.assertEqual(cfg.frontend_origin_map["api-vnet.18ka.net"], "https://vnet.18ka.net")
self.assertEqual(
cfg.frontend_origin_map["api-vnet.18ka.net"], "https://vnet.18ka.net"
)
self.assertIn("https://vnet.18ka.net", cfg.cors_origins)
def test_non_dev_rejects_local_frontend_origin_map(self) -> None: