# retoor import asyncio import pytest from devplacepy.database import ( add_issue_usage, get_issue_usage, get_table, init_db, refresh_snapshot, set_setting, ) from devplacepy.services.gitea import runtime, store from devplacepy.services.gitea.config import gitea_config from devplacepy.services.gitea.enhance import enhance_ticket from devplacepy.services.gitea.fake import FakeGiteaClient from devplacepy.services.gitea.planning import ( collect_open_issues, generate_plan, verbatim_tickets, ) from devplacepy.services.jobs.planning_service import PlanningReportService from devplacepy.services.gitea.service import IssueTrackerService from devplacepy.services.jobs import queue from devplacepy.services.jobs.issue_create_service import IssueCreateService from devplacepy.services.openai_gateway.usage import new_usage_totals from tests.conftest import run_async _GATEWAY_HEADERS = { "X-Gateway-Cost-USD": "0.00010000", "X-Gateway-Model": "molodetz", "X-Gateway-Prompt-Tokens": "500", "X-Gateway-Completion-Tokens": "100", "X-Gateway-Total-Tokens": "600", "X-Gateway-Upstream-Latency-Ms": "400", "X-Gateway-Total-Latency-Ms": "450", } class _FakeGatewayResp: def __init__(self, content, headers): self._content = content self.headers = headers def raise_for_status(self): return None def json(self): return {"choices": [{"message": {"content": self._content}}]} class _FakeGatewayClient: def __init__(self, content, headers): self._content = content self._headers = headers async def __aenter__(self): return self async def __aexit__(self, *exc): return False async def post(self, *args, **kwargs): return _FakeGatewayResp(self._content, self._headers) def _fake_gateway(content): def factory(*args, **kwargs): return _FakeGatewayClient(content, _GATEWAY_HEADERS) return factory _counter_issues_gitea = [0] @pytest.fixture(autouse=True) def _init_db_issues_gitea(): init_db() yield @pytest.fixture def gitea_env(): fake = FakeGiteaClient() runtime.set_client(fake) set_setting("gitea_base_url", "https://gitea.test") set_setting("gitea_owner", "retoor") set_setting("gitea_repo", "pydevplace") set_setting("gitea_token", "test-token") set_setting("issue_ai_enhance", "0") yield fake runtime.set_client(None) for table in ("issue_tickets", "issue_comment_authors"): for row in list(get_table(table).find()): get_table(table).delete(uid=row["uid"]) for row in list(get_table("jobs").find(kind="issue_create")): get_table("jobs").delete(uid=row["uid"]) def _make_user_issues_gitea(): _counter_issues_gitea[0] += 1 uid = f"issuetest-user-{_counter_issues_gitea[0]}" username = f"issuetester{_counter_issues_gitea[0]}" get_table("users").upsert( { "uid": uid, "username": username, "terms_version": "1", "role": "member", "xp": 0, "level": 1, "is_active": True, }, ["uid"], ) return uid, username def _unread(user_uid): return [ n for n in get_table("notifications").find(user_uid=user_uid) if n.get("type") == "issue" ] def _drive_jobs(): async def run(): svc = IssueCreateService() for _ in range(200): await svc.run_once() refresh_snapshot() pending = [ r for r in get_table("jobs").find(kind="issue_create") if r["status"] in ("pending", "running") ] if not pending and not svc._inflight: return await asyncio.sleep(0.02) run_async(run()) def _role_user(role: str): uid, username = _make_user_issues_gitea() get_table("users").update({"uid": uid, "role": role}, ["uid"]) return uid, username def _open_ticket(fake, role: str): import devplacepy.main as m from starlette.testclient import TestClient from devplacepy.utils import create_session uid, _ = _role_user(role) issue = run_async(fake.create_issue("Submit does nothing", "details")) number = int(issue["number"]) store.record_ticket( number=number, author_uid=uid, original_title="Submit does nothing", original_description="details", enhanced_title="Submit does nothing", html_url=issue.get("html_url", ""), status="open", ) client = TestClient(m.app) client.cookies.set("session", create_session(uid)) return number, client def test_fake_client_create_and_list(gitea_env): async def go(): issue = await gitea_env.create_issue("Crash on save", "details") assert issue["number"] == 1 assert issue["state"] == "open" issues, total = await gitea_env.list_issues(state="open") assert total == 1 assert issues[0]["number"] == 1 run_async(go()) def test_fake_client_state_and_comments(gitea_env): async def go(): issue = await gitea_env.create_issue("t", "b") number = issue["number"] await gitea_env.create_comment(number, "first") fetched = await gitea_env.get_issue(number) assert fetched["comments"] == 1 closed = await gitea_env.set_state(number, "closed") assert closed["state"] == "closed" open_issues, _ = await gitea_env.list_issues(state="open") assert open_issues == [] all_issues, total = await gitea_env.list_issues(state="all") assert total == 1 run_async(go()) def test_store_records_and_maps(gitea_env): uid, _ = _make_user_issues_gitea() store.record_ticket(7, uid, "orig", "desc", "Better", "https://x/7", "open") assert store.author_uid_for_issue(7) == uid assert store.author_map([7, 8]) == {7: uid} cid = store.record_comment_author(99, 7, uid) assert cid assert store.comment_author_map([99, 100]) == {99: uid} assert store.local_comment_ids(7) == {99} def test_enhance_fallback_when_disabled(gitea_env): result = run_async(enhance_ticket("Title", "Body text", gitea_config())) assert result.enhanced is False assert "## Summary" in result.body assert "## Steps to Reproduce" in result.body assert result.title == "Title" def test_create_job_files_issue(gitea_env): uid, username = _make_user_issues_gitea() job_uid = queue.enqueue( "issue_create", {"author_uid": uid, "title": "Login broken", "description": "cannot log in"}, "user", uid, "Login broken", ) _drive_jobs() job = queue.get_job(job_uid) assert job["status"] == "done" number = job["result"]["number"] assert number == 1 ticket = store.get_ticket(number) assert ticket["author_uid"] == uid assert _unread(uid) issue = run_async(gitea_env.get_issue(number)) assert username in issue["body"] def test_create_job_fails_without_config(gitea_env): set_setting("gitea_token", "") uid, _ = _make_user_issues_gitea() job_uid = queue.enqueue( "issue_create", {"author_uid": uid, "title": "x", "description": "y"}, "user", uid, "x", ) _drive_jobs() job = queue.get_job(job_uid) assert job["status"] == "failed" assert "not configured" in job["error"].lower() def test_poller_notifies_on_developer_reply(gitea_env): uid, _ = _make_user_issues_gitea() issue = run_async(gitea_env.create_issue("issue", "body")) number = issue["number"] store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open") store.update_ticket_cache(number, "open", 0) gitea_env.add_external_comment(number, "developer", "looking into it") before = len(_unread(uid)) run_async(IssueTrackerService().run_once()) refresh_snapshot() assert len(_unread(uid)) == before + 1 assert int(store.get_ticket(number)["last_comment_count"]) == 1 def test_poller_ignores_local_comment(gitea_env): uid, _ = _make_user_issues_gitea() issue = run_async(gitea_env.create_issue("issue", "body")) number = issue["number"] store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open") store.update_ticket_cache(number, "open", 0) comment = run_async(gitea_env.create_comment(number, "me again")) store.record_comment_author(comment["id"], number, uid) before = len(_unread(uid)) run_async(IssueTrackerService().run_once()) refresh_snapshot() assert len(_unread(uid)) == before def test_poller_notifies_on_status_change(gitea_env): uid, _ = _make_user_issues_gitea() issue = run_async(gitea_env.create_issue("issue", "body")) number = issue["number"] store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open") store.update_ticket_cache(number, "open", 0) run_async(gitea_env.set_state(number, "closed")) before = len(_unread(uid)) run_async(IssueTrackerService().run_once()) refresh_snapshot() assert len(_unread(uid)) == before + 1 assert store.get_ticket(number)["last_status"] == "closed" def test_planning_empty_when_no_issues(gitea_env): markdown, ai_used = run_async(generate_plan([], gitea_config())) assert ai_used is False assert markdown.startswith("# Open Tickets Implementation Plan") assert "no open tickets" in markdown.lower() def test_planning_fallback_is_phased_with_verbatim_body(gitea_env): issues = [ { "number": 41, "title": "Broken functionality", "labels": [{"name": "bug"}], "body": "Something is broken.\nStep one fails.", }, { "number": 7, "title": "Show costs", "labels": [{"name": "feature"}], "body": "Display research costs for admins.", }, ] totals = new_usage_totals() markdown, ai_used = run_async(generate_plan(issues, gitea_config(), totals)) assert ai_used is False assert "## Execution Order" in markdown assert "1. #41 - Broken functionality" in markdown assert "## Phase 1: bug" in markdown assert "## Phase 2: feature" in markdown assert "### #41 Broken functionality" in markdown assert "> Something is broken." in markdown assert "> Step one fails." in markdown assert totals["calls"] == 0 def test_planning_meters_usage_via_gateway(gitea_env, monkeypatch): set_setting("issue_ai_enhance", "1") refresh_snapshot() monkeypatch.setattr( "devplacepy.stealth.stealth_async_client", _fake_gateway("# Open Tickets Implementation Plan\n\nDone."), ) totals = new_usage_totals() issues = [{"number": 1, "title": "t", "labels": [], "body": "b"}] markdown, ai_used = run_async(generate_plan(issues, gitea_config(), totals)) assert ai_used is True assert markdown.startswith("# Open Tickets Implementation Plan\n\nDone.") assert "# Appendix: Source Tickets (verbatim)" in markdown assert totals["calls"] == 1 assert totals["total_tokens"] == 600 def test_planning_ai_output_always_carries_verbatim_appendix(gitea_env, monkeypatch): set_setting("issue_ai_enhance", "1") refresh_snapshot() monkeypatch.setattr( "devplacepy.stealth.stealth_async_client", _fake_gateway("# Open Tickets Implementation Plan\n\nSummarised plan only."), ) issues = [ { "number": 12, "title": "Add export", "labels": [{"name": "feature"}], "html_url": "https://gitea.test/retoor/pydevplace/issues/12", "body": "Full body line A.\nFull body line B.", } ] markdown, ai_used = run_async(generate_plan(issues, gitea_config())) assert ai_used is True assert "Summarised plan only." in markdown assert "# Appendix: Source Tickets (verbatim)" in markdown assert "## #12 Add export" in markdown assert "https://gitea.test/retoor/pydevplace/issues/12" in markdown assert "> Full body line A." in markdown assert "> Full body line B." in markdown def test_verbatim_tickets_reproduces_full_body_and_labels(): issues = [ { "number": 5, "title": "Fix crash", "labels": [{"name": "bug"}, {"name": "urgent"}], "html_url": "https://gitea.test/x/5", "body": "Steps:\n1. open\n2. boom", } ] markdown = verbatim_tickets(issues) assert markdown.startswith("# Appendix: Source Tickets (verbatim)") assert "## #5 Fix crash" in markdown assert "**Labels**: bug, urgent" in markdown assert "**Source**: https://gitea.test/x/5" in markdown assert "> Steps:" in markdown assert "> 1. open" in markdown assert "> 2. boom" in markdown def test_collect_open_issues_pages_through_fake(gitea_env): for index in range(3): run_async(gitea_env.create_issue(f"Ticket {index}", f"Body {index}")) issues = run_async(collect_open_issues(gitea_env)) assert {issue["number"] for issue in issues} == {1, 2, 3} def test_planning_service_filters_to_selected_numbers(gitea_env): for index in range(3): run_async(gitea_env.create_issue(f"Ticket {index}", f"Body {index}")) service = PlanningReportService() selected = run_async(service._collect([3, 1])) assert [issue["number"] for issue in selected] == [3, 1] missing = run_async(service._collect([99])) assert missing == [] everything = run_async(service._collect([])) assert {issue["number"] for issue in everything} == {1, 2, 3} def test_planning_service_parses_payload_numbers(): service = PlanningReportService() job = {"payload": {"numbers": ["4", 7, "x", 7, None]}} assert service._selected_numbers(job) == [4, 7] assert service._selected_numbers({"payload": {}}) == [] def test_enhance_meters_usage_via_gateway(gitea_env, monkeypatch): set_setting("issue_ai_enhance", "1") refresh_snapshot() monkeypatch.setattr( "devplacepy.stealth.stealth_async_client", _fake_gateway('{"title": "Fix login", "body": "## Summary\\nFix it."}'), ) totals = new_usage_totals() result = run_async(enhance_ticket("login", "broken", gitea_config(), totals)) assert result.enhanced is True assert result.title == "Fix login" assert totals["calls"] == 1 assert abs(totals["cost_usd"] - 0.0001) < 1e-9 def test_enhance_fallback_does_not_meter(gitea_env): totals = new_usage_totals() result = run_async(enhance_ticket("Title", "Body", gitea_config(), totals)) assert result.enhanced is False assert totals["calls"] == 0 def test_create_job_meters_usage(gitea_env, monkeypatch): get_table("issue_usage").delete() set_setting("issue_ai_enhance", "1") refresh_snapshot() monkeypatch.setattr( "devplacepy.stealth.stealth_async_client", _fake_gateway('{"title": "Login broken", "body": "## Summary\\nbroken."}'), ) uid, _ = _make_user_issues_gitea() job_uid = queue.enqueue( "issue_create", {"author_uid": uid, "title": "login", "description": "cannot log in"}, "user", uid, "login", ) _drive_jobs() assert queue.get_job(job_uid)["status"] == "done" assert get_issue_usage()["calls"] == 1 def test_issue_tracker_collect_metrics(gitea_env): get_table("issue_usage").delete() add_issue_usage( { "calls": 3, "prompt_tokens": 900, "completion_tokens": 300, "total_tokens": 1200, "cost_usd": 0.003, "upstream_latency_ms": 1200.0, "total_latency_ms": 1500.0, } ) metrics = IssueTrackerService().collect_metrics() by_label = {card["label"]: card["value"] for card in metrics["stats"]} assert by_label["AI calls"] == 3 assert by_label["Total tokens"] == 1200 assert by_label["Total cost"] == "$0.0030"