This commit is contained in:
2026-05-23 21:31:34 +02:00
parent 51b5ddc6f2
commit 641bb10479
6155 changed files with 3775717 additions and 291 deletions

View File

@@ -10,8 +10,10 @@ from pathlib import Path
from unittest.mock import patch
from aza_ai_budget import (
_norm_model,
compute_budget_snapshot,
ensure_ai_budget_schema,
estimate_openai_cost_usd,
insert_usage_event,
resolve_license_for_empfang,
sum_usage_usd_for_period,
@@ -67,6 +69,57 @@ def _mk_db(path: Path) -> None:
con.close()
class TestModelNormalizationAndBudgetDefault(unittest.TestCase):
def test_norm_dated_gpt_4o_mini(self):
self.assertEqual(_norm_model("gpt-4o-mini-2024-07-18"), "gpt-4o-mini")
def test_dated_gpt_4o_mini_uses_cheap_price(self):
cost = estimate_openai_cost_usd(
model="gpt-4o-mini-2024-07-18",
input_tokens=1_000_000,
output_tokens=0,
)
self.assertAlmostEqual(cost, 0.15, places=6)
self.assertEqual(
cost,
estimate_openai_cost_usd(
model="gpt-4o-mini", input_tokens=1_000_000, output_tokens=0
),
)
def test_gpt_4o_mini_transcribe_unchanged(self):
self.assertEqual(_norm_model("gpt-4o-mini-transcribe"), "gpt-4o-mini-transcribe")
cost = estimate_openai_cost_usd(
model="gpt-4o-mini-transcribe", audio_seconds=60.0
)
self.assertAlmostEqual(cost, 0.012, places=6)
def test_unknown_model_stays_conservative_fallback(self):
cost = estimate_openai_cost_usd(
model="totally-unknown-model-xyz",
input_tokens=1_000_000,
output_tokens=0,
)
self.assertAlmostEqual(cost, 5.0, places=6)
def test_env_overrides_budget_default(self):
import importlib
import aza_ai_budget as mod
key = "AZA_AI_BUDGET_USD_DEFAULT"
old = os.environ.get(key)
try:
os.environ[key] = "42.5"
importlib.reload(mod)
self.assertEqual(mod.DEFAULT_MONTHLY_AI_BUDGET_USD, 42.5)
finally:
if old is None:
os.environ.pop(key, None)
else:
os.environ[key] = old
importlib.reload(mod)
class TestBackendGatePracticeFallback(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False)

View File

@@ -123,23 +123,71 @@ class TestPhase1bResolve(unittest.TestCase):
self.assertIsNotNone(lic)
self.assertEqual(lic.subscription_id, "sub_ef")
def test_empfang_prefers_device_then_practice(self):
def test_empfang_prefers_practice_when_both_set(self):
with sqlite3.connect(str(self.db_path)) as con:
lic = resolve_license_for_empfang(
con, x_device_id="dev-ef-test", session_practice_id="prac_ef_test"
)
self.assertIsNotNone(lic)
self.assertEqual(lic.customer_email, "ef@example.test")
self.assertEqual(lic.subscription_id, "sub_ef")
self.assertEqual(lic.practice_id, "prac_ef_test")
def test_empfang_device_practice_conflict_drops_device(self):
def test_empfang_practice_row_used_even_if_email_points_newer(self):
"""Session practice_id mit eigener Zeile schlägt neuere E-Mail-Lizenz ohne practice_id."""
now = 1_700_000_000
with sqlite3.connect(str(self.db_path)) as con:
con.execute(
"""
INSERT INTO licenses(subscription_id, customer_id, status, lookup_key, allowed_users, devices_per_user,
customer_email, client_reference_id, current_period_end, current_period_start, updated_at, license_key, practice_id)
VALUES ('sub_other', 'cus_z', 'active', 'aza_basic_monthly', 1, 2,
'ef@example.test', NULL, ?, ?, ?, 'KEY3', 'prac_other')
""",
(now + 86400 * 60, now + 86400 * 30, now + 86400 * 5),
)
lic = resolve_license_for_empfang(
con, x_device_id="dev-ef-test", session_practice_id="prac_other"
)
self.assertIsNone(lic)
self.assertIsNotNone(lic)
self.assertEqual(lic.subscription_id, "sub_other")
self.assertEqual(lic.practice_id, "prac_other")
def test_empfang_device_only_without_practice(self):
with sqlite3.connect(str(self.db_path)) as con:
lic = resolve_license_for_empfang(
con, x_device_id="dev-ef-test", session_practice_id=None
)
self.assertIsNotNone(lic)
self.assertEqual(lic.subscription_id, "sub_ef")
def test_empfang_device_fallback_when_practice_missing(self):
with sqlite3.connect(str(self.db_path)) as con:
lic = resolve_license_for_empfang(
con, x_device_id="dev-ef-test", session_practice_id="prac_unknown"
)
self.assertIsNotNone(lic)
self.assertEqual(lic.subscription_id, "sub_ef")
def test_empfang_practice_wins_over_newer_email_license(self):
"""Device/E-Mail würde neuere Lizenz ohne practice_id wählen; Practice-ID gewinnt."""
now = 1_700_000_000
with sqlite3.connect(str(self.db_path)) as con:
con.execute(
"""
INSERT INTO licenses(subscription_id, customer_id, status, lookup_key, allowed_users, devices_per_user,
customer_email, client_reference_id, current_period_end, current_period_start, updated_at, license_key, practice_id)
VALUES ('sub_newer_email', 'cus_y', 'active', 'aza_basic_monthly', 1, 2,
'ef@example.test', NULL, ?, ?, ?, 'KEY2', NULL)
""",
(now + 86400 * 60, now + 86400 * 30, now + 86400 * 10),
)
lic = resolve_license_for_empfang(
con, x_device_id="dev-ef-test", session_practice_id="prac_ef_test"
)
self.assertIsNotNone(lic)
self.assertEqual(lic.subscription_id, "sub_ef")
self.assertEqual(lic.practice_id, "prac_ef_test")
class TestPhase1bGateAndAudio(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False)
self.tmp.close()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
"""Tests für practice_id-Sync (Textblöcke, Korrekturen, Autotext) und renewal_date_de."""
import json
import os
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from aza_sync_items import (
ensure_sync_items_schema,
items_to_local_textblocks,
list_sync_items,
local_autotext_to_items,
local_korrekturen_to_items,
local_textblocks_to_items,
soft_delete_sync_item,
upsert_sync_item,
)
from aza_ai_budget import budget_json_for_client
class TestSyncItemsSchema(unittest.TestCase):
def test_schema_idempotent(self):
td = tempfile.mkdtemp()
try:
db = Path(td) / "t.sqlite"
con = sqlite3.connect(db)
try:
ensure_sync_items_schema(con)
ensure_sync_items_schema(con)
cols = {
r[1]
for r in con.execute("PRAGMA table_info(synced_user_items)").fetchall()
}
finally:
con.close()
self.assertIn("practice_id", cols)
self.assertIn("item_type", cols)
finally:
try:
os.remove(Path(td) / "t.sqlite")
except OSError:
pass
os.rmdir(td)
class TestSyncItemsDb(unittest.TestCase):
def tearDown(self):
import shutil
try:
shutil.rmtree(self.td, ignore_errors=True)
except Exception:
pass
def setUp(self):
self.td = tempfile.mkdtemp()
self.db = Path(self.td) / "stripe_webhook.sqlite"
with sqlite3.connect(self.db) as con:
ensure_sync_items_schema(con)
con.execute(
"""
CREATE TABLE licenses (
subscription_id TEXT PRIMARY KEY,
customer_id TEXT,
status TEXT,
lookup_key TEXT,
allowed_users INTEGER,
devices_per_user INTEGER,
customer_email TEXT,
client_reference_id TEXT,
current_period_end INTEGER,
updated_at INTEGER NOT NULL,
practice_id TEXT,
current_period_start INTEGER
)
"""
)
con.execute(
"""
INSERT INTO licenses (
subscription_id, customer_id, status, lookup_key,
allowed_users, devices_per_user, customer_email,
client_reference_id, current_period_end, updated_at,
practice_id, current_period_start
) VALUES ('sub_a', 'c1', 'active', 'k1', 5, 2, 'a@test.ch',
'', 2000000000, 1, 'prac_alpha', 1900000000)
"""
)
con.execute(
"""
INSERT INTO licenses (
subscription_id, customer_id, status, lookup_key,
allowed_users, devices_per_user, customer_email,
client_reference_id, current_period_end, updated_at,
practice_id, current_period_start
) VALUES ('sub_b', 'c2', 'active', 'k2', 5, 2, 'b@test.ch',
'', 2000000000, 1, 'prac_beta', 1900000000)
"""
)
con.commit()
def test_practice_isolation_and_soft_delete(self):
with sqlite3.connect(self.db) as con:
upsert_sync_item(
con,
"prac_alpha",
{
"id": "tb_1",
"item_type": "textblock",
"title": "A",
"content": "alpha",
"sort_order": 1,
},
)
upsert_sync_item(
con,
"prac_beta",
{
"id": "tb_beta_1",
"item_type": "textblock",
"title": "B",
"content": "beta",
"sort_order": 1,
},
)
a_items = list_sync_items(con, "prac_alpha", "textblock")
b_items = list_sync_items(con, "prac_beta", "textblock")
self.assertEqual(len(a_items), 1)
self.assertEqual(a_items[0]["content"], "alpha")
self.assertEqual(b_items[0]["content"], "beta")
soft_delete_sync_item(con, "prac_alpha", "tb_1")
a2 = list_sync_items(con, "prac_alpha", "textblock")
self.assertEqual(len(a2), 0)
def test_item_types_separate(self):
with sqlite3.connect(self.db) as con:
upsert_sync_item(
con,
"prac_alpha",
{
"id": "at_x",
"item_type": "autotext",
"trigger": "kg",
"content": "Krankengeschichte",
"sort_order": 1,
},
)
upsert_sync_item(
con,
"prac_alpha",
{
"id": "corr_1",
"item_type": "correction",
"title": "medikamente",
"trigger": "asprin",
"content": "Aspirin",
"sort_order": 1,
},
)
at = list_sync_items(con, "prac_alpha", "autotext")
co = list_sync_items(con, "prac_alpha", "correction")
self.assertEqual(len(at), 1)
self.assertEqual(len(co), 1)
class TestLocalMapping(unittest.TestCase):
def test_textblock_roundtrip(self):
local = {
"1": {"name": "Urtikaria", "content": "Haut", "updated_at": "t"},
"2": {"name": "TB2", "content": "", "updated_at": "t"},
}
items = local_textblocks_to_items(local)
back = items_to_local_textblocks(items)
self.assertEqual(back["1"]["name"], "Urtikaria")
self.assertGreaterEqual(len(back), 2)
def test_korrekturen_mapping(self):
k = {"medikamente": {"asprin": "Aspirin"}}
items = local_korrekturen_to_items(k)
self.assertEqual(items[0]["item_type"], "correction")
def test_autotext_mapping(self):
at = {"entries": {"kg": "Krankengeschichte"}}
items = local_autotext_to_items(at)
self.assertEqual(items[0]["trigger"], "kg")
class TestBudgetRenewalField(unittest.TestCase):
def test_renewal_date_de_in_client_json(self):
snap = {
"ok": True,
"active": True,
"available_percent": 76,
"period_end": 1781817600,
"show_warning": False,
"user_label": "KI-Kontingent: 76 % verfügbar",
}
body = budget_json_for_client(snap)
self.assertIn("renewal_date_de", body)
self.assertTrue(body["renewal_date_de"])
if __name__ == "__main__":
unittest.main()