from __future__ import annotations import asyncio from unittest.mock import AsyncMock, patch import pytest from fastapi import HTTPException from agent import server from agent.dashboard import routes from agent.dashboard.agent_instructions import ( create_agent_instructions, get_repo_agent_instructions, set_agent_instructions, ) from agent.prompt import construct_system_prompt @pytest.mark.asyncio async def test_get_repo_agent_instructions_returns_trimmed_text() -> None: with patch( "agent.dashboard.agent_instructions.get_agent_instructions", new_callable=AsyncMock, return_value={"instructions": " Always run mypy.\n"}, ): result = await get_repo_agent_instructions("acme", "repo") assert result == "Always run mypy." @pytest.mark.asyncio async def test_get_repo_agent_instructions_returns_none_when_empty() -> None: with patch( "agent.dashboard.agent_instructions.get_agent_instructions", new_callable=AsyncMock, return_value={"instructions": " "}, ): result = await get_repo_agent_instructions("acme", "repo") assert result is None @pytest.mark.asyncio async def test_create_agent_instructions_puts_new_record() -> None: mock_put = AsyncMock() with ( patch( "agent.dashboard.agent_instructions._get_value", new_callable=AsyncMock, return_value=None, ), patch("agent.dashboard.agent_instructions._client") as mock_client, ): mock_client.return_value.store.put_item = mock_put record = await create_agent_instructions("acme/repo", "octo") assert record["full_name"] == "acme/repo" assert record["instructions"] == "" mock_put.assert_awaited_once() @pytest.mark.asyncio async def test_set_agent_instructions_updates_store() -> None: mock_put = AsyncMock() with ( patch( "agent.dashboard.agent_instructions.get_agent_instructions", new_callable=AsyncMock, return_value={"full_name": "acme/repo", "instructions": ""}, ), patch("agent.dashboard.agent_instructions._client") as mock_client, ): mock_client.return_value.store.put_item = mock_put record = await set_agent_instructions("acme/repo", "Use direct tone.") assert record["instructions"] == "Use direct tone." mock_put.assert_awaited_once() def test_construct_system_prompt_appends_repo_instructions() -> None: prompt = construct_system_prompt( working_dir="/work", repo_custom_instructions="Prefer pytest over unittest.", ) assert "Repository-specific Custom Instructions" in prompt assert "Prefer pytest over unittest." in prompt def test_construct_system_prompt_without_repo_instructions() -> None: prompt = construct_system_prompt(working_dir="/work") assert "Repository-specific Custom Instructions" not in prompt def test_resolve_repo_custom_instructions_returns_none_without_repo() -> None: result = asyncio.run(server._resolve_repo_custom_instructions(None)) assert result is None @pytest.mark.asyncio async def test_list_agent_instructions_filters_inaccessible_repos(monkeypatch) -> None: monkeypatch.setattr( routes, "list_agent_instructions", AsyncMock( return_value=[ {"full_name": "acme/visible", "instructions": "visible"}, {"full_name": "acme/private", "instructions": "private"}, ] ), ) async def fake_require_repo_access_for_user(login: str, full_name: str) -> str: if full_name == "acme/private": raise HTTPException(403, "no access") return "token" monkeypatch.setattr(routes, "require_repo_access_for_user", fake_require_repo_access_for_user) result = await routes.api_list_agent_instructions(session={"sub": "octocat"}) assert result == [{"full_name": "acme/visible", "instructions": "visible"}] @pytest.mark.asyncio async def test_get_agent_instructions_requires_repo_access(monkeypatch) -> None: require_access = AsyncMock(return_value="token") monkeypatch.setattr( routes, "get_agent_instructions", AsyncMock(return_value={"full_name": "acme/repo", "instructions": "rules"}), ) monkeypatch.setattr(routes, "require_repo_access_for_user", require_access) result = await routes.api_get_agent_instructions( "https://github.com/acme/repo", session={"sub": "octocat"} ) assert result == {"full_name": "acme/repo", "instructions": "rules"} require_access.assert_awaited_once_with("octocat", "acme/repo") @pytest.mark.asyncio async def test_delete_agent_instructions_requires_repo_access_before_delete(monkeypatch) -> None: delete_instructions = AsyncMock() get_instructions = AsyncMock(return_value={"full_name": "acme/repo", "instructions": "rules"}) monkeypatch.setattr(routes, "get_agent_instructions", get_instructions) monkeypatch.setattr( routes, "require_repo_access_for_user", AsyncMock(side_effect=HTTPException(403, "no access")), ) monkeypatch.setattr(routes, "delete_agent_instructions", delete_instructions) with pytest.raises(HTTPException) as exc: await routes.api_delete_agent_instructions("acme/repo", session={"sub": "octocat"}) assert exc.value.status_code == 403 get_instructions.assert_not_awaited() delete_instructions.assert_not_awaited()