feat(integration): wire v2 workstreams together (engine policy/budget, sandbox, tenant checks, order route, docs)
Engine honours org policy and monthly budget, legacy runner skips engine tasks, sandbox session provisioned on demand, project fallback for threads, reviewer fallback, tenant checks on project ids, memory reflection respects grants, expiry worker, MCP pipeline_status, login redirect, sharing settings, e2e smoke. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
36564e3f2b
commit
fef5a9deab
@@ -0,0 +1,152 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user