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

103 lines
4.9 KiB
Python

"""Iteration-local execution observations, separate from historical obligations."""
from __future__ import annotations
from dataclasses import dataclass, replace
from band_aider.context.class_meta import ClassMeta
from band_aider.context.failing_test import FailingTest
from band_aider.judgement.test_runner_strategy import TestRunOutcome, TestRunResult
@dataclass(frozen=True)
class ValidationReceipt:
"""Retain an actual invocation and the definition it tested, including success.
``ValidationReceipt.from_result(test, result, test_definition=code, generation=1)``
extends captured failure evidence with identity, successful output and freshness.
``call_signature`` is the executor command, never a reconstructed declaration.
"""
test: ClassMeta
result: TestRunResult
test_definition: str
generation: int
file_scope: bool = False
@classmethod
def from_result(cls, test: ClassMeta, result: TestRunResult, *, test_definition: str, generation: int, file_scope: bool = False) -> ValidationReceipt:
"""Capture one executed scope without manufacturing individual method results."""
return cls(test, replace(result, stack_trace=list(result.stack_trace)), test_definition, generation, file_scope)
@property
def test_name(self) -> str:
"""Return the executor's qualified method identity, empty for an unnamed scope."""
return self.result.test_name
@property
def call_signature(self) -> str:
"""Expose exactly the command recorded by the execution adapter."""
return self.result.command
@property
def passed(self) -> bool:
"""Require successful execution, not the runner's no-tests convenience flag."""
return self.result.passed and self.result.returncode == 0
@property
def failure(self) -> FailingTest | None:
"""Reuse the canonical failure evidence only for an unsuccessful invocation."""
if self.passed:
return None
# The runner's empty-scope convenience result proves no execution and
# must not erase incoming evidence in an edit request's validation view.
result = replace(self.result, outcome=TestRunOutcome.Error) if self.result.passed else self.result
return FailingTest.from_result(self.test, result)
@dataclass(frozen=True)
class ValidationReceipts:
"""Latest retained result per test, with freshness relative to accepted code.
``ValidationReceipts(inventory.receipts, inventory.accepted_generation)``
deduplicates an editor ledger before its transaction scope is destroyed.
Earlier-generation results remain visible but cannot clear current obligations.
"""
receipts: tuple[ValidationReceipt, ...] = ()
generation: int = 0
def __post_init__(self) -> None:
"""Resolve repeats in observation order; both PASS→FAIL and FAIL→PASS replace."""
latest = {(receipt.test.location, receipt.test_name, receipt.file_scope): receipt for receipt in self.receipts}
object.__setattr__(self, "receipts", tuple(latest.values()))
def current_for(self, location: str, name: str, *, file_scope: bool = False) -> ValidationReceipt | None:
"""Match current method or explicit file scope; unknown scope covers neither."""
return next((receipt for receipt in self.receipts
if (name or file_scope) and receipt.file_scope == file_scope
and receipt.test.location == location and receipt.test_name == name
and receipt.generation == self.generation), None)
@property
def has_current_pass(self) -> bool:
"""Report a proven current execution, including explicitly recorded file scope."""
return any(receipt.passed and (receipt.test_name or receipt.file_scope) and receipt.generation == self.generation for receipt in self.receipts)
def render(self) -> str:
"""Print every retained receipt in full, outside any contextual elision."""
sections: list[str] = []
# Freshness is explicit even when an older passing result is still the latest observation.
for receipt in self.receipts:
freshness = "CURRENT CODE" if receipt.generation == self.generation else "EARLIER CODE — not current validation"
result = receipt.result
status = "PASS" if receipt.passed else "FAIL" if not result.passed else "NO TEST EXECUTION"
sections.append("\n".join((
f"{status}: {receipt.test.location}::{receipt.test_name or '[file/scope; method unknown]'} ({freshness})",
f"EXECUTOR: {receipt.call_signature}", f"RETURN CODE: {result.returncode}",
f"OUTCOME: {result.outcome.value}", "RUN INFORMATION:", result.run_info,
"TEST DEFINITION:", receipt.test_definition,
"STDOUT:", result.stdout, "STDERR:", result.stderr,
"CAPTURED OUTPUT:", result.output, "STACK TRACE:", *result.stack_trace,
)))
return "\n\n".join(sections)