전 저장소 리팩터링과 SSOT 정비
This commit is contained in:
parent
14ecbd4e7d
commit
3dfddcac6f
173 changed files with 19679 additions and 6952 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue