fix(awooop): clarify apply candidate owner review state
This commit is contained in:
@@ -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"
|
||||
):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user