"""The gate chain: what blocks work, and what leaves a record behind. Every gate is tested through the *real* repository, because a gate that writes no ``gate_results`` row is a gate nobody can audit. The sandbox is the only fake: no command ever leaves the test process. """ from __future__ import annotations from collections.abc import AsyncIterator import pytest_asyncio from app.application.engine.events import RunFeed from app.application.engine.gates import ( MAX_FILE_BYTES, GateRunner, diff_hash, forbidden_paths, ) from app.application.ports import CommandResult from app.application.ports.repo_workspace import DiffResult from app.domain.base import new_id from app.domain.workforce.engine import GateName, GateResult from app.domain.workforce.models import Task from app.domain.workforce.pipeline import TaskKind from app.infrastructure.db.models import OrganizationRow, ProjectRow, TaskRow, UserRow from app.infrastructure.repositories.app_profile import AppProfileRepository from app.infrastructure.repositories.engine import GateResultRepository from tests.engine_fakes import FakeSandbox @pytest_asyncio.fixture async def task(session_factory, seeded) -> AsyncIterator[Task]: async with session_factory() as session: user = UserRow(email=f"g-{new_id()[:8]}@example.com", password_hash="x") session.add(user) await session.flush() org = OrganizationRow( name="سازمان", slug=f"g-{user.id[:6]}", owner_user_id=user.id ) session.add(org) await session.flush() row = TaskRow(organization_id=org.id, title="کار دِرِوازه") session.add(row) await session.flush() await session.commit() yield Task(id=row.id, organization_id=org.id, title=row.title) @pytest_asyncio.fixture async def runner(session_factory, task: Task) -> AsyncIterator[GateRunner]: async with session_factory() as session: yield GateRunner( results=GateResultRepository(session), feed=RunFeed(), profiles=AppProfileRepository(session), ) # --------------------------------------------------------------------------- # pure helpers # --------------------------------------------------------------------------- def test_diff_hash_is_stable_and_sensitive_to_content(): assert diff_hash("same") == diff_hash("same") assert diff_hash("same") != diff_hash("same!") def test_forbidden_paths_flag_git_internals_and_ci(): changed = [".git/config", ".github/workflows/ci.yml", ".gitlab-ci.yml", "app/main.py"] assert forbidden_paths(changed) == [ ".git/config", ".github/workflows/ci.yml", ".gitlab-ci.yml", ] def test_forbidden_paths_allow_ordinary_source_files(): assert forbidden_paths(["app/main.py", "README.md", "tests/test_app.py"]) == [] def test_code_task_excludes_analysis(): assert GateRunner.code_task(TaskKind.FEATURE) is True assert GateRunner.code_task(TaskKind.ANALYSIS) is False # --------------------------------------------------------------------------- # diff guard # --------------------------------------------------------------------------- async def test_diff_guard_fails_an_empty_code_diff(runner, task, session_factory): outcome = await runner.diff_guard(task=task, diff=DiffResult(), code_task=True) assert outcome.ok is False assert outcome.sends_back is True async with session_factory() as session: row = await GateResultRepository(session).latest_for( task.id, str(GateName.DIFF_GUARD) ) assert row is not None and row.ok is False async def test_diff_guard_allows_an_empty_analysis_diff(runner, task): outcome = await runner.diff_guard(task=task, diff=DiffResult(), code_task=False) assert outcome.ok is True async def test_diff_guard_flags_forbidden_paths_symlinks_and_big_files(runner, task): diff = DiffResult( changed_files=["app/main.py", ".github/workflows/ci.yml"], escaping_symlinks=["link-to-outside"], largest_file_bytes=MAX_FILE_BYTES + 1, ) outcome = await runner.diff_guard(task=task, diff=diff, code_task=True) assert outcome.ok is False assert outcome.sends_back is True assert ".github" in outcome.detail # --------------------------------------------------------------------------- # no progress # --------------------------------------------------------------------------- async def test_no_progress_blocks_the_same_diff_twice(runner, task): first, fingerprint, count = await runner.no_progress( task=task, previous_hash="", same_count=0, diff_text="--- a\n+++ b\n" ) assert first.ok is True and count == 1 second, fingerprint2, count2 = await runner.no_progress( task=task, previous_hash=fingerprint, same_count=count, diff_text="--- a\n+++ b\n", ) assert second.ok is False and second.sends_back is True assert fingerprint2 == fingerprint and count2 == 2 async def test_no_progress_accepts_a_new_diff(runner, task): _, first_hash, _ = await runner.no_progress( task=task, previous_hash="", same_count=0, diff_text="a" ) outcome, _, count = await runner.no_progress( task=task, previous_hash=first_hash, same_count=3, diff_text="b" ) assert outcome.ok is True and count == 1 # --------------------------------------------------------------------------- # tests gate # --------------------------------------------------------------------------- async def test_tests_gate_is_green_when_no_command_is_defined(runner, task): outcome = await runner.tests(task=task, sandbox=FakeSandbox(), session_external_id="s") assert outcome.ok is True async def test_tests_gate_reports_red_with_the_failure_tail( runner, task, session_factory ): async with session_factory() as session: project = ProjectRow( id=new_id(), organization_id=task.organization_id, name="app", slug="app", test_command="pytest -q", ) session.add(project) await session.commit() project_id = project.id task.project_id = project_id sandbox = FakeSandbox( commands={ "pytest": [ CommandResult(exit_code=1, stdout="", stderr="AssertionError: boom") ] } ) outcome = await runner.tests(task=task, sandbox=sandbox, session_external_id="s") assert outcome.ok is False and outcome.sends_back is True assert "AssertionError" in outcome.detail # --------------------------------------------------------------------------- # green gate # --------------------------------------------------------------------------- async def test_green_gate_needs_a_passing_tests_row_or_a_waiver(runner, task): blocked = await runner.green(task=task, waived=False, waiver_reason="") assert blocked.ok is False and blocked.sends_back is True waived = await runner.green(task=task, waived=True, waiver_reason="known flake") assert waived.ok is True async def test_green_gate_is_green_once_tests_pass(runner, task, session_factory): async with session_factory() as session: await GateResultRepository(session).create( GateResult( task_id=task.id, gate=GateName.TESTS, ok=True, detail="green", ) ) await session.commit() outcome = await runner.green(task=task, waived=False, waiver_reason="") assert outcome.ok is True