band-aider-pub/tests/judgement/test_search_replace_token_generator.py
2026-10-17 11:57:37 -07:00

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