#!/usr/bin/env python3 """Build structured tester issues and the development feedback route. This module only writes the run contract. Updating a remote PR/issue or notifying a dispatcher remains an explicit caller-side integration step; no network mutation is hidden in the tester. """ from __future__ import annotations from copy import deepcopy from datetime import datetime, timezone from typing import Any, Iterable, Mapping ISSUE_TYPES = ("product", "test") TRIGGERS = ("pr", "requirement", "bug") ISSUE_RESULTS = frozenset({"fail", "environment-fail"}) def _now() -> str: return datetime.now(timezone.utc).isoformat() def issue_type_for(result: str) -> str: if result == "fail": return "product" if result == "environment-fail": return "test" raise ValueError(f"只有 fail/environment-fail 可以生成 issue: {result}") def _value(row: Mapping[str, Any], key: str, defaults: Mapping[str, Any]) -> Any: value = row.get(key) return value if value not in (None, "") else defaults.get(key, "") def build_issue( row: Mapping[str, Any], *, defaults: Mapping[str, Any] | None = None, issue_type: str | None = None, ) -> dict[str, Any]: """Create the stable issue shape required by the run report.""" defaults = defaults or {} result = str(row.get("result") or "") if result not in ISSUE_RESULTS: raise ValueError(f"不能为 {result!r} 结果创建 defect: {row.get('id')}") selected_type = issue_type or row.get("issue_type") or issue_type_for(result) if selected_type not in ISSUE_TYPES: raise ValueError(f"issue_type 必须是 product/test: {selected_type}") testcase = str(row.get("testcase") or row.get("id") or "") if not testcase: raise ValueError("issue 缺少 testcase") return { "id": str(row.get("issue_id") or f"TEST-{testcase}-{result}"), "testcase": testcase, "issue_type": selected_type, "result": result, "phase": _value(row, "phase", defaults), "expected": _value(row, "expected", defaults), "observed": _value(row, "observed", defaults), "artifact_hash": _value(row, "artifact_hash", defaults), "environment": _value(row, "environment", defaults), "evidence_path": _value(row, "evidence_path", defaults), "reproduction": _value(row, "reproduction", defaults), "next_step": _value(row, "next_step", defaults), "created_at": str(row.get("created_at") or _now()), } def collect_issues( results: Iterable[Mapping[str, Any]], *, defaults: Mapping[str, Any] | None = None, ) -> list[dict[str, Any]]: issues: list[dict[str, Any]] = [] for row in results: if not isinstance(row, Mapping) or str(row.get("result") or "") not in ISSUE_RESULTS: continue explicit = row.get("issue") source = dict(row) if isinstance(explicit, Mapping): source.update(explicit) issues.append(build_issue(source, defaults=defaults)) return issues def feedback_plan( trigger: str, *, target: str = "", related_issue: str = "", dispatcher: str = "dispatcher", ) -> dict[str, Any]: """Describe the required handoff without performing remote mutations.""" if trigger not in TRIGGERS: raise ValueError(f"trigger 必须是 {', '.join(TRIGGERS)}") if trigger == "pr": actions = [ {"action": "update_pr", "target": target, "status": "required"}, {"action": "update_related_issue", "target": related_issue, "status": "required"}, {"action": "notify_dispatcher", "target": dispatcher, "status": "required"}, {"action": "reassign_development", "target": dispatcher, "status": "pending"}, ] route = "pr-update-issue-dispatcher-reassign" else: actions = [ {"action": "return_to_development", "target": target or trigger, "status": "required"}, ] route = "development" return { "trigger": trigger, "route": route, "target": target, "related_issue": related_issue, "actions": actions, "remote_mutation": "caller-required", "created_at": _now(), } def attach_feedback( run: Mapping[str, Any], *, trigger: str | None = None, target: str = "", related_issue: str = "", dispatcher: str = "dispatcher", defaults: Mapping[str, Any] | None = None, ) -> dict[str, Any]: """Return a new run mapping with results, defects and routing preserved.""" output = deepcopy(dict(run)) results = output.get("results") if not isinstance(results, list): raise ValueError("run 缺少 results 列表") issues = collect_issues(results, defaults=defaults) output["defects"] = issues # Compatibility name for consumers that call the same structure # ``test-issues``. It is deliberately a copy, not a second source of truth. output["test_issues"] = deepcopy(issues) if trigger: output["feedback"] = feedback_plan( trigger, target=target, related_issue=related_issue, dispatcher=dispatcher, ) return output