added RAG, Multiuser, TG bot
This commit is contained in:
@@ -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()
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user