From a8893d4fa731abf62695f6558ad018c7a1fac4f1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Jul 2026 11:44:13 +0800 Subject: [PATCH] fix(reboot): expose windows99 vmx repair action chain --- .../awoooi_priority_work_order_readback.py | 134 ++++++++++++++++++ .../reboot_auto_recovery_slo_scorecard.py | 125 ++++++++++++++++ .../windows99_vmx_source_repair_package.py | 36 +++++ ...awoooi_priority_work_order_readback_api.py | 44 ++++++ ...windows99_vmx_source_repair_package_api.py | 24 ++++ apps/web/messages/en.json | 17 ++- apps/web/messages/zh-TW.json | 17 ++- .../app/[locale]/awooop/work-items/page.tsx | 102 +++++++++++++ ...est_windows99_vmx_source_repair_package.py | 20 +++ 9 files changed, 517 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index a2314a6e9..0bc5aa79d 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -3029,6 +3029,26 @@ def _enrich_from_current_readbacks( windows99_vmx_source_repair_package = ( build_windows99_vmx_source_repair_package(reboot_slo) ) + windows99_vmx_source_repair_action_chain = _dict( + windows99_vmx_source_repair_package.get("action_chain") + ) + windows99_vmx_source_repair_target_selector = _dict( + windows99_vmx_source_repair_action_chain.get("target_selector") + ) + windows99_vmx_source_repair_check_mode = _dict( + windows99_vmx_source_repair_action_chain.get("check_mode") + ) + windows99_vmx_source_repair_rollback = _dict( + windows99_vmx_source_repair_action_chain.get("rollback") + ) + windows99_vmx_source_repair_diff = ( + windows99_vmx_source_repair_action_chain.get("source_of_truth_diff") + if isinstance( + windows99_vmx_source_repair_action_chain.get("source_of_truth_diff"), + list, + ) + else [] + ) p0_006_event_gated = bool( reboot_slo_rollups.get("blocked_by_fresh_reboot_window_only") is True ) @@ -3269,6 +3289,46 @@ def _enrich_from_current_readbacks( windows99_vmx_source_repair_package.get("apply_allowed_by_this_package") is True ) + state["windows99_vmx_source_repair_work_order_id"] = str( + windows99_vmx_source_repair_package.get("work_order_id") + or windows99_vmx_source_repair_action_chain.get("work_order_id") + or "" + ) + state["windows99_vmx_source_repair_action_stage"] = str( + windows99_vmx_source_repair_package.get("action_stage") + or windows99_vmx_source_repair_action_chain.get("stage") + or "" + ) + state["windows99_vmx_source_repair_next_executable_step"] = str( + windows99_vmx_source_repair_package.get("next_executable_step") + or windows99_vmx_source_repair_action_chain.get("next_executable_step") + or "" + ) + state["windows99_vmx_source_repair_check_mode_allowed"] = bool( + windows99_vmx_source_repair_package.get("check_mode_allowed_by_this_package") + is True + or windows99_vmx_source_repair_check_mode.get("allowed_by_this_package") + is True + ) + state["windows99_vmx_source_repair_rollback_required"] = bool( + windows99_vmx_source_repair_package.get("rollback_required") is True + or windows99_vmx_source_repair_rollback.get("required") is True + ) + state["windows99_vmx_source_repair_vm_power_change_allowed"] = bool( + windows99_vmx_source_repair_package.get("vm_power_change_allowed") is True + or windows99_vmx_source_repair_target_selector.get("vm_power_change_allowed") + is True + ) + state["windows99_vmx_source_repair_destructive_restore_allowed"] = bool( + windows99_vmx_source_repair_package.get("destructive_restore_allowed") is True + or windows99_vmx_source_repair_target_selector.get( + "destructive_restore_allowed" + ) + is True + ) + state["windows99_vmx_source_repair_source_diff_alias_count"] = len( + windows99_vmx_source_repair_diff + ) state["stale_snapshot_or_old_cd_runs_must_not_reopen_closed_work"] = True state["p0_004_template_copy_apply_gate_production_http_status"] = 200 state["p0_004_template_copy_apply_gate_runtime_readback_state"] = ( @@ -3545,6 +3605,30 @@ def _enrich_from_current_readbacks( evidence["windows99_vmx_source_repair_apply_allowed_by_this_package"] = ( state["windows99_vmx_source_repair_apply_allowed_by_this_package"] ) + evidence["windows99_vmx_source_repair_work_order_id"] = state[ + "windows99_vmx_source_repair_work_order_id" + ] + evidence["windows99_vmx_source_repair_action_stage"] = state[ + "windows99_vmx_source_repair_action_stage" + ] + evidence["windows99_vmx_source_repair_next_executable_step"] = state[ + "windows99_vmx_source_repair_next_executable_step" + ] + evidence["windows99_vmx_source_repair_check_mode_allowed"] = state[ + "windows99_vmx_source_repair_check_mode_allowed" + ] + evidence["windows99_vmx_source_repair_rollback_required"] = state[ + "windows99_vmx_source_repair_rollback_required" + ] + evidence["windows99_vmx_source_repair_vm_power_change_allowed"] = state[ + "windows99_vmx_source_repair_vm_power_change_allowed" + ] + evidence["windows99_vmx_source_repair_destructive_restore_allowed"] = state[ + "windows99_vmx_source_repair_destructive_restore_allowed" + ] + evidence["windows99_vmx_source_repair_source_diff_alias_count"] = state[ + "windows99_vmx_source_repair_source_diff_alias_count" + ] evidence["drill_preflight_status"] = str(reboot_preflight.get("status") or "") evidence["drill_preflight_ready"] = ( reboot_preflight_rollups.get("preflight_ready") is True @@ -4194,6 +4278,31 @@ def _set_rollups_and_summary( state.get("windows99_vmx_source_repair_apply_allowed_by_this_package") is True ), + "windows99_vmx_source_repair_work_order_id": str( + state.get("windows99_vmx_source_repair_work_order_id") or "" + ), + "windows99_vmx_source_repair_action_stage": str( + state.get("windows99_vmx_source_repair_action_stage") or "" + ), + "windows99_vmx_source_repair_next_executable_step": str( + state.get("windows99_vmx_source_repair_next_executable_step") or "" + ), + "windows99_vmx_source_repair_check_mode_allowed": ( + state.get("windows99_vmx_source_repair_check_mode_allowed") is True + ), + "windows99_vmx_source_repair_rollback_required": ( + state.get("windows99_vmx_source_repair_rollback_required") is True + ), + "windows99_vmx_source_repair_vm_power_change_allowed": ( + state.get("windows99_vmx_source_repair_vm_power_change_allowed") is True + ), + "windows99_vmx_source_repair_destructive_restore_allowed": ( + state.get("windows99_vmx_source_repair_destructive_restore_allowed") + is True + ), + "windows99_vmx_source_repair_source_diff_alias_count": _int( + state.get("windows99_vmx_source_repair_source_diff_alias_count") + ), "p0_004_runtime_readback_ready": p0_004_ready, "reboot_drill_preflight_runtime_readback_ready": ( state.get("reboot_drill_preflight_runtime_readback_state") == "ready" @@ -4369,6 +4478,31 @@ def _set_rollups_and_summary( state.get("windows99_vmx_source_repair_apply_allowed_by_this_package") is True ), + "windows99_vmx_source_repair_work_order_id": str( + state.get("windows99_vmx_source_repair_work_order_id") or "" + ), + "windows99_vmx_source_repair_action_stage": str( + state.get("windows99_vmx_source_repair_action_stage") or "" + ), + "windows99_vmx_source_repair_next_executable_step": str( + state.get("windows99_vmx_source_repair_next_executable_step") or "" + ), + "windows99_vmx_source_repair_check_mode_allowed": ( + state.get("windows99_vmx_source_repair_check_mode_allowed") is True + ), + "windows99_vmx_source_repair_rollback_required": ( + state.get("windows99_vmx_source_repair_rollback_required") is True + ), + "windows99_vmx_source_repair_vm_power_change_allowed": ( + state.get("windows99_vmx_source_repair_vm_power_change_allowed") is True + ), + "windows99_vmx_source_repair_destructive_restore_allowed": ( + state.get("windows99_vmx_source_repair_destructive_restore_allowed") + is True + ), + "windows99_vmx_source_repair_source_diff_alias_count": _int( + state.get("windows99_vmx_source_repair_source_diff_alias_count") + ), "windows99_winrm_http_open": ( state.get("windows99_winrm_http_open") is True ), diff --git a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py index 3427d12f7..9ecfc0755 100644 --- a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py +++ b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py @@ -2434,6 +2434,112 @@ def _powershell_test_path(path: str) -> str: return f"Test-Path -LiteralPath '{escaped}'" +def _build_windows99_vmx_source_repair_action_chain( + *, + missing_vmx_aliases: list[str], + powered_off_aliases: list[str], +) -> dict[str, Any]: + repair_aliases = missing_vmx_aliases or powered_off_aliases + primary_alias = repair_aliases[0] if repair_aliases else "" + if missing_vmx_aliases: + stage = "source_repair_check_mode_ready" + next_executable_step = "locate_or_relink_missing_vmx_source_check_mode" + candidate_status = "candidate_waiting_existing_vmx_source_or_backup_path" + elif powered_off_aliases: + stage = "power_readback_check_mode_ready" + next_executable_step = "build_vmware_autostart_power_readback_check_mode" + candidate_status = "candidate_waiting_power_policy_check_mode" + else: + stage = "verify_only_ready" + next_executable_step = "rerun_no_secret_collector_and_scorecard" + candidate_status = "no_repair_required" + + source_diffs = [] + for alias in repair_aliases: + candidates = _WINDOWS99_EXPECTED_VMX_CANDIDATES.get(alias, []) + source_diffs.append( + { + "alias": alias, + "current_state": ( + "expected_vmx_source_missing" + if alias in missing_vmx_aliases + else "guest_power_readback_not_ready" + ), + "desired_state": ( + "expected_vmx_source_present_at_known_path" + if alias in missing_vmx_aliases + else "guest_power_state_observable_without_start_or_stop" + ), + "candidate_paths": candidates, + "diff_mode": "path_existence_only_no_file_content_read", + "destructive_restore_allowed": False, + } + ) + + return { + "work_order_id": ( + f"windows99-vmx-source-repair-{primary_alias}" + if primary_alias + else "windows99-vmx-source-repair-verify" + ), + "stage": stage, + "next_executable_step": next_executable_step, + "target_selector": { + "host_alias": "99", + "vm_aliases": repair_aliases, + "path_scope": "windows99_vmware_expected_vmx_candidates", + "secret_collection_allowed": False, + "vm_power_change_allowed": False, + "destructive_restore_allowed": False, + }, + "source_of_truth_diff": source_diffs, + "check_mode": { + "allowed_by_this_package": True, + "probe": "test_expected_vmx_candidate_paths_without_file_content_read", + "probe_command_count": sum( + len(_WINDOWS99_EXPECTED_VMX_CANDIDATES.get(alias, [])) + for alias in repair_aliases + ), + }, + "controlled_apply_candidate": { + "status": candidate_status, + "executor": "windows99_no_secret_management_channel_or_local_console", + "command_id": "windows99_vmx_source_relink_or_restore_scoped", + "apply_allowed_by_this_package": False, + "allowed_when": [ + "existing_vmx_source_or_backup_artifact_is_identified", + "pre_apply_backup_or_copy_plan_is_recorded", + "dry_run_path_check_passed", + "post_apply_no_secret_verifier_is_available", + ], + }, + "rollback": { + "required": True, + "mode": "revert_scoped_vmx_source_link_or_file_copy", + "backup_required_before_apply": True, + }, + "post_apply_verifier": [ + "bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect", + "bash scripts/reboot-recovery/install-reboot-auto-recovery-slo-110.sh --verify-only", + "GET /api/v1/agents/reboot-auto-recovery-slo-scorecard", + "GET /api/v1/agents/windows99-vmx-source-repair-package", + ], + "learning_writeback": [ + "awooop_work_item_receipt", + "reboot_slo_scorecard_rollups", + "km_playbook_trust_writeback_after_verifier", + ], + "manager_summary": { + "primary_alias": primary_alias or "none", + "missing_vmx_count": len(missing_vmx_aliases), + "powered_off_count": len(powered_off_aliases), + "check_mode_allowed": True, + "apply_allowed": False, + "vm_power_change_allowed": False, + }, + } + + def _build_windows99_vmx_source_repair_package( windows99: dict[str, Any], *, @@ -2481,6 +2587,11 @@ def _build_windows99_vmx_source_repair_package( } ) + action_chain = _build_windows99_vmx_source_repair_action_chain( + missing_vmx_aliases=missing_vmx_aliases, + powered_off_aliases=powered_off_aliases, + ) + return { "schema_version": "windows99_vmx_source_repair_check_mode_package_v1", "generated_at": generated_at, @@ -2491,6 +2602,20 @@ def _build_windows99_vmx_source_repair_package( "powered_off_aliases": powered_off_aliases, "required_vm_aliases": required_aliases, "vm_rows": rows, + "action_chain": action_chain, + "work_order_id": action_chain["work_order_id"], + "action_stage": action_chain["stage"], + "next_executable_step": action_chain["next_executable_step"], + "check_mode_allowed_by_this_package": action_chain["check_mode"][ + "allowed_by_this_package" + ], + "rollback_required": action_chain["rollback"]["required"], + "vm_power_change_allowed": action_chain["target_selector"][ + "vm_power_change_allowed" + ], + "destructive_restore_allowed": action_chain["target_selector"][ + "destructive_restore_allowed" + ], "safe_next_step": safe_next_step, "controlled_apply_mode": "check_mode_package_only_no_windows_or_vm_write", "apply_allowed_by_this_package": False, diff --git a/apps/api/src/services/windows99_vmx_source_repair_package.py b/apps/api/src/services/windows99_vmx_source_repair_package.py index 7aac71d6f..6edf75107 100644 --- a/apps/api/src/services/windows99_vmx_source_repair_package.py +++ b/apps/api/src/services/windows99_vmx_source_repair_package.py @@ -72,6 +72,30 @@ def _mark_package_public_redaction_boundary( else: redacted_rows.append(row) payload["vm_rows"] = redacted_rows + + action_chain = payload.get("action_chain") + if isinstance(action_chain, dict): + redacted_action_chain = dict(action_chain) + redacted_action_chain["public_lan_topology_redacted"] = True + redacted_action_chain["redacted_public_display_only"] = True + check_mode = redacted_action_chain.get("check_mode") + if isinstance(check_mode, dict): + redacted_check_mode = dict(check_mode) + redacted_check_mode["probe_commands_executable"] = False + redacted_action_chain["check_mode"] = redacted_check_mode + source_diffs = redacted_action_chain.get("source_of_truth_diff") + if isinstance(source_diffs, list): + redacted_diffs: list[Any] = [] + for diff in source_diffs: + if isinstance(diff, dict): + redacted_diff = dict(diff) + redacted_diff["public_lan_topology_redacted"] = True + redacted_diff["redacted_public_display_only"] = True + redacted_diffs.append(redacted_diff) + else: + redacted_diffs.append(diff) + redacted_action_chain["source_of_truth_diff"] = redacted_diffs + payload["action_chain"] = redacted_action_chain return payload @@ -102,6 +126,17 @@ def _alias_lists_match(package: dict[str, Any], windows99: dict[str, Any]) -> bo ) +def _package_has_action_chain(package: dict[str, Any]) -> bool: + action_chain = _dict(package.get("action_chain")) + target_selector = _dict(action_chain.get("target_selector")) + return bool( + action_chain.get("work_order_id") + and action_chain.get("stage") + and action_chain.get("next_executable_step") + and target_selector.get("host_alias") + ) + + def build_windows99_vmx_source_repair_package( scorecard: dict[str, Any], *, @@ -114,6 +149,7 @@ def build_windows99_vmx_source_repair_package( if existing_package and generated_at is None and _alias_lists_match(existing_package, windows99) + and _package_has_action_chain(existing_package) else _build_windows99_vmx_source_repair_package( windows99, generated_at=generated_at diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index d97482567..da672d0af 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -695,6 +695,20 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a state["windows99_vmx_source_repair_apply_allowed_by_this_package"] is False ) + assert state["windows99_vmx_source_repair_work_order_id"] == ( + "windows99-vmx-source-repair-111" + ) + assert state["windows99_vmx_source_repair_action_stage"] == ( + "source_repair_check_mode_ready" + ) + assert state["windows99_vmx_source_repair_next_executable_step"] == ( + "locate_or_relink_missing_vmx_source_check_mode" + ) + assert state["windows99_vmx_source_repair_check_mode_allowed"] is True + assert state["windows99_vmx_source_repair_rollback_required"] is True + assert state["windows99_vmx_source_repair_vm_power_change_allowed"] is False + assert state["windows99_vmx_source_repair_destructive_restore_allowed"] is False + assert state["windows99_vmx_source_repair_source_diff_alias_count"] == 1 assert payload["summary"]["controlled_service_data_backup_blocker_count"] == 0 assert payload["summary"]["active_p0_primary_blocker"] == ( "reboot_event_required_host_unreachable" @@ -736,6 +750,26 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a ] is False ) + assert payload["summary"]["windows99_vmx_source_repair_work_order_id"] == ( + "windows99-vmx-source-repair-111" + ) + assert payload["summary"]["windows99_vmx_source_repair_action_stage"] == ( + "source_repair_check_mode_ready" + ) + assert payload["summary"]["windows99_vmx_source_repair_check_mode_allowed"] is True + assert ( + payload["rollups"]["windows99_vmx_source_repair_rollback_required"] is True + ) + assert ( + payload["rollups"]["windows99_vmx_source_repair_vm_power_change_allowed"] + is False + ) + assert ( + payload["rollups"][ + "windows99_vmx_source_repair_destructive_restore_allowed" + ] + is False + ) assert "service/data/backup readback is green" in payload[ "next_execution_order" ][0] @@ -763,6 +797,16 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a ] is False ) + assert in_progress["evidence"]["windows99_vmx_source_repair_work_order_id"] == ( + "windows99-vmx-source-repair-111" + ) + assert in_progress["evidence"]["windows99_vmx_source_repair_action_stage"] == ( + "source_repair_check_mode_ready" + ) + assert ( + in_progress["evidence"]["windows99_vmx_source_repair_check_mode_allowed"] + is True + ) assert "Windows99 verify collection" in in_progress["professional_fix"][ "action" ] diff --git a/apps/api/tests/test_windows99_vmx_source_repair_package_api.py b/apps/api/tests/test_windows99_vmx_source_repair_package_api.py index fbd59efe7..c204e0c48 100644 --- a/apps/api/tests/test_windows99_vmx_source_repair_package_api.py +++ b/apps/api/tests/test_windows99_vmx_source_repair_package_api.py @@ -52,6 +52,22 @@ def test_windows99_vmx_source_repair_package_builder_is_check_mode_only() -> Non "check_mode_package_only_no_windows_or_vm_write" ) assert payload["apply_allowed_by_this_package"] is False + assert payload["work_order_id"] == "windows99-vmx-source-repair-111" + assert payload["action_stage"] == "source_repair_check_mode_ready" + assert ( + payload["next_executable_step"] + == "locate_or_relink_missing_vmx_source_check_mode" + ) + assert payload["check_mode_allowed_by_this_package"] is True + assert payload["rollback_required"] is True + assert payload["vm_power_change_allowed"] is False + assert payload["destructive_restore_allowed"] is False + action_chain = payload["action_chain"] + assert action_chain["target_selector"]["host_alias"] == "99" + assert action_chain["target_selector"]["vm_aliases"] == ["111"] + assert action_chain["source_of_truth_diff"][0]["alias"] == "111" + assert action_chain["source_of_truth_diff"][0]["destructive_restore_allowed"] is False + assert action_chain["controlled_apply_candidate"]["apply_allowed_by_this_package"] is False assert payload["operation_boundaries"]["secret_value_read"] is False assert payload["operation_boundaries"]["remote_write_performed"] is False assert payload["operation_boundaries"]["vm_power_change_performed"] is False @@ -82,6 +98,8 @@ def test_windows99_vmx_source_repair_package_endpoint_returns_readback( "windows99_vmx_source_repair_check_mode_package_v1" ) assert payload["target_host_alias"] == "99" + assert payload["work_order_id"] == "windows99-vmx-source-repair-111" + assert payload["action_stage"] == "source_repair_check_mode_ready" assert payload["controlled_apply_mode"] == ( "check_mode_package_only_no_windows_or_vm_write" ) @@ -95,4 +113,10 @@ def test_windows99_vmx_source_repair_package_endpoint_returns_readback( vm_rows = {row["alias"]: row for row in payload["vm_rows"]} assert vm_rows["111"]["public_lan_topology_redacted"] is True assert vm_rows["111"]["check_mode_probe_commands_executable"] is False + action_chain = payload["action_chain"] + assert action_chain["public_lan_topology_redacted"] is True + assert action_chain["redacted_public_display_only"] is True + assert action_chain["check_mode"]["probe_commands_executable"] is False + assert action_chain["source_of_truth_diff"][0]["public_lan_topology_redacted"] is True + assert action_chain["source_of_truth_diff"][0]["redacted_public_display_only"] is True assert payload["operation_boundaries"]["host_reboot_performed"] is False diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index d7a263029..41d105e9d 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9346,7 +9346,22 @@ "poweredOff": "power off {count}", "checkMode": "check-mode", "applyClosed": "apply 0", - "applyOpen": "apply 1" + "applyOpen": "apply 1", + "actionChain": "action chain", + "stageSource": "source repair", + "stagePower": "power readback", + "stageVerify": "verify only", + "nextLocate": "locate source", + "nextPower": "power check", + "nextVerify": "rerun verifier", + "checkReady": "check ready", + "checkWaiting": "check wait", + "rollbackSet": "rollback set", + "rollbackMissing": "rollback missing", + "noPower": "no VM power", + "powerOpen": "VM power open", + "noDestructive": "no destructive", + "destructiveOpen": "destructive open" }, "blockerLanes": { "label": "Blocker lanes", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 407ca32f0..9b28b5b0c 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9346,7 +9346,22 @@ "poweredOff": "power off {count}", "checkMode": "check-mode", "applyClosed": "apply 0", - "applyOpen": "apply 1" + "applyOpen": "apply 1", + "actionChain": "action chain", + "stageSource": "source repair", + "stagePower": "power readback", + "stageVerify": "verify only", + "nextLocate": "locate source", + "nextPower": "power check", + "nextVerify": "rerun verifier", + "checkReady": "check ready", + "checkWaiting": "check wait", + "rollbackSet": "rollback set", + "rollbackMissing": "rollback missing", + "noPower": "no VM power", + "powerOpen": "VM power open", + "noDestructive": "no destructive", + "destructiveOpen": "destructive open" }, "blockerLanes": { "label": "卡點分群", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index adf8b5265..a9d094c80 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1163,6 +1163,14 @@ type PriorityWorkOrderResponse = { windows99_vmx_source_repair_safe_next_step?: string | null; windows99_vmx_source_repair_controlled_apply_mode?: string | null; windows99_vmx_source_repair_apply_allowed_by_this_package?: boolean | null; + windows99_vmx_source_repair_work_order_id?: string | null; + windows99_vmx_source_repair_action_stage?: string | null; + windows99_vmx_source_repair_next_executable_step?: string | null; + windows99_vmx_source_repair_check_mode_allowed?: boolean | null; + windows99_vmx_source_repair_rollback_required?: boolean | null; + windows99_vmx_source_repair_vm_power_change_allowed?: boolean | null; + windows99_vmx_source_repair_destructive_restore_allowed?: boolean | null; + windows99_vmx_source_repair_source_diff_alias_count?: number | null; ai_loop_log_source_grouping_key_count?: number | null; ai_loop_log_source_grouping_keys?: string[] | null; ai_loop_log_source_tagging_contract_count?: number | null; @@ -1209,6 +1217,14 @@ type PriorityWorkOrderResponse = { windows99_vmx_source_repair_safe_next_step?: string | null; windows99_vmx_source_repair_controlled_apply_mode?: string | null; windows99_vmx_source_repair_apply_allowed_by_this_package?: boolean | null; + windows99_vmx_source_repair_work_order_id?: string | null; + windows99_vmx_source_repair_action_stage?: string | null; + windows99_vmx_source_repair_next_executable_step?: string | null; + windows99_vmx_source_repair_check_mode_allowed?: boolean | null; + windows99_vmx_source_repair_rollback_required?: boolean | null; + windows99_vmx_source_repair_vm_power_change_allowed?: boolean | null; + windows99_vmx_source_repair_destructive_restore_allowed?: boolean | null; + windows99_vmx_source_repair_source_diff_alias_count?: number | null; drill_preflight_blocker_count?: number | null; ai_loop_current_blocker_log_source_tags?: AiLoopLogSourceTag[] | null; ai_loop_log_source_tagging_contract?: AiLoopLogSourceContract[] | null; @@ -1316,6 +1332,15 @@ const COMMANDER_INSERTED_REQUIREMENT_FALLBACK: PriorityWorkOrderResponse = { windows99_vmx_source_repair_controlled_apply_mode: "check_mode_package_only_no_windows_or_vm_write", windows99_vmx_source_repair_apply_allowed_by_this_package: false, + windows99_vmx_source_repair_work_order_id: "windows99-vmx-source-repair-111", + windows99_vmx_source_repair_action_stage: "source_repair_check_mode_ready", + windows99_vmx_source_repair_next_executable_step: + "locate_or_relink_missing_vmx_source_check_mode", + windows99_vmx_source_repair_check_mode_allowed: true, + windows99_vmx_source_repair_rollback_required: true, + windows99_vmx_source_repair_vm_power_change_allowed: false, + windows99_vmx_source_repair_destructive_restore_allowed: false, + windows99_vmx_source_repair_source_diff_alias_count: 1, stockplatform_public_api_runtime_status: "stockplatform_public_api_runtime_ready", stockplatform_public_api_controlled_recovery_preflight_status: "not_required_stockplatform_runtime_ready", @@ -1384,6 +1409,15 @@ const COMMANDER_INSERTED_REQUIREMENT_FALLBACK: PriorityWorkOrderResponse = { windows99_vmx_source_repair_controlled_apply_mode: "check_mode_package_only_no_windows_or_vm_write", windows99_vmx_source_repair_apply_allowed_by_this_package: false, + windows99_vmx_source_repair_work_order_id: "windows99-vmx-source-repair-111", + windows99_vmx_source_repair_action_stage: "source_repair_check_mode_ready", + windows99_vmx_source_repair_next_executable_step: + "locate_or_relink_missing_vmx_source_check_mode", + windows99_vmx_source_repair_check_mode_allowed: true, + windows99_vmx_source_repair_rollback_required: true, + windows99_vmx_source_repair_vm_power_change_allowed: false, + windows99_vmx_source_repair_destructive_restore_allowed: false, + windows99_vmx_source_repair_source_diff_alias_count: 1, drill_preflight_blocker_count: 5, stockplatform_public_api_runtime_status: "stockplatform_public_api_runtime_ready", stockplatform_public_api_runtime_ready: true, @@ -9120,9 +9154,43 @@ function ManagerSituationBoard({ summary?.windows99_vmx_source_repair_apply_allowed_by_this_package ?? evidence?.windows99_vmx_source_repair_apply_allowed_by_this_package ?? false; + const vmxRepairActionStage = + summary?.windows99_vmx_source_repair_action_stage ?? + evidence?.windows99_vmx_source_repair_action_stage ?? + ""; + const vmxRepairNextStep = + summary?.windows99_vmx_source_repair_next_executable_step ?? + evidence?.windows99_vmx_source_repair_next_executable_step ?? + ""; + const vmxRepairCheckModeAllowed = + summary?.windows99_vmx_source_repair_check_mode_allowed ?? + evidence?.windows99_vmx_source_repair_check_mode_allowed ?? + false; + const vmxRepairRollbackRequired = + summary?.windows99_vmx_source_repair_rollback_required ?? + evidence?.windows99_vmx_source_repair_rollback_required ?? + false; + const vmxRepairVmPowerAllowed = + summary?.windows99_vmx_source_repair_vm_power_change_allowed ?? + evidence?.windows99_vmx_source_repair_vm_power_change_allowed ?? + false; + const vmxRepairDestructiveAllowed = + summary?.windows99_vmx_source_repair_destructive_restore_allowed ?? + evidence?.windows99_vmx_source_repair_destructive_restore_allowed ?? + false; const vmxRepairMissingLabel = vmxRepairMissingAliases.length ? vmxRepairMissingAliases.join(", ") : "0"; + const vmxRepairStageLabel = vmxRepairActionStage.includes("source") + ? t("vmxRepair.stageSource") + : vmxRepairActionStage.includes("power") + ? t("vmxRepair.stagePower") + : t("vmxRepair.stageVerify"); + const vmxRepairNextLabel = vmxRepairNextStep.includes("locate") + ? t("vmxRepair.nextLocate") + : vmxRepairNextStep.includes("power") + ? t("vmxRepair.nextPower") + : t("vmxRepair.nextVerify"); const readinessPercent = summary?.active_p0_readiness_percent ?? null; const rebootEvidenceReady = evidence?.live_slo_metric_present === true && @@ -9358,6 +9426,40 @@ function ManagerSituationBoard({ : t("vmxRepair.applyClosed")} +
+ + {t("vmxRepair.actionChain")} + + + {vmxRepairStageLabel} + + + {vmxRepairNextLabel} + + + {vmxRepairCheckModeAllowed + ? t("vmxRepair.checkReady") + : t("vmxRepair.checkWaiting")} + + + {vmxRepairRollbackRequired + ? t("vmxRepair.rollbackSet") + : t("vmxRepair.rollbackMissing")} + + + {vmxRepairVmPowerAllowed + ? t("vmxRepair.powerOpen") + : t("vmxRepair.noPower")} + + + {vmxRepairDestructiveAllowed + ? t("vmxRepair.destructiveOpen") + : t("vmxRepair.noDestructive")} + +
{t("blockerLanes.label")}
diff --git a/scripts/reboot-recovery/tests/test_windows99_vmx_source_repair_package.py b/scripts/reboot-recovery/tests/test_windows99_vmx_source_repair_package.py index 27e90a65d..2fcd25d76 100644 --- a/scripts/reboot-recovery/tests/test_windows99_vmx_source_repair_package.py +++ b/scripts/reboot-recovery/tests/test_windows99_vmx_source_repair_package.py @@ -74,6 +74,22 @@ def test_package_routes_missing_vmx_to_source_repair_check_mode() -> None: "192.168.0.111_Ubuntu_64-bit\\192.168.0.111_Ubuntu_64-bit.vmx'" ) ] + assert payload["work_order_id"] == "windows99-vmx-source-repair-111" + assert payload["action_stage"] == "source_repair_check_mode_ready" + assert ( + payload["next_executable_step"] + == "locate_or_relink_missing_vmx_source_check_mode" + ) + assert payload["check_mode_allowed_by_this_package"] is True + assert payload["rollback_required"] is True + assert payload["vm_power_change_allowed"] is False + assert payload["destructive_restore_allowed"] is False + action_chain = payload["action_chain"] + assert action_chain["target_selector"]["vm_aliases"] == ["111"] + assert action_chain["source_of_truth_diff"][0]["current_state"] == ( + "expected_vmx_source_missing" + ) + assert action_chain["check_mode"]["probe_command_count"] == 1 assert row_by_alias["188"]["missing_vmx_source"] is False assert payload["operation_boundaries"]["vm_power_change_performed"] is False assert payload["operation_boundaries"]["secret_value_read"] is False @@ -110,6 +126,10 @@ def test_package_routes_power_only_to_autostart_check_mode() -> None: ) assert payload["vm_rows"][0]["powered_off"] is True assert payload["apply_allowed_by_this_package"] is False + assert payload["action_stage"] == "power_readback_check_mode_ready" + assert payload["next_executable_step"] == ( + "build_vmware_autostart_power_readback_check_mode" + ) def test_cli_writes_package_file(tmp_path: Path) -> None: