This commit is contained in:
@@ -94,6 +94,15 @@ class GitWorkspace:
|
|||||||
self._git("reset", "--hard", "HEAD")
|
self._git("reset", "--hard", "HEAD")
|
||||||
self._git("clean", "-fd")
|
self._git("clean", "-fd")
|
||||||
|
|
||||||
|
def checkpoint(self) -> str:
|
||||||
|
self._git("add", "--all")
|
||||||
|
return self._git("stash", "create").stdout.strip()
|
||||||
|
|
||||||
|
def restore_checkpoint(self, checkpoint: str | None) -> None:
|
||||||
|
self.discard_changes()
|
||||||
|
if checkpoint:
|
||||||
|
self._git("stash", "apply", checkpoint)
|
||||||
|
|
||||||
def summary(self) -> ChangeSummary:
|
def summary(self) -> ChangeSummary:
|
||||||
self._git("add", "-N", "--all")
|
self._git("add", "-N", "--all")
|
||||||
output = self._git("diff", "--numstat").stdout
|
output = self._git("diff", "--numstat").stdout
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class AutomationWorker:
|
|||||||
LOG.info("Report %s branch=%s workspace ready", report.id, workspace.branch)
|
LOG.info("Report %s branch=%s workspace ready", report.id, workspace.branch)
|
||||||
task = build_task(report)
|
task = build_task(report)
|
||||||
metadata["agent_output"] = []
|
metadata["agent_output"] = []
|
||||||
|
valid_checkpoint: str | None = None
|
||||||
for attempt in range(1, self.settings.max_agent_attempts + 1):
|
for attempt in range(1, self.settings.max_agent_attempts + 1):
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Report %s agent attempt %s/%s starting",
|
"Report %s agent attempt %s/%s starting",
|
||||||
@@ -108,15 +109,22 @@ class AutomationWorker:
|
|||||||
report.id,
|
report.id,
|
||||||
attempt,
|
attempt,
|
||||||
)
|
)
|
||||||
workspace.discard_changes()
|
workspace.restore_checkpoint(valid_checkpoint)
|
||||||
|
restored_state = (
|
||||||
|
"The last syntax-valid changes were restored. Correct them without recreating "
|
||||||
|
"the malformed edit."
|
||||||
|
if valid_checkpoint
|
||||||
|
else "The malformed attempt was discarded. Start again from the clean repository."
|
||||||
|
)
|
||||||
task = build_retry_task(
|
task = build_retry_task(
|
||||||
report,
|
report,
|
||||||
RuntimeError(
|
RuntimeError(
|
||||||
f"{exception}\nThe malformed attempt was discarded. Start again from the clean "
|
f"{exception}\n{restored_state} Make smaller edits and run php -l on each "
|
||||||
"repository, make smaller edits, and run php -l on each changed PHP file."
|
"changed PHP file."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
valid_checkpoint = workspace.checkpoint()
|
||||||
try:
|
try:
|
||||||
metadata["tests"] = self._run_tests(workspace, report.id, attempt)
|
metadata["tests"] = self._run_tests(workspace, report.id, attempt)
|
||||||
LOG.info("Report %s attempt %s validation passed", report.id, attempt)
|
LOG.info("Report %s attempt %s validation passed", report.id, attempt)
|
||||||
|
|||||||
@@ -54,3 +54,33 @@ def test_discard_changes_restores_clean_disposable_workspace(tmp_path) -> None:
|
|||||||
|
|
||||||
assert tracked.read_text() == "<?php\nreturn [];\n"
|
assert tracked.read_text() == "<?php\nreturn [];\n"
|
||||||
assert not untracked.exists()
|
assert not untracked.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_checkpoint_restores_last_valid_changes(tmp_path) -> None:
|
||||||
|
workspace = GitWorkspace(settings(workspace_root=tmp_path), 4)
|
||||||
|
workspace.path.mkdir()
|
||||||
|
subprocess.run(["git", "init"], cwd=workspace.path, check=True, capture_output=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "user.email", "test@example.invalid"],
|
||||||
|
cwd=workspace.path,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "user.name", "Test"],
|
||||||
|
cwd=workspace.path,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
tracked = workspace.path / "config.php"
|
||||||
|
tracked.write_text("<?php\nreturn [];\n")
|
||||||
|
subprocess.run(["git", "add", "."], cwd=workspace.path, check=True)
|
||||||
|
subprocess.run(["git", "commit", "-m", "base"], cwd=workspace.path, check=True, capture_output=True)
|
||||||
|
tracked.write_text("<?php\nreturn ['valid'];\n")
|
||||||
|
added = workspace.path / "test.php"
|
||||||
|
added.write_text("<?php\n")
|
||||||
|
checkpoint = workspace.checkpoint()
|
||||||
|
tracked.write_text("<?php\nreturn [broken;\n")
|
||||||
|
|
||||||
|
workspace.restore_checkpoint(checkpoint)
|
||||||
|
|
||||||
|
assert tracked.read_text() == "<?php\nreturn ['valid'];\n"
|
||||||
|
assert added.exists()
|
||||||
@@ -139,6 +139,7 @@ def test_php_syntax_failure_discards_attempt_before_full_validation() -> None:
|
|||||||
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
||||||
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
||||||
]
|
]
|
||||||
|
workspace.checkpoint.return_value = "valid-checkpoint"
|
||||||
workspace.commit.return_value = "commit-id"
|
workspace.commit.return_value = "commit-id"
|
||||||
|
|
||||||
with patch("lai_automation.worker.GitWorkspace", return_value=workspace):
|
with patch("lai_automation.worker.GitWorkspace", return_value=workspace):
|
||||||
@@ -147,10 +148,58 @@ def test_php_syntax_failure_discards_attempt_before_full_validation() -> None:
|
|||||||
assert worker.agent.run.call_count == 2
|
assert worker.agent.run.call_count == 2
|
||||||
assert "unexpected token" in worker.agent.run.call_args_list[1].args[1]
|
assert "unexpected token" in worker.agent.run.call_args_list[1].args[1]
|
||||||
assert "clean repository" in worker.agent.run.call_args_list[1].args[1]
|
assert "clean repository" in worker.agent.run.call_args_list[1].args[1]
|
||||||
workspace.discard_changes.assert_called_once()
|
workspace.restore_checkpoint.assert_called_once_with(None)
|
||||||
worker._run_tests.assert_called_once()
|
worker._run_tests.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_malformed_retry_restores_last_syntax_valid_attempt() -> None:
|
||||||
|
report = FailureReport(
|
||||||
|
id=56,
|
||||||
|
chat_message_id=1,
|
||||||
|
source="user_feedback",
|
||||||
|
category="incorrect_answer",
|
||||||
|
user_feedback="Wrong hours",
|
||||||
|
feedback_type="wrong_result",
|
||||||
|
expected_behavior="Use time registration",
|
||||||
|
prompt="Hours in April",
|
||||||
|
response="0 hours",
|
||||||
|
sql_query="SELECT 0",
|
||||||
|
error_message=None,
|
||||||
|
context={},
|
||||||
|
)
|
||||||
|
config = settings(mode="pr", dry_run=False, gitea_token="token", max_agent_attempts=3)
|
||||||
|
worker = AutomationWorker(config)
|
||||||
|
worker.reports = Mock()
|
||||||
|
worker.reports.claim_next.return_value = report
|
||||||
|
worker.agent = Mock()
|
||||||
|
worker.agent.run.side_effect = ["valid but failing", "broken retry", "corrected retry"]
|
||||||
|
worker.gitea = Mock()
|
||||||
|
worker._run_tests = Mock(side_effect=[RuntimeError("Expected hour rule"), []])
|
||||||
|
workspace = Mock()
|
||||||
|
workspace.path = Path("/tmp/nonexistent-workspace")
|
||||||
|
workspace.branch = "ai/failure-56"
|
||||||
|
workspace.summary.return_value = ChangeSummary(
|
||||||
|
("config/erp_semantics.php", "tests/Unit/FixTest.php"), 20, 2
|
||||||
|
)
|
||||||
|
workspace.run_args.side_effect = [
|
||||||
|
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
||||||
|
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
||||||
|
Mock(returncode=255, stdout="Parse error", stderr="unexpected token"),
|
||||||
|
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
||||||
|
Mock(returncode=0, stdout="No syntax errors", stderr=""),
|
||||||
|
]
|
||||||
|
workspace.checkpoint.side_effect = ["checkpoint-1", "checkpoint-3"]
|
||||||
|
workspace.commit.return_value = "commit-id"
|
||||||
|
|
||||||
|
with patch("lai_automation.worker.GitWorkspace", return_value=workspace):
|
||||||
|
assert worker.run_once()
|
||||||
|
|
||||||
|
assert worker.agent.run.call_count == 3
|
||||||
|
workspace.restore_checkpoint.assert_called_once_with("checkpoint-1")
|
||||||
|
assert "last syntax-valid changes were restored" in worker.agent.run.call_args_list[2].args[1]
|
||||||
|
assert worker._run_tests.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_failed_validation_gets_one_correction_before_pr() -> None:
|
def test_failed_validation_gets_one_correction_before_pr() -> None:
|
||||||
report = FailureReport(
|
report = FailureReport(
|
||||||
id=13,
|
id=13,
|
||||||
|
|||||||
Reference in New Issue
Block a user