Files
Artemis-Eapil/artemis/runtime/case_result.py

133 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""A test verdict is independent of the task's execution/lifecycle status."""
import json
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
from artemis.config import TRACES_PATH
class CaseTestResult(BaseModel):
status: Literal["passed", "failed", "inconclusive", "blocked"] = Field(
description="Judge the system under test against the user's expectations. A correctly executed inspection that reveals an expectation mismatch is failed, even if the executor completed and reported the mismatch correctly."
)
reason: str = Field(description="用简体中文说明提示词中的要求是否满足,以及不能判定的原因。")
evidence: list[str] = Field(
default_factory=list,
description="支撑结论的具体步骤、截图内容或只读探测结果;执行者声称完成不属于独立证据。",
)
RESULT_LABELS = {
"passed": "测试通过",
"failed": "测试不通过",
"inconclusive": "未验证",
"blocked": "测试阻塞",
}
_TERMINAL = {"completed", "success", "failed", "cancelled", "interrupted", "skipped"}
def normalize_case_result(result: CaseTestResult) -> CaseTestResult:
evidence = [item.strip() for item in result.evidence if item.strip()]
if result.status in {"passed", "failed"} and not evidence:
return CaseTestResult(
status="inconclusive",
reason="验收结论缺少具体证据,不能确认测试是否通过。",
)
return result.model_copy(update={"evidence": evidence})
def resolve_case_result(
outcome: dict | None, execution_status: str, error: str = ""
) -> CaseTestResult | None:
"""Preserve confirmed defects; unfinished execution never becomes a pass."""
outcome = outcome if isinstance(outcome, dict) else {}
tests = outcome.get("tests") or {}
tests = tests if isinstance(tests, dict) else {}
candidate = None
if isinstance(outcome.get("test_result"), dict):
try:
candidate = normalize_case_result(CaseTestResult.model_validate(outcome["test_result"]))
except ValidationError:
candidate = None
failed_items = tests.get("failed_items") or []
if not isinstance(failed_items, list):
failed_items = []
failures = [
item
for item in failed_items
if isinstance(item, dict)
and item.get("kind", "assert") == "assert"
and item.get("evidence")
]
if failures:
return CaseTestResult(
status="failed",
reason=";".join(str(item.get("item_text") or "测试断言不满足") for item in failures),
evidence=[str(item["evidence"]) for item in failures],
)
if candidate and candidate.status == "failed":
return candidate
if execution_status not in _TERMINAL:
return None
if execution_status not in {"completed", "success"} or outcome.get("task_status") in {
"blocked",
"partial",
"failed",
"cancelled",
"interrupted",
}:
return CaseTestResult(
status="blocked",
reason=error
or str(
outcome.get("error")
or outcome.get("explanation")
or "任务未完整执行,无法完成验收。"
),
)
if candidate:
if candidate.status == "passed" and (tests.get("inconclusive") or tests.get("unchecked")):
return CaseTestResult(
status="inconclusive",
reason="部分检查项缺少证据或尚未验证。",
evidence=candidate.evidence,
)
return candidate
# Existing Pro runs already have a verdict ledger. A bare 'completed' from
# Flash (or a process exit code) is never used as a positive test result.
if tests.get("failed"):
return CaseTestResult(
status="blocked", reason="执行检查未满足;没有证据确认被测功能不符合预期。"
)
if tests.get("inconclusive") or tests.get("unchecked"):
return CaseTestResult(status="inconclusive", reason="部分检查项缺少证据或尚未验证。")
if tests.get("passed"):
return CaseTestResult(
status="passed",
reason="已记录的验证项全部通过。",
evidence=[f"已有检查结果:{tests['passed']} 项通过。"],
)
return CaseTestResult(
status="inconclusive", reason="没有保存测试验收结论;执行完成不等于测试通过。"
)
def read_case_result(
session_id: str, execution_status: str, error: str = "", *, root: Path | None = None
) -> dict | None:
if not session_id or not all(c.isalnum() or c in "-_" for c in session_id):
raise ValueError("无效的任务编号")
outcome = {}
try:
outcome = json.loads(
((root or TRACES_PATH) / session_id / "run_outcome.json").read_text(encoding="utf-8")
)
except (OSError, ValueError):
pass # Old tasks and active tasks may not have an outcome yet.
result = resolve_case_result(outcome, execution_status, error)
return result.model_dump() if result else None