"""v2 foundation: schema invariants, shared repository, contracts and wiring.""" from __future__ import annotations import pytest from sqlalchemy.exc import IntegrityError import app.application.container as container from app.application.ports.delivery_hooks import DeliveryHooks, NullDeliveryHooks from app.application.ports.deploy_target import ( DeployTargetSpec, DeployTargetTester, FakeDeployTargetTester, ) from app.application.ports.memory import MemoryPort, NullMemory, TaskOutcome from app.application.ports.model_grants import ModelGrantPort, tier_for from app.application.ports.repo_workspace import ( DiffResult, FakeRepoWorkspace, MergeOutcome, RemoteCredentials, RepoWorkspacePort, ) from app.application.ports.workforce_bootstrap import ( NullWorkforceBootstrap, WorkforceBootstrapPort, ) from app.domain.base import new_id from app.domain.identity.models import PlatformRole from app.infrastructure.db.models import ( AiModelRow, AiProviderRow, OrganizationRow, ProjectRow, UserRow, ) from app.infrastructure.repositories.app_profile import AppProfileRepository async def _org_with_user(session, slug: str) -> tuple[str, str]: user = UserRow( id=new_id(), email=f"{slug}@example.com", password_hash="x", name="t", 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() return org.id, user.id @pytest.mark.asyncio async def test_one_project_per_organization(session_factory): async with session_factory() as session: org_id, _ = await _org_with_user(session, "solo") session.add(ProjectRow(id=new_id(), organization_id=org_id, name="a", slug="a")) await session.commit() session.add(ProjectRow(id=new_id(), organization_id=org_id, name="b", slug="b")) with pytest.raises(IntegrityError): await session.commit() @pytest.mark.asyncio async def test_app_profile_repository_is_tenant_scoped(session_factory): async with session_factory() as session: org_a, _ = await _org_with_user(session, "org-a") org_b, _ = await _org_with_user(session, "org-b") project = ProjectRow(id=new_id(), organization_id=org_a, name="a", slug="a") session.add(project) await session.commit() repo = AppProfileRepository(session) profile = await repo.get_for_org(org_a) assert profile is not None assert profile.healthcheck_path == "/" assert profile.git_source == "platform" assert profile.wizard_completed is False # Another org can neither read nor update this project's profile. assert await repo.get_for_org(org_b) is None assert await repo.get(organization_id=org_b, project_id=project.id) is None assert ( await repo.update_profile( organization_id=org_b, project_id=project.id, values={"language": "go"} ) is None ) updated = await repo.update_profile( organization_id=org_a, project_id=project.id, values={"language": "python", "framework": "fastapi", "app_port": 8000}, ) assert updated is not None assert (updated.language, updated.framework, updated.app_port) == ( "python", "fastapi", 8000, ) with pytest.raises(ValueError): await repo.update_profile( organization_id=org_a, project_id=project.id, values={"organization_id": "x"} ) @pytest.mark.asyncio async def test_default_model_grants_return_active_models_with_tiers(session_factory): async with session_factory() as session: provider = AiProviderRow(id=new_id(), name="agy-test") session.add(provider) await session.flush() session.add_all( [ AiModelRow( id="m-strong", provider_id=provider.id, internal_name="s", display_name_for_admin="S", capabilities=["text", "reasoning"], output_price_per_1m=10.0, ), AiModelRow( id="m-fast", provider_id=provider.id, internal_name="f", display_name_for_admin="F", capabilities=["text"], output_price_per_1m=0.5, ), AiModelRow( id="m-off", provider_id=provider.id, internal_name="o", display_name_for_admin="O", capabilities=["text"], is_active=False, ), ] ) await session.commit() grants = container.build_model_grants(session) assert isinstance(grants, ModelGrantPort) granted = {g.ai_model_id: g.tier for g in await grants.granted_models(organization_id="o")} assert granted == {"m-strong": "strong", "m-fast": "fast"} assert await grants.is_granted(organization_id="o", ai_model_id="m-fast") assert not await grants.is_granted(organization_id="o", ai_model_id="m-off") def test_tier_for_never_uses_names(): assert tier_for(capabilities=["reasoning"], output_price_per_1m=99) == "strong" assert tier_for(capabilities=["text"], output_price_per_1m=1) == "fast" assert tier_for(capabilities=["text"], output_price_per_1m=5) == "balanced" @pytest.mark.asyncio async def test_contract_fakes_satisfy_their_protocols(): assert isinstance(FakeRepoWorkspace(), RepoWorkspacePort) assert isinstance(NullDeliveryHooks(), DeliveryHooks) assert isinstance(FakeDeployTargetTester(), DeployTargetTester) assert isinstance(NullMemory(), MemoryPort) assert isinstance(NullWorkforceBootstrap(), WorkforceBootstrapPort) @pytest.mark.asyncio async def test_fake_repo_workspace_full_flow(): repo = FakeRepoWorkspace() ws = await repo.create_task_worktree(project_id="p", task_id="t1") assert ws.branch == "task/t1" and ws.sandbox_path.endswith("/t1") # An empty diff commits nothing. assert await repo.commit(worktree=ws, message="m", author="a") is None repo.diffs["t1"] = DiffResult(text="+x", changed_files=["a.py"]) sha = await repo.commit(worktree=ws, message="m", author="a") assert sha merged = await repo.merge_into(project_id="p", branch=ws.branch) assert merged.ok and merged.sha == sha assert (await repo.push(project_id="p", branch="dev")).ok assert await repo.head(project_id="p", branch="dev") == sha repo.merge_outcomes.append(MergeOutcome(ok=False, conflict_files=["a.py"])) assert (await repo.merge_into(project_id="p", branch="x")).conflict_files == ["a.py"] check = await repo.test_remote(url="https://x/y.git", credentials=RemoteCredentials()) assert check.ok @pytest.mark.asyncio async def test_fake_deploy_tester_and_hooks_and_memory_record_calls(): tester = FakeDeployTargetTester() ok = await tester.test_connection( target=DeployTargetSpec(kind="ssh", host="h.example.com", port=22, username="u") ) bad = await tester.test_connection( target=DeployTargetSpec(kind="ftp", host="fail.example.com", port=21, username="u") ) assert ok.ok and ok.fingerprint and not bad.ok hooks = NullDeliveryHooks() await hooks.on_app_created(organization_id="o", project_id="p") await hooks.on_branch_pushed( organization_id="o", project_id="p", branch="dev", sha="abc", source="engine" ) assert len(hooks.created) == 1 and hooks.pushed[0]["sha"] == "abc" memory = NullMemory() assert await memory.build_context(organization_id="o", role="developer", query="q") == "" await memory.on_task_finished( organization_id="o", task=TaskOutcome(task_id="t", title="x", status="completed") ) assert memory.finished[0].task_id == "t" @pytest.mark.asyncio async def test_container_exposes_all_v2_builders(session_factory): async with session_factory() as session: assert isinstance(container.build_repo_workspace(session), RepoWorkspacePort) assert isinstance(container.build_delivery_hooks(session), DeliveryHooks) assert isinstance(container.build_deploy_target_tester(session), DeployTargetTester) assert isinstance(container.build_memory(session), MemoryPort) assert isinstance(container.build_workforce_bootstrap(session), WorkforceBootstrapPort) @pytest.mark.asyncio async def test_app_builds_with_v2_routers_and_delivery_worker_runs(client): # The full handler app imports every v2 router; /health proves it assembled. assert (await client.get("/health")).status_code == 200 from app.workers.__main__ import _build_parser, _pass_function from app.workers.delivery import DeliveryWorker assert (await DeliveryWorker().run_pass()).failed == 0 # ...and it is selectable from the CLI. one_pass = _pass_function(_build_parser().parse_args(["delivery"])) assert (await one_pass()).name == "delivery"