All checks were successful
Tests / Declarative: Post Actions passed: 1217
Category one of the mechanical-fix work. A linter has already located the defect precisely - file, line, rule, and what is wrong - so passing that through converts an open-ended 'repair this build' request into a narrow instruction whose result can be checked against the same evidence. Recognises ruff/flake8, golangci-lint, and eslint diagnostics from console evidence, deduplicates them, and bounds the list at twenty. A diagnostic for a file outside the write allowlist is dropped at extraction rather than later, so a defect is never reported for a file the patcher could not touch anyway. Only categories whose fix is mechanical belong here; anything needing a judgement about intended behaviour stays on the ordinary escalation path. A build with no linter output is unaffected and falls back to the existing open-ended request. The patch prompt moves to its own module, as the triage prompt already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
4.6 KiB
Python
123 lines
4.6 KiB
Python
"""Tests for recognising mechanically-fixable defects in console evidence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ariadne.services import hermes_code_defects as module
|
|
|
|
|
|
CFG = {"allowed_path_prefixes": ["ariadne/", "src/"], "allowed_suffixes": [".py", ".go", ".ts"]}
|
|
|
|
|
|
def _bundle(*lines: str) -> dict:
|
|
return {"jenkins": {"console_failures": [{"text": "\n".join(lines)}], "console_tail": ""}}
|
|
|
|
|
|
def test_ruff_diagnostic_is_extracted_whole() -> None:
|
|
found = module.extract_lint_defects(
|
|
_bundle("ariadne/app.py:12:5: E501 Line too long (95 > 88)"), CFG
|
|
)
|
|
assert found == [
|
|
{
|
|
"category": module.LINT_VIOLATION,
|
|
"tool": "ruff",
|
|
"path": "ariadne/app.py",
|
|
"line": 12,
|
|
"rule": "E501",
|
|
"message": "Line too long (95 > 88)",
|
|
}
|
|
]
|
|
|
|
|
|
def test_golangci_diagnostic_is_extracted() -> None:
|
|
found = module.extract_lint_defects(_bundle("src/main.go:9:2: unused variable x (govet)"), CFG)
|
|
assert found[0]["tool"] == "golangci"
|
|
assert (found[0]["path"], found[0]["line"], found[0]["rule"]) == ("src/main.go", 9, "govet")
|
|
|
|
|
|
def test_eslint_diagnostics_inherit_the_preceding_file_header() -> None:
|
|
"""eslint prints the path once, then indented diagnostics beneath it."""
|
|
|
|
found = module.extract_lint_defects(
|
|
_bundle("src/panel.ts", " 12:5 error Unexpected console statement no-console"), CFG
|
|
)
|
|
assert (found[0]["path"], found[0]["line"], found[0]["rule"]) == ("src/panel.ts", 12, "no-console")
|
|
|
|
|
|
def test_a_file_outside_the_write_allowlist_is_dropped() -> None:
|
|
"""Reporting a defect the patcher could never write is worse than silence."""
|
|
|
|
assert module.extract_lint_defects(_bundle("vendor/x.py:1:1: E501 Line too long"), CFG) == []
|
|
assert module.extract_lint_defects(_bundle("ariadne/x.rb:1:1: E501 Line too long"), CFG) == []
|
|
|
|
|
|
def test_duplicate_diagnostics_are_collapsed() -> None:
|
|
line = "ariadne/app.py:12:5: E501 Line too long (95 > 88)"
|
|
assert len(module.extract_lint_defects(_bundle(line, line, line), CFG)) == 1
|
|
|
|
|
|
def test_extraction_is_bounded() -> None:
|
|
lines = [f"ariadne/f{i}.py:{i}:1: E501 Line too long" for i in range(50)]
|
|
assert len(module.extract_lint_defects(_bundle(*lines), CFG)) == module._MAX_DEFECTS
|
|
|
|
|
|
def test_ordinary_console_noise_matches_nothing() -> None:
|
|
noise = _bundle("Running tests...", "OK", "+ ruff check ariadne", "All checks passed!")
|
|
assert module.extract_lint_defects(noise, CFG) == []
|
|
|
|
|
|
def test_never_raises_on_hostile_input() -> None:
|
|
for bad in (None, {}, {"jenkins": None}, {"jenkins": {"console_failures": "no"}}, 7):
|
|
assert module.extract_lint_defects(bad, CFG) == []
|
|
assert module.extract_lint_defects(_bundle("ariadne/a.py:1:1: E1 x"), None) == []
|
|
|
|
|
|
def test_instruction_names_rule_and_line() -> None:
|
|
defects = module.extract_lint_defects(_bundle("ariadne/app.py:12:5: E501 Line too long"), CFG)
|
|
text = module.defect_instruction(defects)
|
|
assert "ariadne/app.py line 12: E501" in text
|
|
assert "do not suppress the rule" in text
|
|
|
|
|
|
def test_no_defects_yields_no_instruction() -> None:
|
|
"""The caller must fall back to the open-ended request, not an empty header."""
|
|
|
|
assert module.defect_instruction([]) == ""
|
|
|
|
|
|
def test_the_patch_prompt_carries_the_linter_diagnosis(monkeypatch) -> None:
|
|
"""The instruction must reach Hermes, not just be computed."""
|
|
|
|
from ariadne.services import hermes_code_prompt
|
|
|
|
bundle = {
|
|
"jenkins": {
|
|
"console_failures": [{"text": "ariadne/app.py:12:5: E501 Line too long (95 > 88)"}],
|
|
"console_tail": "",
|
|
}
|
|
}
|
|
cfg = {
|
|
"owner": "bstein",
|
|
"repo": "ariadne",
|
|
"base_branch": "master",
|
|
"allowed_path_prefixes": ["ariadne/"],
|
|
"allowed_suffixes": [".py"],
|
|
}
|
|
prompt = hermes_code_prompt.build_patch_prompt("ariadne/1", bundle, cfg, {"ariadne/app.py": "x"})
|
|
|
|
assert "ariadne/app.py line 12: E501" in prompt
|
|
assert "do not suppress the rule" in prompt
|
|
|
|
|
|
def test_the_patch_prompt_is_unchanged_without_diagnostics() -> None:
|
|
"""A build with no linter output must fall back to the open request."""
|
|
|
|
from ariadne.services import hermes_code_prompt
|
|
|
|
bundle = {"jenkins": {"console_failures": [{"text": "boom"}], "console_tail": ""}}
|
|
cfg = {"owner": "b", "repo": "r", "base_branch": "main",
|
|
"allowed_path_prefixes": ["ariadne/"], "allowed_suffixes": [".py"]}
|
|
prompt = hermes_code_prompt.build_patch_prompt("a/1", bundle, cfg, {"ariadne/a.py": "x"})
|
|
|
|
assert "A linter reported these violations" not in prompt
|
|
assert "MINIMAL source fix" in prompt
|