103 lines
4.9 KiB
Python
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)
|