193 lines
6.7 KiB
Python
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
|