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

3725 lines
148 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
from __future__ import annotations
from band_aider.judgement.test_runner_strategy import TestRunOutcome
from collections.abc import Mapping
from collections.abc import Callable
from band_aider.judgement.source_discovery_strategy import CallTraceNode
import json
import logging
import random
from collections.abc import Callable, Iterator, Mapping, Sequence
from io import StringIO
from pathlib import Path
from typing import TextIO, cast
from unittest.mock import Mock, patch
import pytest
from band_aider.ai_client.openai_compatible_client import OpenAICompatibleClient
from band_aider.ai_client.token_budget_estimator import TokenBudgetEstimator
from band_aider.context.class_meta import ClassMeta, ContentType
from band_aider.context.cot_task_generator import CotTaskRecord
from band_aider.context.failing_test import FailingTest
from band_aider.context.edited_test_inventory import EditedTestInventory
from band_aider.context.memento import Memento
from band_aider.context.token_budget_state import TokenBudgetSettings
from band_aider.context import provenance
from band_aider.context.provenance import DeadEndError, Direction, Phase, Provenance
from band_aider.context.working_backlog import FailedQAItem, InspectionBacklog, TestingBacklog
from band_aider.judgement.direct_code_editor import DirectCodeEditor
from band_aider.judgement.dead_end_decision import DeadEndDecisionBuilder
from band_aider.judgement.edit_request import (
AiderEditRequest,
AiderEditRequestMask,
AiderFailureMode,
)
from band_aider.judgement.file_patcher import FilePatcher, QuickRetryError
from band_aider.judgement.file_manager import FileManager
from band_aider.judgement.code_parser import CodeParser
from band_aider.judgement.dependency_discovery_strategy import DependencyDiscoveryStrategy
from band_aider.judgement.contextual_file_renderer import ContextualFileRenderer
from band_aider.judgement.prompt_localization import PromptLocalization
from band_aider.judgement.search_replace_token_generator import SearchReplaceTokenGenerator
from band_aider.judgement.source_discovery_strategy import (
SearchableBeatValue,
SourceDiscoveryStrategy,
)
from band_aider.judgement.prompt_formatter import MarkdownPromptFormatterBuilder
from band_aider.strategies.compiler import (
BlackboardCompiler,
CompileAttempt,
Compiler,
CompileError,
CompilerFactory,
ReferenceError,
StaticCompileError,
TestRunCompileError,
TypeCheckError,
)
from band_aider.strategies.language_strategy_set import LanguageStrategySet
from band_aider.strategies.directives import CustomDirectives
from band_aider.strategies.python.python_compiler import PythonCompiler, PythonCompilerFactory
from band_aider.strategies.python.python_bom_editor import PythonBomEditor
from band_aider.strategies.python.python_source_discovery import PythonSourceDiscovery
from band_aider.judgement.test_runner_strategy import TestRunResult, TestRunnerStrategy as _TestRunnerStrategy
from band_aider.loop.beta_loop import BetaLoop
from band_aider.loop.alpha_loop import AlphaLoop
from band_aider.ui.dto.supreme_status import SupremeStatusDTO
from band_aider.ui.status import status_publisher_scope
from band_aider.ui.status_publisher import StatusPublisher
from band_aider.strategies.oop_signature_extract import (
Declaration,
DeclarationType,
OOpSignature,
OOpSignatureExtract,
OOpSignatureRender,
)
from tests.support.completion_clients import (
ScriptedCompletionClient,
StaticOpenAICompatibleClient,
)
from tests.support.memory_path import MemoryPath
from tests.support.memory_repo_exec import MemoryRepoExec
from band_aider.judgement.code_editor import CodeEditorResult, EditorRecord
class _AlphaContext:
"""Test workflow context with Alpha's frontier requirement."""
class _ReferenceCardExtract(OOpSignatureExtract):
"""Return current-source-derived cards for renderer-owned prompt tests."""
def extract(self, module: str, name: str) -> OOpSignature | None:
del module, name
return None
def extract_references(
self,
sources_by_location: Mapping[str, str],
) -> tuple[OOpSignature, ...]:
source = "\n".join(sources_by_location.values())
if "internal_mismatch" in source:
argument = Declaration(DeclarationType.ARGUMENT, "location")
return (
OOpSignature(
"tests.support.file_manager",
"_FileManager",
Declaration(
DeclarationType.CLASS,
"_FileManager",
declarations=(
Declaration(
DeclarationType.METHOD,
"source_path",
arguments=(Declaration(DeclarationType.ARGUMENT, "self"), argument),
),
),
),
),
OOpSignature(
"pkg.file_manager",
"FileManager",
Declaration(DeclarationType.CLASS, "FileManager"),
),
)
if "rejected_dependency" in source:
return (
OOpSignature(
"rejected.types",
"RejectedType",
Declaration(DeclarationType.CLASS, "RejectedType"),
),
)
if "accepted_dependency" in source:
return (
OOpSignature(
"accepted.types",
"AcceptedType",
Declaration(DeclarationType.CLASS, "AcceptedType"),
),
)
return ()
def extract_source(
self,
location: str,
source: str,
) -> tuple[OOpSignature, ...]:
"""Return no current-file cards outside the focused renderer tests."""
del location, source
return ()
class _ReferenceCardRender(OOpSignatureRender):
"""Render test cards without coupling editor tests to Python source syntax."""
def render(self, signature: OOpSignature, output: StringIO) -> None:
"""Write a stable declaration label for one test reference card."""
output.write(f"{signature.declaration.decl_type.value} {signature.name}\n")
class _ReferenceSelectionClient(StaticOpenAICompatibleClient):
"""Return every listed test card through the production selection response contract."""
def __init__(self) -> None:
"""Initialize a nominal client without an external endpoint."""
super().__init__("")
def complete(self, prompt: str, logger: logging.Logger | None = None) -> str:
"""Select only module/name pairs explicitly present in one candidate catalogue."""
del logger
# Source chunks use a different response contract from dependency cards.
if "BEGIN CONTEXTUAL FILE SELECTION" in prompt:
return "NO_KEEP\nREASON: no additional chunks"
candidates = [
(module, name)
for module, name in (
("tests.support.file_manager", "_FileManager"),
("pkg.file_manager", "FileManager"),
("accepted.types", "AcceptedType"),
("rejected.types", "RejectedType"),
)
if f"REFERENCE: {module}.{name}" in prompt
]
return json.dumps({
"references": [
{"module": module, "name": name}
for module, name in candidates
],
})
class _CotSelectionClient(_ReferenceSelectionClient):
"""Select one prior COT identity and retain its exact relevance prompt.
Example: ``_CotSelectionClient().complete(prompt)`` selects COT-0000.
"""
def __init__(self) -> None:
"""Initialize the reference selector and an empty COT prompt history."""
super().__init__()
self.cot_prompts: list[str] = []
def complete(self, prompt: str, logger: logging.Logger | None = None) -> str:
"""Select COT-0000 or delegate dependency-card selection to the parent fake."""
# Keep prior-COT selection separate from dependency-reference selection.
if "CONTEXTUAL COT SELECTION" in prompt:
self.cot_prompts.append(prompt)
return "KEEP: COT-0000"
return super().complete(prompt, logger=logger)
@pytest.fixture(autouse=True)
def _isolate_editor_globals() -> Iterator[None]:
"""Restore process-wide strategy selection and a deterministic random stream."""
original_strategy = LanguageStrategySet.instance
random.seed(0)
yield
LanguageStrategySet.set_instance(original_strategy)
@pytest.fixture
def memory_root() -> MemoryPath:
"""Return a fresh lexical repository that never touches the host disk."""
return MemoryPath.from_repo(MemoryRepoExec())
class _StubGenerator(SearchReplaceTokenGenerator):
"""Return configured model output while recording repair feedback."""
def __init__(self, responses: str | list[str]) -> None:
"""Initialize a production-bound generator with stationary responses."""
formatter = MarkdownPromptFormatterBuilder()
super().__init__(
clients=(_ReferenceSelectionClient(),),
repo_root=Path("/testbed"),
repo_exec=MemoryRepoExec(),
prompt_formatter_builder=formatter,
)
self.responses = [responses] if isinstance(responses, str) else list(responses)
self.calls: list[dict[str, object]] = []
self.render_calls: list[dict[str, object]] = []
def render_section(
self,
name: str,
*,
depth: int = 1,
escape: bool = False,
):
"""Generate retry feedback through the same formatter contract as production."""
return self.prompt_formatter_builder.generate(
name,
depth=depth,
escape=escape,
)
def render_prompt(
self,
request: AiderEditRequest | AiderEditRequestMask,
*,
editable_files: list[Path],
readonly_files: list[Path],
renderer: ContextualFileRenderer,
repair_feedback: str = "",
) -> str:
"""Record one baseline render for a Direct edit attempt."""
self.render_calls.append(
{
"request": request,
"editable_files": list(editable_files),
"readonly_files": list(readonly_files),
"renderer": renderer,
"repair_feedback": repair_feedback,
}
)
return "baseline prompt"
def generate(
self,
request: AiderEditRequest,
*,
base_prompt: str,
editable_files: list[Path],
logger: logging.Logger | None = None,
unapplied_attempts: int = 0,
) -> str:
self.calls.append(
{
"request": request,
"base_prompt": base_prompt,
"editable_files": list(editable_files),
"logger": logger,
"unapplied_attempts": unapplied_attempts,
}
)
if len(self.responses) > 1:
return self.responses.pop(0)
return self.responses[0]
class _RenderingStubGenerator(_StubGenerator):
"""Record prompts after rendering through the production evidence boundary."""
def render_prompt(
self,
request: AiderEditRequest | AiderEditRequestMask,
*,
editable_files: list[Path],
readonly_files: list[Path],
renderer: ContextualFileRenderer,
repair_feedback: str = "",
) -> str:
"""Render with the real generator while retaining the scenario inputs."""
prompt = SearchReplaceTokenGenerator.render_prompt(
self,
request,
editable_files=editable_files,
readonly_files=readonly_files,
renderer=renderer,
repair_feedback=repair_feedback,
)
self.render_calls.append(
{
"request": request,
"editable_files": list(editable_files),
"readonly_files": list(readonly_files),
"renderer": renderer,
"repair_feedback": repair_feedback,
}
)
return prompt
class _EditorParser(CodeParser):
"""Provide FileManager's parser contract without opening host files."""
def parse_file(
self,
file_name: str,
prompt: str,
content_type: ContentType = ContentType.Source,
) -> ClassMeta:
"""Return one minimal source record for an in-memory path."""
del prompt
return ClassMeta(file_name, "", [], {}, content_type=content_type)
def parse_stream(
self,
file_name: str,
stream: TextIO,
prompt: str,
content_type: ContentType = ContentType.Source,
) -> ClassMeta:
"""Return one minimal source record without consuming host-backed data."""
del stream
return self.parse_file(file_name, prompt, content_type)
def parse_files(
self,
file_names: list[str],
prompt: str,
content_type: ContentType = ContentType.Source,
) -> list[ClassMeta]:
"""Return one minimal source record per requested in-memory path."""
return [self.parse_file(file_name, prompt, content_type) for file_name in file_names]
class _ManagedFileManager(FileManager):
"""Provide the FileManager/ClassMeta subset needed by the direct editor."""
def __init__(
self,
root: MemoryPath,
locations: list[str],
test_locations: Sequence[str] = (),
) -> None:
"""Seed one nominal FileManager with explicit in-memory metadata."""
self._source_locations = list(locations)
self._test_locations = list(test_locations)
self.metadata_requests: list[tuple[str, ContentType | None]] = []
self.refresh_requests: list[tuple[ClassMeta, str, str]] = []
super().__init__(
parser=_EditorParser(),
client=StaticOpenAICompatibleClient('{"cdc": ""}'),
repo_root=root,
src_root=root / "src",
test_root=root / "tests",
actor="test",
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
repo_exec=root.repository,
prompt="Original stabilization goal.",
)
def resync(self) -> None:
"""Build FileManager indexes from fixed scenario metadata only."""
self.src_classes = [
ClassMeta(
location=location,
summary="",
relevance=[],
relevant_symbols={},
content_type=ContentType.Source,
)
for location in self._source_locations
]
self.test_classes = [
ClassMeta(
location=location,
summary="",
relevance=[],
relevant_symbols={},
content_type=ContentType.Test,
)
for location in self._test_locations
]
self.src_index = {item.location: index for index, item in enumerate(self.src_classes)}
self.test_index = {item.location: index for index, item in enumerate(self.test_classes)}
self.src_dirty.clear()
self.test_dirty.clear()
def git_diff_for(self, location: str | Path) -> str:
"""Direct editor tests do not need repository diff evidence."""
del location
return ""
def current_metadata(
self,
location: str | Path,
*,
content_type: ContentType | None = None,
) -> ClassMeta | None:
"""Return this scenario's canonical metadata within the requested content kind."""
path = Path(str(location))
relative = path.relative_to(self.repo_root).as_posix() if path.is_absolute() else path.as_posix()
self.metadata_requests.append((relative, content_type))
# Match the requested metadata kind at the production factory boundary.
if content_type is ContentType.Test:
metadata = self.current_test(relative)
# A source-only lookup cannot accidentally select a test record.
elif content_type is ContentType.Source:
metadata = self.current_src(relative)
# Unqualified editor captures may select either managed content kind.
else:
metadata = self.current_src(relative) or self.current_test(relative)
return metadata
def refresh_class_meta(
self,
class_meta: ClassMeta,
prompt: str,
tailing_logs: str = "",
) -> ClassMeta:
"""Return the test fixture's canonical metadata for a stale repair file."""
self.refresh_requests.append((class_meta, prompt, tailing_logs))
return self.current_metadata(
class_meta.location,
content_type=class_meta.content_type,
) or class_meta
class _RecordingCompiler(BlackboardCompiler):
"""Compile one recorded attempt from a stationary diagnostics queue."""
def __init__(self, factory: "_RecordingCompilerFactory", attempt: CompileAttempt) -> None:
"""Bind one compiler invocation to its factory-owned attempt evidence."""
self._factory = factory
self._attempt = attempt
self.blackboard = attempt.blackboard or EditedTestInventory(
self._clean_blackboard
)
def get_blackboard(self) -> EditedTestInventory:
"""Return the inventory owned or borrowed by this recorded compiler."""
return self.blackboard
def _clean_blackboard(self) -> None:
"""Record compiler-owned inventory release by forgetting its handle."""
del self.blackboard
def compile(self, paths: Sequence[Path]) -> list[CompileError]:
"""Record current Memento state and return the next configured diagnostics."""
call = {
"paths": list(paths),
"pre": [memento.pre for memento in self._attempt.mementos],
"post": [memento.post for memento in self._attempt.mementos],
"layers": [memento.layer for memento in self._attempt.mementos],
}
self._factory.compile_calls.append(call)
self._factory.candidate_calls.append(
{
"pre": [memento.pre for memento in self._attempt.candidate_mementos],
"post": [memento.post for memento in self._attempt.candidate_mementos],
"previous_errors": list(self._attempt.previous_errors),
}
)
pre = call["pre"]
post = call["post"]
if all(before == after for before, after in zip(pre, post)):
return self._factory.baseline_results.pop(0) if self._factory.baseline_results else []
return self._factory.results.pop(0) if self._factory.results else []
class _RecordingCompilerFactory(CompilerFactory):
"""Record CompileAttempt Mementos and return configured diagnostics."""
def __init__(self, results: Sequence[Sequence[CompileError]] | None = None) -> None:
self.results = [list(result) for result in results or [[]]]
self.baseline_results: list[list[CompileError]] = []
self.build_calls: list[CompileAttempt] = []
self.compile_calls: list[dict[str, object]] = []
self.candidate_calls: list[dict[str, object]] = []
self.compilers: list[_RecordingCompiler] = []
def build(self, attempt: CompileAttempt) -> Compiler:
self.build_calls.append(attempt)
compiler = _RecordingCompiler(self, attempt)
self.compilers.append(compiler)
return compiler
class _EmptySourceDiscovery(SourceDiscoveryStrategy):
"""Provide the unrelated discovery contract for direct-editor scenarios."""
def visit_call_sites(self, target: CallTraceNode, already_seen: set[str] | None = None, predicate: Callable[[CallTraceNode], bool] | None = None, *, target_args: dict[str, CallTraceNode] | None = None, pristine: Mapping[str, str] | None = None) -> None:
"""Supply no imported calls in this stationary discovery double."""
del target, already_seen, predicate
def get_call_tree(self, location: str, name: str | None = None, *, line: int | None = None, depth: int = 1, already_seen: set[str] | None = None, predicate: Callable[[CallTraceNode], bool] | None = None, pristine: Mapping[str, str] | None = None) -> CallTraceNode:
"""Reject origins because this double contains no callable definitions."""
raise LookupError(location)
def get_sources(self, recursive: bool = True, type: object | None = None) -> list[Path]:
"""Return no discovery-owned source paths."""
del recursive, type
return []
def get_tests(self, recursive: bool = True, type: object | None = None) -> list[Path]:
"""Return no discovery-owned test paths."""
del recursive, type
return []
def searchable_beats(self, file_name: str, source: str) -> list[SearchableBeatValue]:
"""Return no searchable beats for the fixed direct-editor fixture."""
del file_name, source
return []
class _FixedStrategySet:
"""Return one configured language strategy set without mutation helpers."""
def __init__(self, strategy_set: LanguageStrategySet) -> None:
"""Store the one scenario-local strategy set."""
self._strategy_set = strategy_set
def __call__(self, file_manager: FileManager, **overrides: object) -> LanguageStrategySet:
"""Return this scenario's strategy set for any bound file manager."""
del file_manager, overrides
return self._strategy_set
class _RecordingPublisher(StatusPublisher):
"""Retain status events through the nominal UI publication boundary."""
def __init__(self, events: list[object] | None = None) -> None:
"""Use supplied event storage or create an isolated scenario-local list."""
self.events = events if events is not None else []
def publish(self, event: object) -> None:
"""Store one immutable event for behavior assertions."""
self.events.append(event)
def _install_strategy(
compiler_factory: CompilerFactory,
signature_extract: OOpSignatureExtract | None = None,
) -> None:
"""Install minimal neutral contracts for one focused editor test."""
strategy_set = LanguageStrategySet(
bindings={
CompilerFactory: compiler_factory,
SourceDiscoveryStrategy: _EmptySourceDiscovery(),
OOpSignatureExtract: signature_extract or _ReferenceCardExtract(),
OOpSignatureRender: _ReferenceCardRender(),
}
)
LanguageStrategySet.set_instance(_FixedStrategySet(strategy_set))
@pytest.fixture
def compiler_factory() -> _RecordingCompilerFactory:
"""Register a deterministic successful compiler."""
factory = _RecordingCompilerFactory()
_install_strategy(factory)
return factory
def _replace(path: str, before: str, after: str) -> str:
"""Render one minimal SEARCH/REPLACE block."""
return "\n".join(
[
f"FILE: {path}",
"<<<<<<< SEARCH",
before,
"=======",
after,
">>>>>>> REPLACE",
]
)
def _runtime_editor(
memory_root: MemoryPath,
generator: _StubGenerator,
) -> tuple[DirectCodeEditor, Path]:
"""Create an editor backed by one discovered source file."""
source = memory_root / "src" / "mod.py"
source.parent.mkdir(parents=True)
source.write_text("value = 'old'\n", encoding="utf-8")
editor = DirectCodeEditor(
token_generator=generator,
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
file_patcher=FilePatcher(memory_root, repo_exec=memory_root.repository),
repo_root=memory_root,
)
editor.file_manager = _ManagedFileManager(memory_root, ["src/mod.py"])
return editor, source
def _generator(editor: DirectCodeEditor) -> _StubGenerator:
"""Return the nominal stationary generator bound by this test harness."""
generator = editor.token_generator
if not isinstance(generator, _StubGenerator):
raise AssertionError("Direct editor did not retain the stationary test generator.")
return generator
def _file_manager(editor: DirectCodeEditor) -> _ManagedFileManager:
"""Return the nominal in-memory FileManager bound by this test harness."""
manager = editor.file_manager
if not isinstance(manager, _ManagedFileManager):
raise AssertionError("Direct editor did not retain the in-memory test FileManager.")
return manager
def _source_meta(editor: DirectCodeEditor, location: str) -> ClassMeta:
"""Return one required source record from the nominal in-memory manager."""
class_meta = _file_manager(editor).current_src(location)
if class_meta is None:
raise AssertionError(f"Scenario did not seed source metadata for {location}.")
return class_meta
def _rendered_masks(editor: DirectCodeEditor) -> list[AiderEditRequestMask]:
"""Return the explicitly projected request masks recorded by the generator."""
masks: list[AiderEditRequestMask] = []
for call in _generator(editor).render_calls:
request = call["request"]
if not isinstance(request, AiderEditRequestMask):
raise AssertionError("Direct editor rendered an unmasked request.")
masks.append(request)
return masks
def _rendered_renderer(editor: DirectCodeEditor, index: int) -> ContextualFileRenderer:
"""Return one nominal contextual renderer captured for a direct-edit turn."""
renderer = _generator(editor).render_calls[index]["renderer"]
if not isinstance(renderer, ContextualFileRenderer):
raise AssertionError("Direct editor did not render through ContextualFileRenderer.")
return renderer
def _request(source: Path, context: type = _AlphaContext) -> AiderEditRequest:
"""Create one workflow-aware request with the appropriate frontier scope."""
return AiderEditRequest(
authoritative_prompt="Update the value.",
editable_files=[source],
readonly_files=[],
context=context,
frontier_provider=lambda: [[]],
)
def test_direct_editor_compiles_memento_pre_and_post_then_commits(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A clean patch is compiled from the editor-owned ClassMeta Memento."""
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'new'")),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert compiler_factory.compile_calls == [
{
"paths": [source],
"pre": ["value = 'old'\n"],
"post": ["value = 'old'\n"],
"layers": [0],
},
{
"paths": [source],
"pre": ["value = 'old'\n"],
"post": ["value = 'new'\n"],
"layers": [0],
}
]
assert compiler_factory.build_calls[0].mementos[0].file_path == source
assert compiler_factory.candidate_calls == [
{"pre": [], "post": [], "previous_errors": []},
{
"pre": ["value = 'old'\n"],
"post": ["value = 'new'\n"],
"previous_errors": [],
},
]
assert _file_manager(editor).metadata_requests == [
("src/mod.py", None),
("src/mod.py", None),
]
assert not (source.parent / ".mod.py~").exists()
def test_direct_editor_repairs_static_failure_without_dead_end_admission(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A failed static candidate continues repair before any restart admission."""
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'done'"),
])
boundary_client = ScriptedCompletionClient((
json.dumps({
"dead_end": True,
"scope_fault": "wrong_editable_file",
"message": "Reconsider the selected source remediation.",
"justification": "The failure evidence indicates the selected source scope is wrong.",
}),
))
generator.clients = (boundary_client,)
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.results = [[TypeCheckError(
"candidate static failure",
path=source,
line=7,
column=3,
)], []]
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
item = InspectionBacklog(
focused_spotlight=_source_meta(editor, "src/mod.py"),
summary="repair the focused source",
)
parent_id = graph.start(Phase.BETA, TestingBacklog(
focused_spotlight=_source_meta(editor, "src/mod.py"),
summary="prior test turn",
))
graph.track_provenance(parent_id, Phase.ALPHA, item)
with provenance.bind(graph):
with provenance.item_scope(Phase.ALPHA, item):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert boundary_client.prompts == []
assert len(generator.calls) == 2
assert "candidate static failure" in str(generator.render_calls[1]["repair_feedback"])
assert source.read_text(encoding="utf-8") == "value = 'done'\n"
assert not any(edge.direction is not None for edge in graph.edges)
assert not Memento.current(source, memory_root.repository)
def test_direct_editor_repairs_runtime_failure_without_dead_end_admission(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A failed runtime candidate continues repair before any restart admission."""
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'done'"),
])
boundary_client = ScriptedCompletionClient((
json.dumps({
"dead_end": True,
"scope_fault": "wrong_editable_file",
"message": "Reconsider the selected source remediation.",
"justification": "The targeted failure indicates the selected source scope is wrong.",
}),
))
generator.clients = (boundary_client,)
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.results = [[TestRunCompileError(
"targeted test still fails",
test_name="test_value",
)], []]
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
item = InspectionBacklog(
focused_spotlight=_source_meta(editor, "src/mod.py"),
summary="repair the focused source",
)
parent_id = graph.start(Phase.BETA, TestingBacklog(
focused_spotlight=_source_meta(editor, "src/mod.py"),
summary="prior test turn",
))
graph.track_provenance(parent_id, Phase.ALPHA, item)
with provenance.bind(graph):
with provenance.item_scope(Phase.ALPHA, item):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert boundary_client.prompts == []
assert len(generator.calls) == 2
assert "targeted test still fails" in str(generator.render_calls[1]["repair_feedback"])
assert source.read_text(encoding="utf-8") == "value = 'done'\n"
assert not any(edge.direction is not None for edge in graph.edges)
assert not Memento.current(source, memory_root.repository)
def test_direct_editor_corrects_invalid_no_patch_admission_without_unwinding(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Invalid admission after NO_PATCH preserves the accepted repair baseline."""
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
'NO_PATCH: {"file": "src/caller.py", "reason": "The caller still uses the stale API."}',
_replace("src/mod.py", "value = 'partial'", "value = 'done'"),
])
boundary_client = ScriptedCompletionClient((
json.dumps({"dead_end": "yes"}),
json.dumps({
"dead_end": False,
"scope_fault": "none",
"message": "",
"justification": "",
}),
))
generator.clients = (boundary_client,)
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.results = [[TestRunCompileError(
"targeted test still fails",
test_name="test_value",
)], []]
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
trace = [f'File "{memory_root}/src/caller.py", line 9']
item = FailedQAItem(
focused_spotlight=_source_meta(editor, "src/mod.py"),
summary="repair the focused source",
failure_stack_trace=trace,
failed_test=FailingTest(ClassMeta("tests/test_value.py", "", [], {}), trace, "Stale caller API"),
)
parent_id = graph.start(Phase.BETA, TestingBacklog(
focused_spotlight=_source_meta(editor, "src/mod.py"),
summary="prior test turn",
))
graph.track_provenance(parent_id, Phase.ALPHA, item)
request = _request(source)
request.is_remediation = True
request.failure_error_message = "Stale caller API"
request.failure_stack_trace = list(trace)
with provenance.bind(graph):
with provenance.item_scope(Phase.ALPHA, item):
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'done'\n"
assert len(generator.calls) == 3
assert len(boundary_client.prompts) == 2
assert "RESPONSE VALIDATION ERROR" in boundary_client.prompts[1]
assert not any(edge.direction is not None for edge in graph.edges)
assert not Memento.current(source, memory_root.repository)
def test_direct_editor_publishes_progress_without_extra_compilation_or_work_queue(
memory_root: MemoryPath,
caplog,
) -> None:
"""Compiler facts travel through SupremeStatusDTO while the established repair sequence remains intact."""
factory = _RecordingCompilerFactory(
results=[
[
TestRunCompileError("render failed", test_name="TestWidget.test_render"),
TestRunCompileError("runner failed"),
],
[],
]
)
_install_strategy(factory)
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'new'"),
_replace("src/mod.py", "value = 'new'", "value = 'final'"),
]
),
)
class _Publisher(StatusPublisher):
"""Collect only UI events from the existing status publisher path."""
def __init__(self) -> None:
self.events: list[object] = []
def publish(self, event: object) -> None:
self.events.append(event)
publisher = _Publisher()
logger = logging.getLogger("tests.direct-code-editor-progress")
caplog.set_level(logging.INFO, logger=logger.name)
with status_publisher_scope(publisher):
_edit_result = editor.edit(_request(source), logger=logger)
changed, _ = _edit_result.changed_paths, _edit_result.summary
status_events = [event for event in publisher.events if isinstance(event, SupremeStatusDTO)]
targeted = [
event.direct_code_editor
for event in status_events
if event.direct_code_editor is not None
and event.direct_code_editor.unresolved_targeted_test_failure_count == 2
]
assert changed == [source]
assert len(factory.compile_calls) == 3
assert len(_generator(editor).calls) == 2
assert all(isinstance(event, SupremeStatusDTO) for event in publisher.events)
assert targeted[-1].model_visible_compiler_error_count == 1
assert targeted[-1].named_targeted_failures == ("FAIL TestWidget.test_render",)
assert targeted[-1].unattributed_targeted_execution_failure_count == 1
assert status_events[-1].direct_code_editor is None
progress_messages = [
record.getMessage()
for record in caplog.records
if record.getMessage().startswith("DirectCodeEditor progress:")
]
assert any(
"phase=initial compile" in message and "blocking_remaining=0" in message
for message in progress_messages
)
assert any(
"named_targeted_failures=('FAIL TestWidget.test_render',)" in message
for message in progress_messages
)
assert progress_messages[-1] == "DirectCodeEditor progress: inactive"
def test_progress_retains_named_failures(memory_root: MemoryPath) -> None:
"""Status retains the 101st named targeted failure instead of losing it at the former limit."""
test_names = tuple(f"TestWidget.test_{index}" for index in range(101))
targeted_errors = list(
map(
lambda test_name: TestRunCompileError("render failed", test_name=test_name),
test_names,
)
)
factory = _RecordingCompilerFactory(results=[targeted_errors, []])
_install_strategy(factory)
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'new'"),
_replace("src/mod.py", "value = 'new'", "value = 'final'"),
]
),
)
class _Publisher(StatusPublisher):
"""Collect status events emitted through the existing publisher boundary."""
def __init__(self) -> None:
self.events: list[object] = []
def publish(self, event: object) -> None:
self.events.append(event)
publisher = _Publisher()
with status_publisher_scope(publisher):
editor.edit(_request(source))
status_events = [event for event in publisher.events if isinstance(event, SupremeStatusDTO)]
targeted = [
event.direct_code_editor
for event in status_events
if event.direct_code_editor is not None
and event.direct_code_editor.unresolved_targeted_test_failure_count == len(test_names)
]
assert targeted[-1].named_targeted_failures == tuple(
f"FAIL {test_name}" for test_name in test_names
)
def test_direct_editor_consumes_no_patch_protocol_before_returning_its_summary(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""NO_PATCH is direct-editor control state, never downstream Alpha or Beta evidence."""
editor, source = _runtime_editor(
memory_root,
_StubGenerator("NO_PATCH: no concrete divergence exists in the focused source"),
)
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == []
assert "NO_PATCH:" not in summary
assert "no concrete divergence exists" in summary
assert compiler_factory.compile_calls
@pytest.mark.parametrize("response", ["NO_PATCH: production caller requires repair", "SUMMARY: unable to finish"])
def test_abandoned_edit_returns_candidate_diff_and_failure_without_changing_identity(
memory_root: MemoryPath, compiler_factory: _RecordingCompilerFactory, response: str,
) -> None:
"""An abandoned test migration restores bytes while its caller receives the production blocker and patch."""
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'active'"), response,
])
editor, source = _runtime_editor(memory_root, generator)
failures: list[CompileError] = [TestRunCompileError("Production caller uses removed clear()", test_name="test_resume")]
compiler_factory.results = [failures]
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
item = TestingBacklog(_source_meta(editor, "src/mod.py"), "Migrate test setup")
identity = graph.start(Phase.BETA, item)
with provenance.bind(graph), provenance.item_scope(Phase.BETA, item):
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == []
assert source.read_text() == "value = 'old'\n"
assert "-value = 'old'" in summary
assert "+value = 'active'" in summary
assert "Production caller uses removed clear()" in summary
assert item.message_id == identity
assert tuple(graph.messages) == (identity,)
assert graph.edges == ()
assert Memento.current(source, memory_root.repository) is None
@pytest.mark.parametrize("response", ["NO_PATCH: looks correct", "", "SUMMARY: nothing to do"])
@pytest.mark.parametrize("remediation", [False, True])
def test_noop_response_keeps_only_remediation_in_the_editor(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
response: str,
remediation: bool,
) -> None:
"""The same no-op completes ordinary work but must retry an incoming failure."""
generator = _StubGenerator([response, _replace("src/mod.py", "value = 'old'", "value = 'fixed'")])
editor, source = _runtime_editor(memory_root, generator)
request = _request(source)
request.is_remediation = remediation
request.failure_error_message = "Expected fixed value" if remediation else ""
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == ([source] if remediation else [])
assert source.read_text() == ("value = 'fixed'\n" if remediation else "value = 'old'\n")
assert len(generator.calls) == (2 if remediation else 1)
assert not Memento.current(source, memory_root.repository)
if remediation:
assert "No restart was admitted" in str(generator.render_calls[1]["repair_feedback"])
def test_remediation_unchanged_patch_requires_another_attempt(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A mechanically valid unchanged patch cannot consume an incoming failure."""
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'old'"),
_replace("src/mod.py", "value = 'old'", "value = 'fixed'"),
])
editor, source = _runtime_editor(memory_root, generator)
request = _request(source)
request.is_remediation = True
request.failure_error_message = "Expected fixed value"
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text() == "value = 'fixed'\n"
assert len(generator.calls) == 2
assert "no net change" in str(generator.render_calls[1]["repair_feedback"])
assert not Memento.current(source, memory_root.repository)
@pytest.mark.parametrize("admitted", [False, True])
def test_remediation_traceback_attribution_requires_dead_end_admission(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
admitted: bool,
) -> None:
"""NO_PATCH after a runtime failure either revives Alpha or keeps the repair baseline."""
attribution = {"file": "src/caller.py", "reason": "The traceback locates a stale call in src/caller.py."}
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
"NO_PATCH: " + json.dumps(attribution),
_replace("src/mod.py", "value = 'partial'", "value = 'fixed'"),
])
boundary_client = ScriptedCompletionClient((json.dumps({
"dead_end": admitted,
"scope_fault": "wrong_editable_file" if admitted else "none",
"message": "Reconsider the attribution." if admitted else "",
"justification": "The caller owns the stale call." if admitted else "",
}),))
generator.clients = (boundary_client,)
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.results = [[TestRunCompileError(
"The caller still uses the stale API.",
test_name="test_value",
)], []]
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
parent = graph.start(Phase.ALPHA, InspectionBacklog(_source_meta(editor, "src/mod.py"), "Inspect source."))
trace = [f'File "{memory_root}/src/caller.py", line 9']
failure = FailingTest(ClassMeta("tests/test_value.py", "", [], {}), trace, "Stale caller API")
item = FailedQAItem(
_source_meta(editor, "src/mod.py"), "Repair source.",
failure_stack_trace=trace, failed_test=failure,
)
graph.track_provenance(parent, Phase.ALPHA, item)
request = _request(source)
request.is_remediation = True
request.failure_error_message = "Stale caller API"
request.failure_stack_trace = list(item.failure_stack_trace)
with provenance.bind(graph), provenance.item_scope(Phase.ALPHA, item):
# Only an admitted attribution may discard the entire repair episode.
if admitted:
with pytest.raises(DeadEndError) as raised:
editor.edit(request)
phase, revived = graph.revive(raised.value)
assert phase is Phase.ALPHA
assert revived.message_id != parent
assert graph.messages[revived.message_id].parent_id == graph.messages[parent].parent_id
assert raised.value.direction is Direction.ROLLBACK
assert attribution["reason"] in revived.future_guidance[-1].message
assert "-value = 'old'" in revived.future_guidance[-1].message
assert "+value = 'partial'" in revived.future_guidance[-1].message
assert source.read_text() == "value = 'old'\n"
else:
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text() == "value = 'fixed'\n"
assert not any(edge.direction is not None for edge in graph.edges)
assert "No restart was admitted" in str(generator.render_calls[2]["repair_feedback"])
assert len(generator.calls) == (2 if admitted else 3)
assert len(boundary_client.prompts) == 1
assert "src/caller.py" in boundary_client.prompts[0]
assert not Memento.current(source, memory_root.repository)
@pytest.mark.parametrize("response", ["", "SUMMARY: nothing to do"])
def test_remediation_empty_response_does_not_request_dead_end_admission(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
response: str,
) -> None:
"""Empty output retries remediation without proposing a historical restart."""
generator = _StubGenerator([
response,
_replace("src/mod.py", "value = 'old'", "value = 'fixed'"),
])
editor, source = _runtime_editor(memory_root, generator)
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
parent = graph.start(Phase.BETA, TestingBacklog(_source_meta(editor, "src/mod.py"), "Test source."))
item = FailedQAItem(_source_meta(editor, "src/mod.py"), "Repair source.")
graph.track_provenance(parent, Phase.ALPHA, item)
request = _request(source)
request.is_remediation = True
request.failure_error_message = "Expected fixed value"
with provenance.bind(graph), provenance.item_scope(Phase.ALPHA, item):
with patch.object(
DeadEndDecisionBuilder,
"for_direct_code_editor",
wraps=DeadEndDecisionBuilder.for_direct_code_editor,
) as admission:
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text() == "value = 'fixed'\n"
assert len(generator.calls) == 2
assert "No restart was admitted" in str(generator.render_calls[1]["repair_feedback"])
admission.assert_not_called()
assert not any(edge.direction is not None for edge in graph.edges)
assert not Memento.current(source, memory_root.repository)
@pytest.mark.parametrize("file", ["src/missing.py", "src/mod.py", "../outside.py"])
def test_remediation_rejects_attribution_outside_its_provenance(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
file: str,
) -> None:
"""A named file alone cannot authorize leaving the current remediation."""
generator = _StubGenerator([
"NO_PATCH: " + json.dumps({"file": file, "reason": "The fault is in this file."}),
_replace("src/mod.py", "value = 'old'", "value = 'fixed'"),
])
boundary_client = ScriptedCompletionClient(())
generator.clients = (boundary_client,)
editor, source = _runtime_editor(memory_root, generator)
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
parent = graph.start(Phase.BETA, TestingBacklog(_source_meta(editor, "src/mod.py"), "Test source."))
item = FailedQAItem(_source_meta(editor, "src/mod.py"), "Repair source.")
graph.track_provenance(parent, Phase.ALPHA, item)
request = _request(source)
request.is_remediation = True
request.failure_error_message = "Expected fixed value"
with provenance.bind(graph), provenance.item_scope(Phase.ALPHA, item):
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert len(generator.calls) == 2
assert boundary_client.prompts == []
assert not any(edge.direction is not None for edge in graph.edges)
def test_remediation_attribution_without_history_retries_locally(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Recorded attribution cannot leave remediation when no parent can be revived."""
generator = _StubGenerator([
'NO_PATCH: {"file": "src/caller.py", "reason": "The traceback locates the stale call in src/caller.py."}',
_replace("src/mod.py", "value = 'old'", "value = 'fixed'"),
])
boundary_client = ScriptedCompletionClient(())
generator.clients = (boundary_client,)
editor, source = _runtime_editor(memory_root, generator)
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
trace = [f'File "{memory_root}/src/caller.py", line 9']
failure = FailingTest(ClassMeta("tests/test_value.py", "", [], {}), trace, "Stale caller API")
item = FailedQAItem(
_source_meta(editor, "src/mod.py"), "Repair source.",
failure_stack_trace=trace, failed_test=failure,
)
graph.start(Phase.ALPHA, item)
request = _request(source)
request.is_remediation = True
request.failure_error_message = "Stale caller API"
request.failure_stack_trace = list(trace)
with provenance.bind(graph), provenance.item_scope(Phase.ALPHA, item):
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text() == "value = 'fixed'\n"
assert len(generator.calls) == 2
assert "no retained parent" in str(generator.render_calls[1]["repair_feedback"])
assert boundary_client.prompts == []
assert not any(edge.direction is not None for edge in graph.edges)
assert not Memento.current(source, memory_root.repository)
def test_remediation_provider_failure_is_not_a_successful_empty_edit(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Provider interruption preserves the failure and rolls back the attempt."""
editor, source = _runtime_editor(memory_root, _StubGenerator("unused"))
request = _request(source)
request.is_remediation = True
request.failure_error_message = "Expected fixed value"
with patch.object(editor.token_generator, "generate", side_effect=ConnectionError("unavailable")):
with pytest.raises(ConnectionError, match="unavailable"):
editor.edit(request)
assert source.read_text() == "value = 'old'\n"
assert request.failure_error_message == "Expected fixed value"
assert not Memento.current(source, memory_root.repository)
def test_direct_editor_treats_literal_no_patch_source_text_as_inert(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Only the direct model-result channel, never source evidence, can activate NO_PATCH."""
source_text = "value = 'old' # NO_PATCH: literal source evidence\n"
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
_replace(
"src/mod.py",
source_text.rstrip(),
"value = 'new'",
)
),
)
source.write_text(source_text, encoding="utf-8")
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert "NO_PATCH: literal source evidence" in _edit_result.records[0].search
assert "NO_PATCH:" not in _edit_result.records[0].render()
assert source.read_text(encoding="utf-8") == "value = 'new'\n"
assert compiler_factory.compile_calls
def test_direct_editor_preserves_caller_memento_and_acceptance_diff(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A nested editor transaction cannot consume the caller's rollback boundary."""
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'new'")),
)
caller_memento = _source_meta(editor, "src/mod.py").set_or_get_memento(
_file_manager(editor),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert compiler_factory.compile_calls[0]["layers"] == [1]
assert compiler_factory.compile_calls[1]["layers"] == [1]
assert caller_memento.layer == 0
assert caller_memento.path.exists()
assert caller_memento.pre == "value = 'old'\n"
assert caller_memento.post == "value = 'new'\n"
assert caller_memento.diff
acceptance_memento = _source_meta(editor, "src/mod.py").set_or_get_memento(
_file_manager(editor),
)
assert acceptance_memento.layer == caller_memento.layer
assert acceptance_memento.pre == "value = 'old'\n"
assert acceptance_memento.post == "value = 'new'\n"
assert acceptance_memento.diff == caller_memento.diff
def test_caller_memento_rollback_remains_authoritative_after_direct_editor(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Alpha can reject a successful nested edit and restore its original source."""
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'new'")),
)
caller_memento = _source_meta(editor, "src/mod.py").set_or_get_memento(
_file_manager(editor),
)
editor.edit(_request(source))
caller_memento.rollback()
assert source.read_text(encoding="utf-8") == "value = 'old'\n"
assert not Memento.current(source, _file_manager(editor).repo_exec)
assert not caller_memento.path.exists()
assert not caller_memento.state_path.exists()
def test_direct_editor_renders_selected_prior_cot_for_one_patch_turn(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""The patch model sees selected prior authority while the original request stays fixed."""
generator = _StubGenerator(
_replace("src/mod.py", "value = 'old'", "value = 'new'")
)
selector = _CotSelectionClient()
generator.clients = (selector,)
editor, source = _runtime_editor(memory_root, generator)
prior = CotTaskRecord(
"Remove Memento.clear.",
"src/memento.py",
ClassMeta("src/memento.py", "remove clear", [], {}),
)
request = _request(source)
request.previous_cot_records = (prior,)
_edit_result = editor.edit(request)
changed, _summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert "Remove Memento.clear." in str(generator.calls[0]["base_prompt"])
assert "CURRENT COT — PRIMARY AUTHORITY AND EDIT SCOPE" in str(
generator.calls[0]["base_prompt"]
)
assert request.authoritative_prompt == "Update the value."
assert len(selector.cot_prompts) == 1
def test_caller_memento_commit_remains_authoritative_after_direct_editor(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Alpha can accept a successful nested edit and finalize its own layer."""
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'new'")),
)
caller_memento = _source_meta(editor, "src/mod.py").set_or_get_memento(
_file_manager(editor),
)
editor.edit(_request(source))
caller_memento.commit()
assert source.read_text(encoding="utf-8") == "value = 'new'\n"
assert not Memento.current(source, _file_manager(editor).repo_exec)
assert not caller_memento.path.exists()
assert not caller_memento.state_path.exists()
def test_direct_editor_deduplicates_editable_mementos_before_committing(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""One editable file produces one nested snapshot even when listed twice."""
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'new'")),
)
request = _request(source)
request.editable_files.append(source)
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert len(compiler_factory.build_calls[0].mementos) == 1
assert not (source.parent / ".mod.py~").exists()
def test_direct_editor_accepts_first_mechanical_candidate_as_repair_baseline(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""The first mechanical compiler error establishes a repair baseline without annealing."""
compiler_factory.results = [[StaticCompileError("bad candidate")], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'bad'"),
_replace("src/mod.py", "value = 'bad'", "value = 'good'"),
]
),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert [call["pre"] for call in compiler_factory.compile_calls] == [
["value = 'old'\n"],
["value = 'old'\n"],
["value = 'old'\n"],
]
assert [call["post"] for call in compiler_factory.compile_calls] == [
["value = 'old'\n"],
["value = 'bad'\n"],
["value = 'good'\n"],
]
repair_feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "==== BEGIN CURRENT COMPILER STATE ====" in repair_feedback
assert "bad candidate" in repair_feedback
assert "LAST REJECTED CANDIDATE FEEDBACK" not in repair_feedback
assert "rejected and rolled back" not in repair_feedback
assert len(_generator(editor).render_calls) == 2
assert _generator(editor).calls[0]["base_prompt"] == "baseline prompt"
assert _generator(editor).calls[1]["base_prompt"] == "baseline prompt"
assert len(_file_manager(editor).refresh_requests) == 1
def test_direct_editor_sends_equal_static_errors_through_annealing(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""An unchanged static-error count rolls back when annealing rejects it."""
compiler_factory.baseline_results = [[StaticCompileError("baseline compiler error")]]
compiler_factory.results = [[StaticCompileError("unchanged compiler error")], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'unchanged'"),
_replace("src/mod.py", "value = 'old'", "value = 'good'"),
]
),
)
# Reject the equal-count candidate through the existing annealing boundary.
with patch("band_aider.judgement.direct_code_editor.random.random", return_value=1.0):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
assert compiler_factory.compile_calls[2]["pre"] == ["value = 'old'\n"]
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "unchanged compiler error" in feedback
assert "This candidate was rejected and rolled back." in feedback
def test_direct_editor_commits_a_targeted_test_failure_fix_forward(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A static-clean runtime semantic failure commits outside annealing."""
compiler_factory.results = [
[TestRunCompileError("targeted test failed", test_name="test_value")],
[],
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'test-failing'"),
_replace("src/mod.py", "value = 'test-failing'", "value = 'passing'"),
]
),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'passing'\n"
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "targeted test failed" in feedback
assert "LAST REJECTED CANDIDATE FEEDBACK" not in feedback
assert compiler_factory.candidate_calls[2]["pre"] == [
"value = 'test-failing'\n"
]
def test_direct_editor_commits_unestablished_execution_with_prior_failure(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Unestablished execution commits while its prior named failure remains current."""
prior_error = TestRunCompileError(
"original test_value failure",
test_name="test_value",
error_message="original assertion",
stack_trace=("original trace",),
raw_output="original output",
)
execution_error = TestRunCompileError(
"pytest could not establish test_value",
error_message="pytest exited with return code 5.",
raw_output="\n",
)
compiler_factory.baseline_results = [[prior_error]]
compiler_factory.results = [[prior_error, execution_error], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'unestablished'"),
_replace("src/mod.py", "value = 'unestablished'", "value = 'done'"),
]
),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'done'\n"
assert compiler_factory.candidate_calls[2] == {
"pre": ["value = 'unestablished'\n"],
"post": ["value = 'done'\n"],
"previous_errors": [prior_error, execution_error],
}
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "original test_value failure" in feedback
assert "pytest could not establish test_value" not in feedback
assert "LAST REJECTED CANDIDATE FEEDBACK" not in feedback
def test_direct_editor_preserves_outer_failure_until_compiler_tracks_test_state(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Compiler-owned test state subsumes outer failure evidence after mechanical repair."""
compiler_factory.baseline_results = [[
StaticCompileError("baseline compiler error one"),
StaticCompileError("baseline compiler error two"),
]]
compiler_factory.results = [
[StaticCompileError("accepted compiler error")],
[TestRunCompileError("latest targeted test failure", test_name="test_value")],
[],
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'mechanical'"),
_replace("src/mod.py", "value = 'mechanical'", "value = 'test-failing'"),
_replace("src/mod.py", "value = 'test-failing'", "value = 'passing'"),
]
),
)
request = _request(source)
request.failure_error_message = "Beta outer test failure"
request.failure_stack_trace = ["outer traceback"]
request.failure_raw_output = "outer output"
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'passing'\n"
prompt_masks = _rendered_masks(editor)
assert [mask.mode for mask in prompt_masks] == [
AiderFailureMode.STATIC,
AiderFailureMode.STATIC,
AiderFailureMode.SEMANTIC,
]
prompt_requests = [mask.projected() for mask in prompt_masks]
assert [item.failure_error_message for item in prompt_requests] == [
"",
"",
"Beta outer test failure",
]
assert [item.failure_stack_trace for item in prompt_requests] == [
[],
[],
["outer traceback"],
]
assert [item.failure_raw_output for item in prompt_requests] == [
"",
"",
"outer output",
]
assert "baseline compiler error" in str(
_generator(editor).render_calls[0]["repair_feedback"]
)
assert "accepted compiler error" in str(
_generator(editor).render_calls[1]["repair_feedback"]
)
assert "latest targeted test failure" in str(
_generator(editor).render_calls[2]["repair_feedback"]
)
# Preserve the evidence-bearing request for every compiler construction.
for attempt in compiler_factory.build_calls:
assert attempt.request is request
assert request.failure_error_message == "Beta outer test failure"
assert request.failure_stack_trace == ["outer traceback"]
assert request.failure_raw_output == "outer output"
assert _rendered_renderer(editor, 0).failure_error_message == ""
def test_direct_editor_commits_partial_test_progress_with_its_catalog(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A reduced unresolved catalog and its source candidate advance together."""
alpha_error = TestRunCompileError(
"alpha still fails",
test_name="test_alpha",
)
beta_error = TestRunCompileError(
"beta still fails",
test_name="test_beta",
)
compiler_factory.baseline_results = [[alpha_error, beta_error]]
compiler_factory.results = [[alpha_error], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'done'"),
]
),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'done'\n"
assert compiler_factory.candidate_calls[1]["previous_errors"] == [
alpha_error,
beta_error,
]
assert compiler_factory.candidate_calls[2] == {
"pre": ["value = 'partial'\n"],
"post": ["value = 'done'\n"],
"previous_errors": [alpha_error],
}
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "alpha still fails" in feedback
assert "beta still fails" not in feedback
def test_direct_editor_rolls_back_a_static_regression_from_test_phase(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A static regression restores accepted source and test evidence deterministically."""
accepted_error = TestRunCompileError(
"accepted test failure",
test_name="test_value",
)
compiler_factory.baseline_results = [[accepted_error]]
compiler_factory.results = [[StaticCompileError("syntax regression")], []]
editor, source = _runtime_editor(
memory_root,
_RenderingStubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'broken'"),
_replace("src/mod.py", "value = 'old'", "value = 'fixed'"),
]
),
)
request = _request(source)
request.failure_error_message = "outer test failure"
request.failure_stack_trace = ["outer traceback"]
request.failure_raw_output = "outer output"
request.backlog_guidance = ["Repair the superseded original test assignment."]
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'fixed'\n"
assert compiler_factory.candidate_calls[2]["previous_errors"] == [accepted_error]
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "CURRENT COMPILER STATE" in feedback
assert "accepted test failure" in feedback
assert "LAST REJECTED CANDIDATE FEEDBACK" in feedback
assert "syntax regression" in feedback
assert "-value = 'old'" in feedback
assert "+value = 'broken'" in feedback
second_mask = _generator(editor).render_calls[1]["request"]
assert isinstance(second_mask, AiderEditRequestMask)
assert second_mask.mode is AiderFailureMode.SEMANTIC
second_request = second_mask.projected()
assert second_request.failure_error_message == "outer test failure"
assert second_request.failure_stack_trace == ["outer traceback"]
assert second_request.failure_raw_output == "outer output"
rendered_prompt = str(_generator(editor).calls[1]["base_prompt"])
assert "outer test failure" in rendered_prompt
assert "accepted test failure" in rendered_prompt
assert "syntax regression" in rendered_prompt
assert "Repair the superseded original test assignment." not in rendered_prompt
assert "-value = 'old'" in rendered_prompt
assert "+value = 'broken'" in rendered_prompt
assert "This candidate was rejected and rolled back." in rendered_prompt
# Preserve the outer evidence for compiler reconstruction after rollback.
for attempt in compiler_factory.build_calls:
assert attempt.request is request
def test_direct_editor_keeps_equal_test_failures_fix_forward(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""An unchanged runtime semantic failure cannot roll back static-clean source."""
alpha_error = TestRunCompileError("alpha fails", test_name="test_alpha")
beta_error = TestRunCompileError("beta fails", test_name="test_beta")
compiler_factory.baseline_results = [[alpha_error, beta_error]]
compiler_factory.results = [[alpha_error, beta_error], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'forward'"),
_replace("src/mod.py", "value = 'forward'", "value = 'done'"),
]
),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert compiler_factory.candidate_calls[2]["pre"] == ["value = 'forward'\n"]
assert compiler_factory.candidate_calls[2]["previous_errors"] == [
alpha_error,
beta_error,
]
def test_direct_editor_shows_initial_compiler_failures(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Every initial compiler failure is part of the repair assignment."""
compiler_factory.baseline_results = [[StaticCompileError("existing baseline error")]]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'good'")),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
feedback = str(_generator(editor).render_calls[0]["repair_feedback"])
assert "CURRENT COMPILER STATE" in feedback
assert "existing baseline error" in feedback
def test_direct_editor_preserves_first_pyright_location_in_feedback(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""The repair prompt retains the first Pyright error's structured location."""
compiler_factory.baseline_results = [[
TypeCheckError(
"first type mismatch",
path=Path("src/mod.py"),
line=7,
column=12,
),
TypeCheckError("later mismatch", path=Path("src/other.py"), line=3, column=1),
]]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'good'")),
)
request = _request(source)
request.failure_error_message = "first type mismatch"
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
feedback = str(_generator(editor).render_calls[0]["repair_feedback"])
assert "src/mod.py:7:12: first type mismatch" in feedback
assert "later mismatch" not in feedback
def test_direct_editor_scores_multiline_type_check_errors_as_diagnostics(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Two Pyright diagnostics are worse than one multiline baseline diagnostic."""
compiler_factory.baseline_results = [
[TypeCheckError("baseline line one\nbaseline line two\nbaseline line three")]
]
compiler_factory.results = [
[
TypeCheckError("first candidate"),
TypeCheckError("second candidate"),
],
[],
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'good'"),
]
),
)
# Select the accepted mechanical branch deterministically for this scenario.
with patch("band_aider.judgement.direct_code_editor.random.random", return_value=0.0):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert compiler_factory.compile_calls[2]["pre"] == ["value = 'old'\n"]
def test_direct_editor_limits_compiler_diagnostics_in_feedback(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Model feedback admits only the current ordered compiler diagnostic prefix."""
compiler_factory.results = [
[
TypeCheckError("type one"),
TypeCheckError("type two"),
ReferenceError("reference problem", ["bad"], ["available"], ["closest"]),
StaticCompileError("other problem"),
StaticCompileError("another problem"),
],
[],
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'bad'"),
_replace("src/mod.py", "value = 'bad'", "value = 'good'"),
]
),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "type one" in feedback
assert "type two" not in feedback
assert "reference problem" not in feedback
assert "other problem" not in feedback
assert "another problem" not in feedback
@pytest.mark.parametrize(
("word_count", "expected_tokens", "complete_evidence"),
((45713, 127997, False), (45714, 128000, False), (45715, 128002, True)),
ids=("below-128k", "exactly-128k", "over-128k"),
)
def test_direct_editor_exposes_complete_evidence_only_above_128k(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
word_count: int,
expected_tokens: int,
complete_evidence: bool,
) -> None:
"""Final prompt size expands diagnostics while preserving selected source and repair authority."""
generator = _StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'good'"))
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.baseline_results = [[
TypeCheckError("first type diagnostic", path=source, line=7, column=3),
TypeCheckError("second type diagnostic"),
StaticCompileError("last static diagnostic"),
TestRunCompileError("current runtime diagnostic\ncurrent runtime output tail"),
]]
request = _request(source)
request.failure_run_info = "supplied isolated test invocation"
request.failure_error_message = "supplied test assertion"
request.failure_stack_trace = ["first supplied frame", "last supplied frame"]
request.failure_raw_output = "supplied output beginning\nsupplied output tail\n"
selected_source = "CURRENT FILE EVIDENCE\nvalue = 'old'\n⋮\n"
source_words = TokenBudgetEstimator(tokens_per_word=1).estimate(selected_source)
composed_prompt = selected_source + "context " * (word_count - source_words)
assert TokenBudgetEstimator().estimate(composed_prompt) == expected_tokens
publisher = _RecordingPublisher()
with (
patch.object(PromptLocalization, "localize", return_value=composed_prompt),
status_publisher_scope(publisher),
):
result = editor.edit(request)
sent_prompt = str(generator.calls[0]["base_prompt"])
assert sent_prompt.startswith(composed_prompt)
assert sent_prompt.count(selected_source) == 1
assert len(generator.render_calls) == 1
masked_request = generator.render_calls[0]["request"]
assert isinstance(masked_request, AiderEditRequestMask)
assert masked_request.mode is AiderFailureMode.STATIC
assert masked_request.projected().failure_error_message == ""
awaiting = tuple(filter(
lambda event: event is not None and event.phase == "awaiting model",
map(lambda event: cast(SupremeStatusDTO, event).direct_code_editor, filter(
lambda event: isinstance(event, SupremeStatusDTO), publisher.events,
)),
))
assert len(awaiting) == 1
assert awaiting[0] is not None
# Above the threshold, the generator receives all diagnostics and the unmasked captured failure.
if complete_evidence:
assert f"{source}:7:3: first type diagnostic" in sent_prompt
assert "second type diagnostic" in sent_prompt
assert "last static diagnostic" in sent_prompt
assert "current runtime output tail" in sent_prompt
assert request.failure_run_info in sent_prompt
assert request.failure_error_message in sent_prompt
assert "\n".join(request.failure_stack_trace) in sent_prompt
assert request.failure_raw_output in sent_prompt
assert awaiting[0].model_visible_compiler_error_count == 4
# Small and exactly-at-budget prompts retain their existing evidence projection.
else:
assert sent_prompt == composed_prompt
assert awaiting[0].model_visible_compiler_error_count == 1
assert request.failure_stack_trace == ["first supplied frame", "last supplied frame"]
assert request.failure_raw_output == "supplied output beginning\nsupplied output tail\n"
assert result.changed_paths == [source]
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
def test_direct_editor_rechecks_evidence_visibility_for_each_prompt(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A smaller prompt after an accepted baseline restores normal evidence visibility."""
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'good'"),
])
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.baseline_results = [[
TypeCheckError("initial diagnostic one"),
TypeCheckError("initial diagnostic two"),
TypeCheckError("initial diagnostic three"),
]]
compiler_factory.results = [[
TypeCheckError("remaining diagnostic one"),
TypeCheckError("remaining diagnostic two"),
], []]
request = _request(source)
request.failure_error_message = "supplied failure remains captured"
request.failure_raw_output = "complete supplied output"
oversized_prompt = "context " * 45715
smaller_prompt = "selected current source\nvalue = 'partial'\n⋮\n"
publisher = _RecordingPublisher()
with (
patch.object(PromptLocalization, "localize", side_effect=(oversized_prompt, smaller_prompt)),
status_publisher_scope(publisher),
):
result = editor.edit(request)
assert len(generator.calls) == 2
assert "initial diagnostic three" in str(generator.calls[0]["base_prompt"])
assert request.failure_raw_output in str(generator.calls[0]["base_prompt"])
assert generator.calls[1]["base_prompt"] == smaller_prompt
visible_counts = tuple(map(
lambda event: event.model_visible_compiler_error_count if event is not None else None,
filter(
lambda event: event is not None and event.phase == "awaiting model",
map(lambda event: cast(SupremeStatusDTO, event).direct_code_editor, filter(
lambda event: isinstance(event, SupremeStatusDTO), publisher.events,
)),
),
))
assert visible_counts == (3, 1)
assert result.changed_paths == [source]
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
def test_direct_editor_preserves_already_complete_oversized_prompt(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Complete evidence already in the composed prompt is not appended a second time."""
generator = _StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'good'"))
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.baseline_results = [[
StaticCompileError("first diagnostic"),
StaticCompileError("last diagnostic"),
]]
request = _request(source)
request.failure_run_info = "isolated invocation"
request.failure_error_message = "captured assertion"
request.failure_stack_trace = ["captured frame"]
request.failure_raw_output = "captured output\n"
complete_errors = generator.render_section(
"CURRENT COMPILER STATE", depth=2, escape=True,
)("first diagnostic\nlast diagnostic")
complete_failure = generator.render_section(
"FAILURE EVIDENCE", depth=2, escape=True,
)("Run information:\nisolated invocation\nError message: captured assertion\n"
"Failure evidence:\ncaptured frame\nRaw failure output:\ncaptured output\n")
composed_prompt = "context " * 45715 + complete_errors + "\n\n" + complete_failure
with patch.object(PromptLocalization, "localize", return_value=composed_prompt):
result = editor.edit(request)
assert generator.calls[0]["base_prompt"] == composed_prompt
assert result.changed_paths == [source]
def test_direct_editor_widens_visible_compiler_errors_geometrically(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Every third unaccepted compiler block doubles visible compiler diagnostics."""
baseline_errors: list[CompileError] = [
TypeCheckError("baseline type mismatch 1"),
TypeCheckError("baseline type mismatch 2"),
TypeCheckError("baseline type mismatch 3"),
TypeCheckError("baseline type mismatch 4"),
TypeCheckError("baseline type mismatch 5"),
TypeCheckError("baseline type mismatch 6"),
TypeCheckError("baseline type mismatch 7"),
TypeCheckError("baseline type mismatch 8"),
]
rejected_errors: list[CompileError] = [
*baseline_errors,
TypeCheckError("candidate extra mismatch"),
]
compiler_factory.baseline_results = [baseline_errors]
compiler_factory.results = [rejected_errors] * 8 + [[]]
responses: list[str] = []
# Each rejected candidate starts from the unchanged committed baseline.
for number in range(1, 9):
responses.append(
_replace("src/mod.py", "value = 'old'", f"value = 'rejected-{number}'")
)
responses.append(_replace("src/mod.py", "value = 'old'", "value = 'good'"))
generator = _StubGenerator(responses)
editor, source = _runtime_editor(memory_root, generator)
request = _request(source)
request.failure_error_message = "\n".join(error.message for error in baseline_errors)
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
permits = (1, 1, 2, 2, 2, 4, 4, 4, 8)
# Each rendered compiler-state section proves the allowance for that repair block.
for index, permit in enumerate(permits):
feedback = str(generator.render_calls[index]["repair_feedback"])
compiler_state = feedback.split("BEGIN CURRENT COMPILER STATE", 1)[1].split(
"END CURRENT COMPILER STATE", 1
)[0]
# The compiler reports diagnostics in order, so each permit exposes its prefix.
for number in range(1, permit + 1):
assert f"baseline type mismatch {number}" in compiler_state
# The next diagnostic remains withheld until a later widening block.
if permit < len(baseline_errors):
assert f"baseline type mismatch {permit + 1}" not in compiler_state
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
def test_direct_editor_resets_visible_compiler_errors_after_accepted_baseline(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""An accepted intermediate baseline restores one visible compiler diagnostic."""
baseline_errors: list[CompileError] = [
TypeCheckError("initial type mismatch one"),
TypeCheckError("initial type mismatch two"),
]
rejected_errors: list[CompileError] = [
*baseline_errors,
TypeCheckError("candidate extra mismatch"),
]
accepted_errors: list[CompileError] = [TypeCheckError("accepted type mismatch one")]
compiler_factory.baseline_results = [baseline_errors]
compiler_factory.results = [
rejected_errors,
rejected_errors,
accepted_errors,
[],
]
generator = _StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'rejected-one'"),
_replace("src/mod.py", "value = 'old'", "value = 'rejected-two'"),
_replace("src/mod.py", "value = 'old'", "value = 'accepted'"),
_replace("src/mod.py", "value = 'accepted'", "value = 'good'"),
]
)
editor, source = _runtime_editor(memory_root, generator)
request = _request(source)
request.failure_error_message = "\n".join(error.message for error in baseline_errors)
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
widened_state = str(generator.render_calls[2]["repair_feedback"])
widened_state = widened_state.split("BEGIN CURRENT COMPILER STATE", 1)[1].split(
"END CURRENT COMPILER STATE", 1
)[0]
reset_state = str(generator.render_calls[3]["repair_feedback"])
reset_state = reset_state.split("BEGIN CURRENT COMPILER STATE", 1)[1].split(
"END CURRENT COMPILER STATE", 1
)[0]
assert "initial type mismatch one" in widened_state
assert "initial type mismatch two" in widened_state
assert "accepted type mismatch one" in reset_state
assert "accepted type mismatch two" not in reset_state
assert changed == [source]
def test_direct_editor_renders_compile_cards_when_static_gate_has_not_cleared(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Static gate progress exposes both initial failures and their reference cards."""
compiler_factory.baseline_results = [
[TypeCheckError('"_FileManager" is incompatible with "FileManager"')]
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
_replace(
"src/mod.py",
"import internal_mismatch\nvalue = 'old'",
"value = 'good'",
)
),
)
source.write_text("import internal_mismatch\nvalue = 'old'\n", encoding="utf-8")
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
feedback = str(_generator(editor).render_calls[0]["repair_feedback"])
assert "CURRENT COMPILER STATE" in feedback
assert '"_FileManager" is incompatible' in feedback
assert "COMPILE REFERENCE CARDS" in feedback
assert "REFERENCE: tests.support.file_manager._FileManager" in feedback
def test_direct_editor_renders_reference_card_section_for_test_run_only_state(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Test execution state neither creates nor suppresses source-derived cards."""
compiler_factory.baseline_results = [
[TestRunCompileError("test failed", test_name="test_value")]
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'good'")),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
feedback = str(_generator(editor).render_calls[0]["repair_feedback"])
assert "test failed" in feedback
assert "COMPILE REFERENCE CARDS" in feedback
assert "No reference cards resolved." in feedback
def test_direct_editor_reference_cards_follow_candidate_repair_state(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A candidate-introduced static error retains its current repair-state reference cards."""
compiler_factory.baseline_results = [
[TypeCheckError("accepted mismatch")]
]
compiler_factory.results = [
[
TypeCheckError("candidate mismatch"),
StaticCompileError("candidate extra regression"),
],
[],
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace(
"src/mod.py",
"import accepted_dependency\nvalue = 'old'",
"import rejected_dependency\nvalue = 'bad'",
),
_replace(
"src/mod.py",
"import rejected_dependency\nvalue = 'bad'",
"value = 'good'",
),
]
),
)
source.write_text("import accepted_dependency\nvalue = 'old'\n", encoding="utf-8")
# Select the accepted mechanical branch deterministically for this scenario.
with patch("band_aider.judgement.direct_code_editor.random.random", return_value=0.0):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "accepted mismatch" not in feedback
assert "candidate mismatch" in feedback
assert "REFERENCE: accepted.types.AcceptedType" not in feedback
assert "REFERENCE: rejected.types.RejectedType" in feedback
def test_partial_mutation_that_compiles_is_accepted(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A partial patch is accepted when the changed source compiles."""
class _PartialPatcher(FilePatcher):
"""Apply one partial in-memory candidate before returning retry evidence."""
def __init__(self) -> None:
"""Bind the nominal patcher to this test's in-memory repository."""
super().__init__(memory_root, repo_exec=memory_root.repository)
def apply(
self,
raw_output: str,
*,
editable_files: list[Path],
readonly_files: list[Path],
) -> CodeEditorResult:
"""Mutate the candidate and return patcher-owned partial evidence."""
del raw_output
del readonly_files
memory_root.repository.write_text(editable_files[0], "value = 'partial'\n")
raise QuickRetryError(
"partial",
changed_paths=editable_files,
summary="partially applied",
issues=["SEARCH text was not found exactly once in src/mod.py"],
)
source = memory_root / "src" / "mod.py"
source.parent.mkdir(parents=True)
source.write_text("value = 'old'\n", encoding="utf-8")
editor = DirectCodeEditor(
token_generator=_StubGenerator(["FILE: src/mod.py", "NO_PATCH: done"]),
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
file_patcher=_PartialPatcher(),
repo_root=memory_root,
)
editor.file_manager = _ManagedFileManager(memory_root, ["src/mod.py"])
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert "PARTIAL PATCH APPLIED" in summary
assert "SEARCH text was not found exactly once in src/mod.py" in summary
assert len(_generator(editor).calls) == 1
assert compiler_factory.compile_calls[-1]["pre"] == ["value = 'old'\n"]
assert compiler_factory.compile_calls[-1]["post"] == ["value = 'partial'\n"]
def test_partial_patch_with_compiler_errors_establishes_a_repair_baseline(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A partial first mechanical error is retained as the editor's repair baseline."""
class _PartialThenNormalPatcher(FilePatcher):
"""Return one partial candidate before production in-memory application."""
def __init__(self) -> None:
"""Bind the nominal patcher to this test's in-memory repository."""
super().__init__(memory_root, repo_exec=memory_root.repository)
self.calls = 0
def apply(
self,
raw_output: str,
*,
editable_files: list[Path],
readonly_files: list[Path],
) -> CodeEditorResult:
"""Return partial evidence once, then use normal memory-backed application."""
self.calls += 1
if self.calls == 1:
memory_root.repository.write_text(editable_files[0], "value = 'partial'\n")
raise QuickRetryError(
"partial",
changed_paths=editable_files,
summary="partially applied",
issues=["SEARCH text was not found exactly once in src/mod.py"],
)
return super().apply(
raw_output,
editable_files=editable_files,
readonly_files=readonly_files,
)
compiler_factory.results = [[StaticCompileError("partial is invalid")], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'good'"),
]
),
)
editor.file_patcher = _PartialThenNormalPatcher()
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
assert [call["pre"] for call in compiler_factory.compile_calls] == [
["value = 'old'\n"],
["value = 'old'\n"],
["value = 'old'\n"],
]
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "partial is invalid" in feedback
assert "LAST REJECTED CANDIDATE FEEDBACK" not in feedback
assert len(_file_manager(editor).refresh_requests) == 1
def test_partial_candidate_refreshes_its_changed_class_meta_before_next_repair(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""An accepted QuickRetry mutation becomes fresh renderer evidence before retrying."""
class _PartialThenNormalPatcher(FilePatcher):
"""Return one refreshable partial candidate before normal application."""
def __init__(self) -> None:
"""Bind the nominal patcher to this test's in-memory repository."""
super().__init__(memory_root, repo_exec=memory_root.repository)
self.calls = 0
def apply(
self,
raw_output: str,
*,
editable_files: list[Path],
readonly_files: list[Path],
) -> CodeEditorResult:
"""Return partial evidence once, then use normal memory-backed application."""
self.calls += 1
if self.calls == 1:
memory_root.repository.write_text(editable_files[0], "value = 'partial'\n")
raise QuickRetryError(
"partial",
changed_paths=editable_files,
summary="partially applied",
issues=["second SEARCH block did not apply"],
)
return super().apply(
raw_output,
editable_files=editable_files,
readonly_files=readonly_files,
)
compiler_factory.results = [[StaticCompileError("partial candidate error")], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'good'"),
]
),
)
editor.file_patcher = _PartialThenNormalPatcher()
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
assert _file_manager(editor).refresh_requests == [
(
_file_manager(editor).current_src("src/mod.py"),
"ORIGINAL COT: IMMUTABLE STABILIZATION TARGET\n"
"====== BEGIN ORIGINAL STABILIZATION GOAL ======\n"
"Original stabilization goal.\n"
"====== END ORIGINAL STABILIZATION GOAL ======\n\n"
"ROLE AND EDIT BOUNDARIES\n"
"Update the value.",
"partial candidate error",
)
]
assert len(_generator(editor).render_calls) == 2
def test_direct_editor_retries_past_the_former_compiler_repair_cap(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Compiler rejection keeps retrying until a mechanically valid candidate arrives."""
rejections: list[list[CompileError]] = [[
StaticCompileError("bad candidate"),
StaticCompileError("candidate regression"),
]] * 6
compiler_factory.baseline_results = [[StaticCompileError("baseline compiler error")]]
compiler_factory.results = rejections
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'bad-1'"),
_replace("src/mod.py", "value = 'old'", "value = 'bad-2'"),
_replace("src/mod.py", "value = 'old'", "value = 'bad-3'"),
_replace("src/mod.py", "value = 'old'", "value = 'bad-4'"),
_replace("src/mod.py", "value = 'old'", "value = 'bad-5'"),
_replace("src/mod.py", "value = 'old'", "value = 'bad-6'"),
_replace("src/mod.py", "value = 'old'", "value = 'good'"),
],
),
)
# Reject every worse candidate through annealing before the clean repair arrives.
with patch("band_aider.judgement.direct_code_editor.random.random", return_value=1.0):
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert len(compiler_factory.compile_calls) == 8
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
assert _edit_result.records == [EditorRecord("", "value = 'old'", "value = 'good'")]
assert not (source.parent / ".mod.py~").exists()
def test_direct_editor_renders_current_feedback_for_each_mechanical_candidate(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Each candidate render receives the repair evidence current for that iteration."""
compiler_factory.baseline_results = [[StaticCompileError("baseline compiler error")]]
compiler_factory.results = [
[StaticCompileError("first compiler rejection"), StaticCompileError("first extra error")],
[StaticCompileError("second compiler rejection"), StaticCompileError("second extra error")],
[],
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'first-candidate'"),
_replace("src/mod.py", "value = 'first-candidate'", "value = 'second-candidate'"),
_replace("src/mod.py", "value = 'second-candidate'", "value = 'good'"),
]
),
)
# Select the accepted mechanical branch deterministically for this scenario.
with patch("band_aider.judgement.direct_code_editor.random.random", return_value=0.0):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
initial_feedback = str(_generator(editor).render_calls[0]["repair_feedback"])
first_feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
latest_feedback = str(_generator(editor).render_calls[2]["repair_feedback"])
assert "baseline compiler error" in initial_feedback
assert "first compiler rejection" not in initial_feedback
assert "first compiler rejection" in first_feedback
assert "first-candidate" not in first_feedback
assert "second compiler rejection" not in first_feedback
assert "second compiler rejection" in latest_feedback
assert "second-candidate" not in latest_feedback
assert "first compiler rejection" not in latest_feedback
assert "first-candidate" not in latest_feedback
assert len(_generator(editor).render_calls) == len(_generator(editor).calls) == 3
def test_direct_editor_records_keep_complete_patch_payload(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Retained records preserve complete source while summaries cannot replay patch control."""
replacement = "\n".join(f"line-{index}" for index in range(60))
raw_output = "\n".join(
[
"SUMMARY: beginning marker",
"FILE: src/mod.py",
"<<<<<<< SEARCH",
"value = 'old'",
"=======",
replacement,
">>>>>>> REPLACE",
]
)
editor, source = _runtime_editor(memory_root, _StubGenerator(raw_output))
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert _edit_result.records == [EditorRecord("beginning marker", "value = 'old'", replacement)]
assert "<<<<<<< SEARCH" not in summary
assert ">>>>>>> REPLACE" not in summary
assert compiler_factory.compile_calls
def test_editor_keeps_static_work_logs_but_discards_a_rejected_candidate(
memory_root: MemoryPath, compiler_factory: _RecordingCompilerFactory,
) -> None:
"""The next static repair sees retained history, and rollback removes only the rejected attempt."""
compiler_factory.baseline_results = [[StaticCompileError("one"), StaticCompileError("two")]]
compiler_factory.results = [
[StaticCompileError("one")],
[StaticCompileError("one"), StaticCompileError("regression")],
[],
]
generator = _StubGenerator([
"SUMMARY: first static repair\n" + _replace("src/mod.py", "value = 'old'", "value = 'partial'"),
"SUMMARY: rejected repair\n" + _replace("src/mod.py", "value = 'partial'", "value = 'bad'"),
"SUMMARY: final repair\n" + _replace("src/mod.py", "value = 'partial'", "value = 'good'"),
])
editor, source = _runtime_editor(memory_root, generator)
request = _request(source)
with patch("band_aider.judgement.direct_code_editor.random.random", return_value=1.0), patch.object(
_StubGenerator, "render_prompt", SearchReplaceTokenGenerator.render_prompt,
):
result = editor.edit(request)
assert source.read_text() == "value = 'good'\n"
assert result.records == [
EditorRecord("first static repair", "value = 'old'", "value = 'partial'"),
EditorRecord("final repair", "value = 'partial'", "value = 'good'"),
]
assert request.editor_records == result.records
assert "Work Logs\n\nSummary:\nfirst static repair" in str(generator.calls[1]["base_prompt"])
third_prompt = str(generator.calls[2]["base_prompt"])
work_logs = third_prompt.split("Work Logs", 1)[1].split("END BACKLOG GUIDANCE", 1)[0]
assert "first static repair" in work_logs
assert "rejected repair" not in work_logs
@pytest.mark.parametrize("terminal", ["NO_PATCH: production owns the next repair", ""])
def test_editor_whole_rollback_restores_its_incoming_work_logs(
memory_root: MemoryPath, compiler_factory: _RecordingCompilerFactory, terminal: str,
) -> None:
"""Whole-edit rollback removes its intermediate history while retaining earlier surviving work."""
remaining: list[CompileError] = [StaticCompileError("still broken")]
compiler_factory.results = [remaining]
editor, source = _runtime_editor(memory_root, _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'partial'"), terminal,
]))
earlier = EditorRecord("earlier retained edit", "value = 0", "value = 'old'")
request = _request(source)
request.editor_records = [earlier]
result = editor.edit(request)
assert source.read_text() == "value = 'old'\n"
assert result.changed_paths == []
assert result.records == [earlier]
assert request.editor_records == [earlier]
def test_editor_records_only_successful_blocks_of_an_accepted_partial_patch(
memory_root: MemoryPath, compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A partial response contributes the block that was applied, without its unmatched sibling."""
editor, source = _runtime_editor(memory_root, _StubGenerator(
"SUMMARY: repair value\n" + _replace("src/mod.py", "value = 'old'", "value = 'good'")
+ "\n" + _replace("src/mod.py", "absent = 0", "absent = 1")
))
result = editor.edit(_request(source))
assert source.read_text() == "value = 'good'\n"
assert result.records == [EditorRecord("repair value", "value = 'old'", "value = 'good'")]
def test_direct_editor_fails_closed_without_compilation_context(memory_root: MemoryPath) -> None:
"""Mutation never begins when the edit has no workflow context."""
source = memory_root / "src" / "mod.py"
source.parent.mkdir(parents=True)
source.write_text("value = 'old'\n", encoding="utf-8")
generator = _StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'new'"))
editor = DirectCodeEditor(
token_generator=generator,
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
file_patcher=FilePatcher(memory_root, repo_exec=memory_root.repository),
repo_root=memory_root,
)
editor.file_manager = _ManagedFileManager(memory_root, ["src/mod.py"])
_edit_result = editor.edit(
AiderEditRequest("Change it.", editable_files=[source], readonly_files=[])
)
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == []
assert "owning workflow context" in summary
assert generator.calls == []
assert source.read_text(encoding="utf-8") == "value = 'old'\n"
def test_direct_editor_preserves_patch_repair_feedback(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Ordinary patch failures remain in the editor's local repair loop."""
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "missing", "value = 'bad'"),
_replace("src/mod.py", "value = 'old'", "value = 'new'"),
]
),
)
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "==== BEGIN PATCH APPLICATION FEEDBACK ====" in feedback
assert "Patch application stage: PATCH APPLICATION DID NOT APPLY" in feedback
assert compiler_factory.compile_calls[-1]["post"] == ["value = 'new'\n"]
def test_unexpected_patcher_failure_rolls_back_the_candidate_memento(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""An unexpected patcher failure cannot leave a candidate mutation or snapshot."""
class _FailingPatcher(FilePatcher):
"""Raise one unexpected patcher failure after a visible in-memory mutation."""
def __init__(self) -> None:
"""Bind the nominal patcher to this test's in-memory repository."""
super().__init__(memory_root, repo_exec=memory_root.repository)
def apply(
self,
raw_output: str,
*,
editable_files: list[Path],
readonly_files: list[Path],
) -> CodeEditorResult:
"""Raise the non-retry failure after changing candidate state."""
del raw_output
del readonly_files
memory_root.repository.write_text(editable_files[0], "value = 'broken'\n")
raise RuntimeError("patcher transport failed")
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'broken'")),
)
editor.file_patcher = _FailingPatcher()
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == []
assert "patcher transport failed" in summary
assert "-value = 'old'" in summary
assert "+value = 'broken'" in summary
assert source.read_text(encoding="utf-8") == "value = 'old'\n"
assert not (source.parent / ".mod.py~").exists()
assert len(compiler_factory.compile_calls) == 1
def test_prepare_edit_scope_still_uses_discovery_membership(memory_root: MemoryPath) -> None:
"""The compile gate does not change language-neutral scope filtering."""
source = memory_root / "src" / "main.rb"
test = memory_root / "spec" / "main_spec.rb"
ignored = memory_root / "notes.py"
editor = DirectCodeEditor(
token_generator=_StubGenerator("NO_PATCH: unused"),
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
file_patcher=FilePatcher(memory_root, repo_exec=memory_root.repository),
repo_root=memory_root,
)
editor.file_manager = _ManagedFileManager(
memory_root,
["src/main.rb"],
["spec/main_spec.rb"],
)
editable, readonly, _ = editor.prepare_edit_scope(
AiderEditRequest("scope", [source, ignored], [test])
)
assert editable == [source]
assert readonly == [test]
def test_direct_editor_can_commit_a_worse_compiler_state_for_repair(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Annealing remains available only for a worse mechanical compiler result."""
compiler_factory.baseline_results = [[StaticCompileError("baseline compiler error")]]
compiler_factory.results = [
[StaticCompileError("first"), StaticCompileError("second")],
[],
]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'partial'"),
_replace("src/mod.py", "value = 'partial'", "value = 'good'"),
]
),
)
# Select the accepted mechanical branch deterministically for this scenario.
with patch("band_aider.judgement.direct_code_editor.random.random", return_value=0.0):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'good'\n"
assert compiler_factory.compile_calls[2]["pre"] == ["value = 'old'\n"]
assert "CURRENT COMPILER STATE" in str(
_generator(editor).render_calls[1]["repair_feedback"]
)
assert _file_manager(editor).refresh_requests == [
(
_file_manager(editor).current_src("src/mod.py"),
"ORIGINAL COT: IMMUTABLE STABILIZATION TARGET\n"
"====== BEGIN ORIGINAL STABILIZATION GOAL ======\n"
"Original stabilization goal.\n"
"====== END ORIGINAL STABILIZATION GOAL ======\n\n"
"ROLE AND EDIT BOUNDARIES\n"
"Update the value.",
"first",
)
]
def test_changed_class_metas_uses_only_mementos_with_actual_diffs(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Stale propagation uses canonical ClassMeta, not editable path strings."""
source_a = memory_root / "src" / "a.py"
source_b = memory_root / "src" / "b.py"
source_a.parent.mkdir(parents=True)
source_a.write_text("value = 'old-a'\n", encoding="utf-8")
source_b.write_text("value = 'old-b'\n", encoding="utf-8")
editor = DirectCodeEditor(
token_generator=_StubGenerator("NO_PATCH: unused"),
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
file_patcher=FilePatcher(memory_root, repo_exec=memory_root.repository),
repo_root=memory_root,
)
editor.file_manager = _ManagedFileManager(memory_root, ["src/a.py", "src/b.py"])
del compiler_factory
mementos = editor._capture_mementos([source_a, source_b])
source_a.write_text("value = 'new-a'\n", encoding="utf-8")
assert editor._changed_class_metas(mementos) == [
_file_manager(editor).current_src("src/a.py")
]
assert all(memento.diff for memento in mementos[:1])
assert not mementos[1].diff
for memento in reversed(mementos):
memento.rollback()
def test_semantic_watermark_survives_a_rolled_back_static_candidate(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A rejected static candidate cannot lower an attained semantic workflow level."""
static_error = TypeCheckError("current type mismatch")
test_error = TestRunCompileError("current test failure", test_name="test_value")
compiler_factory.results = [[static_error], [test_error], []]
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'static'"),
_replace("src/mod.py", "value = 'old'", "value = 'semantic'"),
_replace("src/mod.py", "value = 'semantic'", "value = 'done'"),
]
),
)
request = _request(source)
request.backlog_guidance = ["Repair test_value only."]
request.failure_error_message = "AssertionError: wrong value"
request.failure_stack_trace = ["tests/test_value.py:7: AssertionError"]
request.failure_raw_output = "FAILED tests/test_value.py::test_value"
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
masks = _rendered_masks(editor)
assert [mask.mode for mask in masks] == [
AiderFailureMode.SEMANTIC,
AiderFailureMode.SEMANTIC,
AiderFailureMode.SEMANTIC,
]
second_feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "current type mismatch" in second_feedback
assert "This candidate was rejected and rolled back." in second_feedback
assert masks[1].projected().failure_error_message == "AssertionError: wrong value"
assert all(mask.original is request for mask in masks)
assert request.failure_error_message == "AssertionError: wrong value"
assert request.failure_stack_trace == ["tests/test_value.py:7: AssertionError"]
assert request.failure_raw_output == "FAILED tests/test_value.py::test_value"
assert changed == [source]
assert source.read_text(encoding="utf-8") == "value = 'done'\n"
def test_ambiguous_response_writes_nothing_and_skips_candidate_compile(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A duplicate SEARCH rejects every response block before compiler admission."""
generator = _StubGenerator(
[
"\n".join(
[
_replace("src/mod.py", "value = 'old'", "value = 'wrong'"),
_replace("src/mod.py", "return self.value", "return 'ambiguous'"),
]
),
_replace(
"src/mod.py",
" def first(self):\n return self.value",
" def first(self):\n return 'fixed'",
),
]
)
boundary_client = ScriptedCompletionClient(())
generator.clients = (boundary_client,)
editor, source = _runtime_editor(
memory_root,
generator,
)
source.write_text(
"value = 'old'\n\n"
"class Example:\n"
" def first(self):\n"
" return self.value\n\n"
" def second(self):\n"
" return self.value\n",
encoding="utf-8",
)
events: list[object] = []
publisher = _RecordingPublisher(events)
graph = Provenance.open(repo_root=memory_root, repo_exec=memory_root.repository)
item = InspectionBacklog(
focused_spotlight=_source_meta(editor, "src/mod.py"),
summary="repair the focused source",
)
graph.start(Phase.ALPHA, item)
with provenance.bind(graph):
with provenance.item_scope(Phase.ALPHA, item):
with status_publisher_scope(publisher):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text(encoding="utf-8") == (
"value = 'old'\n\n"
"class Example:\n"
" def first(self):\n"
" return 'fixed'\n\n"
" def second(self):\n"
" return self.value\n"
)
assert len(compiler_factory.compile_calls) == 2
ambiguity = str(_generator(editor).render_calls[1]["repair_feedback"])
assert "SEARCH AMBIGUITY" in ambiguity
assert "MATCH COUNT: 2" in ambiguity
assert "MATCH 1:" in ambiguity
assert "MATCH 2:" in ambiguity
assert "Did you mean one of these 2 locations?" in ambiguity
assert ambiguity.count("<<<<<<< SEARCH") == 2
assert ambiguity.count(">>>>>>> REPLACE") == 2
assert "Previous model patch output" not in ambiguity
assert "Do not answer with only a match number." in ambiguity
assert "Every line added while expanding a choice appears identically" in ambiguity
retry = [
event.direct_code_editor
for event in events
if isinstance(event, SupremeStatusDTO)
and event.direct_code_editor is not None
and event.direct_code_editor.phase == "applying patch"
and event.direct_code_editor.patch_outcome == "not applied"
]
assert boundary_client.prompts == []
assert retry[-1].unresolved_patch_issue_count == 1
assert retry[-1].decision_branch == "retrying"
assert retry[-1].decision_reason == (
"SEARCH payload is ambiguous and no candidate changes exist"
)
def test_direct_editor_presents_patcher_owned_retry_policy(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""The editor orchestrates a retry without interpreting patcher issue prose."""
class _FeedbackPatcher(FilePatcher):
"""Expose one patcher-owned retry result before applying a unique patch."""
def __init__(self) -> None:
"""Retain the real patcher used after the owned retry evidence is presented."""
super().__init__(memory_root, repo_exec=memory_root.repository)
self.calls = 0
def apply(
self,
raw_output: str,
*,
editable_files: list[Path],
readonly_files: list[Path],
) -> CodeEditorResult:
"""Return patcher policy without requiring the caller to classify its issue."""
self.calls += 1
# Supply an issue phrase whose presentation must not control editor behavior.
if self.calls == 1:
raise QuickRetryError(
"owned retry",
issues=["SEARCH text appeared more than once in decoy.py"],
retry_feedback="PATCHER-OWNED RETRY FEEDBACK",
decision_reason="patcher-owned retry reason",
)
return super().apply(
raw_output,
editable_files=editable_files,
readonly_files=readonly_files,
)
editor, source = _runtime_editor(
memory_root,
_StubGenerator(
[
_replace("src/mod.py", "missing", "value = 'wrong'"),
_replace("src/mod.py", "value = 'old'", "value = 'new'"),
]
),
)
editor.file_patcher = _FeedbackPatcher()
events: list[object] = []
publisher = _RecordingPublisher(events)
with status_publisher_scope(publisher):
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
feedback = str(_generator(editor).render_calls[1]["repair_feedback"])
retry = [
event.direct_code_editor
for event in events
if isinstance(event, SupremeStatusDTO)
and event.direct_code_editor is not None
and event.direct_code_editor.phase == "applying patch"
and event.direct_code_editor.patch_outcome == "not applied"
]
assert changed == [source]
assert "PATCHER-OWNED RETRY FEEDBACK" in feedback
assert "Previous model patch output" not in feedback
assert retry[-1].decision_reason == "patcher-owned retry reason"
assert len(compiler_factory.compile_calls) == 2
def test_direct_editor_widens_visible_compiler_errors_after_patch_retries(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Patch retries widen one episode's visible compiler diagnostics only."""
class _RetryingPatcher(FilePatcher):
"""Reject two candidate blocks before normal later application."""
def __init__(self) -> None:
"""Retain the normal patcher after the owned retry sequence."""
super().__init__(memory_root, repo_exec=memory_root.repository)
self.calls = 0
def apply(
self,
raw_output: str,
*,
editable_files: list[Path],
readonly_files: list[Path],
) -> CodeEditorResult:
"""Return two editor-validation retries before normal application."""
self.calls += 1
# Keep source unchanged until the retry cadence has reached its third block.
if self.calls <= 2:
raise QuickRetryError(
"retry required",
issues=["SEARCH text is stale"],
retry_feedback="PATCHER RETRY FEEDBACK",
decision_reason="patcher requested retry",
)
return super().apply(
raw_output,
editable_files=editable_files,
readonly_files=readonly_files,
)
baseline_errors: list[CompileError] = [
TypeCheckError("patch retry type mismatch one"),
TypeCheckError("patch retry type mismatch two"),
]
compiler_factory.baseline_results = [baseline_errors, baseline_errors]
compiler_factory.results = [[]]
generator = _StubGenerator(
[
_replace("src/mod.py", "value = 'old'", "value = 'retry-one'"),
_replace("src/mod.py", "value = 'old'", "value = 'retry-two'"),
"NO_PATCH: stop the first repair episode",
_replace("src/mod.py", "value = 'old'", "value = 'good'"),
]
)
editor, source = _runtime_editor(memory_root, generator)
editor.file_patcher = _RetryingPatcher()
request = _request(source)
# Compiler-only work can end without a patch; incoming remediation cannot.
_edit_result = editor.edit(request)
unchanged, _ = _edit_result.changed_paths, _edit_result.summary
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
first_state = str(generator.render_calls[0]["repair_feedback"])
first_state = first_state.split("BEGIN CURRENT COMPILER STATE", 1)[1].split(
"END CURRENT COMPILER STATE", 1
)[0]
second_state = str(generator.render_calls[1]["repair_feedback"])
second_state = second_state.split("BEGIN CURRENT COMPILER STATE", 1)[1].split(
"END CURRENT COMPILER STATE", 1
)[0]
widened_state = str(generator.render_calls[2]["repair_feedback"])
widened_state = widened_state.split("BEGIN CURRENT COMPILER STATE", 1)[1].split(
"END CURRENT COMPILER STATE", 1
)[0]
reset_state = str(generator.render_calls[3]["repair_feedback"])
reset_state = reset_state.split("BEGIN CURRENT COMPILER STATE", 1)[1].split(
"END CURRENT COMPILER STATE", 1
)[0]
assert "patch retry type mismatch one" in first_state
assert "patch retry type mismatch two" not in first_state
assert "patch retry type mismatch one" in second_state
assert "patch retry type mismatch two" not in second_state
assert "patch retry type mismatch one" in widened_state
assert "patch retry type mismatch two" in widened_state
assert "patch retry type mismatch one" in reset_state
assert "patch retry type mismatch two" not in reset_state
assert unchanged == []
assert changed == [source]
def test_direct_editor_retests_alpha_source_until_the_failure_clears(memory_root: MemoryPath) -> None:
"""Clearing static errors cannot finish Alpha while its unchanged test still fails."""
from band_aider.judgement.stack_trace_order_strategy import StackTraceOrderStrategy
from band_aider.strategies.python.python_stack_trace_order_strategy import PythonStackTraceOrderStrategy
target = memory_root / "src/value.py"
target.parent.mkdir(parents=True)
target.write_text("value = 'old'\n")
test_path = memory_root / "tests/test_value.py"
test_path.parent.mkdir(parents=True)
test_source = "def test_value():\n assert False\n\ndef test_unrelated():\n assert False\n"
test_path.write_text(test_source)
manager = _ManagedFileManager(memory_root, ["src/value.py"], ("tests/test_value.py",))
runner = Mock(spec=_TestRunnerStrategy)
runner.run_tests_for.side_effect = (
TestRunResult("pytest test_value", TestRunOutcome.Failure, 1, "fresh lifecycle failure", ["tests/test_value.py:2: in test_value"]),
TestRunResult("pytest test_value", TestRunOutcome.Success, 0, "passed"),
)
dependency_strategy = Mock(spec=DependencyDiscoveryStrategy)
dependency_strategy.upstream_sources_for.return_value = []
factory = PythonCompilerFactory(
file_manager=manager,
repo_exec=manager.repo_exec,
bom_editor=PythonBomEditor(memory_root, manager.repo_exec),
dependency_strategy=dependency_strategy,
repo_root=memory_root,
source_discovery=PythonSourceDiscovery(
src_root=manager.src_root,
test_root=manager.test_root,
repo_exec=manager.repo_exec,
repo_root=memory_root,
),
test_runner=runner,
)
_install_strategy(factory)
generator = _StubGenerator([
_replace("src/value.py", "value = 'old'", "value = 'typed'"),
_replace("src/value.py", "value = 'typed'", "value = 'fixed'"),
])
editor = DirectCodeEditor(
token_generator=generator,
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
file_patcher=FilePatcher(memory_root, repo_exec=manager.repo_exec),
repo_root=memory_root,
)
editor.file_manager = manager
request = _request(target, AlphaLoop)
request.readonly_files = [test_path]
request.is_remediation = True
request.remediation_originator_test = manager.current_test("tests/test_value.py")
request.failure_raw_output = "FAILED tests/test_value.py::test_value - incoming lifecycle failure"
request.failure_stack_trace = ["tests/test_value.py:2: in test_value"]
LanguageStrategySet.get(manager).bindings[SourceDiscoveryStrategy] = factory.source_discovery
LanguageStrategySet.get(manager).bindings[StackTraceOrderStrategy] = PythonStackTraceOrderStrategy()
with patch.object(PythonCompiler, "type_check", side_effect=(
[TypeCheckError("compiler factory needs a cast")], [], [],
)):
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [target]
assert target.read_text() == "value = 'fixed'\n"
assert test_path.read_text() == test_source
assert tuple(call.kwargs["test_names"] for call in runner.run_tests_for.call_args_list) == (
("test_value",), ("test_value",),
)
assert len(generator.render_calls) == 2
feedback = str(generator.render_calls[1]["repair_feedback"])
assert "fresh lifecycle failure" in feedback
assert "tests/test_value.py:2: in test_value" in feedback
receipts = request.validation_receipts
assert len(receipts.receipts) == 1
assert receipts.receipts[0].test_name == "test_value"
assert receipts.receipts[0].passed
assert receipts.receipts[0].generation == receipts.generation
assert "fresh lifecycle failure" not in receipts.render()
assert "def test_value():" in receipts.receipts[0].test_definition
def test_direct_editor_clears_helper_failures(memory_root: MemoryPath) -> None:
"""A real Beta compiler carries monitored tests across one editor episode."""
target = memory_root / "tests/test_widget.py"
target.parent.mkdir(parents=True)
original = (
"def helper():\n return 1\n\n"
"def test_alpha():\n assert helper() == 2\n\n"
"def test_beta():\n assert helper() == 3\n"
)
target.write_text(original)
manager = _ManagedFileManager(memory_root, [], test_locations=("tests/test_widget.py",))
runner = Mock(spec=_TestRunnerStrategy)
runner.run_tests_for.side_effect = (
TestRunResult("pytest alpha", TestRunOutcome.Success, 0, ""),
TestRunResult(
"pytest beta",
TestRunOutcome.Failure,
1,
"tests/test_widget.py:7: in test_beta\nassert 2 == 3",
),
TestRunResult("pytest beta", TestRunOutcome.Success, 0, ""),
)
dependency_strategy = Mock(spec=DependencyDiscoveryStrategy)
dependency_strategy.upstream_sources_for.return_value = []
factory = Mock(spec=CompilerFactory, wraps=PythonCompilerFactory(
file_manager=manager,
repo_exec=manager.repo_exec,
bom_editor=PythonBomEditor(memory_root, manager.repo_exec),
dependency_strategy=dependency_strategy,
repo_root=memory_root,
source_discovery=PythonSourceDiscovery(
src_root=manager.src_root,
test_root=manager.test_root,
repo_exec=manager.repo_exec,
repo_root=memory_root,
),
test_runner=runner,
))
_install_strategy(factory)
generator = _StubGenerator([
_replace("tests/test_widget.py", "return 1", "return 2"),
_replace("tests/test_widget.py", "return 2", "return 3"),
])
editor = DirectCodeEditor(
token_generator=generator,
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
file_patcher=FilePatcher(memory_root, repo_exec=manager.repo_exec),
repo_root=memory_root,
)
editor.file_manager = manager
request = _request(target, BetaLoop)
request.failure_raw_output = (
"FAILED tests/test_widget.py::test_alpha\n"
"FAILED tests/test_widget.py::test_beta"
)
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [target]
assert target.read_text() == original.replace("return 1", "return 3")
assert tuple(call.kwargs["test_names"] for call in runner.run_tests_for.call_args_list) == (
("test_alpha",), ("test_beta",), ("test_beta",),
)
feedback = str(generator.render_calls[1]["repair_feedback"])
assert "tests/test_widget.py:7: in test_beta" in feedback
assert "assert 2 == 3" in feedback
assert "Known failure for test_alpha" not in feedback
attempts = tuple(call.args[0] for call in factory.build.call_args_list)
assert len(attempts) == 3
assert attempts[0].blackboard is None
inventory = attempts[1].blackboard
assert inventory is not None
assert attempts[2].blackboard is inventory
assert inventory.monitored == ()
assert inventory.failures == ()
receipts = request.validation_receipts
assert receipts.receipts == ()
assert inventory.receipts == ()
def test_direct_editor_keeps_admission_when_candidate_is_rejected(
memory_root: MemoryPath,
) -> None:
"""Rejection discards test results while preserving immediate admission."""
decisions: list[tuple[str, tuple[str, ...]]] = []
class _DecisionInventory(EditedTestInventory):
"""Record inventory state at editor-owned candidate decisions."""
def commit_candidate_results(self) -> None:
super().commit_candidate_results()
decisions.append(("commit", self.monitored))
def discard_candidate_results(self) -> None:
super().discard_candidate_results()
decisions.append(("discard", self.monitored))
class _AdmissionCompiler(_RecordingCompiler):
"""Admit once, then stage a passing result for both candidate checks."""
def compile(self, paths: Sequence[Path]) -> list[CompileError]:
errors = super().compile(paths)
# Candidate checks stage one exact test result for the editor's decision.
if self._attempt.candidate_mementos:
# Only the rejected first candidate performs admission.
if not self.blackboard.monitored:
self.blackboard.add_monitored({"test_changed"})
self.blackboard.set_candidate_results({"test_changed": None})
return errors
class _AdmissionCompilerFactory(_RecordingCompilerFactory):
"""Share one decision-recording inventory across compiler instances."""
def build(self, attempt: CompileAttempt) -> Compiler:
self.build_calls.append(attempt)
compiler = _AdmissionCompiler(self, attempt)
# The initial compiler owns the episode inventory.
if attempt.blackboard is None:
compiler.blackboard = _DecisionInventory(compiler._clean_blackboard)
self.compilers.append(compiler)
return compiler
factory = _AdmissionCompilerFactory([
[StaticCompileError("candidate remains mechanically invalid")],
[],
])
factory.baseline_results = [[TestRunCompileError(
"incoming semantic failure",
test_name="test_changed",
)]]
_install_strategy(factory)
generator = _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'rejected'"),
_replace("src/mod.py", "value = 'old'", "value = 'accepted'"),
])
editor, source = _runtime_editor(memory_root, generator)
request = _request(source)
request.failure_error_message = "incoming semantic failure"
_edit_result = editor.edit(request)
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text() == "value = 'accepted'\n"
assert decisions == [
("discard", ("test_changed",)),
("commit", ()),
]
def test_direct_editor_isolates_blackboards(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""Reusing one editor scopes independent compiler inventories per episode."""
generator = _StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'first'"))
editor, source = _runtime_editor(memory_root, generator)
assert editor.edit(_request(source)).changed_paths == [source]
first = compiler_factory.build_calls[1].blackboard
assert compiler_factory.build_calls[0].blackboard is None
assert not hasattr(compiler_factory.compilers[0], "blackboard")
generator.responses = [_replace("src/mod.py", "value = 'first'", "value = 'second'")]
assert editor.edit(_request(source)).changed_paths == [source]
second = compiler_factory.build_calls[3].blackboard
assert compiler_factory.build_calls[2].blackboard is None
assert not hasattr(compiler_factory.compilers[2], "blackboard")
assert first is not None
assert second is not None
assert second is not first
assert first.monitored == second.monitored == ()
assert source.read_text() == "value = 'second'\n"
def test_direct_editor_releases_blackboard_on_failure(memory_root: MemoryPath) -> None:
"""An exception after admission releases the inventory and rolls back episode source."""
factory = Mock(spec=CompilerFactory)
compiler = Mock(spec=BlackboardCompiler)
factory.build.side_effect = (compiler, RuntimeError("compiler construction failed"))
forgotten: list[bool] = []
inventory = EditedTestInventory(lambda: forgotten.append(True))
compiler.get_blackboard.return_value = inventory
_install_strategy(factory)
editor, source = _runtime_editor(
memory_root,
_StubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'candidate'")),
)
# Add identified failures before the next compiler construction fails.
compiler.compile.side_effect = lambda paths: (
inventory.add_identified_failures((
TestRunCompileError("incoming failure", test_name="test_value"),
)) or []
)
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == []
assert "compiler construction failed" in summary
assert "-value = 'old'" in summary
assert "+value = 'candidate'" in summary
assert source.read_text() == "value = 'old'\n"
assert inventory.monitored == ()
assert inventory.failures == ()
assert forgotten == [True]
def test_direct_editor_cleanup_failure_preserves_error_after_rollback(memory_root: MemoryPath) -> None:
"""Cleanup failure after NO_PATCH must not try to read an already deleted snapshot."""
factory = Mock(spec=CompilerFactory)
compiler = Mock(spec=BlackboardCompiler)
factory.build.return_value = compiler
compiler.compile.return_value = []
compiler.get_blackboard.return_value = EditedTestInventory(
Mock(side_effect=RuntimeError("inventory release failed")),
)
_install_strategy(factory)
editor, source = _runtime_editor(memory_root, _StubGenerator("NO_PATCH: no edit"))
_edit_result = editor.edit(_request(source))
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == []
assert "inventory release failed" in summary
assert source.read_text() == "value = 'old'\n"
assert not Memento.current(source, memory_root.repository)
def test_direct_editor_rejects_hidden_static_errors(
memory_root: MemoryPath,
compiler_factory: _RecordingCompilerFactory,
) -> None:
"""A returning initial diagnostic blocks completion after semantic validation begins."""
initial = StaticCompileError("original static error")
test_failure = TestRunCompileError("test still fails", test_name="test_value")
compiler_factory.baseline_results = [[initial]]
compiler_factory.results = [[test_failure], [initial], []]
editor, source = _runtime_editor(memory_root, _StubGenerator([
_replace("src/mod.py", "value = 'old'", "value = 'semantic'"),
_replace("src/mod.py", "value = 'semantic'", "value = 'regression'"),
_replace("src/mod.py", "value = 'semantic'", "value = 'fixed'"),
]))
_edit_result = editor.edit(_request(source))
changed, _ = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert source.read_text() == "value = 'fixed'\n"
assert len(compiler_factory.build_calls) == 4
assert compiler_factory.candidate_calls[3]["pre"] == ["value = 'semantic'\n"]
initial_feedback = str(_generator(editor).render_calls[0]["repair_feedback"])
rejected_feedback = str(_generator(editor).render_calls[2]["repair_feedback"])
assert "original static error" in initial_feedback
assert "original static error" in rejected_feedback
assert "This candidate was rejected and rolled back." in rejected_feedback
assert _rendered_masks(editor)[2].mode is AiderFailureMode.SEMANTIC
@pytest.mark.parametrize("failure_kind", ("unmatched", "ambiguous", "malformed"))
def test_patch_failures_reset_counter_after_widening_and_retry_past_whole_source(
memory_root: MemoryPath, compiler_factory: _RecordingCompilerFactory, failure_kind: str,
) -> None:
"""The third model request follows refinement; whole context never exhausts retries."""
editor, source = _runtime_editor(memory_root, _StubGenerator(""))
baseline = (
"value = 'old'\n"
"def first():\n" + " item = 1\n" * 24 + " return item\n"
"def second():\n" + " item = 2\n" * 24 + " return item\n"
)
source.write_text(baseline)
bad = {
"unmatched": _replace("src/mod.py", "missing text", "wrong"),
"ambiguous": _replace("src/mod.py", " return item", " return 0"),
"malformed": "FILE: src/mod.py\n<<<<<<< SEARCH\nmissing separator\n",
}[failure_kind]
responses = iter([bad] * 12 + [_replace("src/mod.py", "value = 'old'", "value = 'fixed'")])
client = ScriptedCompletionClient(())
editor.token_generator = SearchReplaceTokenGenerator(
(client,), memory_root, repo_exec=memory_root.repository,
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
)
editor.file_manager = _file_manager(editor)
strategies = LanguageStrategySet.get(_file_manager(editor))
strategies.bindings[CustomDirectives] = CustomDirectives({})
strategies.bindings[SourceDiscoveryStrategy] = PythonSourceDiscovery(
memory_root / "src", memory_root / "tests", repo_exec=memory_root.repository,
)
real_refine = ContextualFileRenderer.refine
refinement_attempts: list[int] = []
with (
patch.object(client, "complete", side_effect=lambda prompt, logger=None: (
next(responses) if "BEGIN CURRENT FILE EVIDENCE" in prompt else "NO_KEEP\nREASON: none\n"
)) as completion,
patch("band_aider.judgement.contextual_file_renderer.get_token_budget_settings", return_value=TokenBudgetSettings(max_prompt_tokens=1)),
patch.object(ContextualFileRenderer, "refine", autospec=True, side_effect=lambda *args, **kwargs: (
refinement_attempts.append(editor.attempts_since_acceptance) or real_refine(*args, **kwargs)
)) as refinement,
patch.object(editor.token_generator, "generate", wraps=editor.token_generator.generate) as generation,
patch.object(editor, "_render_compiler_errors", wraps=editor._render_compiler_errors) as diagnostics,
):
_edit_result = editor.edit(_request(source))
changed, _summary = _edit_result.changed_paths, _edit_result.summary
prompts = tuple(
call.args[0] for call in completion.call_args_list
if "BEGIN CURRENT FILE EVIDENCE" in call.args[0]
)
assert changed == [source]
assert source.read_text() == baseline.replace("value = 'old'", "value = 'fixed'")
assert len(prompts) == 13
assert refinement.call_count == 6
assert refinement_attempts == [3, 6, 9, 12, 15, 18]
assert tuple(call.kwargs["unapplied_attempts"] for call in generation.call_args_list) == (0, 1, 2) * 6 + (0,)
# Each exhausted group reaches the existing cadence once, without revisiting it on retry.
assert diagnostics.call_count == 8 # Initial compile, six cadence widenings, final candidate.
assert "def first():" not in prompts[0]
assert "def first():" not in prompts[2] # Progressive NO_KEEP is not a terminal result.
assert baseline in prompts[8] # Existing fourth refinement exposes whole source.
assert baseline in prompts[12] # Further failures still reach generation.
assert "PATCH APPLICATION FEEDBACK" in prompts[2]
assert len(compiler_factory.compile_calls) == 2
assert compiler_factory.candidate_calls[-1]["pre"] == [baseline]
@pytest.mark.parametrize("static_gate", (False, True))
def test_initial_visible_failure_reaches_context_selection(
memory_root: MemoryPath, compiler_factory: _RecordingCompilerFactory, static_gate: bool,
) -> None:
"""First-attempt source visibility follows the same failure mask as the repair prompt."""
generator = _RenderingStubGenerator(_replace("src/mod.py", "value = 'old'", "value = 'fixed'"))
editor, source = _runtime_editor(memory_root, generator)
source.write_text(
"value = 'old'\n"
"def traced():\n" + " item = 1\n" * 24 + " return item\n"
"def unrelated():\n" + " item = 2\n" * 24 + " return item\n"
)
failure = TestRunCompileError("traced failed", stack_trace=("traced",))
compiler_factory.baseline_results = [[StaticCompileError("Unknown type")] if static_gate else [failure]]
strategies = LanguageStrategySet.get(_file_manager(editor))
strategies.bindings[SourceDiscoveryStrategy] = PythonSourceDiscovery(
memory_root / "src", memory_root / "tests", repo_exec=memory_root.repository,
)
request = _request(source)
request.failure_stack_trace = ["traced"]
with patch("band_aider.judgement.contextual_file_renderer.get_token_budget_settings", return_value=TokenBudgetSettings(max_prompt_tokens=1)):
_edit_result = editor.edit(request)
changed, _summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
source_evidence = str(generator.calls[0]["base_prompt"]).split("BEGIN CURRENT FILE EVIDENCE", 1)[1]
assert ("def traced():" in source_evidence) is not static_gate
assert "def unrelated():" not in source_evidence
@pytest.mark.parametrize("partial", (False, True))
def test_applied_candidate_resets_unapplied_count_before_compiler_repair(
memory_root: MemoryPath, compiler_factory: _RecordingCompilerFactory, partial: bool,
) -> None:
"""Applied and partial candidates move into compiler tracking rather than patch exhaustion."""
middle = _replace("src/mod.py", "value = 'old'", "value = 'middle'")
# A partially applicable response still belongs to the existing compiler transition.
if partial:
middle += "\n" + _replace("src/mod.py", "missing second block", "unused")
generator = _StubGenerator([
_replace("src/mod.py", "missing first block", "unused"),
middle,
_replace("src/mod.py", "value = 'middle'", "value = 'fixed'"),
])
editor, source = _runtime_editor(memory_root, generator)
compiler_factory.results = [[StaticCompileError("Repair the intermediate candidate")], []]
_edit_result = editor.edit(_request(source))
changed, _summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [source]
assert tuple(call["unapplied_attempts"] for call in generator.calls) == (0, 1, 0)
assert source.read_text() == "value = 'fixed'\n"
assert len(compiler_factory.compile_calls) == 3