added RAG, Multiuser, TG bot

This commit is contained in:
2026-06-13 20:20:56 +00:00
parent 66e1b0e29e
commit c8a9429bed
142 changed files with 19901 additions and 8790 deletions
+84
View File
@@ -0,0 +1,84 @@
import unittest
from app.fitness.activity_budget import (
compute_activity_bonus,
scale_targets,
steps_bonus_kcal,
)
PROFILE = {
"weight_kg": 70,
"activity_level": "moderate",
"weekly_workouts": 3,
"calorie_target": 2000,
"protein_g": 126,
"fat_g": 56,
"carbs_g": 250,
"water_l": 2.5,
}
class ActivityBudgetTests(unittest.TestCase):
def test_no_bonus_at_baseline(self) -> None:
bonus = compute_activity_bonus(
PROFILE,
steps_total=9000,
workouts=[{"active_calories": 85}],
)
self.assertEqual(bonus.steps_bonus_kcal, 0.0)
self.assertEqual(bonus.workout_bonus_kcal, 0.0)
self.assertEqual(bonus.total_bonus_kcal, 0.0)
self.assertEqual(bonus.scale_factor, 1.0)
def test_steps_and_workout_bonus(self) -> None:
bonus = compute_activity_bonus(
PROFILE,
steps_total=21800,
workouts=[{"active_calories": 155}],
)
self.assertGreater(bonus.steps_bonus_kcal, 0)
self.assertGreater(bonus.workout_bonus_kcal, 0)
self.assertEqual(
bonus.total_bonus_kcal,
round(bonus.steps_bonus_kcal + bonus.workout_bonus_kcal, 1),
)
self.assertGreater(bonus.scale_factor, 1.0)
def test_steps_bonus_formula(self) -> None:
kcal = steps_bonus_kcal(steps=21800, baseline_steps=9000, weight_kg=70)
self.assertEqual(kcal, round(12800 * 70 * 0.0005, 1))
def test_proportional_macro_scale(self) -> None:
base = {
"calories": 2000,
"protein_g": 100,
"fat_g": 50,
"carbs_g": 200,
"water_ml": 2500,
}
effective, targets_base = scale_targets(base, 500)
self.assertEqual(effective["calories"], 2500)
self.assertEqual(targets_base, base)
self.assertEqual(effective["water_ml"], 2500)
ratio = effective["calories"] / base["calories"]
self.assertAlmostEqual(effective["protein_g"] / base["protein_g"], ratio, places=1)
self.assertAlmostEqual(effective["fat_g"] / base["fat_g"], ratio, places=1)
self.assertAlmostEqual(effective["carbs_g"] / base["carbs_g"], ratio, places=1)
def test_floor_at_base_when_no_activity(self) -> None:
effective, _ = scale_targets(
{
"calories": 2045,
"protein_g": 156,
"fat_g": 57,
"carbs_g": 227,
"water_ml": 2900,
},
0,
)
self.assertEqual(effective["calories"], 2045)
if __name__ == "__main__":
unittest.main()
+102
View File
@@ -0,0 +1,102 @@
import pytest
from app.fitness.body_composition import (
compute_body_composition,
ffmi,
lean_body_mass,
navy_body_fat_pct,
whr,
)
def test_navy_male_reasonable_range():
bf = navy_body_fat_pct(
sex="male",
height_cm=180,
neck_cm=38,
waist_cm=84,
)
assert bf is not None
assert 5 <= bf <= 35
def test_navy_female_requires_hip():
assert navy_body_fat_pct(
sex="female",
height_cm=165,
neck_cm=34,
waist_cm=72,
hip_cm=None,
) is None
bf = navy_body_fat_pct(
sex="female",
height_cm=165,
neck_cm=34,
waist_cm=72,
hip_cm=98,
)
assert bf is not None
assert 10 <= bf <= 45
def test_navy_invalid_waist_neck():
assert navy_body_fat_pct(
sex="male",
height_cm=180,
neck_cm=40,
waist_cm=39,
) is None
def test_whr():
assert whr(80, 100) == 0.8
def test_lean_body_mass_and_ffmi():
lbm = lean_body_mass(80, 20)
assert lbm == 64.0
score = ffmi(80, 180, 20)
assert score is not None
assert 15 <= score <= 25
def test_compute_manual_body_fat():
result = compute_body_composition(
sex="male",
height_cm=180,
weight_kg=80,
body_fat_pct=18,
waist_cm=84,
hip_cm=100,
)
assert result["body_fat_pct"] == 18.0
assert result["body_fat_method"] == "manual"
assert result["whr"] == 0.84
assert result["lbm_kg"] == 65.6
assert result["ffmi"] is not None
def test_compute_navy_auto():
result = compute_body_composition(
sex="male",
height_cm=180,
weight_kg=82,
neck_cm=38,
waist_cm=84,
)
assert result["body_fat_pct"] is not None
assert result["body_fat_method"] == "navy"
assert result["lbm_kg"] is not None
def test_compute_female_warning_without_hip():
result = compute_body_composition(
sex="female",
height_cm=165,
weight_kg=60,
neck_cm=34,
waist_cm=72,
)
assert result["body_fat_pct"] is None
assert any("бёдер" in w for w in result["warnings"])
+154
View File
@@ -0,0 +1,154 @@
import os
import tempfile
import uuid
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.auth.tokens import hash_token
from app.db.base import Base, get_db
from app.db.models import CharacterCard, ChatSession, MemoryFact, ShoppingList, User
@pytest.fixture()
def client():
db_path = Path(tempfile.gettempdir()) / f"test_multi_{uuid.uuid4().hex}.db"
os.environ["DATABASE_URL"] = f"sqlite:///{db_path.as_posix()}"
os.environ["DEFAULT_API_TOKEN"] = "unused-in-tests"
os.environ["AUTH_REQUIRED"] = "true"
os.environ["RAG_ENABLED"] = "false"
from app.config import get_settings
get_settings.cache_clear()
from app.main import create_app
engine = create_engine(
f"sqlite:///{db_path.as_posix()}",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
token_a = "token-user-a"
token_b = "token-user-b"
db = TestingSessionLocal()
user_a = User(
username="alice",
display_name="Alice",
api_token_hash=hash_token(token_a),
is_active=True,
)
user_b = User(
username="bob",
display_name="Bob",
api_token_hash=hash_token(token_b),
is_active=True,
)
db.add_all([user_a, user_b])
db.commit()
db.refresh(user_a)
db.refresh(user_b)
db.add(ChatSession(user_id=user_a.id, title="Alice chat"))
db.add(ChatSession(user_id=user_b.id, title="Bob chat"))
db.add(ShoppingList(user_id=user_a.id, name="groceries"))
db.add(ShoppingList(user_id=user_b.id, name="groceries"))
db.add(
CharacterCard(
user_id=user_a.id,
card_json='{"spec":"chara_card_v2","spec_version":"2.0","data":{"name":"A","rp_persona_id":"persona-a"}}',
)
)
db.add(
CharacterCard(
user_id=user_b.id,
card_json='{"spec":"chara_card_v2","spec_version":"2.0","data":{"name":"B","rp_persona_id":"persona-b"}}',
)
)
db.add(
MemoryFact(
user_id=user_a.id,
category="person",
content="Секрет только для owner",
source="test",
)
)
db.commit()
db.close()
app = create_app()
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
test_client.tokens = {"a": token_a, "b": token_b}
yield test_client
app.dependency_overrides.clear()
get_settings.cache_clear()
try:
db_path.unlink(missing_ok=True)
except OSError:
pass
def _headers(client: TestClient, who: str) -> dict[str, str]:
return {"Authorization": f"Bearer {client.tokens[who]}"}
def test_chat_sessions_isolated(client: TestClient):
res_a = client.get("/api/v1/chat/sessions", headers=_headers(client, "a"))
res_b = client.get("/api/v1/chat/sessions", headers=_headers(client, "b"))
assert res_a.status_code == 200
assert res_b.status_code == 200
titles_a = {s["title"] for s in res_a.json()}
titles_b = {s["title"] for s in res_b.json()}
assert titles_a == {"Alice chat"}
assert titles_b == {"Bob chat"}
def test_character_cards_isolated(client: TestClient):
res_a = client.get("/api/v1/character", headers=_headers(client, "a"))
res_b = client.get("/api/v1/character", headers=_headers(client, "b"))
assert res_a.json()["data"]["rp_persona_id"] == "persona-a"
assert res_b.json()["data"]["rp_persona_id"] == "persona-b"
def test_shopping_same_name_different_users(client: TestClient):
res_a = client.get("/api/v1/shopping", headers=_headers(client, "a"))
res_b = client.get("/api/v1/shopping", headers=_headers(client, "b"))
assert res_a.status_code == 200
assert res_b.status_code == 200
assert len(res_a.json()["lists"]) == 1
assert len(res_b.json()["lists"]) == 1
def test_missing_token_unauthorized(client: TestClient):
res = client.get("/api/v1/chat/sessions")
assert res.status_code == 401
def test_memory_facts_isolated(client: TestClient):
res_a = client.get("/api/v1/memory", headers=_headers(client, "a"))
res_b = client.get("/api/v1/memory", headers=_headers(client, "b"))
assert res_a.status_code == 200
assert res_b.status_code == 200
facts_a = res_a.json().get("facts") or []
facts_b = res_b.json().get("facts") or []
assert any("Секрет только для owner" in f.get("content", "") for f in facts_a)
assert not any("Секрет только для owner" in f.get("content", "") for f in facts_b)
assert res_b.json().get("total_facts", 0) == 0