From 4990118c321d4da242b4f42a0cb55865a19100ee Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 02:35:44 +0800 Subject: [PATCH] fix(windows99): accept scoped vmx alias confirmations --- .../windows99_vmx_source_locator_readback.py | 24 ++++++- ...ndows99_vmx_source_locator_readback_api.py | 71 +++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/apps/api/src/services/windows99_vmx_source_locator_readback.py b/apps/api/src/services/windows99_vmx_source_locator_readback.py index f45fe6a9f..7861ed693 100644 --- a/apps/api/src/services/windows99_vmx_source_locator_readback.py +++ b/apps/api/src/services/windows99_vmx_source_locator_readback.py @@ -298,8 +298,16 @@ def _build_projected_scoped_relink_dry_run_plan( locator: dict[str, Any], packet: dict[str, Any], ) -> dict[str, Any]: - target_aliases = _strings(locator.get("target_vm_aliases")) - missing_aliases = _strings(locator.get("missing_vmx_aliases")) or target_aliases + submitted_aliases = _strings(packet.get("target_vm_aliases")) + locator_target_aliases = _strings(locator.get("target_vm_aliases")) + target_aliases = ( + submitted_aliases if accepted and submitted_aliases else locator_target_aliases + ) + missing_aliases = ( + submitted_aliases + if accepted and submitted_aliases + else _strings(locator.get("missing_vmx_aliases")) or target_aliases + ) evidence_ref = ( str(packet.get("candidate_identity_evidence_ref") or "").strip() if accepted @@ -403,7 +411,14 @@ def validate_windows99_vmx_source_candidate_confirmation_packet( """ locator = locator_readback or load_latest_windows99_vmx_source_locator_readback() current_target_aliases = _strings(locator.get("target_vm_aliases")) + current_missing_aliases = _strings(locator.get("missing_vmx_aliases")) submitted_aliases = _strings(packet.get("target_vm_aliases")) + allowed_target_aliases = current_missing_aliases or current_target_aliases + submitted_aliases_valid = bool( + submitted_aliases + and allowed_target_aliases + and set(submitted_aliases).issubset(set(allowed_target_aliases)) + ) missing_fields = [ field for field in _CANDIDATE_CONFIRMATION_REQUIRED_FIELDS @@ -416,7 +431,7 @@ def validate_windows99_vmx_source_candidate_confirmation_packet( rejected_fields.append("schema_version") if str(packet.get("target_host_alias") or "") != "99": rejected_fields.append("target_host_alias") - if not submitted_aliases or submitted_aliases != current_target_aliases: + if not submitted_aliases_valid: rejected_fields.append("target_vm_aliases") if str(packet.get("candidate_identity_kind") or "") not in ( _CANDIDATE_CONFIRMATION_ALLOWED_KINDS @@ -477,6 +492,9 @@ def validate_windows99_vmx_source_candidate_confirmation_packet( "accepted": accepted, "target_host_alias": "99", "target_vm_aliases": current_target_aliases, + "missing_vmx_aliases": current_missing_aliases, + "submitted_target_vm_aliases": submitted_aliases, + "validated_target_vm_aliases": submitted_aliases if accepted else [], "current_locator_status": str(locator.get("status") or ""), "current_candidate_confirmation_status": str( locator.get("candidate_confirmation_status") or "" diff --git a/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py b/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py index e4133ffa3..63b9306ee 100644 --- a/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py +++ b/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py @@ -313,6 +313,77 @@ def test_windows99_candidate_confirmation_validator_accepts_redacted_packet() -> assert payload["operation_boundaries"]["vm_power_change_performed"] is False +def test_windows99_candidate_confirmation_accepts_scoped_missing_alias_subset() -> None: + repair_package = build_windows99_vmx_source_repair_package( + _scorecard(), + generated_at="2026-07-03T11:21:00+08:00", + ) + locator = build_windows99_vmx_source_locator_readback( + _scorecard(), + repair_package=repair_package, + collector_receipt={ + "schema_version": "windows99_vmx_source_locator_receipt_v1", + "collector_readback_present": True, + "target_host_alias": "99", + "target_vm_aliases": ["111"], + "expected_vmx_path_present_count": 0, + "candidate_source_count": 8, + "candidate_source_fingerprint_count": 0, + "alias_hint_candidate_count": 0, + "identity_metadata_read": True, + "bounded_identity_metadata_only": True, + "identity_target_hint_count": 0, + "identity_unassigned_ubuntu_candidate_count": 1, + "locate_status": ( + "single_identity_unassigned_ubuntu_candidate_requires_confirmation" + ), + }, + ) + locator["target_vm_aliases"] = ["111", "112", "120", "121", "188"] + locator["missing_vmx_aliases"] = ["111", "112", "120", "121", "188"] + + payload = validate_windows99_vmx_source_candidate_confirmation_packet( + { + "schema_version": "windows99_vmx_source_candidate_confirmation_packet_v1", + "target_host_alias": "99", + "target_vm_aliases": ["111"], + "candidate_identity_kind": "verified_backup_path_fingerprint", + "candidate_identity_evidence_ref": ( + "backup://awoooi/windows99/redacted-fingerprint-111" + ), + "candidate_match_count": 1, + "redacted_metadata_only": True, + "bounded_identity_metadata_only": True, + "path_fingerprint_only": True, + "raw_path_output": False, + "secret_value_read": False, + "remote_write_performed": False, + "host_reboot_performed": False, + "vm_power_change_performed": False, + "windows_registry_apply_performed": False, + "scheduled_task_write_performed": False, + "apply_requested": False, + "vm_power_change_requested": False, + }, + locator_readback=locator, + ) + + assert payload["accepted"] is True + assert payload["target_vm_aliases"] == ["111", "112", "120", "121", "188"] + assert payload["missing_vmx_aliases"] == ["111", "112", "120", "121", "188"] + assert payload["submitted_target_vm_aliases"] == ["111"] + assert payload["validated_target_vm_aliases"] == ["111"] + assert payload["rejected_fields"] == [] + plan = payload["projected_scoped_relink_dry_run_plan"] + assert plan["ready"] is True + assert plan["plan_id"] == "windows99-vmx-source-relink-dry-run-111" + assert plan["target_selector"]["vm_aliases"] == ["111"] + assert plan["target_selector"]["missing_vmx_aliases"] == ["111"] + assert plan["apply_allowed_by_this_plan"] is False + assert plan["remote_write_performed"] is False + assert plan["vm_power_change_performed"] is False + + def test_windows99_candidate_confirmation_validator_rejects_sensitive_payload() -> None: locator = build_windows99_vmx_source_locator_readback( _scorecard(),