Elevate MiroFish/CrowdSight from single-container dev to a SaaS foundation: - Local memory backend (Zep-compatible): memory services/models, local graph builder + updater, AgentActivity seam, import-boundary isolation; Zep stays default, local is opt-in behind MEMORY_BACKEND. Semantic parity not yet proven. - Durable product persistence: projects/simulations/reports schema (migration 0007) + tenant/owner-scoped ProductRepository + dual-write + scoped_project read-first + ArtifactStore abstraction; durable JobQueue + worker.py. - SaaS hardening: durable RateLimiter (wired to login), UsageService (LLM accounting), redacted AuditService, idempotency, CORS allowlist, safe API errors, single-use PasswordResetService + endpoints (covers invite-pending). - Exactly 3 roles (super_admin/admin/user) with tenant authz policy. - Admin UI: GET/POST/PATCH /api/admin/users + GET/PUT /api/admin/settings (super-admin only, encrypted/masked); AdminView.vue + SettingsView.vue with admin/super-admin route guards, th/en i18n. - Production deploy topology: multi-stage Dockerfile (frontend build + gunicorn wsgi + nginx SPA-proxy + supervisord worker), backend/wsgi.py, gunicorn dep. Backend 197 passed; frontend 10 tests + build green. ruff unavailable (gap). No commit of credentials; secrets handled via env/.env.example. Deferred: Zep semantic A/B parity, object storage cutover, mobile QA, EasyPanel container build of deploy topology.
48 lines
1.9 KiB
Python
48 lines
1.9 KiB
Python
import importlib.util
|
|
from pathlib import Path
|
|
import sys
|
|
import unittest
|
|
|
|
|
|
_MODULE_PATH = Path(__file__).parents[1] / "app" / "utils" / "language_policy.py"
|
|
_SPEC = importlib.util.spec_from_file_location("language_policy_under_test", _MODULE_PATH)
|
|
assert _SPEC is not None and _SPEC.loader is not None
|
|
_LANGUAGE_POLICY = importlib.util.module_from_spec(_SPEC)
|
|
sys.modules[_SPEC.name] = _LANGUAGE_POLICY
|
|
_SPEC.loader.exec_module(_LANGUAGE_POLICY)
|
|
|
|
DEFAULT_LOCALE = _LANGUAGE_POLICY.DEFAULT_LOCALE
|
|
SUPPORTED_LOCALES = _LANGUAGE_POLICY.SUPPORTED_LOCALES
|
|
locale_from_accept_language = _LANGUAGE_POLICY.locale_from_accept_language
|
|
normalize_locale = _LANGUAGE_POLICY.normalize_locale
|
|
|
|
|
|
class LanguagePolicyTests(unittest.TestCase):
|
|
def test_supported_locales_are_thai_and_english(self):
|
|
self.assertEqual(SUPPORTED_LOCALES, ("th", "en"))
|
|
self.assertEqual(DEFAULT_LOCALE, "th")
|
|
|
|
def test_normalize_locale_accepts_language_region_values(self):
|
|
self.assertEqual(normalize_locale("en-US"), "en")
|
|
self.assertEqual(normalize_locale("TH_th"), "th")
|
|
|
|
def test_normalize_locale_migrates_legacy_chinese_to_default(self):
|
|
self.assertEqual(normalize_locale("zh"), "th")
|
|
self.assertEqual(normalize_locale("zh-CN"), "th")
|
|
|
|
def test_normalize_locale_falls_back_for_unknown_values(self):
|
|
self.assertEqual(normalize_locale(None), "th")
|
|
self.assertEqual(normalize_locale("fr"), "th")
|
|
self.assertEqual(normalize_locale(""), "th")
|
|
|
|
def test_accept_language_uses_quality_and_skips_zero_quality(self):
|
|
header = "zh-CN;q=1, fr-FR;q=0.9, en-US;q=0.8, th;q=0"
|
|
self.assertEqual(locale_from_accept_language(header), "en")
|
|
|
|
def test_accept_language_returns_default_when_no_supported_language_exists(self):
|
|
self.assertEqual(locale_from_accept_language("ja-JP, de;q=0.8"), "th")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|