2634 lines
101 KiB
Python
2634 lines
101 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
|
|
import logging
|
|
from collections.abc import Callable, Mapping, Sequence
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from band_aider.context.beta_workflow_story import (
|
|
AcceptedSourceStory,
|
|
DirectTestCandidate,
|
|
)
|
|
from band_aider.context.beta_workflow_judgement import BetaWorkflowJudgementService
|
|
from band_aider.context.cot_task_generator import CotTaskRecord
|
|
from band_aider.context.targeted_failure_owner_check import TargetedAlphaOrBetaRoutingDecision
|
|
from band_aider.context.memento import Memento
|
|
from band_aider.context import admitted_sources, provenance
|
|
from band_aider.context.provenance import FutureGuidance, Phase, Provenance
|
|
from band_aider.context.work_completion import WorkCompletion
|
|
from band_aider.context.working_backlog import BackLog as DurableBackLog
|
|
from band_aider.context.working_backlog import BacklogLane
|
|
from band_aider.judgement.alpha_blank_source_correction_check import (
|
|
AlphaBlankSourceCorrectionDecision,
|
|
)
|
|
from band_aider.judgement.bi_dependency_discovery_strategy import (
|
|
BiDependencyDiscoveryStrategy,
|
|
)
|
|
from band_aider.judgement.edit_request import AiderEditRequest
|
|
from band_aider.context.validation_receipts import ValidationReceipt, ValidationReceipts
|
|
from band_aider.judgement.code_editor import CodeEditor, CodeEditorResult, EditorRecord
|
|
from band_aider.judgement.repo_exec import current_repo_exec
|
|
from band_aider.judgement.code_edit_or_leave_it import (
|
|
CODE_EDIT_OR_LEAVE_IT_LEAVE_TOKEN,
|
|
CodeEditLeaveSummary,
|
|
)
|
|
|
|
from tests.band_aider_loop_impl import *
|
|
|
|
|
|
def test_alpha_remediation_keeps_historical_context_through_acceptance_rollback(tmp_path: Path) -> None:
|
|
"""A later remediation keeps prior inspection support in both editor and acceptance attempts."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(file_manager.src_root / "snapshot.py", "def snapshot():\n return 1\n")
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
support = file_manager.require_src("src/snapshot.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
file_manager.client = AlphaAcceptanceSequenceClient([
|
|
{"outcome": "fail", "reason": ["return 1"], "instruction": "Recheck the source contract."},
|
|
{"outcome": "pass", "reason": ["return 1"], "instruction": ""},
|
|
])
|
|
context = _alpha_context(file_manager, BackLog(), source, test)
|
|
graph = Provenance.open(repo_root=file_manager.repo_root, repo_exec=file_manager.repo_exec)
|
|
inspection = InspectionBacklog(source, "inspect", upstream_classes=[support], downstream_classes=[source])
|
|
graph.start(Phase.ALPHA, inspection)
|
|
testing = TestingBacklog(test, "test")
|
|
graph.track_provenance(inspection.message_id, Phase.BETA, testing)
|
|
remediation = FailedQAItem(source, "repair", downstream_classes=[source])
|
|
graph.track_provenance(testing.message_id, Phase.ALPHA, remediation)
|
|
worker = SequenceSourceWorker(
|
|
lambda request: CodeEditorResult([], "no changes", []),
|
|
lambda request: CodeEditorResult([], "no changes", []),
|
|
)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)
|
|
with provenance.bind(graph), provenance.item_scope(Phase.ALPHA, remediation):
|
|
before = (dict(graph.messages), graph.edges)
|
|
loop._run_source_work([source], {source.location: "repair"}, [], [], is_remediation=True)
|
|
assert admitted_sources.current_sources(file_manager) == [source, support]
|
|
assert (graph.messages, graph.edges) == before
|
|
assert len(worker.requests) == 2
|
|
assert all(support.resolve_path(file_manager) in request.readonly_files for request in worker.requests)
|
|
assert len(file_manager.client.acceptance_prompts) == 2
|
|
assert all("snapshot.py" in prompt for prompt in file_manager.client.acceptance_prompts)
|
|
|
|
|
|
def test_inspection_carrier_keeps_cot_out_of_aider_authority() -> None:
|
|
"""Inspection retains its CoT as a judgement input rather than an edit boundary."""
|
|
|
|
file_manager = _file_manager(Path("/testbed"))
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["return 2"], "instruction": ""}]
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "focus.py",
|
|
"def focus():\n return 1\n",
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "upstream.py",
|
|
"def upstream():\n return 1\n",
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "downstream.py",
|
|
"def downstream():\n return 1\n",
|
|
)
|
|
file_manager.prompt = "IMMUTABLE INSPECTION COT"
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/focus.py")
|
|
upstream = file_manager.require_src("src/upstream.py")
|
|
downstream = file_manager.require_src("src/downstream.py")
|
|
backlog = BackLog()
|
|
context = _alpha_context(
|
|
file_manager,
|
|
backlog,
|
|
source,
|
|
file_manager.require_test("tests/test_foo.py"),
|
|
)
|
|
context.backlog = InspectionScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
source=source,
|
|
readonly_sources=[upstream, downstream],
|
|
guidance="inspect the selected source",
|
|
)
|
|
item = InspectionBacklog(
|
|
focused_spotlight=source,
|
|
summary="inspect the focused source",
|
|
upstream_classes=[upstream],
|
|
downstream_classes=[downstream],
|
|
)
|
|
|
|
handled = AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
aider_worker=InspectionBoundaryEditor(
|
|
editable=file_manager.source_path(source.location),
|
|
readonly=[
|
|
file_manager.source_path(downstream.location),
|
|
file_manager.source_path(upstream.location),
|
|
],
|
|
),
|
|
)._run_inspection_source_item(item)
|
|
|
|
assert [item.location for item in handled] == [source.location]
|
|
assert file_manager.repo_exec.read_text(file_manager.source_path(source.location)) == (
|
|
"def focus():\n return 2\n"
|
|
)
|
|
|
|
|
|
def test_inspection_no_work_closes_before_the_alpha_editor() -> None:
|
|
"""A grounded no-work inspection result must not create a blank edit assignment."""
|
|
|
|
file_manager = _file_manager(Path("/testbed"))
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "downstream.py",
|
|
"def downstream():\n return 1\n",
|
|
)
|
|
file_manager.prompt = "IMMUTABLE INSPECTION COT"
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/downstream.py")
|
|
backlog = BackLog()
|
|
context = _alpha_context(
|
|
file_manager,
|
|
backlog,
|
|
source,
|
|
file_manager.require_test("tests/test_foo.py"),
|
|
)
|
|
context.backlog = InspectionScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
source=source,
|
|
readonly_sources=[],
|
|
guidance="",
|
|
)
|
|
item = InspectionBacklog(focused_spotlight=source, summary="downstream inspection")
|
|
|
|
assert AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
)._run_inspection_source_item(item) == []
|
|
|
|
|
|
def test_alpha_postflight_callback_runs_after_testing_staging_and_persist(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha reports the original item only after postflight persistence."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
item = InspectionBacklog(source, "inspect source", downstream_classes=[source])
|
|
backlog.queue_inspection(item)
|
|
file_manager.src_dirty[source.location] = True
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
context.backlog = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=[test],
|
|
candidates=[
|
|
DirectTestCandidate(
|
|
focused_test=test,
|
|
targeted_result=TestRunResult("pytest", TestRunOutcome.Success, 0, "passed"),
|
|
)
|
|
],
|
|
routed_by_test={},
|
|
)
|
|
claimed = context.move_inbound_from_backlog()
|
|
assert claimed is not None
|
|
|
|
def after_persist(received_sources: tuple[ClassMeta, ...]) -> None:
|
|
"""Validate that the durable Alpha state exists before the completion callback."""
|
|
|
|
assert received_sources == (source,)
|
|
assert len(context.outbound_queue) == 1
|
|
assert file_manager.repo_exec.list_files(
|
|
file_manager.repo_root / ".band-aider-chkpts",
|
|
["*.pkl"],
|
|
recursive=False,
|
|
)
|
|
|
|
context.alpha_postflight_callback = after_persist
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context)._stage_postflight_alpha_results(
|
|
claimed.value,
|
|
[source],
|
|
)
|
|
|
|
assert context.outbound_queue[0].value.focused_spotlight is test
|
|
|
|
|
|
def _failed_item(
|
|
source: ClassMeta,
|
|
test: ClassMeta,
|
|
*,
|
|
summary: str = "repair source",
|
|
upstream_sources: list[ClassMeta] | None = None,
|
|
stack_trace: list[str] | None = None,
|
|
error_message: str = "failed",
|
|
raw_output: str = "",
|
|
) -> FailedQAItem:
|
|
"""Create one remediation item in the new topology shape."""
|
|
|
|
return FailedQAItem(
|
|
focused_spotlight=source,
|
|
summary=summary,
|
|
upstream_classes=list(upstream_sources or []),
|
|
downstream_classes=[source],
|
|
failed_test=FailingTest(
|
|
test=test,
|
|
stack_trace=stack_trace or ["failed"],
|
|
error_message=error_message,
|
|
raw_output=raw_output,
|
|
),
|
|
)
|
|
|
|
|
|
class ScenarioContributorBacklog(ContributorBacklog):
|
|
"""Contributor backlog with fixed direct observations for one isolated scenario."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
backlog: DurableBackLog,
|
|
file_manager: FileManager,
|
|
tests: list[ClassMeta],
|
|
candidates: list[DirectTestCandidate],
|
|
routed_by_test: Mapping[str, Sequence[FailedQAItem | TestingBacklog]],
|
|
use_real_routes: bool = False,
|
|
) -> None:
|
|
"""Bind stationary observations to the real ContributorBacklog selection workflow."""
|
|
|
|
super().__init__(
|
|
backlog=backlog,
|
|
get_tests=lambda _sources: list(tests),
|
|
file_manager=file_manager,
|
|
parser=file_manager.parser,
|
|
language_strategy_set=_language_strategy_set(
|
|
file_manager,
|
|
runner=PassingRunner(),
|
|
),
|
|
)
|
|
self._scenario_candidates = list(candidates)
|
|
self._routed_by_test = {
|
|
location: list(items)
|
|
for location, items in routed_by_test.items()
|
|
}
|
|
self._use_real_routes = use_real_routes
|
|
|
|
def _direct_test_candidates(
|
|
self,
|
|
story: AcceptedSourceStory,
|
|
test_files: list[ClassMeta],
|
|
) -> list[DirectTestCandidate]:
|
|
"""Return the scenario's complete direct-test census for its accepted source."""
|
|
|
|
del story
|
|
expected_locations = [candidate.focused_test.location for candidate in self._scenario_candidates]
|
|
actual_locations = [test.location for test in test_files]
|
|
if actual_locations != expected_locations:
|
|
raise AssertionError(
|
|
f"Expected direct-test census {expected_locations}, received {actual_locations}."
|
|
)
|
|
return list(self._scenario_candidates)
|
|
|
|
def _route_failed_direct_candidate(
|
|
self,
|
|
story: AcceptedSourceStory,
|
|
candidate: DirectTestCandidate,
|
|
assignment: str,
|
|
*,
|
|
regression_star: bool,
|
|
future_guidance: tuple[FutureGuidance, ...] = (),
|
|
logger: logging.Logger | None = None,
|
|
) -> list[FailedQAItem | TestingBacklog]:
|
|
"""Return the fixed owner-specific work for one observed failed test."""
|
|
|
|
if self._use_real_routes:
|
|
return super()._route_failed_direct_candidate(
|
|
story,
|
|
candidate,
|
|
assignment,
|
|
regression_star=regression_star,
|
|
future_guidance=future_guidance,
|
|
logger=logger,
|
|
)
|
|
del story, assignment, regression_star, future_guidance, logger
|
|
routed = self._routed_by_test.get(candidate.focused_test.location)
|
|
if routed is None:
|
|
raise AssertionError(
|
|
f"No route is permitted for {candidate.focused_test.location}."
|
|
)
|
|
return list(routed)
|
|
|
|
|
|
def _failed_candidate(test: ClassMeta, result: TestRunResult) -> DirectTestCandidate:
|
|
"""Build one complete failed direct observation from stationary runner evidence."""
|
|
|
|
return DirectTestCandidate(
|
|
focused_test=test,
|
|
targeted_result=result,
|
|
failing=FailingTest(
|
|
test=test,
|
|
stack_trace=list(result.stack_trace),
|
|
error_message=f"Test command failed with return code {result.returncode}.",
|
|
raw_output=result.output,
|
|
),
|
|
)
|
|
|
|
|
|
def _failed_test_of(item: FailedQAItem) -> FailingTest:
|
|
"""Return required remediation evidence while making the scenario invariant explicit."""
|
|
|
|
if item.failed_test is None:
|
|
raise AssertionError("A remediation scenario must preserve its failed-test evidence.")
|
|
return item.failed_test
|
|
|
|
|
|
def _focused_class_of(item: InspectionBacklog) -> ClassMeta:
|
|
"""Return the required focus of one outbound item in an Alpha scenario."""
|
|
|
|
if item.focused_spotlight is None:
|
|
raise AssertionError("An Alpha outbound scenario requires a focused class.")
|
|
return item.focused_spotlight
|
|
|
|
|
|
class InspectionScenarioContributorBacklog(ContributorBacklog):
|
|
"""Contributor backlog that supplies one fixed inspection conclusion."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
backlog: DurableBackLog,
|
|
file_manager: FileManager,
|
|
source: ClassMeta,
|
|
readonly_sources: list[ClassMeta],
|
|
guidance: str,
|
|
) -> None:
|
|
"""Bind fixed inspection guidance to the real Alpha-facing backlog surface."""
|
|
|
|
super().__init__(
|
|
backlog=backlog,
|
|
get_tests=lambda _sources: [],
|
|
file_manager=file_manager,
|
|
parser=file_manager.parser,
|
|
language_strategy_set=_language_strategy_set(
|
|
file_manager,
|
|
runner=PassingRunner(),
|
|
),
|
|
)
|
|
self._expected_source = source
|
|
self._expected_source_path = file_manager.source_path(source.location)
|
|
self._expected_readonly_sources = list(readonly_sources)
|
|
self._guidance = guidance
|
|
|
|
def inspection_work_needed_for(
|
|
self,
|
|
class_meta: ClassMeta,
|
|
prompt: str,
|
|
inspection_items: list[InspectionBacklog],
|
|
*,
|
|
request: AiderEditRequest,
|
|
readonly_sources: list[ClassMeta],
|
|
) -> str:
|
|
"""Validate Alpha's inspection boundary and return stationary work guidance."""
|
|
|
|
assert class_meta is self._expected_source
|
|
assert prompt == "IMMUTABLE INSPECTION COT"
|
|
assert len(inspection_items) == 1
|
|
assert request.authoritative_prompt == ""
|
|
assert request.editable_files == [self._expected_source_path]
|
|
assert readonly_sources == self._expected_readonly_sources
|
|
return self._guidance
|
|
|
|
|
|
class InspectionBoundaryEditor(CodeEditor):
|
|
"""Concrete source editor that validates the final inspection edit boundary."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
editable: Path,
|
|
readonly: list[Path],
|
|
) -> None:
|
|
"""Store the sole legal source-edit scope for this scenario."""
|
|
|
|
self._editable = editable
|
|
self._readonly = list(readonly)
|
|
self.new_files_callback: Callable[..., bool] | None = None
|
|
|
|
@property
|
|
def inner(self) -> CodeEditor | None:
|
|
"""Expose this deterministic editor as an undecorated worker."""
|
|
|
|
return None
|
|
|
|
def edit(
|
|
self,
|
|
request: AiderEditRequest,
|
|
logger: logging.Logger | None = None,
|
|
) -> CodeEditorResult:
|
|
"""Assert the semantic boundary and return one stationary source edit."""
|
|
|
|
del logger
|
|
assert request.authoritative_prompt.startswith("You are Alpha.")
|
|
assert "IMMUTABLE INSPECTION COT" not in request.authoritative_prompt
|
|
assert request.editable_files == [self._editable]
|
|
assert request.readonly_files == self._readonly
|
|
current_repo_exec().write_text(self._editable, "def focus():\n return 2\n")
|
|
return CodeEditorResult([self._editable], "inspect the selected source", [])
|
|
|
|
|
|
class SourceThenTestRoutingService(BetaWorkflowJudgementService):
|
|
"""Real routing-service subtype with one source decision followed by test decisions."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Set the finite stationary routing sequence required by the retry scenario."""
|
|
|
|
super().__init__()
|
|
self._outcomes = ["source", "test", "test"]
|
|
|
|
def route_targeted_failure(
|
|
self,
|
|
*,
|
|
file_manager: FileManager,
|
|
story: TargetedFailureStory,
|
|
previous_cot_records: tuple[CotTaskRecord, ...] = (),
|
|
logger: logging.Logger | None = None,
|
|
) -> TargetedAlphaOrBetaRoutingDecision:
|
|
"""Return the next configured ownership decision without endpoint activity."""
|
|
|
|
# An exhausted script is an invalid test setup, not another routing decision.
|
|
if not self._outcomes:
|
|
raise AssertionError("The routing scenario received an unexpected extra decision.")
|
|
outcome = self._outcomes.pop(0)
|
|
assert story.focused_test is not None
|
|
decision = TargetedAlphaOrBetaRoutingDecision(
|
|
client=file_manager.client,
|
|
file_manager=file_manager,
|
|
request=AiderEditRequest(
|
|
authoritative_prompt="Route the stationary failure.",
|
|
editable_files=[file_manager.test_path(story.focused_test.location)],
|
|
readonly_files=list(map(lambda source: source.resolve_path(file_manager), story.candidate_sources)),
|
|
previous_cot_records=previous_cot_records,
|
|
),
|
|
story=story,
|
|
logger=logger,
|
|
)
|
|
decision.outcome = outcome
|
|
decision.reason = f"Stationary test scenario selected {outcome} ownership."
|
|
return decision
|
|
|
|
|
|
class RoutingClientThatReturnsTest(FakeClient):
|
|
"""Completion client that establishes ordinary Alpha-postflight test ownership."""
|
|
|
|
def complete(self, prompt: str, logger: logging.Logger | None = None) -> str:
|
|
"""Return a grounded test route and defer unrelated prompts to the shared fake."""
|
|
|
|
if "Judge Alpha-or-Beta routing for this targeted test failure." in prompt:
|
|
return json.dumps(
|
|
{
|
|
"outcome": "test",
|
|
"reason": "The observed failure belongs to the selected test assignment.",
|
|
"additional": {
|
|
"test_work_needed": "Repair the selected test assertion.",
|
|
},
|
|
}
|
|
)
|
|
return super().complete(prompt, logger)
|
|
|
|
|
|
class AlphaBlankRepairClient(FakeClient):
|
|
"""AlphaBlankRepairClient serves one source preflight response and one correction response."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
preflight_paths: list[str],
|
|
correction_response: str,
|
|
) -> None:
|
|
"""Store deterministic responses and capture prompt counts."""
|
|
|
|
super().__init__()
|
|
self.preflight_paths = preflight_paths
|
|
self.correction_response = correction_response
|
|
self.preflight_calls = 0
|
|
self.correction_calls = 0
|
|
|
|
def complete(self, prompt: str, logger: logging.Logger | None = None) -> str:
|
|
"""Return the configured preflight or repair response for Alpha prompts."""
|
|
|
|
if "Return exactly one JSON array of repo-relative source paths." in prompt:
|
|
self.preflight_calls += 1
|
|
return json.dumps(self.preflight_paths)
|
|
if "Infer the missing Alpha source-edit step." in prompt:
|
|
self.correction_calls += 1
|
|
return self.correction_response
|
|
return super().complete(prompt, logger)
|
|
|
|
|
|
class BlankThenFilledSourceWorker(CodeEditor):
|
|
"""BlankThenFilledSourceWorker leaves one preflight source blank, then fills it on retry."""
|
|
|
|
def __init__(self, backlog: DurableBackLog | None = None) -> None:
|
|
"""Store request history and optional backlog assertions."""
|
|
|
|
self.backlog = backlog
|
|
self.requests: list[AiderEditRequest] = []
|
|
self.new_files_callback: Callable[..., bool] | None = None
|
|
|
|
@property
|
|
def inner(self) -> CodeEditor | None:
|
|
"""Expose this nominal fake as an undecorated editor."""
|
|
|
|
return None
|
|
|
|
def edit(
|
|
self,
|
|
request: AiderEditRequest,
|
|
logger: logging.Logger | None = None,
|
|
) -> CodeEditorResult:
|
|
"""Perform one blank first pass and one repaired second pass."""
|
|
|
|
del logger
|
|
self.requests.append(request)
|
|
if len(self.requests) == 1:
|
|
current_repo_exec().write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "first source edit", [])
|
|
|
|
if self.backlog is not None:
|
|
assert not self.backlog.testing
|
|
current_repo_exec().write_text(
|
|
request.editable_files[1],
|
|
"class Extracted:\n pass\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[1]], "repaired blank source", [])
|
|
|
|
|
|
class SpyCorrectionAlphaLoop(AlphaLoop):
|
|
"""SpyCorrectionAlphaLoop records specialized blank-source correction checks."""
|
|
|
|
def _blank_source_correction_check(
|
|
self,
|
|
*,
|
|
editable_sources: list[ClassMeta],
|
|
readonly_sources: list[ClassMeta],
|
|
readonly_tests: list[ClassMeta],
|
|
blank_sources: list[ClassMeta],
|
|
work_needed_by_location: dict[str, str],
|
|
previous_summary: str,
|
|
request: AiderEditRequest,
|
|
) -> AlphaBlankSourceCorrectionDecision:
|
|
"""Record the blank-source correction check before returning it."""
|
|
|
|
if not hasattr(self, "correction_checks"):
|
|
self.correction_checks = []
|
|
check = super()._blank_source_correction_check(
|
|
editable_sources=editable_sources,
|
|
readonly_sources=readonly_sources,
|
|
readonly_tests=readonly_tests,
|
|
blank_sources=blank_sources,
|
|
work_needed_by_location=work_needed_by_location,
|
|
previous_summary=previous_summary,
|
|
request=request,
|
|
)
|
|
self.correction_checks.append(check)
|
|
return check
|
|
|
|
|
|
class AlphaAcceptanceSequenceClient(FakeClient):
|
|
"""AlphaAcceptanceSequenceClient returns configured acceptance outcomes in order."""
|
|
|
|
def __init__(
|
|
self,
|
|
outcomes: list[dict[str, object] | Exception],
|
|
*,
|
|
preflight_paths: list[str] | None = None,
|
|
) -> None:
|
|
"""Store scripted acceptance responses and optional preflight file requests."""
|
|
|
|
super().__init__()
|
|
self.outcomes = list(outcomes)
|
|
self.preflight_paths = list(preflight_paths or [])
|
|
self.acceptance_prompts: list[str] = []
|
|
|
|
def complete(self, prompt: str, logger: logging.Logger | None = None) -> str:
|
|
"""Return configured preflight and Alpha acceptance responses."""
|
|
|
|
if "Return exactly one JSON array of repo-relative source paths." in prompt:
|
|
return json.dumps(self.preflight_paths)
|
|
if (
|
|
"ACCEPTANCE ROLE" in prompt
|
|
and "ALPHA EDIT ACCEPTANCE" in prompt
|
|
and "DECISION RULE" in prompt
|
|
):
|
|
self.acceptance_prompts.append(prompt)
|
|
outcome = self.outcomes.pop(0) if self.outcomes else {
|
|
"outcome": "pass",
|
|
"reason": ["def foo():"],
|
|
"instruction": "",
|
|
}
|
|
if isinstance(outcome, Exception):
|
|
raise outcome
|
|
return json.dumps(outcome)
|
|
return super().complete(prompt, logger)
|
|
|
|
|
|
class SequenceSourceWorker(CodeEditor):
|
|
"""SequenceSourceWorker runs one configured mutation per edit request."""
|
|
|
|
def __init__(self, *actions: Callable[[AiderEditRequest], CodeEditorResult]) -> None:
|
|
self.actions = list(actions)
|
|
self.requests: list[AiderEditRequest] = []
|
|
self.new_files_callback: Callable[..., bool] | None = None
|
|
|
|
@property
|
|
def inner(self) -> CodeEditor | None:
|
|
"""Expose this nominal fake as an undecorated editor."""
|
|
|
|
return None
|
|
|
|
def edit(
|
|
self,
|
|
request: AiderEditRequest,
|
|
logger: logging.Logger | None = None,
|
|
) -> CodeEditorResult:
|
|
"""Record the request and execute the next configured action."""
|
|
|
|
del logger
|
|
self.requests.append(request)
|
|
action = self.actions.pop(0)
|
|
return action(request)
|
|
|
|
|
|
def test_alpha_request_includes_pinned_context(tmp_path: Path) -> None:
|
|
"""Verify Alpha passes pinned summaries through AiderEditRequest."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
context.pinned_context = [
|
|
"Interface summary:\nRoot interface summary.\n\nDiff summary:\nRoot diff summary."
|
|
]
|
|
worker = NoChangeWorker()
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker).run()
|
|
|
|
assert worker.requests[0].pinned_context == context.pinned_context
|
|
|
|
def test_alpha_consumes_inspection_before_source_work(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha consumes inspection before creating downstream testing."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
inspection = InspectionBacklog(source, "inspect foo")
|
|
backlog.queue_inspection(inspection)
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
|
|
graph = Provenance.open(repo_root=file_manager.repo_root, repo_exec=file_manager.repo_exec)
|
|
loop = HandlingAlphaLoop(gate=_alpha_gate(context), context=context, provenance=graph)
|
|
loop.run()
|
|
|
|
assert loop.work_needed_by_location[source.location]
|
|
assert not backlog.inspection
|
|
assert not backlog.testing
|
|
|
|
def test_alpha_consumes_remediation_before_source_work(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha consumes remediation before creating downstream testing."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
remediation = _failed_item(source, test)
|
|
backlog.queue_remediation(remediation)
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
|
|
graph = Provenance.open(repo_root=file_manager.repo_root, repo_exec=file_manager.repo_exec)
|
|
loop = HandlingAlphaLoop(gate=_alpha_gate(context), context=context, provenance=graph)
|
|
loop.run()
|
|
|
|
assert not backlog.testing
|
|
assert not backlog.remediation
|
|
|
|
|
|
def test_alpha_source_remediation_request_passes_structured_failure_evidence(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha forwards remediation failure evidence into AiderEditRequest."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "helper.py",
|
|
"def helper():\n return 1\n",
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / "support.py",
|
|
"def support():\n return 1\n",
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
readonly_source = file_manager.require_src("src/helper.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
support = file_manager.require_test("tests/support.py")
|
|
backlog = BackLog(
|
|
remediation=[
|
|
_failed_item(
|
|
source,
|
|
test,
|
|
upstream_sources=[readonly_source],
|
|
stack_trace=[
|
|
"tests/test_foo.py:4: AssertionError: expected 2 but got 1",
|
|
"src/foo.py:1: in foo",
|
|
],
|
|
error_message="AssertionError: expected 2 but got 1",
|
|
raw_output=(
|
|
"tests/test_foo.py:4: AssertionError: expected 2 but got 1\n"
|
|
"src/foo.py:1: in foo\n"
|
|
),
|
|
)
|
|
]
|
|
)
|
|
backlog.remediation[0].failure_context_sources = [support, test, readonly_source]
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
worker = RecordingSourceWorker([])
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker).run()
|
|
|
|
assert worker.requests[0].is_remediation
|
|
assert worker.requests[0].remediation_originator_test == test
|
|
assert worker.requests[0].failure_error_message == "AssertionError: expected 2 but got 1"
|
|
assert worker.requests[0].failure_stack_trace == [
|
|
"tests/test_foo.py:4: AssertionError: expected 2 but got 1",
|
|
"src/foo.py:1: in foo",
|
|
]
|
|
assert worker.requests[0].failure_raw_output == (
|
|
"tests/test_foo.py:4: AssertionError: expected 2 but got 1\n"
|
|
"src/foo.py:1: in foo\n"
|
|
)
|
|
assert worker.requests[0].readonly_files == [
|
|
file_manager.src_root / "helper.py",
|
|
file_manager.test_root / "support.py",
|
|
file_manager.test_root / "test_foo.py",
|
|
]
|
|
assert worker.requests[0].backlog_guidance == ["src/foo.py: Refresh metadata."]
|
|
|
|
|
|
def test_alpha_readonly_paths_preserve_duplicate_topology_context(tmp_path: Path) -> None:
|
|
"""Verify Alpha keeps duplicate readonly topology paths when upstream/downstream repeat them."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "helper.py",
|
|
"def helper():\n return 1\n",
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
helper = file_manager.require_src("src/helper.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
context = _alpha_context(file_manager, BackLog(), source, test)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context)
|
|
|
|
readonly = loop._readonly_paths(
|
|
[source],
|
|
[helper, helper],
|
|
[test, test],
|
|
)
|
|
|
|
assert readonly == [
|
|
file_manager.src_root / "helper.py",
|
|
file_manager.src_root / "helper.py",
|
|
file_manager.test_root / "test_foo.py",
|
|
file_manager.test_root / "test_foo.py",
|
|
]
|
|
|
|
|
|
def test_alpha_source_remediation_skips_preflight_new_file_requests(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify remediation work skips Alpha preflight and never requests new files."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaPreflightClient({"src/foo.py": ["src/pkg/extracted_class.py"]})
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog(remediation=[_failed_item(source, test)])
|
|
requested_payloads: list[dict[object, str]] = []
|
|
|
|
def new_files_callback(files=None, **kwargs):
|
|
payload = {}
|
|
if files:
|
|
payload.update(files)
|
|
payload.update(kwargs)
|
|
requested_payloads.append(payload)
|
|
return True
|
|
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
context.new_files_callback = new_files_callback
|
|
worker = RecordingSourceWorker([])
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker).run()
|
|
|
|
assert worker.requests
|
|
assert file_manager.client.prompts == []
|
|
assert requested_payloads == []
|
|
assert not file_manager.repo_exec.exists(
|
|
file_manager.src_root / "pkg/extracted_class.py"
|
|
)
|
|
|
|
def test_alpha_worker_edit_refreshes_source_and_creates_testing_backlog(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha calls AiderWorker, refreshes changed source files, and routes testing backlog."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
worker = EditingSourceWorker()
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker).run()
|
|
|
|
assert worker.requests
|
|
assert file_manager.src_dirty[source.location] is True
|
|
assert [_focused_class_of(item).location for item in backlog.testing] == ["tests/test_foo.py"]
|
|
|
|
def test_alpha_worker_no_changed_paths_creates_no_testing_backlog(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha does not fake handled sources when Aider reports no changed paths."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
worker = NoChangeWorker()
|
|
|
|
summary = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker).run()
|
|
|
|
assert worker.requests
|
|
assert "no changes" in summary
|
|
assert not backlog.testing
|
|
|
|
|
|
def test_alpha_no_memento_diff_still_runs_acceptance_and_skips_testing_backlog(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify no-diff Alpha acceptance consumes mementos without creating Beta work."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["return 1"], "instruction": ""}]
|
|
)
|
|
file_manager.client = client
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
worker = NoChangeWorker()
|
|
|
|
summary = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker).run()
|
|
|
|
assert worker.requests
|
|
assert client.acceptance_prompts
|
|
assert "no changes" in summary
|
|
assert not file_manager.repo_exec.exists(
|
|
Memento(file_manager.source_path(source.location), file_manager.repo_exec).path
|
|
)
|
|
assert not backlog.testing
|
|
|
|
|
|
def test_alpha_preflight_runs_once_per_source_and_includes_only_that_source_content(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha preflights each source separately with full content for that source only."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "bar.py",
|
|
"class Bar:\n pass\n",
|
|
)
|
|
client = AlphaPreflightClient(
|
|
{
|
|
"src/foo.py": [],
|
|
"src/bar.py": [],
|
|
}
|
|
)
|
|
file_manager.client = client
|
|
file_manager.resync()
|
|
foo = file_manager.require_src("src/foo.py")
|
|
bar = file_manager.require_src("src/bar.py")
|
|
backlog = BackLog()
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
context = _alpha_context(file_manager, backlog, foo, test)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context)
|
|
|
|
graph = Provenance.open(repo_root=file_manager.repo_root, repo_exec=file_manager.repo_exec)
|
|
item = InspectionBacklog(foo, "inspect foo")
|
|
prior_alpha = graph.start(Phase.ALPHA, InspectionBacklog(foo, "prior source work"))
|
|
beta = graph.track_provenance(
|
|
prior_alpha,
|
|
Phase.BETA,
|
|
TestingBacklog(test, "test the prior source work"),
|
|
)
|
|
graph.track_provenance(beta, Phase.ALPHA, item)
|
|
with provenance.bind(graph):
|
|
with provenance.item_scope(Phase.ALPHA, item):
|
|
assert loop._preflight_new_source_files(
|
|
[foo, bar],
|
|
{
|
|
"src/foo.py": "inspect foo",
|
|
"src/bar.py": "inspect bar",
|
|
},
|
|
) == []
|
|
|
|
assert len(client.prompts) == 2
|
|
first, second = client.prompts
|
|
assert "====== BEGIN LOCATION ======\nsrc/foo.py\n====== END LOCATION ======" in first
|
|
assert "def foo():\n return 1\n" in first
|
|
assert "BEGIN SUMMARY" not in first
|
|
assert "BEGIN RELEVANCE" not in first
|
|
assert "BEGIN RELEVANT SYMBOLS" not in first
|
|
assert "class Bar" not in first
|
|
assert "====== BEGIN LOCATION ======\nsrc/bar.py\n====== END LOCATION ======" in second
|
|
assert "class Bar:\n pass\n" in second
|
|
assert "def foo" not in second
|
|
|
|
def test_alpha_preflight_concatenates_dedupes_and_keeps_multiple_paths_per_source(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha merges ordered per-source responses without collapsing multi-file requests."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "bar.py",
|
|
"class Bar:\n pass\n",
|
|
)
|
|
file_manager.client = AlphaPreflightClient(
|
|
{
|
|
"src/foo.py": [
|
|
"src/pkg/model_response_error.py",
|
|
"src/pkg/malformed_model_response_error.py",
|
|
],
|
|
"src/bar.py": [
|
|
"src/pkg/malformed_model_response_error.py",
|
|
"src/pkg/openai_compatible_client.py",
|
|
],
|
|
}
|
|
)
|
|
file_manager.resync()
|
|
foo = file_manager.require_src("src/foo.py")
|
|
bar = file_manager.require_src("src/bar.py")
|
|
backlog = BackLog()
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
context = _alpha_context(file_manager, backlog, foo, test)
|
|
requested_payloads: list[dict[Path, str]] = []
|
|
|
|
def new_files_callback(files=None, **kwargs):
|
|
payload = {}
|
|
if files:
|
|
payload.update(files)
|
|
payload.update(kwargs)
|
|
requested_payloads.append(payload)
|
|
for file_name, reason in payload.items():
|
|
path = file_manager.source_path(str(file_name))
|
|
file_manager.repo_exec.write_text(path, "")
|
|
file_manager.upsert_src(
|
|
ClassMeta(
|
|
location=file_manager.source_location(path),
|
|
summary=reason,
|
|
relevance=[reason],
|
|
relevant_symbols={},
|
|
),
|
|
reason,
|
|
)
|
|
return True
|
|
|
|
context.new_files_callback = new_files_callback
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context)
|
|
|
|
requested = loop._preflight_new_source_files(
|
|
[foo, bar],
|
|
{
|
|
"src/foo.py": "extract foo classes into separate files",
|
|
"src/bar.py": "extract bar client into separate file",
|
|
},
|
|
)
|
|
|
|
assert [source.location for source in requested] == [
|
|
"src/pkg/model_response_error.py",
|
|
"src/pkg/malformed_model_response_error.py",
|
|
"src/pkg/openai_compatible_client.py",
|
|
]
|
|
assert requested_payloads == [
|
|
{
|
|
Path("src/pkg/model_response_error.py"): "Required by Alpha source preflight for src/foo.py.",
|
|
Path("src/pkg/malformed_model_response_error.py"): "Required by Alpha source preflight for src/foo.py.",
|
|
Path("src/pkg/openai_compatible_client.py"): "Required by Alpha source preflight for src/bar.py.",
|
|
}
|
|
]
|
|
|
|
def test_alpha_preflight_created_source_becomes_editable_before_aider_runs(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha preflight provisions extraction targets before the edit request is built."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaPreflightClient(
|
|
{"src/foo.py": ["src/pkg/extracted_class.py"]}
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
events: list[tuple[str, object]] = []
|
|
|
|
def new_files_callback(files=None, **kwargs):
|
|
payload = {}
|
|
if files:
|
|
payload.update(files)
|
|
payload.update(kwargs)
|
|
events.append(("callback", dict(payload)))
|
|
for file_name, reason in payload.items():
|
|
path = file_manager.source_path(str(file_name))
|
|
file_manager.repo_exec.write_text(path, "")
|
|
file_manager.upsert_src(
|
|
ClassMeta(
|
|
location=file_manager.source_location(path),
|
|
summary=reason,
|
|
relevance=[reason],
|
|
relevant_symbols={},
|
|
),
|
|
reason,
|
|
)
|
|
return True
|
|
|
|
context.new_files_callback = new_files_callback
|
|
|
|
def fill_preflight_source(request: AiderEditRequest) -> CodeEditorResult:
|
|
"""Fill the provisioned source after the callback has installed its blank file."""
|
|
|
|
assert not request.is_remediation
|
|
assert request.remediation_originator_test is None
|
|
events.append(("edit", list(request.editable_files)))
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[1],
|
|
"class ExtractedClass:\n pass\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[1]], "filled preflight source", [])
|
|
|
|
worker = SequenceSourceWorker(fill_preflight_source)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)
|
|
|
|
loop._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "extract Foo into its own file"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
requested_path = file_manager.src_root / "pkg/extracted_class.py"
|
|
assert events[0][0] == "callback"
|
|
assert events[1][0] == "edit"
|
|
assert file_manager.require_src("src/pkg/extracted_class.py") is not None
|
|
assert worker.requests[0].editable_files == [
|
|
file_manager.src_root / "foo.py",
|
|
requested_path,
|
|
]
|
|
|
|
|
|
def test_alpha_blank_preflight_source_triggers_correction_retry_with_same_scope(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha retries blank preflight sources with the same scope and repair guidance."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaBlankRepairClient(
|
|
preflight_paths=["src/pkg/extracted_class.py"],
|
|
correction_response=json.dumps(
|
|
{
|
|
"instruction": "Populate src/pkg/extracted_class.py with the extracted implementation.",
|
|
"reason": "The new extraction target stayed blank after the first edit.",
|
|
"blank_files": ["src/pkg/extracted_class.py"],
|
|
}
|
|
),
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "helper.py",
|
|
"def helper():\n return 3\n",
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / "test_helper.py",
|
|
"def test_helper():\n assert True\n",
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
readonly_source = file_manager.require_src("src/helper.py")
|
|
readonly_test = file_manager.require_test("tests/test_helper.py")
|
|
backlog = BackLog()
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
|
|
def new_files_callback(files=None, **kwargs):
|
|
payload = {}
|
|
if files:
|
|
payload.update(files)
|
|
payload.update(kwargs)
|
|
for file_name, reason in payload.items():
|
|
path = file_manager.source_path(str(file_name))
|
|
file_manager.repo_exec.write_text(path, "")
|
|
file_manager.upsert_src(
|
|
ClassMeta(
|
|
location=file_manager.source_location(path),
|
|
summary=reason,
|
|
relevance=[reason],
|
|
relevant_symbols={},
|
|
),
|
|
reason,
|
|
)
|
|
return True
|
|
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
context.new_files_callback = new_files_callback
|
|
worker = BlankThenFilledSourceWorker()
|
|
loop = SpyCorrectionAlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
aider_worker=worker,
|
|
)
|
|
|
|
graph = Provenance.open(repo_root=file_manager.repo_root, repo_exec=file_manager.repo_exec)
|
|
item = InspectionBacklog(source, "extract Foo")
|
|
graph.start(Phase.ALPHA, item)
|
|
with provenance.bind(graph):
|
|
with provenance.item_scope(Phase.ALPHA, item):
|
|
loop._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "extract Foo into src/pkg/extracted_class.py"},
|
|
readonly_sources=[readonly_source],
|
|
readonly_tests=[readonly_test],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert len(worker.requests) == 2
|
|
assert file_manager.client.preflight_calls == 1
|
|
assert file_manager.client.correction_calls == 1
|
|
assert len(loop.correction_checks) == 2
|
|
assert isinstance(loop.correction_checks[0], AlphaBlankSourceCorrectionDecision)
|
|
assert loop.correction_checks[0].outcome == "fail"
|
|
assert loop.correction_checks[1].outcome == "pass"
|
|
assert worker.requests[1].editable_files == worker.requests[0].editable_files
|
|
assert worker.requests[1].readonly_files == worker.requests[0].readonly_files
|
|
assert worker.requests[1].backlog_guidance[:-1] == worker.requests[0].backlog_guidance
|
|
assert "ALPHA EDIT ACCEPTANCE FAILED" in worker.requests[1].backlog_guidance[-1]
|
|
assert "Populate src/pkg/extracted_class.py with the extracted implementation." in (
|
|
worker.requests[1].backlog_guidance[-1]
|
|
)
|
|
assert file_manager.repo_exec.exists(
|
|
file_manager.src_root / "pkg/extracted_class.py"
|
|
)
|
|
|
|
|
|
def test_alpha_waits_for_nonblank_preflight_source_before_creating_beta_backlog(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha delays Beta backlog creation until preflight-created source files are nonblank."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaBlankRepairClient(
|
|
preflight_paths=["src/pkg/extracted_class.py"],
|
|
correction_response=json.dumps(
|
|
{
|
|
"instruction": "Populate src/pkg/extracted_class.py with the extracted implementation.",
|
|
"reason": "The extraction target is still blank.",
|
|
"blank_files": ["src/pkg/extracted_class.py"],
|
|
}
|
|
),
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
|
|
def new_files_callback(files=None, **kwargs):
|
|
payload = {}
|
|
if files:
|
|
payload.update(files)
|
|
payload.update(kwargs)
|
|
for file_name, reason in payload.items():
|
|
path = file_manager.source_path(str(file_name))
|
|
file_manager.repo_exec.write_text(path, "")
|
|
file_manager.upsert_src(
|
|
ClassMeta(
|
|
location=file_manager.source_location(path),
|
|
summary=reason,
|
|
relevance=[reason],
|
|
relevant_symbols={},
|
|
),
|
|
reason,
|
|
)
|
|
return True
|
|
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
context.new_files_callback = new_files_callback
|
|
worker = BlankThenFilledSourceWorker(backlog)
|
|
|
|
summary = AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
aider_worker=worker,
|
|
).run()
|
|
|
|
assert len(worker.requests) == 2
|
|
assert file_manager.repo_exec.read_text(
|
|
file_manager.src_root / "pkg/extracted_class.py"
|
|
) == "class Extracted:\n pass\n"
|
|
assert file_manager.src_dirty["src/pkg/extracted_class.py"] is True
|
|
assert backlog.testing
|
|
assert all(
|
|
source.location in [downstream.location for downstream in item.downstream_classes]
|
|
for item in backlog.testing
|
|
)
|
|
assert all(item.downstream_classes[0].location == "src/pkg/extracted_class.py" for item in backlog.testing)
|
|
assert "Alpha completed source work for 1 file(s)." in summary
|
|
|
|
|
|
def test_alpha_rolls_back_existing_memento_before_preflight(tmp_path: Path) -> None:
|
|
"""Verify stale source mementos are rolled back before preflight and editing."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["def foo():"], "instruction": ""}]
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
source_path = file_manager.src_root / "foo.py"
|
|
file_manager.repo_exec.write_text(source_path, "corrupted\n")
|
|
file_manager.repo_exec.write_text(
|
|
Memento(source_path, file_manager.repo_exec).path,
|
|
"def foo():\n return 1\n",
|
|
)
|
|
|
|
def action(request):
|
|
assert file_manager.repo_exec.read_text(
|
|
request.editable_files[0]
|
|
) == "def foo():\n return 1\n"
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "accepted edit", [])
|
|
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
worker = SequenceSourceWorker(action)
|
|
|
|
handled, _ = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "change foo"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert [item.location for item in handled] == ["src/foo.py"]
|
|
assert not file_manager.repo_exec.exists(
|
|
Memento(source_path, file_manager.repo_exec).path
|
|
)
|
|
|
|
|
|
def test_alpha_preflight_memento_rolls_back_before_editor_and_source_remains(tmp_path: Path) -> None:
|
|
"""Verify post-preflight recovery restores stale preflight mementos without deleting the source."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["def foo():"], "instruction": ""}],
|
|
preflight_paths=["src/pkg/extracted.py"],
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
|
|
def new_files_callback(files=None, **kwargs):
|
|
payload = {}
|
|
if files:
|
|
payload.update(files)
|
|
payload.update(kwargs)
|
|
for file_name, reason in payload.items():
|
|
path = file_manager.source_path(str(file_name))
|
|
file_manager.repo_exec.write_text(path, "corrupted preflight\n")
|
|
memento = Memento(path, file_manager.repo_exec)
|
|
file_manager.repo_exec.write_text(memento.path, "")
|
|
file_manager.repo_exec.write_text(
|
|
Memento(path, file_manager.repo_exec, layer=1).path,
|
|
"corrupted preflight\n",
|
|
)
|
|
file_manager.upsert_src(
|
|
ClassMeta(file_manager.source_location(path), reason, [reason], {}),
|
|
reason,
|
|
)
|
|
return True
|
|
|
|
def action(request):
|
|
preflight_path = request.editable_files[1]
|
|
assert file_manager.repo_exec.exists(preflight_path)
|
|
assert file_manager.repo_exec.read_text(preflight_path) == ""
|
|
assert file_manager.repo_exec.exists(
|
|
Memento(preflight_path, file_manager.repo_exec).path
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
preflight_path,
|
|
"class Extracted:\n pass\n",
|
|
)
|
|
return CodeEditorResult([preflight_path], "accepted preflight edit", [])
|
|
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
context.new_files_callback = new_files_callback
|
|
worker = SequenceSourceWorker(action)
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "extract class"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
preflight_path = file_manager.src_root / "pkg/extracted.py"
|
|
assert file_manager.repo_exec.exists(preflight_path)
|
|
assert file_manager.repo_exec.read_text(preflight_path) == "class Extracted:\n pass\n"
|
|
assert not file_manager.repo_exec.exists(
|
|
Memento(preflight_path, file_manager.repo_exec).path
|
|
)
|
|
|
|
|
|
def test_alpha_no_work_inbound_clears_previous_receipts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""An inspection no-op cannot retain another iteration's remediation evidence."""
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
context = _alpha_context(file_manager, BackLog(), source, test)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=SequenceSourceWorker())
|
|
loop.validation_receipts = ValidationReceipts((ValidationReceipt.from_result(test,
|
|
TestRunResult("pytest previous", TestRunOutcome.Success, 0, "previous pass", test_name="test_foo"),
|
|
test_definition="def test_foo(): pass", generation=1),), generation=1)
|
|
inspection = Mock(return_value=[])
|
|
postflight = Mock()
|
|
monkeypatch.setattr(loop, "_run_inspection_source_item", inspection)
|
|
monkeypatch.setattr(loop, "_stage_postflight_alpha_results", postflight)
|
|
item = InspectionBacklog(source, "inspect foo")
|
|
|
|
loop._process_inbound_item(item, resume=True)
|
|
|
|
assert loop.validation_receipts.receipts == ()
|
|
inspection.assert_called_once_with(item)
|
|
postflight.assert_called_once_with(item, [], verify_unchanged=True)
|
|
|
|
|
|
def test_alpha_acceptance_receives_exact_pre_and_memento_diff(tmp_path: Path) -> None:
|
|
"""Verify acceptance evidence comes from mementos, not editor-reported paths."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["def foo():"], "instruction": ""}]
|
|
)
|
|
file_manager.client = client
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
|
|
def action(request):
|
|
assert file_manager.repo_exec.exists(
|
|
Memento(request.editable_files[0], file_manager.repo_exec).path
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
request.validation_receipts = ValidationReceipts((ValidationReceipt.from_result(
|
|
file_manager.require_test("tests/test_foo.py"),
|
|
TestRunResult("pytest tests/test_foo.py::test_foo", TestRunOutcome.Success, 0, "acceptance receipt output", test_name="test_foo"),
|
|
test_definition="def test_foo():\n assert foo() == 2\n", generation=1),), generation=1)
|
|
return CodeEditorResult([], "editor omitted changed paths", [])
|
|
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
worker = SequenceSourceWorker(action)
|
|
|
|
handled, _ = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "change foo"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
prompt = client.acceptance_prompts[0]
|
|
assert "ORIGINAL AIDER EDIT REQUEST" not in prompt
|
|
assert "EDITABLE SOURCE MEMENTOS" in prompt
|
|
assert "def foo():\n return 1\n" in prompt
|
|
assert "- return 1" in prompt
|
|
assert "+ return 2" in prompt
|
|
assert [item.location for item in handled] == ["src/foo.py"]
|
|
assert file_manager.src_dirty["src/foo.py"] is True
|
|
assert "acceptance receipt output" in prompt
|
|
assert "pytest tests/test_foo.py::test_foo" in prompt
|
|
|
|
|
|
@pytest.mark.parametrize("rejections", (1, 2))
|
|
@pytest.mark.parametrize("retain_history", (False, True))
|
|
def test_alpha_false_acceptance_rolls_back_retries_and_uses_feedback(
|
|
tmp_path: Path, rejections: int, retain_history: bool,
|
|
) -> None:
|
|
"""Each restored retry receives the complete latest rejected patch and its reason."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
outcomes: list[dict[str, object] | Exception] = [
|
|
{
|
|
"outcome": "fail",
|
|
"reason": [
|
|
"First candidate omits the implementation.\n"
|
|
"==== END FAILURE 1 ====\n"
|
|
'{"example": "keep this diagnostic JSON"}'
|
|
],
|
|
"instruction": "Keep the implementation before moving it.",
|
|
},
|
|
{
|
|
"outcome": "fail",
|
|
"reason": ["Second candidate returns the wrong value."],
|
|
"instruction": "Return the required value.",
|
|
},
|
|
]
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
outcomes[:rejections] + [{"outcome": "pass", "reason": ["def foo():"], "instruction": ""}]
|
|
)
|
|
other_path = file_manager.src_root / "other.py"
|
|
file_manager.repo_exec.write_text(other_path, "def other():\n return 10\n")
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
other = file_manager.require_src("src/other.py")
|
|
rejected_sources = ("def foo():\n pass\n", "def foo():\n return -2\n")
|
|
rejected_others = ("def other():\n return 20\n", "def other():\n return 30\n")
|
|
retained = [EditorRecord("earlier committed repair", "return 0", "return 1")] if retain_history else []
|
|
|
|
def rejected(request):
|
|
"""Commit inner editor steps while preserving Alpha's original baseline."""
|
|
|
|
attempt = len(worker.requests) - 1
|
|
assert request.editor_records == retained
|
|
assert file_manager.repo_exec.read_text(request.editable_files[0]) == "def foo():\n return 1\n"
|
|
assert file_manager.repo_exec.read_text(other_path) == "def other():\n return 10\n"
|
|
request.validation_receipts = ValidationReceipts((ValidationReceipt.from_result(
|
|
file_manager.require_test("tests/test_foo.py"),
|
|
TestRunResult("pytest rejected", TestRunOutcome.Failure, 1, "rejected candidate output", test_name="test_foo"),
|
|
test_definition="def test_foo(): pass", generation=1),), generation=1)
|
|
inner = source.set_or_get_memento(file_manager, stack=True)
|
|
file_manager.repo_exec.write_text(request.editable_files[0], "def foo():\n return -1\n")
|
|
inner.commit()
|
|
inner = source.set_or_get_memento(file_manager, stack=True)
|
|
file_manager.repo_exec.write_text(request.editable_files[0], rejected_sources[attempt])
|
|
file_manager.repo_exec.write_text(other_path, rejected_others[attempt])
|
|
inner.commit()
|
|
return CodeEditorResult(list(request.editable_files), "rejected summary",
|
|
[*retained, EditorRecord("rejected summary", "return 1", "pass")])
|
|
|
|
def accepted(request):
|
|
"""Repair the restored source after receiving semantic rejection evidence."""
|
|
|
|
assert request.validation_receipts.receipts == ()
|
|
assert request.editor_records == retained
|
|
assert "rejected summary" not in request.work_logs
|
|
assert file_manager.repo_exec.read_text(
|
|
request.editable_files[0]
|
|
) == "def foo():\n return 1\n"
|
|
assert file_manager.repo_exec.read_text(other_path) == "def other():\n return 10\n"
|
|
assert "RETRY REFINEMENT STEPS" in request.backlog_guidance[-1]
|
|
assert "1. Re-check the established concrete divergence" in request.backlog_guidance[-1]
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "accepted summary",
|
|
[*retained, EditorRecord("accepted summary", "return 1", "return 2")])
|
|
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
# Earlier committed history survives restoration of a later rejected Alpha attempt.
|
|
if retain_history:
|
|
assert context.move_inbound_from_backlog() is not None
|
|
assert context.active_inbound is not None
|
|
context.active_inbound.value.editor_records = list(retained)
|
|
worker = SequenceSourceWorker(*([rejected] * rejections), accepted)
|
|
|
|
handled, summary = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)._run_source_work(
|
|
[source, other],
|
|
{"src/foo.py": "change foo", "src/other.py": "change other"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert len(worker.requests) == rejections + 1
|
|
# Publish only the accepted attempt to the durable same-phase work item.
|
|
if retain_history:
|
|
assert context.active_inbound is not None
|
|
assert context.active_inbound.value.editor_records == [
|
|
*retained, EditorRecord("accepted summary", "return 1", "return 2"),
|
|
]
|
|
first_feedback = worker.requests[1].backlog_guidance[-1]
|
|
assert first_feedback.startswith("== BEGIN ALPHA EDIT ACCEPTANCE FAILED ==\n")
|
|
assert first_feedback.endswith("\n== END ALPHA EDIT ACCEPTANCE FAILED ==")
|
|
assert "==== BEGIN REJECTED PATCH ====\n" in first_feedback
|
|
assert "\n==== END REJECTED PATCH ====" in first_feedback
|
|
assert "First candidate omits the implementation." in first_feedback
|
|
assert '\\==== END FAILURE 1 ====\n{"example": "keep this diagnostic JSON"}' in first_feedback
|
|
assert "Keep the implementation before moving it." in first_feedback
|
|
assert (
|
|
f"--- {file_manager.src_root / '.foo.py~'}\n+++ {file_manager.src_root / 'foo.py'}\n"
|
|
"@@ -1,2 +1,2 @@\n def foo():\n- return 1\n+ pass\n"
|
|
) in first_feedback
|
|
assert (
|
|
f"--- {file_manager.src_root / '.other.py~'}\n+++ {other_path}\n"
|
|
"@@ -1,2 +1,2 @@\n def other():\n- return 10\n+ return 20\n"
|
|
) in first_feedback
|
|
assert "return -1" not in first_feedback
|
|
assert worker.requests[-1].backlog_guidance[0] == "src/foo.py: change foo"
|
|
# A second rejection replaces the first candidate's evidence instead of accumulating it.
|
|
if rejections == 2:
|
|
second_feedback = worker.requests[2].backlog_guidance[-1]
|
|
assert "Second candidate returns the wrong value." in second_feedback
|
|
assert "Return the required value." in second_feedback
|
|
assert "- return 1\n+ return -2\n" in second_feedback
|
|
assert "- return 10\n+ return 30\n" in second_feedback
|
|
assert "First candidate" not in second_feedback
|
|
assert "+ pass" not in second_feedback
|
|
assert "+ return 20" not in second_feedback
|
|
assert [item.location for item in handled] == ["src/foo.py"]
|
|
assert summary == "accepted summary"
|
|
assert "rejected summary" not in summary
|
|
|
|
|
|
def test_alpha_acceptance_exception_leaves_memento_for_reentry_recovery(tmp_path: Path) -> None:
|
|
"""Verify acceptance exceptions preserve mementos for the next Alpha entry."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[
|
|
ConnectionError("endpoint unreachable"),
|
|
{"outcome": "pass", "reason": ["def foo():"], "instruction": ""},
|
|
]
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
assert source is not None
|
|
|
|
def first(request):
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 9\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "endpoint failure summary", [])
|
|
|
|
def accepted(request):
|
|
assert file_manager.repo_exec.read_text(
|
|
request.editable_files[0]
|
|
) == "def foo():\n return 1\n"
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "accepted summary", [])
|
|
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
assert test is not None
|
|
context = _alpha_context(file_manager, BackLog(), source, test)
|
|
source_path = file_manager.src_root / "foo.py"
|
|
worker = SequenceSourceWorker(first, accepted)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)
|
|
|
|
with pytest.raises(ConnectionError, match="endpoint unreachable"):
|
|
loop._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "change foo"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert len(worker.requests) == 1
|
|
assert file_manager.repo_exec.read_text(source_path) == "def foo():\n return 9\n"
|
|
assert file_manager.repo_exec.exists(
|
|
Memento(source_path, file_manager.repo_exec).path
|
|
)
|
|
|
|
handled, _ = loop._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "change foo"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert [item.location for item in handled] == ["src/foo.py"]
|
|
assert len(worker.requests) == 2
|
|
assert file_manager.repo_exec.read_text(source_path) == "def foo():\n return 2\n"
|
|
assert not file_manager.repo_exec.exists(
|
|
Memento(source_path, file_manager.repo_exec).path
|
|
)
|
|
|
|
|
|
def test_alpha_leave_it_summary_skips_blank_and_acceptance_checks(tmp_path: Path) -> None:
|
|
"""Verify leave-it judgement summary bypasses Alpha acceptance checks as tie-breaker."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient([RuntimeError("should not run")])
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
|
|
def leave_it(request):
|
|
return CodeEditorResult([], CodeEditLeaveSummary(
|
|
f"CODE EDIT OR LEAVE IT\n{CODE_EDIT_OR_LEAVE_IT_LEAVE_TOKEN}\n"
|
|
"Judgement summary:\nNo changes needed."
|
|
), [])
|
|
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
worker = SequenceSourceWorker(leave_it)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)
|
|
|
|
handled, summary = loop._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "change foo"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert handled == []
|
|
assert CODE_EDIT_OR_LEAVE_IT_LEAVE_TOKEN not in summary
|
|
assert len(worker.requests) == 1
|
|
assert file_manager.client.acceptance_prompts == []
|
|
|
|
|
|
def test_alpha_literal_leave_token_in_ordinary_editor_text_does_not_skip_acceptance(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A token quoted by source or a normal editor summary is not runtime leave control."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["LEAVE_TOKEN_LITERAL = '[CODE_EDIT_OR_LEAVE_IT:LEAVE_IT]'"], "instruction": ""}]
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
|
|
def literal_source_text(request):
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"LEAVE_TOKEN_LITERAL = '[CODE_EDIT_OR_LEAVE_IT:LEAVE_IT]'\n"
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "Rendered source quoted [CODE_EDIT_OR_LEAVE_IT:LEAVE_IT] as ordinary evidence.", [])
|
|
|
|
context = _alpha_context(
|
|
file_manager,
|
|
BackLog(),
|
|
source,
|
|
file_manager.require_test("tests/test_foo.py"),
|
|
)
|
|
worker = SequenceSourceWorker(literal_source_text)
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)
|
|
|
|
handled, _ = loop._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "update the focused source"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert [item.location for item in handled] == ["src/foo.py"]
|
|
assert file_manager.client.acceptance_prompts
|
|
|
|
|
|
def test_alpha_blank_deleted_preflight_source_is_recreated_for_retry(tmp_path: Path) -> None:
|
|
"""Verify blank preflight files deleted by an edit are restored as blank real files."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaBlankRepairClient(
|
|
preflight_paths=["src/pkg/extracted.py"],
|
|
correction_response=json.dumps(
|
|
{
|
|
"instruction": "Populate src/pkg/extracted.py with the extracted implementation.",
|
|
"reason": "The preflight source was deleted and must be restored.",
|
|
"blank_files": ["src/pkg/extracted.py"],
|
|
}
|
|
),
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
|
|
def new_files_callback(files=None, **kwargs):
|
|
payload = {}
|
|
if files:
|
|
payload.update(files)
|
|
payload.update(kwargs)
|
|
for file_name, reason in payload.items():
|
|
path = file_manager.source_path(str(file_name))
|
|
file_manager.repo_exec.write_text(path, "")
|
|
file_manager.upsert_src(ClassMeta(file_manager.source_location(path), reason, [reason], {}), reason)
|
|
return True
|
|
|
|
def deletes_blank(request):
|
|
file_manager.repo_exec.delete(request.editable_files[1])
|
|
return CodeEditorResult([request.editable_files[1]], "deleted blank preflight", [])
|
|
|
|
def fills_blank(request):
|
|
assert file_manager.repo_exec.exists(request.editable_files[1])
|
|
assert file_manager.repo_exec.read_text(request.editable_files[1]) == ""
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[1],
|
|
"class Extracted:\n pass\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[1]], "accepted preflight", [])
|
|
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
context.new_files_callback = new_files_callback
|
|
worker = SequenceSourceWorker(deletes_blank, fills_blank)
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)._run_source_work(
|
|
[source],
|
|
{"src/foo.py": "extract class"},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert len(worker.requests) == 2
|
|
assert file_manager.client.correction_calls == 1
|
|
assert file_manager.repo_exec.read_text(
|
|
file_manager.src_root / "pkg/extracted.py"
|
|
) == "class Extracted:\n pass\n"
|
|
|
|
|
|
def test_alpha_unexpected_editor_exception_leaves_memento_for_reentry_recovery(tmp_path: Path) -> None:
|
|
"""Verify non-acceptance exceptions preserve mementos for the next Alpha entry."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["def foo():"], "instruction": ""}]
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
source_path = file_manager.src_root / "foo.py"
|
|
|
|
def raises(request):
|
|
file_manager.repo_exec.write_text(request.editable_files[0], "corrupted\n")
|
|
raise RuntimeError("editor failed")
|
|
|
|
def accepted(request):
|
|
assert file_manager.repo_exec.read_text(
|
|
request.editable_files[0]
|
|
) == "def foo():\n return 1\n"
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "accepted after recovery", [])
|
|
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=SequenceSourceWorker(raises))
|
|
|
|
with pytest.raises(RuntimeError):
|
|
loop._run_source_work([source], {"src/foo.py": "change foo"}, [], [], is_remediation=False)
|
|
|
|
assert file_manager.repo_exec.exists(
|
|
Memento(source_path, file_manager.repo_exec).path
|
|
)
|
|
loop.aider_worker = SequenceSourceWorker(accepted)
|
|
|
|
handled, _ = loop._run_source_work([source], {"src/foo.py": "change foo"}, [], [], is_remediation=False)
|
|
|
|
assert [item.location for item in handled] == ["src/foo.py"]
|
|
assert not file_manager.repo_exec.exists(
|
|
Memento(source_path, file_manager.repo_exec).path
|
|
)
|
|
|
|
|
|
def test_alpha_commits_mementos_before_testing_backlog_creation(tmp_path: Path) -> None:
|
|
"""Verify Beta backlog creation observes committed Alpha mementos."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["def foo():"], "instruction": ""}]
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
observed_mementos: list[bool] = []
|
|
contributor = ContributorBacklog(
|
|
backlog=backlog,
|
|
get_tests=lambda source_files: observed_mementos.append(
|
|
file_manager.repo_exec.exists(
|
|
Memento(
|
|
file_manager.source_path(source_files[0].location),
|
|
file_manager.repo_exec,
|
|
).path
|
|
)
|
|
) or [test],
|
|
file_manager=file_manager,
|
|
parser=file_manager.parser,
|
|
language_strategy_set=_language_strategy_set(file_manager, runner=PassingRunner()),
|
|
)
|
|
context = _alpha_context(file_manager, backlog, source, test)
|
|
context.backlog = contributor
|
|
|
|
def action(request):
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "accepted edit", [])
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=SequenceSourceWorker(action)).run()
|
|
|
|
assert observed_mementos == [False]
|
|
assert backlog.testing
|
|
|
|
|
|
def test_alpha_acceptance_commits_each_outer_memento(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify accepted Alpha work removes every outer source snapshot."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "bar.py",
|
|
"def bar():\n return 1\n",
|
|
)
|
|
file_manager.client = AlphaAcceptanceSequenceClient(
|
|
[{"outcome": "pass", "reason": ["def foo():"], "instruction": ""}]
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
other_source = file_manager.require_src("src/bar.py")
|
|
source_path = file_manager.source_path(source.location)
|
|
other_source_path = file_manager.source_path(other_source.location)
|
|
context = _alpha_context(file_manager, BackLog(), source, file_manager.require_test("tests/test_foo.py"))
|
|
|
|
def action(request):
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[0],
|
|
"def foo():\n return 2\n",
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
request.editable_files[1],
|
|
"def bar():\n return 2\n",
|
|
)
|
|
return CodeEditorResult([request.editable_files[0]], "accepted edit", [])
|
|
|
|
AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
aider_worker=SequenceSourceWorker(action),
|
|
)._run_source_work(
|
|
[source, other_source],
|
|
{
|
|
source.location: "change foo",
|
|
other_source.location: "change bar",
|
|
},
|
|
readonly_sources=[],
|
|
readonly_tests=[],
|
|
is_remediation=False,
|
|
)
|
|
|
|
assert not file_manager.repo_exec.exists(
|
|
Memento(source_path, file_manager.repo_exec).path
|
|
)
|
|
assert not file_manager.repo_exec.exists(
|
|
Memento(other_source_path, file_manager.repo_exec).path
|
|
)
|
|
|
|
|
|
def test_alpha_skips_testing_backlog_for_completed_source_test_pair(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify completed source/test pairs do not re-enter testing backlog."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
source_key = source.location
|
|
backlog = BackLog(
|
|
completion={
|
|
source_key: WorkCompletion(
|
|
location=source.location,
|
|
completed=True,
|
|
tests={
|
|
test.location: WorkCompletion(
|
|
location=test.location,
|
|
completed=True,
|
|
)
|
|
},
|
|
)
|
|
}
|
|
)
|
|
contributor = ContributorBacklog(
|
|
backlog=backlog,
|
|
get_tests=lambda source_files: [test],
|
|
file_manager=file_manager,
|
|
parser=file_manager.parser,
|
|
language_strategy_set=_language_strategy_set(file_manager, runner=PassingRunner()),
|
|
)
|
|
|
|
created = contributor.create_outbound_for(source, original_cot=file_manager.prompt)
|
|
|
|
assert created == []
|
|
assert not backlog.testing
|
|
|
|
def test_alpha_pops_source_backlog_one_item_per_worker_request(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha drains source backlog one popped item per worker request."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.client = AlphaAcceptanceSequenceClient([
|
|
{"outcome": "pass", "reason": ["def foo():"], "instruction": ""},
|
|
{"outcome": "pass", "reason": ["def bar():"], "instruction": ""},
|
|
])
|
|
file_manager.client.acceptance_reasons = ("return 1",)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "bar.py",
|
|
"def bar():\n return 1\n",
|
|
)
|
|
file_manager.resync()
|
|
foo = file_manager.require_src("src/foo.py")
|
|
bar = file_manager.require_src("src/bar.py")
|
|
test = file_manager.require_test("tests/test_foo.py")
|
|
backlog = BackLog()
|
|
backlog.queue_inspection(InspectionBacklog(foo, "inspect foo"))
|
|
backlog.queue_inspection(
|
|
InspectionBacklog(
|
|
bar,
|
|
"inspect bar",
|
|
upstream_classes=[foo],
|
|
)
|
|
)
|
|
context = _alpha_context(file_manager, backlog, foo, test)
|
|
worker = NoChangeWorker()
|
|
|
|
AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker).run()
|
|
|
|
assert not backlog.inspection
|
|
assert len(worker.requests) == 2
|
|
assert worker.requests[0].editable_files == [file_manager.src_root / "foo.py"]
|
|
assert worker.requests[1].editable_files == [file_manager.src_root / "bar.py"]
|
|
assert file_manager.src_root / "foo.py" in worker.requests[1].readonly_files
|
|
|
|
|
|
def test_alpha_readonly_paths_ignore_readonly_tests_and_editables(tmp_path: Path) -> None:
|
|
"""Verify Alpha readonly preloading keeps only supplied readonly sources."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "bar.py",
|
|
"from foo import foo\n",
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / "test_helper.py",
|
|
"def test_helper():\n assert True\n",
|
|
)
|
|
file_manager.resync()
|
|
foo = file_manager.require_src("src/foo.py")
|
|
bar = file_manager.require_src("src/bar.py")
|
|
readonly_test = file_manager.require_test("tests/test_helper.py")
|
|
context = _alpha_context(file_manager, BackLog(), bar, file_manager.require_test("tests/test_foo.py"))
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context)
|
|
|
|
readonly_paths = loop._readonly_paths(
|
|
[bar],
|
|
[foo],
|
|
[readonly_test],
|
|
)
|
|
|
|
assert readonly_paths == [
|
|
file_manager.source_path(foo.location),
|
|
file_manager.test_path(readonly_test.location),
|
|
]
|
|
|
|
|
|
def test_alpha_preserves_prior_testing_and_all_source_remediation_before_stopping(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify Alpha retains prior test work and every source repair before stopping."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "helper.py",
|
|
"def helper():\n return 1\n",
|
|
)
|
|
for suffix in ("b", "c", "d"):
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / f"test_{suffix}.py",
|
|
"from foo import foo\n\n"
|
|
f"def test_{suffix}():\n"
|
|
" assert foo() == 1\n"
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
helper = file_manager.require_src("src/helper.py")
|
|
tests = [
|
|
file_manager.require_test(location)
|
|
for location in (
|
|
"tests/test_foo.py",
|
|
"tests/test_b.py",
|
|
"tests/test_c.py",
|
|
"tests/test_d.py",
|
|
)
|
|
]
|
|
file_manager.src_dirty[source.location] = True
|
|
results = [
|
|
TestRunResult("pytest", TestRunOutcome.Failure, 1, f"failure {test.location}")
|
|
for test in tests
|
|
]
|
|
backlog = BackLog()
|
|
inbound = InspectionBacklog(source, "change foo", downstream_classes=[source])
|
|
backlog.queue_inspection(inbound)
|
|
contributor = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=tests,
|
|
candidates=[
|
|
_failed_candidate(test, result)
|
|
for test, result in zip(tests, results)
|
|
],
|
|
routed_by_test={
|
|
tests[0].location: [_testing_item(source, tests[0])],
|
|
tests[1].location: [_testing_item(source, tests[1])],
|
|
tests[2].location: [
|
|
_failed_item(source, tests[2]),
|
|
_failed_item(helper, tests[2]),
|
|
],
|
|
},
|
|
)
|
|
context = _alpha_context(file_manager, backlog, source, tests[0])
|
|
context.backlog = contributor
|
|
claimed = context.move_inbound_from_backlog()
|
|
assert claimed is not None
|
|
|
|
AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
)._stage_postflight_alpha_results(claimed.value, [source])
|
|
|
|
assert [staged.lane for staged in context.outbound_queue] == [
|
|
BacklogLane.Testing,
|
|
BacklogLane.Testing,
|
|
BacklogLane.Remediation,
|
|
BacklogLane.Remediation,
|
|
]
|
|
claimed.commit()
|
|
context.flush_outbound_with_wal()
|
|
|
|
assert [item.focused_spotlight for item in backlog.testing] == tests[:2]
|
|
assert {_focused_class_of(item).location for item in backlog.remediation} == {
|
|
source.location,
|
|
helper.location,
|
|
}
|
|
assert all(_failed_test_of(item).test is tests[2] for item in backlog.remediation)
|
|
|
|
|
|
def test_alpha_first_source_remediation_stops_later_routing_after_census(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify the first source repair stops later routing after the direct-test census."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "memento.py",
|
|
"class Memento:\n"
|
|
" pass\n"
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.src_root / "class_meta.py",
|
|
"from memento import Memento\n\n"
|
|
"class ClassMeta:\n"
|
|
" def set_or_get_memento(self):\n"
|
|
" return Memento().capture()\n"
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / "test_cached_file_patcher.py",
|
|
"from class_meta import ClassMeta\n\n"
|
|
"def test_cached_file_patcher_trains_after_three_successful_editor_calls():\n"
|
|
" assert ClassMeta().set_or_get_memento() is not None\n"
|
|
)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / "test_class_meta_cache.py",
|
|
"from class_meta import ClassMeta\n\n"
|
|
"def test_class_meta_cache_reuses_the_current_memento():\n"
|
|
" assert ClassMeta().set_or_get_memento() is not None\n"
|
|
)
|
|
file_manager.resync()
|
|
memento = file_manager.require_src("src/memento.py")
|
|
class_meta = file_manager.require_src("src/class_meta.py")
|
|
focused_test = file_manager.require_test("tests/test_cached_file_patcher.py")
|
|
other_test = file_manager.require_test("tests/test_class_meta_cache.py")
|
|
file_manager.src_dirty[memento.location] = True
|
|
result = TestRunResult(
|
|
"pytest",
|
|
TestRunOutcome.Failure,
|
|
1,
|
|
"AttributeError: 'Memento' object has no attribute 'capture'",
|
|
[
|
|
"src/class_meta.py:5: in set_or_get_memento",
|
|
"return Memento().capture()",
|
|
],
|
|
)
|
|
other_result = TestRunResult(
|
|
"pytest",
|
|
TestRunOutcome.Failure,
|
|
1,
|
|
"AttributeError: ClassMeta cache cannot capture Memento",
|
|
["src/class_meta.py:5: return Memento().capture()"],
|
|
)
|
|
backlog = BackLog()
|
|
inbound = InspectionBacklog(
|
|
memento,
|
|
"Remove public Memento.capture.",
|
|
downstream_classes=[memento],
|
|
)
|
|
backlog.queue_inspection(inbound)
|
|
contributor = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=[focused_test, other_test],
|
|
candidates=[
|
|
_failed_candidate(focused_test, result),
|
|
_failed_candidate(other_test, other_result),
|
|
],
|
|
routed_by_test={
|
|
focused_test.location: [_failed_item(
|
|
class_meta,
|
|
focused_test,
|
|
error_message="Test command failed with return code 1.",
|
|
stack_trace=result.stack_trace,
|
|
raw_output=result.output,
|
|
)],
|
|
},
|
|
)
|
|
context = _alpha_context(file_manager, backlog, memento, focused_test)
|
|
context.backlog = contributor
|
|
claimed = context.move_inbound_from_backlog()
|
|
assert claimed is not None
|
|
|
|
AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
)._stage_postflight_alpha_results(claimed.value, [memento])
|
|
|
|
assert len(context.outbound_queue) == 1
|
|
assert all(
|
|
staged.lane is BacklogLane.Remediation
|
|
for staged in context.outbound_queue
|
|
)
|
|
assert not backlog.remediation
|
|
assert not backlog.testing
|
|
claimed.commit()
|
|
context.flush_outbound_with_wal()
|
|
|
|
assert len(backlog.remediation) == 1
|
|
remediation = backlog.remediation[0]
|
|
assert remediation.focused_spotlight is class_meta
|
|
observed_failure = _failed_test_of(remediation)
|
|
assert observed_failure.test is focused_test
|
|
assert observed_failure.error_message == "Test command failed with return code 1."
|
|
assert observed_failure.stack_trace == result.stack_trace
|
|
assert observed_failure.raw_output == result.output
|
|
assert not backlog.testing
|
|
|
|
|
|
def test_alpha_direct_failure_routes_to_testing_with_failure_context(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify test ownership preserves the existing TestingBacklog failure contract only."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
focused_test = file_manager.require_test("tests/test_foo.py")
|
|
file_manager.src_dirty[source.location] = True
|
|
result = TestRunResult(
|
|
"pytest",
|
|
TestRunOutcome.Failure,
|
|
1,
|
|
"AssertionError: stale test expectation",
|
|
["tests/test_foo.py:4: assert foo() == 1"],
|
|
)
|
|
backlog = BackLog()
|
|
inbound = InspectionBacklog(source, "change foo", downstream_classes=[source])
|
|
backlog.queue_inspection(inbound)
|
|
contributor = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=[focused_test],
|
|
candidates=[_failed_candidate(focused_test, result)],
|
|
routed_by_test={
|
|
focused_test.location: [
|
|
TestingBacklog(
|
|
focused_spotlight=focused_test,
|
|
summary="FAILED CANDIDATE AND REGRESSION STAR",
|
|
downstream_classes=[source],
|
|
original_cot="goal",
|
|
change_type=ChangeType.Update,
|
|
failure_error_message="Test command failed with return code 1.",
|
|
failure_stack_trace=result.stack_trace,
|
|
failure_raw_output=result.output,
|
|
)
|
|
],
|
|
},
|
|
)
|
|
context = _alpha_context(file_manager, backlog, source, focused_test)
|
|
context.backlog = contributor
|
|
claimed = context.move_inbound_from_backlog()
|
|
assert claimed is not None
|
|
|
|
AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
)._stage_postflight_alpha_results(claimed.value, [source])
|
|
|
|
assert len(context.outbound_queue) == 1
|
|
assert context.outbound_queue[0].lane is BacklogLane.Testing
|
|
assert not backlog.remediation
|
|
claimed.commit()
|
|
context.flush_outbound_with_wal()
|
|
|
|
assert not backlog.remediation
|
|
assert len(backlog.testing) == 1
|
|
testing = backlog.testing[0]
|
|
assert testing.focused_spotlight is focused_test
|
|
assert testing.failure_error_message == "Test command failed with return code 1."
|
|
assert testing.failure_stack_trace == result.stack_trace
|
|
assert testing.failure_raw_output == result.output
|
|
assert testing.failure_context_sources == []
|
|
assert testing.upstream_classes == []
|
|
assert testing.downstream_classes == [source]
|
|
assert testing.original_cot == "goal"
|
|
assert testing.change_type is ChangeType.Update
|
|
assert testing.summary == "FAILED CANDIDATE AND REGRESSION STAR"
|
|
|
|
|
|
def test_alpha_direct_failure_preserves_source_obligation_without_dependency_candidates(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A routed source remains the likely culprit when dependency discovery is empty."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / "test_other.py",
|
|
"from foo import foo\n\n"
|
|
"def test_other():\n"
|
|
" assert foo() == 1\n"
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
focused_test = file_manager.require_test("tests/test_foo.py")
|
|
other_test = file_manager.require_test("tests/test_other.py")
|
|
file_manager.src_dirty[source.location] = True
|
|
result = TestRunResult(
|
|
"pytest",
|
|
TestRunOutcome.Failure,
|
|
1,
|
|
"AssertionError: ownership needs reconsideration",
|
|
["src/foo.py:1: def foo():", "tests/test_foo.py:4: assert foo() == 1"],
|
|
)
|
|
other_result = TestRunResult(
|
|
"pytest",
|
|
TestRunOutcome.Failure,
|
|
1,
|
|
"AssertionError: second test-owned candidate",
|
|
["tests/test_other.py:4: assert foo() == 1"],
|
|
)
|
|
runner = SequenceTargetedRunner([result, other_result])
|
|
backlog = BackLog()
|
|
inbound = InspectionBacklog(source, "change foo", downstream_classes=[source])
|
|
backlog.queue_inspection(inbound)
|
|
file_manager.client = FakeClient()
|
|
backlog.judgement_service = SourceThenTestRoutingService()
|
|
contributor = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=[focused_test, other_test],
|
|
candidates=[
|
|
_failed_candidate(focused_test, result),
|
|
_failed_candidate(other_test, other_result),
|
|
],
|
|
routed_by_test={},
|
|
use_real_routes=True,
|
|
)
|
|
contributor._language_strategy_set.bindings[BiDependencyDiscoveryStrategy] = (
|
|
EmptyDependencyStrategy()
|
|
)
|
|
context = _alpha_context(file_manager, backlog, source, focused_test)
|
|
context.backlog = contributor
|
|
claimed = context.move_inbound_from_backlog()
|
|
assert claimed is not None
|
|
|
|
AlphaLoop(
|
|
gate=_alpha_gate(context),
|
|
context=context,
|
|
)._stage_postflight_alpha_results(claimed.value, [source])
|
|
|
|
assert len(context.outbound_queue) == 1
|
|
assert context.outbound_queue[0].lane is BacklogLane.Remediation
|
|
assert not backlog.remediation
|
|
claimed.commit()
|
|
context.flush_outbound_with_wal()
|
|
|
|
assert not backlog.testing
|
|
assert len(backlog.remediation) == 1
|
|
assert backlog.remediation[0].focused_spotlight == source
|
|
assert backlog.remediation[0].failure_raw_output == result.output
|
|
|
|
|
|
def test_alpha_postflight_routing_uses_result_despite_restart_history(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A completed test-owned route cannot be reinterpreted as a dead end."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
focused_test = file_manager.require_test("tests/test_foo.py")
|
|
result = TestRunResult(
|
|
"pytest",
|
|
TestRunOutcome.Failure,
|
|
1,
|
|
"AssertionError: selected test premise is stale",
|
|
["tests/test_foo.py:4: assert foo() == 1"],
|
|
)
|
|
backlog = BackLog()
|
|
file_manager.client = RoutingClientThatReturnsTest()
|
|
contributor = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=[focused_test],
|
|
candidates=[_failed_candidate(focused_test, result)],
|
|
routed_by_test={},
|
|
use_real_routes=True,
|
|
)
|
|
contributor._language_strategy_set.bindings[BiDependencyDiscoveryStrategy] = (
|
|
EmptyDependencyStrategy()
|
|
)
|
|
context = _alpha_context(file_manager, backlog, source, focused_test)
|
|
context.backlog = contributor
|
|
graph = Provenance.open(repo_root=file_manager.repo_root, repo_exec=file_manager.repo_exec)
|
|
item = InspectionBacklog(source, "change foo", downstream_classes=[source])
|
|
prior_beta = graph.start(Phase.BETA, TestingBacklog(focused_test, "Prior test work."))
|
|
graph.track_provenance(prior_beta, Phase.ALPHA, item)
|
|
|
|
with provenance.bind(graph):
|
|
with provenance.item_scope(Phase.ALPHA, item):
|
|
routed = contributor._route_failed_direct_candidate(
|
|
AcceptedSourceStory(
|
|
current_source=source,
|
|
source_effect="changed",
|
|
source_diff="",
|
|
upstream_classes=[],
|
|
downstream_classes=[source],
|
|
original_cot="goal",
|
|
),
|
|
_failed_candidate(focused_test, result),
|
|
"FAILED CANDIDATE",
|
|
regression_star=True,
|
|
logger=None,
|
|
)
|
|
|
|
assert len(routed) == 1
|
|
assert isinstance(routed[0], TestingBacklog)
|
|
|
|
|
|
def test_alpha_routes_every_candidate_when_all_failures_are_test_owned(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify no early stop occurs when every direct candidate produces TestingBacklog."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
for suffix in ("b", "c"):
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / f"test_{suffix}.py",
|
|
"from foo import foo\n\n"
|
|
f"def test_{suffix}():\n"
|
|
" assert foo() == 1\n"
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
tests = [
|
|
file_manager.require_test(location)
|
|
for location in (
|
|
"tests/test_foo.py",
|
|
"tests/test_b.py",
|
|
"tests/test_c.py",
|
|
)
|
|
]
|
|
file_manager.src_dirty[source.location] = True
|
|
results = [TestRunResult("pytest", TestRunOutcome.Failure, 1, test.location) for test in tests]
|
|
backlog = BackLog()
|
|
contributor = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=tests,
|
|
candidates=[
|
|
_failed_candidate(test, result)
|
|
for test, result in zip(tests, results)
|
|
],
|
|
routed_by_test={
|
|
test.location: [_testing_item(source, test)]
|
|
for test in tests
|
|
},
|
|
)
|
|
created = contributor.create_outbound_for(source, original_cot="goal")
|
|
|
|
assert [item.focused_spotlight for item in created] == tests
|
|
assert [item.focused_spotlight for item in backlog.testing] == tests
|
|
assert not backlog.remediation
|
|
|
|
|
|
def test_alpha_passing_direct_regression_star_remains_testing_work(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Verify passing direct candidates retain semantic-vote regression-star selection."""
|
|
|
|
file_manager = _file_manager(tmp_path)
|
|
file_manager.repo_exec.write_text(
|
|
file_manager.test_root / "test_other.py",
|
|
"from foo import foo\n\n"
|
|
"def test_other():\n"
|
|
" assert foo() == 1\n"
|
|
)
|
|
file_manager.resync()
|
|
source = file_manager.require_src("src/foo.py")
|
|
first_test = file_manager.require_test("tests/test_foo.py")
|
|
regression_star = file_manager.require_test("tests/test_other.py")
|
|
file_manager.src_dirty[source.location] = True
|
|
backlog = BackLog()
|
|
contributor = ScenarioContributorBacklog(
|
|
backlog=backlog,
|
|
file_manager=file_manager,
|
|
tests=[first_test, regression_star],
|
|
candidates=[
|
|
DirectTestCandidate(
|
|
focused_test=first_test,
|
|
targeted_result=TestRunResult("pytest", TestRunOutcome.Success, 0, "first passed"),
|
|
semantic_vote_tally=0,
|
|
),
|
|
DirectTestCandidate(
|
|
focused_test=regression_star,
|
|
targeted_result=TestRunResult("pytest", TestRunOutcome.Success, 0, "star passed"),
|
|
semantic_vote_tally=1,
|
|
),
|
|
],
|
|
routed_by_test={},
|
|
)
|
|
created = contributor.create_outbound_for(
|
|
source,
|
|
queue=False,
|
|
original_cot="goal",
|
|
)
|
|
|
|
assert len(created) == 1
|
|
assert isinstance(created[0], TestingBacklog)
|
|
assert created[0].focused_spotlight is regression_star
|
|
assert created[0].summary == "REGRESSION STAR"
|
|
assert created[0].failure_error_message == ""
|
|
assert not backlog.remediation
|
|
assert not backlog.testing
|
|
|
|
|
|
@pytest.mark.parametrize("during_construction", (False, True))
|
|
def test_alpha_acceptance_stale_failure_rolls_back_before_historical_revival(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, during_construction: bool,
|
|
) -> None:
|
|
"""A redirect raised during acceptance construction cannot leave its candidate applied."""
|
|
|
|
from unittest.mock import Mock
|
|
from band_aider.context.provenance import DeadEndError, Direction
|
|
|
|
manager = _file_manager(tmp_path)
|
|
source = manager.require_src("src/foo.py")
|
|
test = manager.require_test("tests/test_foo.py")
|
|
path = source.resolve_path(manager)
|
|
original = manager.repo_exec.read_text(path)
|
|
context = _alpha_context(manager, BackLog(), source, test)
|
|
worker = EditingSourceWorker()
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)
|
|
error = DeadEndError(
|
|
direction=Direction.ROLLFORWARD, current_message_id="active",
|
|
message="Stale remediation failure", justification="Fresh test changed; return through existing history.",
|
|
)
|
|
# The owning transaction must unwind either acceptance entry point.
|
|
if during_construction:
|
|
monkeypatch.setattr("band_aider.judgement.alpha_edit_acceptance_check.RemediationCheck", Mock(side_effect=error))
|
|
else:
|
|
monkeypatch.setattr("band_aider.loop.alpha_loop.AlphaEditAcceptanceDecision", Mock(return_value=Mock(side_effect=error)))
|
|
|
|
with pytest.raises(DeadEndError) as caught:
|
|
loop._run_source_work(
|
|
[source], {source.location: "repair"}, readonly_sources=[], readonly_tests=[test],
|
|
failing_test=FailingTest(test, ["tests/test_foo.py:2: in test_foo"], "old error"), is_remediation=True,
|
|
)
|
|
assert caught.value is error
|
|
assert "- return 1" in error.message
|
|
assert "+ return 2" in error.message
|
|
assert manager.repo_exec.read_text(path) == original
|
|
assert len(worker.requests) == 1
|
|
assert not manager.repo_exec.exists(Memento(path, manager.repo_exec).path)
|
|
|
|
|
|
def test_alpha_carries_cleared_snapshot_diff_into_next_edit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Clearing a leftover stack carries the entire discarded attempt into the next edit."""
|
|
|
|
from unittest.mock import Mock
|
|
|
|
manager = _file_manager(tmp_path)
|
|
source = manager.require_src("src/foo.py")
|
|
test = manager.require_test("tests/test_foo.py")
|
|
path = source.resolve_path(manager)
|
|
source.set_or_get_memento(manager)
|
|
manager.repo_exec.write_text(path, "def foo():\n return -5\n")
|
|
# An unused higher snapshot must not hide the base-to-current candidate being discarded.
|
|
source.set_or_get_memento(manager, stack=True)
|
|
context = _alpha_context(manager, BackLog(), source, test)
|
|
worker = EditingSourceWorker()
|
|
loop = AlphaLoop(gate=_alpha_gate(context), context=context, aider_worker=worker)
|
|
monkeypatch.setattr("band_aider.loop.alpha_loop.AlphaEditAcceptanceDecision", Mock(return_value=Mock(return_value=True)))
|
|
|
|
loop._run_source_work(
|
|
[source], {source.location: "repair"}, readonly_sources=[], readonly_tests=[test],
|
|
failing_test=None, is_remediation=True,
|
|
)
|
|
|
|
guidance = "\n".join(worker.requests[0].backlog_guidance)
|
|
assert "- return 1\n+ return -5" in guidance
|
|
assert manager.repo_exec.read_text(path) == "def foo():\n return 2\n"
|
|
assert not Memento.current(path, manager.repo_exec)
|
|
|
|
|
|
def test_direct_postflight_retains_canonical_test_failure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Direct Alpha postflight carries exactly the failure captured by the shared runner."""
|
|
|
|
from unittest.mock import Mock
|
|
from band_aider.judgement.test_runner_strategy import TestRunnerStrategy
|
|
|
|
manager = _file_manager(tmp_path)
|
|
source = manager.require_src("src/foo.py")
|
|
test = manager.require_test("tests/test_foo.py")
|
|
result = TestRunResult("direct test", TestRunOutcome.Failure, 1, "\nraw λ \r\n", ["tests/test_foo.py:2 ", "", "assert λ\r"])
|
|
runner = Mock(spec=TestRunnerStrategy)
|
|
runner.run_tests_for.return_value = result
|
|
runner.failure_reports.return_value = [FailingTest.from_result(test, result)]
|
|
strategies = _language_strategy_set(manager, runner=runner)
|
|
backlog = ContributorBacklog(
|
|
backlog=BackLog(), get_tests=lambda _sources: [test], file_manager=manager,
|
|
parser=manager.parser, language_strategy_set=strategies,
|
|
)
|
|
scorer = Mock()
|
|
scorer.score_candidates.return_value = []
|
|
monkeypatch.setattr("band_aider.context.working_backlog.Model2VecSimilarityCheck", Mock(return_value=scorer))
|
|
story = AcceptedSourceStory(source, "accepted", "source diff", [], [], "original COT")
|
|
|
|
[candidate] = backlog._direct_test_candidates(story, [test])
|
|
|
|
assert candidate.failing is not None
|
|
assert candidate.failing.to_record() == {
|
|
"outcome": "failure",
|
|
"error_message": "Test command failed with return code 1.",
|
|
"stack_trace": result.stack_trace,
|
|
"raw_output": result.output,
|
|
}
|
|
runner.run_tests_for.assert_called_once_with([test], max_failures=1)
|