"""Org memory reflection runs only on a model the owner granted the organization. The invariants under test: * an organization with no granted model learns nothing from a finished task: no provider call, and the refusal is recorded as ``memory.reflection_no_grant``; * a granted model is the one that answers, and the cheapest tier wins: the ``fast`` model is chosen over the ``strong`` one, whatever the grant order. """ from __future__ import annotations from contextlib import asynccontextmanager from sqlalchemy import select from app.application.container_org import ( build_memory_service, build_model_grant_service, build_model_grants, ) from app.application.ports import ModelResponse from app.application.ports.memory import TaskOutcome from app.domain.base import new_id from app.domain.identity.models import PlatformRole, Role from app.domain.memory.models import MemoryKind from app.infrastructure.db.models import ( AiModelRow, ErrorEventRow, MembershipRow, OrganizationRow, TaskRow, UserRow, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- async def _org(session_factory, slug: str) -> str: """An organization with one owner, written directly to the database.""" async with session_factory() as session: user = UserRow( id=new_id(), email=f"{slug}@example.com", password_hash="x", name=slug, platform_role=PlatformRole.MANAGER.value, ) session.add(user) await session.flush() org = OrganizationRow(id=new_id(), name=slug, slug=slug, owner_user_id=user.id) session.add(org) await session.flush() session.add( MembershipRow(organization_id=org.id, user_id=user.id, role=Role.OWNER.value) ) await session.commit() return org.id async def _task(session_factory, org_id: str, task_id: str) -> None: """A real task row: a memory's evidence is a reference, not a free string.""" async with session_factory() as session: session.add(TaskRow(id=task_id, organization_id=org_id, title=f"کار {task_id}")) await session.commit() async def _grant(session_factory, org_id: str, *internal_names: str) -> None: """Grant the named seeded models to the organization, in the order given.""" async with session_factory() as session: service = build_model_grant_service(session) for name in internal_names: row = ( await session.execute(select(AiModelRow).where(AiModelRow.internal_name == name)) ).scalar_one() await service.grant_model(organization_id=org_id, ai_model_id=row.id) await session.commit() class _RecordingProvider: """Answers every reflection with one lesson, and remembers which model was asked.""" def __init__(self) -> None: self.asked: list[str] = [] async def complete(self, **kwargs): model = kwargs["model"] self.asked.append(model.internal_name) return ModelResponse( content="درس ۱: تست‌ها را قبل از ادغام اجرا کن.", model_internal_name=model.internal_name, input_tokens=20, output_tokens=8, ) @asynccontextmanager async def _service(session_factory, provider): """A memory service wired the way the engine wires it, committed on exit.""" session = session_factory() try: service = build_memory_service( session, provider=provider, grants=build_model_grants(session) ) yield service await session.commit() finally: await session.close() def _outcome(task_id: str) -> TaskOutcome: return TaskOutcome(task_id=task_id, title="ساخت صفحه", status="completed") # --------------------------------------------------------------------------- # Reflection and grants # --------------------------------------------------------------------------- async def test_an_organization_without_a_grant_learns_nothing( session_factory, redirect_error_log, seeded ): org_id = await _org(session_factory, "no-grant") provider = _RecordingProvider() async with _service(session_factory, provider) as service: # The catalogue has models, but the owner granted this organization none. await service.on_task_finished(organization_id=org_id, task=_outcome("task-no-grant")) assert await service.list(org_id) == [] assert provider.asked == [] async with session_factory() as session: kinds = {row.kind for row in (await session.execute(select(ErrorEventRow))).scalars()} assert "memory.reflection_no_grant" in kinds async def test_a_granted_model_answers_and_the_cheapest_tier_wins( session_factory, redirect_error_log, seeded ): org_id = await _org(session_factory, "granted") await _task(session_factory, org_id, "task-granted") # Both models are granted; the reflection must pick the fast one whatever the order. await _grant(session_factory, org_id, "claude-sonnet-4-5-thinking", "gemini-3-flash") provider = _RecordingProvider() async with _service(session_factory, provider) as service: await service.on_task_finished(organization_id=org_id, task=_outcome("task-granted")) memories = await service.list(org_id) assert provider.asked == ["gemini-3-flash"] assert [m.kind for m in memories] == [MemoryKind.LESSON] assert all(m.evidence_task_id == "task-granted" for m in memories)