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

193 lines
6.7 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
from __future__ import annotations
import logging
from pathlib import Path
from band_aider.context.class_meta import ClassMeta
from band_aider.judgement.cached_code_editor import CachedCodeEditor
from band_aider.judgement.cached_file_patcher import (
CachedFilePatcher,
CachedPatchApplyResult,
)
from band_aider.judgement.code_edit_or_leave_it import CodeEditLeaveSummary
from band_aider.judgement.code_editor import CodeEditor, CodeEditorResult, EditorRecord
from band_aider.judgement.edit_request import AiderEditRequest
from band_aider.judgement.file_manager import FileManager
class _InnerEditor(CodeEditor):
"""Return one explicit delegated edit outcome through the CodeEditor contract."""
def __init__(self, summary: str = "inner summary") -> None:
"""Store the stationary summary that distinguishes delegated behavior."""
self.summary = summary
self.file_manager: FileManager | None = None
@property
def inner(self) -> CodeEditor | None:
"""Expose no wrapped editor because this is the terminal inner fixture."""
return None
def edit(
self,
request: AiderEditRequest,
logger: logging.Logger | None = None,
) -> CodeEditorResult:
"""Return the stationary delegated edit without accessing a repository."""
del request, logger
return CodeEditorResult([Path("changed.py")], self.summary, [])
class _Patcher(CachedFilePatcher):
"""Model cached-patch state in memory through the concrete patcher contract."""
def __init__(self) -> None:
"""Initialize only the cache interactions owned by CachedCodeEditor tests."""
self.file_manager: FileManager | None = None
self.active_topics: list[str] = []
self.deactivated = False
self.cached_result = CachedPatchApplyResult(False, [], "")
self.recorded: list[tuple[list[Path], str]] = []
self.infer_files: list[ClassMeta] = []
def activate(self, topic: str, *, file_manager: FileManager | None) -> None:
"""Record the active topic and the explicit runtime file-manager binding."""
self.active_topics.append(topic)
self.file_manager = file_manager
def deactivate(self) -> None:
"""Record cache-scope closure without clearing prior semantic evidence."""
self.deactivated = True
def apply_cached(self, *, request: AiderEditRequest) -> CachedPatchApplyResult:
"""Return the stationary cache result for the supplied request."""
del request
return self.cached_result
def record_code_editor_result(
self,
*,
request: AiderEditRequest,
changed_paths: list[Path],
summary: str,
) -> None:
"""Record one successful delegated result as cache-training evidence."""
del request
self.recorded.append((list(changed_paths), summary))
def infer_from(self, files: list[ClassMeta]) -> None:
"""Retain explicit authoritative infer-file evidence for session tests."""
self.infer_files = list(files)
def _request() -> AiderEditRequest:
"""Return a minimal direct-edit request with no external path access."""
return AiderEditRequest(
authoritative_prompt="edit",
editable_files=[Path("tests/test_foo.py")],
readonly_files=[],
)
def test_cached_code_editor_is_dormant_outside_enable_cache() -> None:
"""An inactive cache returns the terminal inner editor's explicit outcome."""
editor = CachedCodeEditor(inner=_InnerEditor(), file_patcher=_Patcher())
_edit_result = editor.edit(_request())
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [Path("changed.py")]
assert summary == "inner summary"
def test_cached_code_editor_falls_back_after_a_rejected_cache_candidate() -> None:
"""An active cache miss delegates and records the resulting source evidence."""
patcher = _Patcher()
editor = CachedCodeEditor(inner=_InnerEditor(), file_patcher=patcher)
with editor.enable_cache("src/foo.py:abc"):
_edit_result = editor.edit(_request())
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [Path("changed.py")]
assert summary == "inner summary"
assert patcher.active_topics == ["src/foo.py:abc"]
assert patcher.deactivated is True
assert patcher.recorded == [([Path("changed.py")], "inner summary")]
def test_cached_code_editor_does_not_record_runtime_leave_control() -> None:
"""A typed gate LEAVE result reaches its owner without becoming cache history."""
patcher = _Patcher()
leave = CodeEditLeaveSummary("CODE EDIT OR LEAVE IT\n[CODE_EDIT_OR_LEAVE_IT:LEAVE_IT]")
editor = CachedCodeEditor(
inner=_InnerEditor(leave),
file_patcher=patcher,
)
with editor.enable_cache("src/foo.py:abc"):
_edit_result = editor.edit(_request())
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [Path("changed.py")]
assert type(summary) is CodeEditLeaveSummary
assert summary is leave
assert patcher.recorded == []
def test_cached_code_editor_returns_an_active_cache_hit() -> None:
"""An active accepted cache result wins over the terminal inner editor outcome."""
patcher = _Patcher()
patcher.cached_result = CachedPatchApplyResult(
True,
[Path("tests/test_foo.py")],
"cached summary",
[EditorRecord("", "old", "cached")],
)
editor = CachedCodeEditor(inner=_InnerEditor(), file_patcher=patcher)
request = _request()
request.editor_records = [EditorRecord("earlier", "first", "old")]
with editor.enable_cache("src/foo.py:abc"):
_edit_result = editor.edit(request)
changed, summary = _edit_result.changed_paths, _edit_result.summary
assert changed == [Path("tests/test_foo.py")]
assert summary == "cached summary"
assert _edit_result.records == request.editor_records == [
EditorRecord("earlier", "first", "old"), EditorRecord("", "old", "cached"),
]
assert patcher.recorded == []
def test_cached_code_editor_forwards_runtime_file_manager_to_inner_and_patcher() -> None:
"""Dependency rebinding preserves one nominal FileManager identity across both collaborators."""
inner = _InnerEditor()
patcher = _Patcher()
editor = CachedCodeEditor(inner=inner, file_patcher=patcher)
marker = object.__new__(FileManager)
editor.file_manager = marker
assert inner.file_manager is marker
with editor.enable_cache("src/foo.py:abc"):
assert patcher.file_manager is marker