1372 lines
52 KiB
Python
1372 lines
52 KiB
Python
# SPDX-FileCopyrightText: 2026 Art Sea LLC
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from band_aider.ai_client.completion_client import CompletionClient, OpenAIConfig
|
|
from band_aider.context.class_meta import ClassMeta
|
|
from band_aider.judgement.edit_request import (
|
|
AiderEditRequest,
|
|
AiderEditRequestMask,
|
|
AiderFailureMode,
|
|
)
|
|
from band_aider.judgement.custom_directives import (
|
|
CustomDirectives,
|
|
Directive,
|
|
DirectiveGroup,
|
|
)
|
|
from band_aider.judgement.edit_escalation import ESCALATION_COOLDOWN_TURNS, ESCALATION_DURATION
|
|
from band_aider.judgement.contextual_file_renderer import ContextualFileRenderer
|
|
from band_aider.judgement.file_manager import FileManager
|
|
from band_aider.judgement.prompt_formatter import (
|
|
MarkdownPromptFormatter,
|
|
MarkdownPromptFormatterBuilder,
|
|
PromptFormatter,
|
|
PromptFormatterBuilder,
|
|
)
|
|
from band_aider.judgement.search_replace_token_generator import (
|
|
RepairEvidenceBuilder,
|
|
SearchReplaceTokenGenerator,
|
|
)
|
|
from band_aider.judgement import search_replace_token_generator as search_replace
|
|
from band_aider.strategies.oop_signature_extract import (
|
|
OOpSignature,
|
|
OOpSignatureExtract,
|
|
OOpSignatureRender,
|
|
)
|
|
from band_aider.strategies.language_strategy_set import LanguageStrategySet
|
|
from band_aider.strategies.compiler import (
|
|
StaticCompileError,
|
|
TestRunCompileError,
|
|
TypeCheckError,
|
|
)
|
|
from band_aider.ui.dto.model_escalation import ModelEscalationDTO
|
|
from tests.support.completion_clients import StaticOpenAICompatibleClient
|
|
from tests.support.memory_path import MemoryPath
|
|
|
|
|
|
class _StubClient(CompletionClient):
|
|
"""_StubClient records prompts and returns a labeled response."""
|
|
|
|
def __init__(self, label: str) -> None:
|
|
self.label = label
|
|
self.prompts: list[str] = []
|
|
|
|
@property
|
|
def inner(self) -> CompletionClient | None:
|
|
return None
|
|
|
|
def complete(self, prompt: str, logger=None) -> str:
|
|
self.prompts.append(prompt)
|
|
return self.label
|
|
|
|
@property
|
|
def openai_configs(self) -> tuple[OpenAIConfig, ...]:
|
|
return (OpenAIConfig(model=self.label, api_base="https://example.invalid", api_key="key"),)
|
|
|
|
|
|
class _NoCards(OOpSignatureExtract):
|
|
"""Keep token-generator rendering tests independent from source extraction."""
|
|
|
|
def extract(self, module: str, name: str):
|
|
del module, name
|
|
return None
|
|
|
|
def extract_references(self, sources_by_location):
|
|
del sources_by_location
|
|
return ()
|
|
|
|
def extract_source(self, location, source):
|
|
del location, source
|
|
return ()
|
|
|
|
|
|
class _NoCardRender(OOpSignatureRender):
|
|
"""Satisfy the renderer's required language rendering strategy in IO tests."""
|
|
|
|
def render(self, signature: OOpSignature, output) -> None:
|
|
"""Fail if a no-card fixture unexpectedly asks for rendering."""
|
|
|
|
del signature, output
|
|
raise AssertionError("No reference cards should be rendered by this fixture.")
|
|
|
|
|
|
class _Publisher:
|
|
def __init__(self) -> None:
|
|
self.events: list[object] = []
|
|
|
|
def publish(self, event: object) -> None:
|
|
self.events.append(event)
|
|
|
|
|
|
class _BootstrapContext:
|
|
"""Identify the direct-edit workflow that admits an APPLY directive."""
|
|
|
|
|
|
class _NoDirectiveContext:
|
|
"""Identify a workflow with no registered directive group."""
|
|
|
|
|
|
class _BindingContext:
|
|
"""Identify the workflow used to prove language-bound directive resolution."""
|
|
|
|
|
|
class _DirectiveStrategies:
|
|
"""Bind one stationary directive router through the real strategy-set boundary."""
|
|
|
|
def __init__(self, directives: CustomDirectives) -> None:
|
|
"""Store the router returned for the generator's bound file manager."""
|
|
|
|
self.directives = directives
|
|
|
|
def __call__(
|
|
self,
|
|
file_manager: FileManager,
|
|
**overrides: object,
|
|
) -> LanguageStrategySet:
|
|
"""Return the fixed router binding without inspecting the repository."""
|
|
|
|
del file_manager, overrides
|
|
return LanguageStrategySet({CustomDirectives: self.directives})
|
|
|
|
|
|
def _directive_readout(value: object) -> str:
|
|
"""Return stationary side-effect evidence for one accepted directive value."""
|
|
|
|
return f"accepted directive: {value}"
|
|
|
|
|
|
class _FileManager(FileManager):
|
|
"""_FileManager exposes current repository evidence for prompt integration."""
|
|
|
|
def __init__(self, repo_root: MemoryPath, diffs: dict[str, str]) -> None:
|
|
"""Store one test repository and configured diffs."""
|
|
|
|
self.repo_root = repo_root
|
|
self.src_root = repo_root / "src"
|
|
self.test_root = repo_root / "tests"
|
|
self.prompt = "Original stabilization goal."
|
|
self.repo_exec = repo_root.repository
|
|
self.client = StaticOpenAICompatibleClient('{"cdc": ""}')
|
|
self.prompt_formatter_builder = MarkdownPromptFormatterBuilder()
|
|
self.src_classes: list[ClassMeta] = []
|
|
self.test_classes: list[ClassMeta] = []
|
|
self.src_index: dict[str, int] = {}
|
|
self.test_index: dict[str, int] = {}
|
|
self.diffs = diffs
|
|
|
|
def repo_location(self, location: str | Path) -> str:
|
|
"""Return one normalized repository-relative location."""
|
|
|
|
path = Path(location)
|
|
absolute = path if path.is_absolute() else self.repo_root / path
|
|
return absolute.relative_to(self.repo_root).as_posix()
|
|
|
|
def repo_path(self, location: str | Path) -> Path:
|
|
"""Return one absolute repository path."""
|
|
|
|
return self.repo_root / self.repo_location(location)
|
|
|
|
def git_diff_for(self, location: str | Path) -> str:
|
|
"""Return configured current diff evidence."""
|
|
|
|
return self.diffs.get(self.repo_location(location), "")
|
|
|
|
def current_src(self, location: str) -> ClassMeta | None:
|
|
"""Return no source metadata."""
|
|
|
|
del location
|
|
return None
|
|
|
|
def current_test(self, location: str) -> ClassMeta | None:
|
|
"""Return no test metadata."""
|
|
|
|
del location
|
|
return None
|
|
|
|
|
|
class _BoundGenerator(SearchReplaceTokenGenerator):
|
|
"""Give test construction the production file-manager type explicitly."""
|
|
|
|
file_manager: FileManager | None
|
|
|
|
|
|
def _generator(
|
|
repo_root: MemoryPath,
|
|
*labels: str,
|
|
publisher: _Publisher | None = None,
|
|
) -> _BoundGenerator:
|
|
"""Return a token generator backed by labeled stub clients."""
|
|
|
|
return _BoundGenerator(
|
|
tuple(_StubClient(label) for label in labels),
|
|
repo_root,
|
|
event_publisher=publisher,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
|
|
|
|
def _renderer(
|
|
generator: _BoundGenerator,
|
|
request: AiderEditRequest,
|
|
editable: list[Path],
|
|
readonly: list[Path],
|
|
) -> ContextualFileRenderer:
|
|
"""Build the caller-owned direct-edit presenter used by prompt tests."""
|
|
|
|
manager = generator.file_manager
|
|
if manager is None:
|
|
raise AssertionError("Prompt renderer requires an explicit test file manager.")
|
|
return ContextualFileRenderer(
|
|
file_manager=manager,
|
|
client=generator.clients[0],
|
|
editable_files=editable,
|
|
readonly_files=readonly,
|
|
prompt=(
|
|
"ORIGINAL COT: IMMUTABLE STABILIZATION TARGET\n"
|
|
"BEGIN ORIGINAL STABILIZATION GOAL\n"
|
|
f"{manager.prompt}\n"
|
|
"END ORIGINAL STABILIZATION GOAL\n\n"
|
|
"ROLE AND EDIT BOUNDARIES\n"
|
|
f"{request.authoritative_prompt}"
|
|
),
|
|
pinned_context=request.pinned_context,
|
|
backlog_guidance=request.backlog_guidance,
|
|
failure_error_message=request.failure_error_message,
|
|
failure_stack_trace=request.failure_stack_trace,
|
|
logger=logging.getLogger("search-replace-generator-test"),
|
|
prompt_formatter_builder=generator.prompt_formatter_builder,
|
|
oop_signature_extract=_NoCards(),
|
|
oop_signature_render=_NoCardRender(),
|
|
)
|
|
|
|
|
|
def test_markdown_prompt_formatter_renders_recursive_sections_once() -> None:
|
|
"""A nested formatter preserves both boundary depths in rendered output."""
|
|
|
|
builder = MarkdownPromptFormatterBuilder()
|
|
root = builder.generate("PARENT", depth=4)
|
|
child = builder.generate("CHILD")
|
|
assert isinstance(root, MarkdownPromptFormatter)
|
|
assert isinstance(child, MarkdownPromptFormatter)
|
|
def render_child() -> str:
|
|
"""Render one nested child while the root formatter is active."""
|
|
|
|
return child("child body")
|
|
|
|
rendered = root(render_child)
|
|
|
|
assert PromptFormatter in type(root).__mro__
|
|
assert PromptFormatterBuilder in type(builder).__mro__
|
|
assert root.depth == 4
|
|
assert child.depth == 5
|
|
assert root("literal body") == (
|
|
"======== BEGIN PARENT ========\n"
|
|
"literal body\n"
|
|
"======== END PARENT ========"
|
|
)
|
|
assert rendered == (
|
|
"======== BEGIN PARENT ========\n"
|
|
"========== BEGIN CHILD ==========\n"
|
|
"child body\n"
|
|
"========== END CHILD ==========\n"
|
|
"======== END PARENT ========"
|
|
)
|
|
|
|
|
|
def _client_label(client: CompletionClient) -> str:
|
|
"""Return the test-only label after proving the selected nominal client type."""
|
|
|
|
if not isinstance(client, _StubClient):
|
|
raise AssertionError("Token generator selected an unexpected completion client.")
|
|
return client.label
|
|
|
|
|
|
def test_markdown_prompt_formatter_validates_names_and_preserves_source() -> None:
|
|
"""Formatter rejects ambiguous names without changing exact unescaped source text."""
|
|
|
|
builder = MarkdownPromptFormatterBuilder()
|
|
formatter = builder.generate("SOURCE")
|
|
source = " value = 'quoted'\n\n return value\n"
|
|
|
|
rendered = formatter(source)
|
|
|
|
assert source in rendered
|
|
for name in ("", "bad\nname", "bad\rname"):
|
|
try:
|
|
builder.generate(name)("content")
|
|
except ValueError:
|
|
continue
|
|
raise AssertionError(f"Expected {name!r} to be rejected.")
|
|
|
|
|
|
def test_markdown_prompt_formatter_escapes_only_boundary_content() -> None:
|
|
"""Plain-text content cannot impersonate a section boundary when escaping is requested."""
|
|
|
|
formatter = MarkdownPromptFormatterBuilder().generate("DIAGNOSTIC", escape=True)
|
|
content = "ordinary\n== BEGIN FALSE SECTION ==\nlast\n"
|
|
|
|
rendered = formatter(content)
|
|
|
|
assert "ordinary\n\\== BEGIN FALSE SECTION ==\nlast\n" in rendered
|
|
|
|
|
|
def test_markdown_prompt_formatter_neutralizes_nested_json_structures() -> None:
|
|
"""Nested and schematic JSON remove every touched line, including adjacent prose."""
|
|
|
|
formatter = MarkdownPromptFormatterBuilder().generate(
|
|
"NESTED PROMPT",
|
|
neutralize_json=True,
|
|
)
|
|
content = (
|
|
'Return {"outcome":"pass","reason":["line {inside string}"],'
|
|
'"additional":{"instruction":"retry"}} or {\n "retryable": true|false\n}. '
|
|
'Remove this suffix too.\nKeep {unparsed} literal and [tests] label.'
|
|
)
|
|
|
|
rendered = formatter(content)
|
|
|
|
assert rendered == (
|
|
'== BEGIN NESTED PROMPT ==\n'
|
|
'---REDACTED---\n---REDACTED---\n---REDACTED---\n'
|
|
'Keep {unparsed} literal and [tests] label.\n'
|
|
'== END NESTED PROMPT =='
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("neutralize_json", (True, False))
|
|
def test_markdown_prompt_formatter_redacts_only_opted_in_response_contracts(
|
|
neutralize_json: bool,
|
|
) -> None:
|
|
"""The observed include contract disappears only from an opted-in nested copy."""
|
|
|
|
formatter = MarkdownPromptFormatterBuilder().generate(
|
|
"DECISION", neutralize_json=neutralize_json,
|
|
)
|
|
content = 'Task evidence\nReturn strict JSON only: {"include": true} or {"include": false}.\nNext step'
|
|
expected = {
|
|
True: 'Task evidence\n---REDACTED---\nNext step',
|
|
False: content,
|
|
}
|
|
|
|
assert formatter(content) == f"== BEGIN DECISION ==\n{expected[neutralize_json]}\n== END DECISION =="
|
|
assert content == 'Task evidence\nReturn strict JSON only: {"include": true} or {"include": false}.\nNext step'
|
|
|
|
|
|
@pytest.mark.parametrize("newline", ("\n", "\r\n", "\r"))
|
|
@pytest.mark.parametrize("final_newline", (True, False))
|
|
def test_markdown_prompt_formatter_preserves_redacted_line_endings(
|
|
newline: str, final_newline: bool,
|
|
) -> None:
|
|
"""Every line within an array is redacted without changing its newline or final EOF."""
|
|
|
|
formatter = MarkdownPromptFormatterBuilder().generate("NESTED", neutralize_json=True)
|
|
ending = newline * final_newline
|
|
content = newline.join(('Before', ' [', '', ' {"value": "quoted \\\" } ["}', '] after')) + ending
|
|
expected = newline.join(('Before', '---REDACTED---', '---REDACTED---', '---REDACTED---', '---REDACTED---')) + ending
|
|
|
|
assert formatter(content) == f"== BEGIN NESTED ==\n{expected}\n== END NESTED =="
|
|
|
|
|
|
def test_markdown_prompt_formatter_preserves_non_json_lines_and_boundary_escaping() -> None:
|
|
"""Redaction leaves unrelated brackets intact and composes with section escaping."""
|
|
|
|
formatter = MarkdownPromptFormatterBuilder().generate(
|
|
"NESTED", neutralize_json=True, escape=True,
|
|
)
|
|
content = (
|
|
'== BEGIN FALSE SECTION ==\n'
|
|
'{unparsed} [tests] mapping[key]\n'
|
|
'Return {"include": false}.\n'
|
|
'== END FALSE SECTION ==\n'
|
|
'Incomplete {"include":'
|
|
)
|
|
|
|
assert formatter(content) == (
|
|
'== BEGIN NESTED ==\n'
|
|
'\\== BEGIN FALSE SECTION ==\n'
|
|
'{unparsed} [tests] mapping[key]\n'
|
|
'---REDACTED---\n'
|
|
'\\== END FALSE SECTION ==\n'
|
|
'Incomplete {"include":\n'
|
|
'== END NESTED =='
|
|
)
|
|
|
|
|
|
def test_generator_uses_default_client_when_only_one_client_exists(tmp_path: MemoryPath) -> None:
|
|
"""Verify one configured client always stays on the default path."""
|
|
|
|
generator = _generator(tmp_path, "default")
|
|
editable: list[Path] = [tmp_path / "pkg" / "module.py"]
|
|
|
|
for _ in range(6):
|
|
selected = generator._client_for_next_edit(editable, prompt="prompt", logger=None)
|
|
assert _client_label(selected) == "default"
|
|
|
|
|
|
def test_generator_selects_first_escalation_client_after_four_repeats(tmp_path: MemoryPath) -> None:
|
|
"""Verify the fourth repeated editable-file cycle selects escalation client index 1."""
|
|
|
|
generator = _generator(tmp_path, "default", "escalated-1", "escalated-2")
|
|
editable: list[Path] = [tmp_path / "pkg" / "module.py"]
|
|
|
|
for _ in range(3):
|
|
generator._record_editable_scope(editable)
|
|
|
|
selected = generator._client_for_next_edit(editable, prompt="prompt", logger=None)
|
|
assert _client_label(selected) == "escalated-1"
|
|
|
|
|
|
def test_generator_selects_second_escalation_client_after_five_repeats(tmp_path: MemoryPath) -> None:
|
|
"""Verify the fifth repeated editable-file cycle selects escalation client index 2."""
|
|
|
|
generator = _generator(tmp_path, "default", "escalated-1", "escalated-2")
|
|
editable: list[Path] = [tmp_path / "pkg" / "module.py"]
|
|
|
|
for _ in range(4):
|
|
generator._record_editable_scope(editable)
|
|
|
|
selected = generator._client_for_next_edit(editable, prompt="prompt", logger=None)
|
|
assert _client_label(selected) == "escalated-2"
|
|
|
|
|
|
def test_generator_holds_selected_escalation_for_duration_then_enters_cooldown(tmp_path: MemoryPath) -> None:
|
|
"""Verify an activated escalation remains active, then falls back to cooldown."""
|
|
|
|
generator = _generator(tmp_path, "default", "escalated-1", "escalated-2")
|
|
editable: list[Path] = [tmp_path / "pkg" / "module.py"]
|
|
|
|
selected: CompletionClient = generator.clients[0]
|
|
for _ in range(4):
|
|
selected = generator._client_for_next_edit(editable, prompt="prompt", logger=None)
|
|
|
|
assert _client_label(selected) == "escalated-1"
|
|
assert generator._escalation_active_turns_remaining == ESCALATION_DURATION - 1
|
|
|
|
other_editable: list[Path] = [tmp_path / "pkg" / "other.py"]
|
|
for _ in range(ESCALATION_DURATION - 1):
|
|
selected = generator._client_for_next_edit(
|
|
other_editable, prompt="prompt", logger=None
|
|
)
|
|
assert _client_label(selected) == "escalated-1"
|
|
|
|
assert generator._escalation_active_turns_remaining == 0
|
|
assert generator._active_floor_config_index == 0
|
|
assert generator._escalation_cooldown_remaining == ESCALATION_COOLDOWN_TURNS
|
|
|
|
for remaining in range(ESCALATION_COOLDOWN_TURNS - 1, -1, -1):
|
|
selected = generator._client_for_next_edit(editable, prompt="prompt", logger=None)
|
|
assert _client_label(selected) == "default"
|
|
assert generator._escalation_cooldown_remaining == remaining
|
|
|
|
|
|
def test_generator_can_rachet_up_during_active_window(tmp_path: MemoryPath) -> None:
|
|
"""Verify an active escalation floor can still rise when repetition increases further."""
|
|
|
|
generator = _generator(tmp_path, "default", "escalated-1", "escalated-2")
|
|
editable: list[Path] = [tmp_path / "pkg" / "module.py"]
|
|
|
|
for _ in range(4):
|
|
generator._client_for_next_edit(editable, prompt="prompt", logger=None)
|
|
|
|
assert generator._active_floor_config_index == 1
|
|
selected = generator._client_for_next_edit(editable, prompt="prompt", logger=None)
|
|
assert _client_label(selected) == "escalated-2"
|
|
assert generator._active_floor_config_index == 2
|
|
|
|
|
|
def test_generator_publishes_escalation_event_for_direct_editor_path(tmp_path: MemoryPath) -> None:
|
|
"""Verify direct-edit escalation publishes through the existing UI DTO."""
|
|
|
|
publisher = _Publisher()
|
|
generator = _generator(tmp_path, "default", "escalated-1", publisher=publisher)
|
|
editable: list[Path] = [tmp_path / "pkg" / "module.py"]
|
|
|
|
selected: CompletionClient = generator.clients[0]
|
|
for _ in range(4):
|
|
selected = generator._client_for_next_edit(
|
|
editable,
|
|
prompt="AUTHORITATIVE TASK PROMPT\nFix module",
|
|
logger=None,
|
|
)
|
|
|
|
assert _client_label(selected) == "escalated-1"
|
|
events = [event for event in publisher.events if isinstance(event, ModelEscalationDTO)]
|
|
assert len(events) == 1
|
|
assert events[0].trigger_name == "DirectCodeEditor"
|
|
assert events[0].local_model == "default"
|
|
assert events[0].active_model == "escalated-1"
|
|
assert events[0].state == "activated"
|
|
assert events[0].tokens_escalated > 0
|
|
assert events[0].active_endpoint == "https://example.invalid"
|
|
assert "AUTHORITATIVE TASK PROMPT" in events[0].prompt_preview
|
|
|
|
|
|
def test_prompt_uses_contextual_current_evidence_when_file_manager_is_bound(
|
|
tmp_path: MemoryPath,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The direct sender retains ordinary source evidence instead of cheap cards."""
|
|
|
|
monkeypatch.setattr(
|
|
ContextualFileRenderer, "cheap_read",
|
|
Mock(return_value="REFERENCE CARD WITHOUT IMPLEMENTATION"),
|
|
)
|
|
|
|
editable = tmp_path / "src" / "editable.py"
|
|
readonly = tmp_path / "src" / "readonly.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("editable_value = 1\n")
|
|
readonly.write_text("readonly_value = 2\n")
|
|
generator = _generator(tmp_path, "default")
|
|
generator.file_manager = _FileManager(
|
|
tmp_path,
|
|
{
|
|
"src/editable.py": "EDITABLE DIFF MUST NOT BE AUTOMATIC",
|
|
"src/readonly.py": "readonly upstream diff",
|
|
},
|
|
)
|
|
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Update the editable value.",
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
)
|
|
prompt = generator.render_prompt(
|
|
request,
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
renderer=_renderer(generator, request, [editable], [readonly]),
|
|
)
|
|
|
|
assert "REFERENCE CARD WITHOUT IMPLEMENTATION" not in prompt
|
|
assert "FILE: src/editable.py\neditable_value = 1\n" in prompt
|
|
assert "EDITABLE DIFF MUST NOT BE AUTOMATIC" not in prompt
|
|
assert "FILE: src/readonly.py\nreadonly_value = 2\n" in prompt
|
|
assert "UPSTREAM GIT DIFF\nreadonly upstream diff" in prompt
|
|
assert "FILE -> SEARCH -> SEPARATOR -> REPLACE" in prompt
|
|
assert "Blocks cannot nest." in prompt
|
|
assert "all remaining output is discarded" in prompt
|
|
assert prompt.count("Never include `⋮` in a SEARCH block or REPLACE block.") == 1
|
|
assert (
|
|
"New local production references may target current or traversed COT source files, "
|
|
"supplied editable or readonly files, or direct upstream dependencies of the "
|
|
"supplied files."
|
|
) in prompt
|
|
assert (
|
|
"Readonly and upstream files may be referenced but only files listed as editable "
|
|
"may be changed."
|
|
) in prompt
|
|
assert (
|
|
"Dependency reference cards highlight likely declarations from the supplied files; "
|
|
"they do not expand or narrow the permitted reference set."
|
|
) in prompt
|
|
assert (
|
|
"Production code may not reference test code, and tests may not reference other "
|
|
"test files."
|
|
) in prompt
|
|
assert "BEGIN PATCH REPAIR FEEDBACK" in prompt
|
|
assert "\nNone.\n" in prompt
|
|
for section in (
|
|
"AUTHORITATIVE DIRECTIVES",
|
|
"TASK GUIDANCE",
|
|
"REPAIR EVIDENCE",
|
|
"CURRENT FILE EVIDENCE",
|
|
"EDITABLE FILES",
|
|
"READONLY FILES",
|
|
):
|
|
assert f"BEGIN {section}" in prompt
|
|
assert "====== BEGIN src/editable.py ======" in prompt
|
|
assert "====== BEGIN src/readonly.py ======" in prompt
|
|
|
|
|
|
def test_prompt_requires_one_atomic_clean_candidate_for_all_editable_file_diagnostics(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""All cited diagnostics in the selected file are closure obligations in one response."""
|
|
|
|
editable = tmp_path / "tests" / "example" / "test_widget.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text(
|
|
"def test_assigned_failure():\n assert False\n\n"
|
|
"class _Manager: pass\n\n"
|
|
"def test_duplicate(): pass\n\n"
|
|
"phase = object().missing_phase\n",
|
|
)
|
|
readonly = tmp_path / "tests" / "other_test.py"
|
|
readonly.write_text("def test_other(): pass\n")
|
|
generator = _generator(tmp_path, "default")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
|
|
request = AiderEditRequest(
|
|
authoritative_prompt=(
|
|
"Repair the selected test file and leave it clean before commit."
|
|
),
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
backlog_guidance=["Repair test_assigned_failure only."],
|
|
failure_error_message="test_assigned_failure failed",
|
|
failure_stack_trace=[
|
|
"_Manager is not assignable to FileManager",
|
|
"duplicate test declaration: test_duplicate",
|
|
"phase attribute no longer exists",
|
|
"resolution attribute no longer exists",
|
|
"tests/other_test.py: unrelated diagnostic",
|
|
],
|
|
)
|
|
prompt = generator.render_prompt(
|
|
request,
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
renderer=_renderer(generator, request, [editable], [readonly]),
|
|
)
|
|
|
|
assert "Multiple small SEARCH/REPLACE blocks may form one incremental repair candidate." in prompt
|
|
assert "Compile errors take priority while resolving an established edit episode." in prompt
|
|
assert "Fix every compile error attributable to or directly blocking the current intended change" in prompt
|
|
assert (
|
|
"Every diagnostic listed under CURRENT COMPILER STATE has already been "
|
|
"classified by the harness as blocking. Fix all listed diagnostics before "
|
|
"returning to the supplied test failure or backlog assignment; a no-change "
|
|
"response does not clear them."
|
|
) in prompt
|
|
assert (
|
|
"Static compile diagnostics do not independently broaden the runtime semantic assignment."
|
|
not in prompt
|
|
)
|
|
assert "Do not repair unrelated diagnostics merely because they occur in an editable file." not in prompt
|
|
assert "Do not continue adding or changing the requested test behavior while a relevant compiler error remains." in prompt
|
|
assert "Once CURRENT COMPILER STATE is clean, address the supplied test failure or backlog assignment." in prompt
|
|
assert "The repair loop will continue supplying the next compiler or test failure until both are clear." in prompt
|
|
assert "The Original Stabilization Goal remains an immutable behavior constraint throughout every repair." in prompt
|
|
assert 'NO_PATCH: {"file": "repo-relative/path", "reason": "concrete causal attribution"}' not in prompt
|
|
assert "NO_PATCH: <reason>" in prompt
|
|
assert "Dead-end admission must accept it" not in prompt
|
|
assert "traceback or other supplied failure evidence" not in prompt
|
|
assert "no blocking static compile or runtime semantic errors commits" in prompt
|
|
assert "AUTHORITATIVE TASK > COMPATIBLE GUIDANCE > APPLICABLE PRE-EXISTING PRECEDENTS" in prompt
|
|
assert "Backlog guidance adds compatible focused-work requirements." in prompt
|
|
assert "Its diagnoses do not prove that an alleged operation exists" in prompt
|
|
assert "You do not need to find every possible change in one response." not in prompt
|
|
for diagnostic in (
|
|
"_Manager is not assignable to FileManager",
|
|
"duplicate test declaration: test_duplicate",
|
|
"phase attribute no longer exists",
|
|
"resolution attribute no longer exists",
|
|
):
|
|
assert diagnostic in prompt
|
|
assert "tests/other_test.py: unrelated diagnostic" in prompt
|
|
|
|
|
|
def test_prompt_renders_empty_repair_feedback_as_none(tmp_path: MemoryPath) -> None:
|
|
"""An empty current repair state is explicit in the preserved prompt section."""
|
|
|
|
editable = tmp_path / "pkg" / "module.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("value = 1\n", encoding="utf-8")
|
|
generator = _generator(tmp_path, "response")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Change source.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
)
|
|
|
|
prompt = generator.render_prompt(
|
|
request,
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
renderer=_renderer(generator, request, [editable], []),
|
|
)
|
|
|
|
repair_section = prompt.split("BEGIN PATCH REPAIR FEEDBACK", 1)[1].split(
|
|
"END PATCH REPAIR FEEDBACK", 1
|
|
)[0]
|
|
assert "\nNone.\n" in repair_section
|
|
|
|
|
|
def test_prompt_renders_nonempty_repair_feedback_in_its_existing_section(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""Current retry evidence is complete before generation begins."""
|
|
|
|
client = _StubClient("response")
|
|
generator = _BoundGenerator(
|
|
(client,),
|
|
tmp_path,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
editable = tmp_path / "pkg" / "module.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("value = 1\n", encoding="utf-8")
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Change source.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
)
|
|
prompt = generator.render_prompt(
|
|
request,
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
renderer=_renderer(generator, request, [editable], []),
|
|
repair_feedback=" Compilation failed: bad type. ",
|
|
)
|
|
|
|
response = generator.generate(
|
|
request,
|
|
base_prompt=prompt,
|
|
editable_files=[editable],
|
|
)
|
|
|
|
assert response == "response"
|
|
assert "BEGIN PATCH REPAIR FEEDBACK" in prompt
|
|
assert "\nCompilation failed: bad type.\n" in prompt
|
|
assert client.prompts == [prompt]
|
|
|
|
|
|
def test_generator_uses_the_base_prompt_directly(tmp_path: MemoryPath) -> None:
|
|
"""Generation does not inspect or substitute any prompt content."""
|
|
|
|
client = _StubClient("response")
|
|
generator = _BoundGenerator(
|
|
(client,),
|
|
tmp_path,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
editable = tmp_path / "pkg" / "module.py"
|
|
base_prompt = "stable evidence\n__BAND_AIDER_REPAIR_FEEDBACK__\n"
|
|
|
|
response = generator.generate(
|
|
AiderEditRequest(
|
|
authoritative_prompt="Change source.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
),
|
|
base_prompt=base_prompt,
|
|
editable_files=[editable],
|
|
)
|
|
|
|
assert response == "response"
|
|
assert client.prompts == [base_prompt]
|
|
|
|
|
|
@pytest.mark.parametrize("threshold", (2, 4))
|
|
def test_generator_signals_widening_before_spending_another_model_request(
|
|
tmp_path: MemoryPath, monkeypatch: pytest.MonkeyPatch, threshold: int,
|
|
) -> None:
|
|
"""One configurable threshold limits unchanged context, never total model work."""
|
|
|
|
monkeypatch.setattr(search_replace, "SEARCH_REPLACE_ATTEMPTS_BEFORE_WIDENING", threshold)
|
|
monkeypatch.setattr(LanguageStrategySet, "instance", _DirectiveStrategies(CustomDirectives({})))
|
|
client = _StubClient("response")
|
|
generator = _BoundGenerator((client,), tmp_path, prompt_formatter_builder=MarkdownPromptFormatterBuilder())
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
editable = tmp_path / "src" / "mod.py"
|
|
request = AiderEditRequest(authoritative_prompt="Repair.", editable_files=[editable], readonly_files=[])
|
|
|
|
# Every admitted attempt below the threshold reaches the ordinary model client.
|
|
for count in range(threshold):
|
|
assert generator.generate(request, base_prompt="source", editable_files=[editable], unapplied_attempts=count) == "response"
|
|
with pytest.raises(search_replace.ContextWideningRequired):
|
|
generator.generate(request, base_prompt="source", editable_files=[editable], unapplied_attempts=threshold)
|
|
assert client.prompts == ["source"] * threshold
|
|
|
|
assert generator.generate(request, base_prompt="expanded source", editable_files=[editable], unapplied_attempts=0) == "response"
|
|
assert client.prompts[-1] == "expanded source"
|
|
assert len(generator._editable_history) == threshold + 1
|
|
|
|
|
|
def test_search_instructions_require_verbatim_whitespace(tmp_path: MemoryPath) -> None:
|
|
"""SEARCH must preserve physical source lines rather than copy formatted trace text."""
|
|
|
|
generator = _generator(tmp_path, "response")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
source = tmp_path / "src" / "mod.py"
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("value = 1\n\nvalue += 1\n")
|
|
request = AiderEditRequest(authoritative_prompt="Repair.", editable_files=[source], readonly_files=[])
|
|
|
|
prompt = generator.render_prompt(
|
|
request, editable_files=[source], readonly_files=[],
|
|
renderer=_renderer(generator, request, [source], []),
|
|
)
|
|
|
|
assert "mechanically and verbatim" in prompt
|
|
assert "blank and whitespace-only lines" in prompt
|
|
assert "indentation, tabs, spaces, and trailing whitespace" in prompt
|
|
assert "not a reformatted traceback" in prompt
|
|
|
|
|
|
def test_legacy_sentinel_literal_in_source_does_not_affect_direct_generation(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""Editable and readonly source content is never interpreted as prompt protocol."""
|
|
|
|
legacy_literal = "__BAND_AIDER_REPAIR_FEEDBACK__"
|
|
editable = tmp_path / "pkg" / "editable.py"
|
|
readonly = tmp_path / "pkg" / "readonly.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text(f'editable_value = "{legacy_literal}"\n', encoding="utf-8")
|
|
readonly.write_text(f'readonly_value = "{legacy_literal}"\n', encoding="utf-8")
|
|
client = _StubClient("response")
|
|
generator = _BoundGenerator(
|
|
(client,),
|
|
tmp_path,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Change source.",
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
)
|
|
prompt = generator.render_prompt(
|
|
request,
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
renderer=_renderer(generator, request, [editable], [readonly]),
|
|
)
|
|
|
|
response = generator.generate(
|
|
request,
|
|
base_prompt=prompt,
|
|
editable_files=[editable],
|
|
)
|
|
|
|
assert response == "response"
|
|
assert prompt.count(legacy_literal) == 2
|
|
assert client.prompts == [prompt]
|
|
|
|
|
|
def test_readonly_source_can_be_direct_generation_context(tmp_path: MemoryPath) -> None:
|
|
"""A readonly source body remains ordinary direct-generation evidence."""
|
|
|
|
editable = tmp_path / "band_aider" / "judgement" / "direct_code_editor.py"
|
|
readonly = tmp_path / "band_aider" / "judgement" / "search_replace_token_generator.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("def edit_current_source():\n return 'editable'\n")
|
|
readonly.write_text("def render_readonly_source():\n return 'readonly'\n")
|
|
client = _StubClient("response")
|
|
generator = _BoundGenerator(
|
|
(client,),
|
|
tmp_path,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Change direct editing.",
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
)
|
|
prompt = generator.render_prompt(
|
|
request,
|
|
editable_files=[editable],
|
|
readonly_files=[readonly],
|
|
renderer=_renderer(generator, request, [editable], [readonly]),
|
|
)
|
|
|
|
response = generator.generate(
|
|
request,
|
|
base_prompt=prompt,
|
|
editable_files=[editable],
|
|
)
|
|
|
|
assert response == "response"
|
|
assert "BEGIN band_aider/judgement/search_replace_token_generator.py" in prompt
|
|
assert client.prompts == [prompt]
|
|
|
|
|
|
def test_generator_wraps_same_selected_client_with_custom_directives(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""An admitted directive is consumed from the selected escalation response."""
|
|
|
|
normal_clients = (
|
|
_StubClient("cached-default"),
|
|
_StubClient("APPLY:: setup.py\nordinary patch\n"),
|
|
)
|
|
directives = CustomDirectives({
|
|
_BootstrapContext: (
|
|
DirectiveGroup(
|
|
name="direct edit",
|
|
directives=(
|
|
Directive(
|
|
symbol="APPLY",
|
|
example="APPLY:: setup.py",
|
|
present="{symbol}:: <file>",
|
|
parse=str,
|
|
handle=_directive_readout,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
})
|
|
LanguageStrategySet.set_instance(_DirectiveStrategies(directives))
|
|
generator = _BoundGenerator(
|
|
normal_clients,
|
|
tmp_path,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
editable = tmp_path / "pkg" / "module.py"
|
|
for _ in range(3):
|
|
generator._record_editable_scope([editable])
|
|
|
|
response = generator.generate(
|
|
AiderEditRequest(
|
|
authoritative_prompt="Add a dependency.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
context=_BootstrapContext,
|
|
),
|
|
base_prompt="edit prompt\n",
|
|
editable_files=[editable],
|
|
)
|
|
|
|
assert response == "ordinary patch\n"
|
|
assert normal_clients[0].prompts == []
|
|
assert "CUSTOM DIRECTIVES" in normal_clients[1].prompts[0]
|
|
assert "APPLY:: <file>" in normal_clients[1].prompts[0]
|
|
|
|
|
|
def test_generator_wraps_even_when_the_context_has_no_directive_groups(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""A context without directives sends the exact base prompt unchanged."""
|
|
|
|
normal_client = _StubClient("cached")
|
|
LanguageStrategySet.set_instance(_DirectiveStrategies(CustomDirectives({})))
|
|
generator = _BoundGenerator(
|
|
(normal_client,),
|
|
tmp_path,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
editable = tmp_path / "pkg" / "module.py"
|
|
|
|
response = generator.generate(
|
|
AiderEditRequest(
|
|
authoritative_prompt="Change source.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
context=_NoDirectiveContext,
|
|
),
|
|
base_prompt="edit prompt\n",
|
|
editable_files=[editable],
|
|
)
|
|
|
|
assert response == "cached"
|
|
assert normal_client.prompts == ["edit prompt\n"]
|
|
|
|
|
|
def test_generator_resolves_custom_directives_from_the_language_strategy_binding(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""Language-bound directives consume a response before it leaves generation."""
|
|
|
|
normal_client = _StubClient("NOTE:: language binding\nordinary edit\n")
|
|
generator = _BoundGenerator(
|
|
(normal_client,),
|
|
tmp_path,
|
|
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
|
|
)
|
|
file_manager = _FileManager(tmp_path, {})
|
|
generator.file_manager = file_manager
|
|
directives = CustomDirectives({
|
|
_BindingContext: (
|
|
DirectiveGroup(
|
|
name="language binding",
|
|
directives=(
|
|
Directive(
|
|
symbol="NOTE",
|
|
example="NOTE:: language binding",
|
|
present="{symbol}:: <message>",
|
|
parse=str,
|
|
handle=_directive_readout,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
})
|
|
LanguageStrategySet.set_instance(_DirectiveStrategies(directives))
|
|
editable = tmp_path / "pkg" / "module.py"
|
|
|
|
response = generator.generate(
|
|
AiderEditRequest(
|
|
authoritative_prompt="Change source.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
context=_BindingContext,
|
|
),
|
|
base_prompt="edit prompt\n",
|
|
editable_files=[editable],
|
|
)
|
|
|
|
assert response == "ordinary edit\n"
|
|
assert "CUSTOM DIRECTIVES" in normal_client.prompts[0]
|
|
assert "NOTE:: <message>" in normal_client.prompts[0]
|
|
|
|
|
|
def test_prompt_passes_structured_request_context_to_callable_selector(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""Verify DirectCodeEditor selection sees every structured request input."""
|
|
|
|
generator = _generator(tmp_path, "default")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
editable = tmp_path / "src" / "editable.py"
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Fix the selected behavior.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
pinned_context=["Pinned contract."],
|
|
backlog_guidance=["Preserve the compatibility path."],
|
|
failure_error_message="AssertionError: wrong value",
|
|
failure_stack_trace=["tests/test_example.py:7: AssertionError"],
|
|
failure_raw_output="FAILED tests/test_example.py::test_value\n",
|
|
)
|
|
|
|
renderer = _renderer(generator, request, [editable], [])
|
|
generator.render_prompt(
|
|
request,
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
renderer=renderer,
|
|
)
|
|
|
|
assert "ORIGINAL COT: IMMUTABLE STABILIZATION TARGET" in renderer.prompt
|
|
assert "Original stabilization goal." in renderer.prompt
|
|
assert "ROLE AND EDIT BOUNDARIES" in renderer.prompt
|
|
assert request.authoritative_prompt in renderer.prompt
|
|
assert renderer.prompt.index("Original stabilization goal.") < renderer.prompt.index(
|
|
"ROLE AND EDIT BOUNDARIES"
|
|
)
|
|
assert renderer.pinned_context == tuple(request.pinned_context)
|
|
assert renderer.backlog_guidance == tuple(request.backlog_guidance)
|
|
assert renderer.failure_error_message == request.failure_error_message
|
|
assert renderer.failure_stack_trace == tuple(request.failure_stack_trace)
|
|
assert renderer.failure_raw_output == ""
|
|
assert renderer.fault == ""
|
|
|
|
|
|
def test_request_mask_projects_failure_evidence_without_mutating_owned_state(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""A request can be projected repeatedly without clearing caller-owned evidence."""
|
|
|
|
editable: list[Path] = [tmp_path / "src" / "module.py"]
|
|
readonly: list[Path] = [tmp_path / "tests" / "test_module.py"]
|
|
backlog = ["Repair the assigned semantic assertion."]
|
|
stack = ["tests/test_module.py:9: AssertionError"]
|
|
roots: list[Path] = [tmp_path / "generated"]
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Repair the selected behavior.",
|
|
editable_files=editable,
|
|
readonly_files=readonly,
|
|
backlog_guidance=backlog,
|
|
failure_error_message="AssertionError: wrong value",
|
|
failure_stack_trace=stack,
|
|
failure_raw_output="FAILED tests/test_module.py::test_value",
|
|
allowed_additional_roots=roots,
|
|
)
|
|
|
|
patch = AiderEditRequestMask(request, AiderFailureMode.DEFAULT).projected()
|
|
static = AiderEditRequestMask(request, AiderFailureMode.STATIC).projected()
|
|
semantic = AiderEditRequestMask(request, AiderFailureMode.SEMANTIC).projected()
|
|
|
|
assert patch.failure_error_message == ""
|
|
assert patch.failure_stack_trace == []
|
|
assert patch.failure_raw_output == ""
|
|
assert patch.backlog_guidance == []
|
|
assert static.failure_error_message == ""
|
|
assert static.failure_stack_trace == []
|
|
assert static.failure_raw_output == ""
|
|
assert static.backlog_guidance == []
|
|
assert semantic.failure_error_message == request.failure_error_message
|
|
assert semantic.failure_stack_trace is stack
|
|
assert semantic.failure_raw_output == request.failure_raw_output
|
|
assert semantic.backlog_guidance == []
|
|
assert semantic.editable_files is editable
|
|
assert semantic.readonly_files is readonly
|
|
assert semantic.allowed_additional_roots is roots
|
|
assert request.failure_error_message == "AssertionError: wrong value"
|
|
assert request.failure_stack_trace is stack
|
|
assert request.failure_raw_output == "FAILED tests/test_module.py::test_value"
|
|
assert request.backlog_guidance is backlog
|
|
backlog_only = AiderEditRequest(
|
|
authoritative_prompt="Repair the selected behavior.",
|
|
editable_files=editable,
|
|
readonly_files=readonly,
|
|
backlog_guidance=["Provisional assignment only."],
|
|
failure_error_message=" ",
|
|
failure_stack_trace=["\n"],
|
|
failure_raw_output="\t",
|
|
)
|
|
assert not AiderEditRequestMask(
|
|
backlog_only,
|
|
AiderFailureMode.DEFAULT,
|
|
).has_semantic_failure
|
|
|
|
|
|
def test_request_mask_exposes_evidence_through_its_high_watermark() -> None:
|
|
"""PATCH < COMPILE < SEMANTIC selects one inclusive evidence prefix."""
|
|
|
|
request = AiderEditRequest("Repair it.", [], [])
|
|
assert AiderEditRequestMask(
|
|
request, AiderFailureMode.DEFAULT
|
|
).visible(
|
|
patch_level="patch", compile_level="compile", semantic_level="semantic"
|
|
) == ("patch",)
|
|
assert AiderEditRequestMask(
|
|
request, AiderFailureMode.STATIC
|
|
).visible(
|
|
patch_level="patch", compile_level="compile", semantic_level="semantic"
|
|
) == ("patch", "compile")
|
|
assert AiderEditRequestMask(
|
|
request, AiderFailureMode.SEMANTIC
|
|
).visible(
|
|
patch_level="patch", compile_level="compile", semantic_level="semantic"
|
|
) == ("patch", "compile", "semantic")
|
|
|
|
|
|
def test_attempt_level_stops_at_the_earliest_failed_validation_gate() -> None:
|
|
"""An attempt reaches only the earliest level represented by its diagnostics."""
|
|
|
|
static = StaticCompileError("syntax failed")
|
|
semantic = TestRunCompileError("test failed", test_name="test_value")
|
|
|
|
assert AiderFailureMode.attempt_level(
|
|
[semantic], default=AiderFailureMode.DEFAULT
|
|
) is AiderFailureMode.SEMANTIC
|
|
assert AiderFailureMode.attempt_level(
|
|
[semantic, static], default=AiderFailureMode.DEFAULT
|
|
) is AiderFailureMode.STATIC
|
|
assert isinstance(TypeCheckError("type failed"), StaticCompileError)
|
|
assert not isinstance(semantic, StaticCompileError)
|
|
|
|
|
|
def test_initial_repair_evidence_includes_dependency_reference_cards(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""The first model turn receives advisory dependency declarations."""
|
|
|
|
generator = _generator(tmp_path, "default")
|
|
request = AiderEditRequest("Repair it.", [], [])
|
|
mask = AiderEditRequestMask(request, AiderFailureMode.DEFAULT)
|
|
|
|
evidence = (
|
|
RepairEvidenceBuilder(generator, mask)
|
|
.with_reference_cards("REFERENCE: pkg.lifecycle.LifecycleError")
|
|
.render([], "")
|
|
)
|
|
|
|
assert "COMPILE REFERENCE CARDS" in evidence
|
|
assert "REFERENCE: pkg.lifecycle.LifecycleError" in evidence
|
|
assert "supplied editable and readonly files" in evidence
|
|
assert "do not expand or narrow the permitted reference set" in evidence
|
|
|
|
|
|
def test_compile_repair_evidence_retains_dependency_reference_cards(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""A corrective model turn retains the same advisory dependency declarations."""
|
|
|
|
generator = _generator(tmp_path, "default")
|
|
request = AiderEditRequest("Repair it.", [], [])
|
|
mask = AiderEditRequestMask(request, AiderFailureMode.STATIC)
|
|
|
|
evidence = (
|
|
RepairEvidenceBuilder(generator, mask)
|
|
.with_reference_cards("REFERENCE: pkg.lifecycle.LifecycleError")
|
|
.render([StaticCompileError("syntax failed")], "syntax failed")
|
|
)
|
|
|
|
assert "COMPILE REFERENCE CARDS" in evidence
|
|
assert "REFERENCE: pkg.lifecycle.LifecycleError" in evidence
|
|
assert "supplied editable and readonly files" in evidence
|
|
assert "do not expand or narrow the permitted reference set" in evidence
|
|
|
|
|
|
def test_static_mask_stops_before_semantic_instructions(tmp_path: MemoryPath) -> None:
|
|
"""Compile repair receives patch and compile instructions without test instructions."""
|
|
|
|
editable = tmp_path / "src" / "module.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("value = manager.build()\n")
|
|
generator = _generator(tmp_path, "default")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Repair the current type mismatch.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
)
|
|
renderer = _renderer(generator, request, [editable], [])
|
|
feedback = "CURRENT COMPILER STATE\ncurrent type mismatch"
|
|
|
|
projected = generator.render_prompt(
|
|
AiderEditRequestMask(request, AiderFailureMode.STATIC),
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
renderer=renderer,
|
|
repair_feedback=feedback,
|
|
)
|
|
|
|
assert "Compile errors take priority" in projected
|
|
assert "direct upstream dependencies of the supplied files" in projected
|
|
assert "do not expand or narrow the permitted reference set" in projected
|
|
assert "tests may not reference other test files" in projected
|
|
assert "Once CURRENT COMPILER STATE is clean" not in projected
|
|
assert "FAILURE EVIDENCE" not in projected
|
|
assert projected.count(
|
|
"Every SEARCH payload must match exactly one location in the current FILE."
|
|
) == 1
|
|
|
|
|
|
def test_mixed_failure_mask_renders_only_current_static_evidence(tmp_path: MemoryPath) -> None:
|
|
"""Mechanical diagnostics mask simultaneous semantic failure material completely."""
|
|
|
|
editable = tmp_path / "src" / "module.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("value = manager.build()\n")
|
|
generator = _generator(tmp_path, "default")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Repair the assigned implementation.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
is_remediation=True,
|
|
backlog_guidance=["Change the semantic assertion in test_value."],
|
|
failure_error_message="AssertionError: wrong value",
|
|
failure_stack_trace=["tests/test_module.py:9: AssertionError"],
|
|
failure_raw_output="FAILED tests/test_module.py::test_value",
|
|
)
|
|
feedback = "\n".join(
|
|
[
|
|
"CURRENT COMPILER STATE",
|
|
'Argument of type "_FileManager" cannot be assigned to "FileManager"',
|
|
"COMPILE REFERENCE CARDS",
|
|
"class FileManager:",
|
|
]
|
|
)
|
|
|
|
prompt = generator.render_prompt(
|
|
AiderEditRequestMask(request, AiderFailureMode.STATIC),
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
renderer=_renderer(generator, request, [editable], []),
|
|
repair_feedback=feedback,
|
|
)
|
|
|
|
assert "CURRENT COMPILER STATE" in prompt
|
|
assert 'NO_PATCH: {"file"' in prompt
|
|
assert "an empty response, summary alone, or unchanged patch cannot resolve the failure" in prompt
|
|
assert 'Argument of type "_FileManager"' in prompt
|
|
assert "COMPILE REFERENCE CARDS" in prompt
|
|
assert "class FileManager:" in prompt
|
|
assert request.failure_error_message not in prompt
|
|
assert request.failure_stack_trace[0] not in prompt
|
|
assert request.failure_raw_output not in prompt
|
|
assert request.backlog_guidance[0] not in prompt
|
|
assert "FAILURE EVIDENCE" not in prompt
|
|
assert "Once CURRENT COMPILER STATE is clean" not in prompt
|
|
assert "will continue supplying" not in prompt
|
|
assert request.failure_error_message == "AssertionError: wrong value"
|
|
assert request.backlog_guidance == ["Change the semantic assertion in test_value."]
|
|
|
|
|
|
def test_semantic_mask_omits_backlog_guidance_and_keeps_failure_evidence(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""Semantic repair keeps runtime evidence while dropping provisional backlog work."""
|
|
|
|
editable = tmp_path / "tests" / "test_module.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("def test_value():\n assert value == 2\n")
|
|
generator = _generator(tmp_path, "default")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Repair the selected test behavior.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
backlog_guidance=["Repair test_value only."],
|
|
failure_error_message="AssertionError: wrong value",
|
|
failure_stack_trace=["tests/test_module.py:2: AssertionError"],
|
|
failure_raw_output="FAILED tests/test_module.py::test_value",
|
|
)
|
|
|
|
prompt = generator.render_prompt(
|
|
AiderEditRequestMask(request, AiderFailureMode.SEMANTIC),
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
renderer=_renderer(generator, request, [editable], []),
|
|
repair_feedback="SEARCH payload must match exactly once.",
|
|
)
|
|
|
|
assert request.failure_error_message in prompt
|
|
assert request.failure_stack_trace[0] in prompt
|
|
assert request.failure_raw_output in prompt
|
|
assert request.backlog_guidance[0] not in prompt
|
|
assert "SEARCH payload must match exactly once." in prompt
|
|
assert "Compile errors take priority" in prompt
|
|
assert "Once CURRENT COMPILER STATE is clean" in prompt
|
|
|
|
|
|
def test_semantic_mask_retains_prior_level_feedback_sections(
|
|
tmp_path: MemoryPath,
|
|
) -> None:
|
|
"""Semantic repair retains supplied patch and compile evidence."""
|
|
|
|
editable = tmp_path / "tests" / "test_module.py"
|
|
editable.parent.mkdir(parents=True)
|
|
editable.write_text("def test_value():\n assert value == 2\n")
|
|
generator = _generator(tmp_path, "default")
|
|
generator.file_manager = _FileManager(tmp_path, {})
|
|
request = AiderEditRequest(
|
|
authoritative_prompt="Repair the selected test behavior.",
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
backlog_guidance=["Repair test_value only."],
|
|
failure_error_message="AssertionError: wrong value",
|
|
)
|
|
feedback = "\n\n".join(
|
|
[
|
|
generator.render_section("CURRENT COMPILER STATE", depth=2)(
|
|
"stale type mismatch"
|
|
),
|
|
generator.render_section("COMPILE REFERENCE CARDS", depth=2)(
|
|
"stale compiler card"
|
|
),
|
|
generator.render_section("LAST REJECTED CANDIDATE FEEDBACK", depth=2)(
|
|
"stale static candidate"
|
|
),
|
|
generator.render_section("PATCH APPLICATION FEEDBACK", depth=2)(
|
|
"SEARCH payload must match exactly once."
|
|
),
|
|
]
|
|
)
|
|
|
|
prompt = generator.render_prompt(
|
|
AiderEditRequestMask(request, AiderFailureMode.SEMANTIC),
|
|
editable_files=[editable],
|
|
readonly_files=[],
|
|
renderer=_renderer(generator, request, [editable], []),
|
|
repair_feedback=feedback,
|
|
)
|
|
|
|
assert "stale type mismatch" in prompt
|
|
assert "stale compiler card" in prompt
|
|
assert "stale static candidate" in prompt
|
|
assert "CURRENT COMPILER STATE" in prompt
|
|
assert "COMPILE REFERENCE CARDS" in prompt
|
|
assert "LAST REJECTED CANDIDATE FEEDBACK" in prompt
|
|
assert "PATCH APPLICATION FEEDBACK" in prompt
|
|
assert "SEARCH payload must match exactly once." in prompt
|