fix(awooop): clarify apply candidate owner review state
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m34s
CD Pipeline / build-and-deploy (push) Successful in 4m50s
CD Pipeline / post-deploy-checks (push) Successful in 1m33s

This commit is contained in:
Your Name
2026-06-26 00:19:48 +08:00
parent 3dd4373dac
commit 5ce6fc4924
9 changed files with 129 additions and 22 deletions

View File

@@ -144,6 +144,14 @@ def _build_execution_result(
failure_status = "no_command_failed"
summary = "已執行成功,但缺少修復驗證結果"
terminal = False
elif state == "apply_candidate_owner_review_ready":
approval_status = "owner_review_required"
completion_status = "dry_run_passed_apply_candidate_ready"
command_status = "check_mode_succeeded"
repair_status = "not_executed"
failure_status = "not_applicable"
summary = "AI 已完成安全乾跑並產生 apply candidate等待 owner review 後才可執行"
terminal = False
elif state == "dry_run_only_owner_review_required":
approval_status = "owner_review_required"
completion_status = "dry_run_completed_no_apply"
@@ -313,12 +321,12 @@ def build_operator_outcome(
summary = "已執行但驗證退化,需人工確認"
reason = first_blocker or f"verification={verification}"
elif verdict == "ansible_check_mode_only" or ansible_dry_run_only:
state = "dry_run_only_owner_review_required"
state = "apply_candidate_owner_review_ready"
severity = "warning"
needs_human = True
next_action = "owner_review_apply_gate_or_create_verifier_plan"
summary = "完成 Ansible check-mode 乾跑,尚未執行修復"
reason = first_blocker or "ansible_check_mode_without_apply"
next_action = "open_apply_gate_work_item_review_verifier_and_km"
summary = "AI 已完成 Ansible check-mode 並產生 apply candidate等待 owner review尚未執行修復"
reason = first_blocker or "apply_candidate_requires_owner_review"
elif verdict == "execution_unverified" or (
has_repair_execution and verification == "missing"
):

View File

@@ -4132,7 +4132,7 @@ def _status_chain_ansible_apply_gate_handoff(
"runtime_execution_authorized": False,
"writes_runtime_state": False,
"owner_review_gate": "required_before_apply",
"next_action": "owner_review_apply_gate_or_create_verifier_plan",
"next_action": "open_apply_gate_work_item_review_verifier_and_km",
"asset_ids": {
"dry_run": f"ansible-check-mode:{catalog_id}",
"apply_candidate": f"ansible-apply-candidate:{catalog_id}",
@@ -5018,7 +5018,7 @@ def _build_awooop_status_chain(
if ansible_dry_run_only:
verdict = "ansible_check_mode_only"
repair_state = "ansible_check_mode_only"
next_step = "owner_review_apply_gate_or_create_verifier_plan"
next_step = "open_apply_gate_work_item_review_verifier_and_km"
needs_human = True
source_section = _status_chain_source_section(truth_chain)
if source_correlation is not None:

View File

@@ -1795,18 +1795,22 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
assert chain["verdict"] == "ansible_check_mode_only"
assert chain["repair_state"] == "ansible_check_mode_only"
assert chain["next_step"] == "owner_review_apply_gate_or_create_verifier_plan"
assert chain["next_step"] == "open_apply_gate_work_item_review_verifier_and_km"
assert chain["needs_human"] is True
assert chain["evidence"]["ansible_dry_run_only"] is True
assert chain["operator_outcome"]["state"] == "dry_run_only_owner_review_required"
assert chain["operator_outcome"]["state"] == "apply_candidate_owner_review_ready"
assert "apply candidate" in chain["operator_outcome"]["summary_zh"]
assert (
chain["operator_outcome"]["execution_result"]["completion_status"]
== "dry_run_completed_no_apply"
== "dry_run_passed_apply_candidate_ready"
)
assert "verification_missing" in chain["blockers"]
assert "verification_recorded" not in chain["blockers"]
assert chain["automation_handoff"]["kind"] == "ansible_check_mode_apply_gate"
assert chain["automation_handoff"]["status"] == "owner_review_required"
assert chain["automation_handoff"]["next_action"] == (
"open_apply_gate_work_item_review_verifier_and_km"
)
assert chain["automation_handoff"]["runtime_execution_authorized"] is False
assert chain["automation_handoff"]["work_item_id"].startswith(
"ansible-apply-gate:awoooi:INC-20260625-977E5F"

View File

@@ -98,7 +98,7 @@ def test_operator_outcome_marks_unverified_execution_as_human_review() -> None:
assert outcome["next_action"] == "run_or_review_post_execution_verification"
def test_operator_outcome_marks_ansible_check_mode_as_dry_run_only() -> None:
def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() -> None:
outcome = build_operator_outcome(
truth_status={
"current_stage": "execution_succeeded",
@@ -119,10 +119,12 @@ def test_operator_outcome_marks_ansible_check_mode_as_dry_run_only() -> None:
},
)
assert outcome["state"] == "dry_run_only_owner_review_required"
assert outcome["state"] == "apply_candidate_owner_review_ready"
assert outcome["needs_human"] is True
assert outcome["next_action"] == "owner_review_apply_gate_or_create_verifier_plan"
assert outcome["execution_result"]["completion_status"] == "dry_run_completed_no_apply"
assert outcome["next_action"] == "open_apply_gate_work_item_review_verifier_and_km"
assert outcome["execution_result"]["completion_status"] == (
"dry_run_passed_apply_candidate_ready"
)
assert outcome["execution_result"]["command_status"] == "check_mode_succeeded"
assert outcome["execution_result"]["repair_status"] == "not_executed"
@@ -230,6 +232,40 @@ def test_execution_result_message_includes_operator_outcome_and_human_channels()
assert "manual_review_no_action_decision" in text
def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() -> None:
outcome = build_operator_outcome(
truth_status={
"current_stage": "execution_succeeded",
"stage_status": "success",
"needs_human": True,
"blockers": ["incident_open_after_successful_execution"],
},
automation_quality={
"verdict": "ansible_check_mode_only",
"facts": {
"ansible_check_mode_total": 1,
"ansible_apply_total": 0,
"auto_repair_execution_records": 0,
"effective_execution_records": 1,
"automation_operation_records": 2,
"verification_result": None,
"knowledge_entries": 0,
},
"blockers": ["auto_repair_recorded", "verification_recorded", "learning_recorded"],
},
)
assert outcome["state"] == "apply_candidate_owner_review_ready"
assert outcome["next_action"] == "open_apply_gate_work_item_review_verifier_and_km"
assert "apply candidate" in outcome["summary_zh"]
assert outcome["execution_result"]["completion_status"] == (
"dry_run_passed_apply_candidate_ready"
)
assert "auto_repair_missing" in outcome["blockers"]
assert "verification_missing" in outcome["blockers"]
assert "learning_missing" in outcome["blockers"]
def test_execution_result_message_does_not_call_diagnostic_success_repair_done() -> None:
service = ApprovalExecutionService()
approval = SimpleNamespace(

View File

@@ -768,10 +768,10 @@ def test_awooop_status_chain_lines_mark_ansible_check_mode_as_dry_run_only() ->
joined = "\n".join(lines)
assert "ansible_check_mode_only" in joined
assert "owner_review_apply_gate_or_create_verifier_plan" in joined
assert "dry_run_only_owner_review_required" in joined
assert "dry_run_completed_no_apply" in joined
assert "完成 Ansible check-mode 乾跑,尚未執行修復" in joined
assert "open_apply_gate_work_item_review_verifier_and_km" in joined
assert "apply_candidate_owner_review_ready" in joined
assert "dry_run_passed_apply_candidate_ready" in joined
assert "AI 已完成 Ansible check-mode 並產生 apply candidate" in joined
assert "executed_pending_verification" not in joined
assert "run_or_review_post_execution_verification" not in joined

View File

@@ -10062,6 +10062,16 @@
"writes": "寫入旗標",
"verdict": "判定"
},
"nextActions": {
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件",
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
"collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook",
"manualInvestigation": "人工調查卡點並補證據",
"reviewVerifier": "執行或審查修復後 verifier",
"monitorRegression": "持續觀察是否復發",
"collectEvidenceOrWait": "補收證據或等待下一筆 recurrence"
},
"evidence": {
"autoRepair": "Auto-repair",
"ops": "Ops",
@@ -10515,6 +10525,11 @@
"verifier": "Verifier",
"nextStep": "下一步"
},
"nextActions": {
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件"
},
"items": {
"km": "KM",
"playbook": "PlayBook",

View File

@@ -10062,6 +10062,16 @@
"writes": "寫入旗標",
"verdict": "判定"
},
"nextActions": {
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件",
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
"collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook",
"manualInvestigation": "人工調查卡點並補證據",
"reviewVerifier": "執行或審查修復後 verifier",
"monitorRegression": "持續觀察是否復發",
"collectEvidenceOrWait": "補收證據或等待下一筆 recurrence"
},
"evidence": {
"autoRepair": "Auto-repair",
"ops": "Ops",
@@ -10515,6 +10525,11 @@
"verifier": "Verifier",
"nextStep": "下一步"
},
"nextActions": {
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件"
},
"items": {
"km": "KM",
"playbook": "PlayBook",

View File

@@ -26,6 +26,11 @@ function valueLabel(value: number, fallback: string) {
return value > 0 ? String(value) : fallback;
}
function nextActionLabel(value: unknown, labels: Record<string, string>, fallback: string) {
const key = String(value ?? "");
return labels[key] ?? (value ? String(value) : fallback);
}
export function AwoooPAutomationAssetLedger({
chain,
remediationSummary,
@@ -38,6 +43,12 @@ export function AwoooPAutomationAssetLedger({
className?: string;
}) {
const t = useTranslations("awooop.automationAssetLedger");
const nextActionLabels: Record<string, string> = {
open_apply_gate_work_item_review_verifier_and_km: t("nextActions.openApplyGateWorkItem"),
owner_review_apply_gate_or_create_verifier_plan: t("nextActions.openApplyGateWorkItem"),
owner_review_repair_candidate_draft: t("nextActions.ownerReviewRepairCandidateDraft"),
manual_review_no_action_decision: t("nextActions.manualReviewNoActionDecision"),
};
const handoff = chain?.automation_handoff;
const handoffAssets = handoff?.asset_ids;
const kmCount = chain?.evidence?.knowledge_entries ?? 0;
@@ -116,7 +127,10 @@ export function AwoooPAutomationAssetLedger({
{ key: "dryRun", value: handoffAssets?.dry_run },
{ key: "apply", value: handoffAssets?.apply_candidate },
{ key: "verifier", value: handoffAssets?.verifier },
{ key: "nextStep", value: handoff?.next_action ?? chain?.next_step },
{
key: "nextStep",
value: nextActionLabel(handoff?.next_action ?? chain?.next_step, nextActionLabels, "--"),
},
].filter((item) => Boolean(item.value));
return (

View File

@@ -274,6 +274,21 @@ export function AwoooPStatusChainPanel({
const automationHandoff = chain?.automation_handoff;
const blockers = chain?.blockers ?? [];
const sourceCorrelation = chain?.source_refs?.correlation;
const nextActionLabel = (value: unknown) => {
const key = String(value ?? "");
const labels: Record<string, string> = {
open_apply_gate_work_item_review_verifier_and_km: t("nextActions.openApplyGateWorkItem"),
owner_review_apply_gate_or_create_verifier_plan: t("nextActions.openApplyGateWorkItem"),
manual_review_no_action_decision: t("nextActions.manualReviewNoActionDecision"),
owner_review_repair_candidate_draft: t("nextActions.ownerReviewRepairCandidateDraft"),
manual_review_or_collect_repair_evidence: t("nextActions.collectRepairEvidence"),
manual_investigation: t("nextActions.manualInvestigation"),
run_or_review_post_execution_verification: t("nextActions.reviewVerifier"),
monitor_for_regression: t("nextActions.monitorRegression"),
collect_evidence_or_wait: t("nextActions.collectEvidenceOrWait"),
};
return labels[key] ?? valueOrEmpty(value, emptyLabel);
};
if (!chain) {
return (
@@ -295,7 +310,7 @@ export function AwoooPStatusChainPanel({
{ label: t("fields.stage"), value: `${valueOrEmpty(chain.current_stage, emptyLabel)} / ${valueOrEmpty(chain.stage_status, emptyLabel)}` },
{ label: t("fields.repair"), value: valueOrEmpty(chain.repair_state, emptyLabel) },
{ label: t("fields.verification"), value: valueOrEmpty(chain.verification, emptyLabel) },
{ label: t("fields.nextStep"), value: valueOrEmpty(outcome?.next_action ?? chain.next_step, emptyLabel) },
{ label: t("fields.nextStep"), value: nextActionLabel(outcome?.next_action ?? chain.next_step) },
];
const evidenceMetrics = [
{ label: t("evidence.autoRepair"), value: evidence.auto_repair_records ?? 0 },
@@ -516,7 +531,7 @@ export function AwoooPStatusChainPanel({
}),
detail: t("toolchain.learningDetail", {
verification: valueOrEmpty(chain.verification, emptyLabel),
nextStep: valueOrEmpty(chain.next_step, emptyLabel),
nextStep: nextActionLabel(chain.next_step),
}),
},
];
@@ -633,7 +648,7 @@ export function AwoooPStatusChainPanel({
}),
detail: t("drilldown.learningDetail", {
verification: valueOrEmpty(chain.verification, emptyLabel),
nextStep: valueOrEmpty(chain.next_step, emptyLabel),
nextStep: nextActionLabel(chain.next_step),
}),
},
{
@@ -644,7 +659,7 @@ export function AwoooPStatusChainPanel({
value: chain.needs_human ? t("human.yes") : t("human.no"),
detail: t("drilldown.handoffDetail", {
reason: valueOrEmpty(outcome?.human_action_reason, emptyLabel),
nextAction: valueOrEmpty(outcome?.next_action ?? chain.next_step, emptyLabel),
nextAction: nextActionLabel(outcome?.next_action ?? chain.next_step),
}),
},
];