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

187 lines
6.4 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
"""Repository-boundary regression coverage using a nominal in-memory executor."""
from __future__ import annotations
from io import StringIO
import logging
from typing import Mapping
import pytest
from band_aider.context.class_meta import ClassMeta
from band_aider.context.provenance import FutureGuidance
from band_aider.judgement.contextual_file_renderer import ContextualFileRenderer
from band_aider.judgement.edit_request import AiderEditRequest
from band_aider.judgement.file_manager import FileManager
from band_aider.judgement.prompt_formatter import MarkdownPromptFormatterBuilder
from band_aider.judgement.search_replace_token_generator import SearchReplaceTokenGenerator
from band_aider.reporting.change_report_writer import ChangeReportWriter
from band_aider.strategies.oop_signature_extract import (
OOpSignature,
OOpSignatureExtract,
OOpSignatureRender,
)
from band_aider.strategies.python.python_source_discovery import PythonSourceDiscovery
from tests.band_aider_loop_impl import _file_manager
from tests.support.completion_clients import StaticCompletionClient
from tests.support.memory_path import MemoryPath
from tests.support.memory_repo_exec import MemoryRepoExec
class _NoCards(OOpSignatureExtract):
"""Return no dependency cards for the isolated prompt rendering scenario."""
def extract(self, module: str, name: str) -> OOpSignature | None:
"""Return no card because the scenario owns only direct file evidence."""
del module, name
return None
def extract_references(
self,
sources_by_location: Mapping[str, str],
) -> tuple[OOpSignature, ...]:
"""Return no cards because no hidden declaration evidence is in scope."""
del sources_by_location
return ()
def extract_source(
self,
location: str,
source: str,
) -> tuple[OOpSignature, ...]:
"""Return no cards because the scenario owns direct file evidence only."""
del location, source
return ()
class _NoCardRender(OOpSignatureRender):
"""Reject unexpected card rendering in the direct-file evidence scenario."""
def render(self, signature: OOpSignature, output: StringIO) -> None:
"""Fail the test if a scenario unexpectedly requires a declaration card."""
del signature, output
raise AssertionError("No reference cards should be rendered by this fixture.")
class _BoundGenerator(SearchReplaceTokenGenerator):
"""Declare the production FileManager binding used by this renderer scenario."""
file_manager: FileManager | None
@pytest.fixture
def memory_root() -> MemoryPath:
"""Return one lexical project root backed by a fresh memory repository."""
return MemoryPath.from_repo(MemoryRepoExec())
def test_class_meta_get_source_uses_repo_exec(memory_root: MemoryPath) -> None:
"""Verify ClassMeta reads source through FileManager's execution boundary."""
file_manager = _file_manager(memory_root)
source_path = file_manager.src_root / "pkg" / "mod.py"
file_manager.repo_exec.write_text(source_path, "hello from memory")
meta = ClassMeta(
location="src/pkg/mod.py",
summary="",
relevance=[],
relevant_symbols={},
)
assert meta.get_source(file_manager) == "hello from memory"
def test_python_ast_maintainer_reads_through_repo_exec(memory_root: MemoryPath) -> None:
"""Verify Python discovery receives source text from the in-memory boundary."""
source = "def f():\n return 1\n"
source_path = memory_root / "src" / "any.py"
memory_root.repository.write_text(source_path, source)
discovery = PythonSourceDiscovery(
src_root=memory_root / "src",
test_root=memory_root / "tests",
repo_exec=memory_root.repository,
)
beats = discovery.searchable_beats(
str(source_path),
memory_root.repository.read_text(source_path),
)
assert any(getattr(beat, "kind", "") == "dependency_callable" for beat in beats)
def test_change_report_writer_uses_repo_exec_for_report(memory_root: MemoryPath) -> None:
"""Verify reports are persisted through the FileManager repository boundary."""
file_manager = _file_manager(memory_root)
writer = ChangeReportWriter(repo_root=memory_root)
path = writer.write(
original_job="job",
cot_tasks=[],
file_manager=file_manager,
final_summary="ok",
)
assert file_manager.repo_exec.exists(path)
assert "Final summary: ok" in file_manager.repo_exec.read_text(path)
def test_generator_prompt_reads_through_repo_exec(memory_root: MemoryPath) -> None:
"""Verify the direct-edit prompt excludes decision-only future guidance."""
file_manager = _file_manager(memory_root)
editable_path = file_manager.src_root / "x.py"
file_manager.repo_exec.write_text(editable_path, "FROM_REPO_EXEC\n")
generator = _BoundGenerator(
(StaticCompletionClient("stationary response"),),
memory_root,
memory_root.repository,
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
)
generator.file_manager = file_manager
request = AiderEditRequest(
authoritative_prompt="do it",
editable_files=[editable_path],
readonly_files=[],
future_guidance=(FutureGuidance(
origin_message_id="later-phase",
addressed_message_id="earlier-phase",
message="Revisit the selected source file.",
justification="The observed failure belongs to another operation.",
),),
)
renderer = ContextualFileRenderer(
file_manager=file_manager,
client=generator.clients[0],
editable_files=[editable_path],
readonly_files=[],
prompt="do it",
logger=logging.getLogger("repo-exec-io-test"),
prompt_formatter_builder=MarkdownPromptFormatterBuilder(),
oop_signature_extract=_NoCards(),
oop_signature_render=_NoCardRender(),
)
prompt = generator.render_prompt(
request,
editable_files=[editable_path],
readonly_files=[],
renderer=renderer,
)
assert "FROM_REPO_EXEC" in prompt
assert "REVISIT CONTEXT" not in prompt
assert "FUTURE GUIDANCE" not in prompt
assert "Revisit the selected source file." not in prompt
assert "The observed failure belongs to another operation." not in prompt