Files
lai_automatisierung/tests/test_worker.py
Christian Meyer 4b5ac5f9d7
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Has been cancelled
feat: improve worker diagnostics
2026-08-31 17:03:40 +02:00

219 lines
6.7 KiB
Python

from __future__ import annotations
from pathlib import Path
from unittest.mock import Mock, call, patch
from lai_automation.models import (
AgentResult,
ChangeSummary,
Issue,
Label,
PullRequest,
)
from lai_automation.worker import Worker
def queued_issue() -> Issue:
return Issue(
number=7,
title="Wrong calculation",
body="The original report",
html_url="https://gitea.example.test/owner/lai/issues/7",
labels=(Label(1, "ai:queued"), Label(9, "bug")),
)
def labels() -> dict[str, Label]:
return {
"ai:queued": Label(1, "ai:queued"),
"ai:running": Label(2, "ai:running"),
"ai:ready": Label(3, "ai:ready"),
"ai:failed": Label(4, "ai:failed"),
}
@patch("lai_automation.worker.GitWorkspace")
def test_successful_issue_creates_pr_and_ready_label(
GitWorkspace,
settings,
) -> None:
issue = queued_issue()
running = Issue(
**{
**issue.__dict__,
"labels": (Label(2, "ai:running"), Label(9, "bug")),
}
)
gitea = Mock()
gitea.get_issue.return_value = issue
gitea.set_automation_state.side_effect = [running, running]
gitea.create_pull_request.return_value = PullRequest(
18,
"https://gitea.example.test/owner/lai/pulls/18",
)
agent = Mock()
agent.run.return_value = AgentResult(
"Submitted",
"Implemented and tested",
4,
0.2,
)
workspace = GitWorkspace.return_value
workspace.path = settings.workspace_root / "issue-7" / "repo"
workspace.branch = "ai/issue-7"
workspace.summary.return_value = ChangeSummary(
("app/Services/Fix.php", "tests/Feature/FixTest.php"),
20,
2,
)
workspace.has_symlink.return_value = False
workspace.has_path.return_value = False
workspace.commit.return_value = "abc123"
workspace.normalize_changed_whitespace.return_value = (
"tests/Feature/FixTest.php",
)
Worker(settings, gitea, agent).process(issue, labels())
workspace.prepare.assert_called_once()
workspace.run_trusted.assert_has_calls(
[
call(
"composer install",
extra_environment=settings.test_environment,
),
call(
"php artisan test",
extra_environment=settings.test_environment,
),
]
)
workspace.push.assert_called_once()
workspace.normalize_changed_whitespace.assert_called_once()
assert workspace.summary.call_count == 3
assert workspace.check_diff.call_count == 2
assert gitea.create_pull_request.call_args.args[0] == "ai/issue-7"
assert "Fixes #7" in gitea.create_pull_request.call_args.args[2]
assert "The original report" in gitea.create_pull_request.call_args.args[2]
assert gitea.set_automation_state.call_args_list[-1].args[1] == "ai:ready"
workspace.cleanup.assert_called_once()
@patch("lai_automation.worker.GitWorkspace")
def test_failed_tests_prevent_push_and_mark_issue_failed(
GitWorkspace,
settings,
) -> None:
issue = queued_issue()
running = Issue(
**{
**issue.__dict__,
"labels": (Label(2, "ai:running"),),
}
)
gitea = Mock()
gitea.get_issue.side_effect = [issue, running]
gitea.set_automation_state.return_value = running
agent = Mock()
agent.run.return_value = AgentResult("Submitted", "Done", 2, 0.1)
workspace = GitWorkspace.return_value
workspace.path = settings.workspace_root / "issue-7" / "repo"
workspace.summary.return_value = ChangeSummary(("app/Fix.php",), 2, 0)
workspace.has_symlink.return_value = False
workspace.has_path.return_value = False
workspace.normalize_changed_whitespace.return_value = ()
workspace.run_trusted.side_effect = [
Mock(),
RuntimeError("tests failed with token=hidden"),
]
Worker(settings, gitea, agent).process(issue, labels())
workspace.push.assert_not_called()
gitea.create_pull_request.assert_not_called()
assert gitea.set_automation_state.call_args_list[-1].args[1] == "ai:failed"
failure_comment = gitea.comment.call_args_list[-1].args[1]
assert "tests failed" in failure_comment
assert "hidden" not in failure_comment
def test_run_once_only_processes_first_queued_issue(settings) -> None:
gitea = Mock()
issue = queued_issue()
second_issue = Issue(**{**issue.__dict__, "number": 8})
expected_labels = labels()
gitea.ensure_automation_labels.return_value = expected_labels
gitea.list_queued_issues.return_value = [issue, second_issue]
worker = Worker(settings, gitea, Mock())
worker.process = Mock()
assert worker.run_once() is True
worker.process.assert_called_once_with(issue, expected_labels)
def test_run_once_returns_false_without_work(settings) -> None:
gitea = Mock()
gitea.ensure_automation_labels.return_value = labels()
gitea.list_queued_issues.return_value = []
assert Worker(settings, gitea, Mock()).run_once() is False
def test_agent_failure_details_identify_step_limit(settings) -> None:
worker = Worker(settings, Mock(), Mock())
result = AgentResult("LimitsExceeded", "", settings.agent_step_limit, 0.2)
details = worker._agent_failure_details(
result,
elapsed=180.0,
trajectory=Path("/workspaces/trajectories/issue-43.traj.json"),
)
assert f"step limit reached ({settings.agent_step_limit})" in details
assert "model_calls=40" in details
assert "cost=0.2000" in details
assert "issue-43.traj.json" in details
@patch("lai_automation.worker.GitWorkspace")
def test_preflight_runs_setup_and_tests_without_model(
GitWorkspace,
settings,
) -> None:
workspace = GitWorkspace.return_value
agent = Mock()
Worker(settings, Mock(), agent).preflight()
GitWorkspace.assert_called_once_with(settings, 0)
workspace.prepare.assert_called_once()
workspace.run_trusted.assert_has_calls(
[
call(
"composer install",
extra_environment=settings.test_environment,
),
call(
"php artisan test",
extra_environment=settings.test_environment,
),
]
)
agent.run.assert_not_called()
workspace.cleanup.assert_called_once()
@patch("lai_automation.worker.GitWorkspace")
def test_preflight_cleans_up_after_failure(GitWorkspace, settings) -> None:
workspace = GitWorkspace.return_value
workspace.run_trusted.side_effect = RuntimeError("test failure")
try:
Worker(settings, Mock(), Mock()).preflight()
except RuntimeError:
pass
else:
raise AssertionError("Expected preflight failure")
workspace.cleanup.assert_called_once()