feat(windows99): validate scoped relink dry run
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m9s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m9s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -507,6 +507,7 @@ from src.services.windows99_vmx_source_backup_search_package import (
|
||||
from src.services.windows99_vmx_source_locator_readback import (
|
||||
load_latest_windows99_vmx_source_locator_readback,
|
||||
validate_windows99_vmx_source_candidate_confirmation_packet,
|
||||
validate_windows99_vmx_source_scoped_relink_dry_run_packet,
|
||||
)
|
||||
from src.services.windows99_vmx_source_repair_package import (
|
||||
load_latest_windows99_vmx_source_repair_package,
|
||||
@@ -1741,6 +1742,39 @@ async def validate_windows99_vmx_source_candidate_confirmation(
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/windows99-vmx-source-locator-readback/validate-scoped-relink-dry-run",
|
||||
response_model=dict[str, Any],
|
||||
summary="驗證 Windows99 VMX source scoped relink dry-run packet",
|
||||
description=(
|
||||
"針對 P0-006 Windows99 VMX source locator 的 redacted scoped relink "
|
||||
"dry-run packet 做 no-persist reviewer validation。此端點只判斷 reviewer "
|
||||
"validation 是否 passed;不保存 payload、不回傳 raw VMX path、不讀 VMX "
|
||||
"內容、不讀 secret、不 SSH、不啟停 VM、不寫 Windows、不改 registry/task、"
|
||||
"不開 apply gate。"
|
||||
),
|
||||
)
|
||||
async def validate_windows99_vmx_source_scoped_relink_dry_run(
|
||||
scoped_relink_dry_run_payload: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Return no-persist validation for one Windows99 relink dry-run packet."""
|
||||
try:
|
||||
payload = await asyncio.to_thread(
|
||||
validate_windows99_vmx_source_scoped_relink_dry_run_packet,
|
||||
scoped_relink_dry_run_payload,
|
||||
)
|
||||
return redact_public_lan_topology(payload)
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
logger.error(
|
||||
"windows99_vmx_source_scoped_relink_dry_run_invalid",
|
||||
error=str(exc),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Windows99 VMX source scoped relink dry-run 驗證器無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/windows99-vmx-source-backup-search-package",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
@@ -38,6 +38,15 @@ _CANDIDATE_CONFIRMATION_PACKET_SCHEMA_VERSION = (
|
||||
_SCOPED_RELINK_DRY_RUN_SCHEMA_VERSION = (
|
||||
"windows99_vmx_source_scoped_relink_dry_run_v1"
|
||||
)
|
||||
_SCOPED_RELINK_DRY_RUN_PACKET_SCHEMA_VERSION = (
|
||||
"windows99_vmx_source_scoped_relink_dry_run_packet_v1"
|
||||
)
|
||||
_SCOPED_RELINK_DRY_RUN_VALIDATION_SCHEMA_VERSION = (
|
||||
"windows99_vmx_source_scoped_relink_dry_run_validation_v1"
|
||||
)
|
||||
_SCOPED_RELINK_DRY_RUN_RECEIPT_SCHEMA_VERSION = (
|
||||
"windows99_vmx_source_scoped_relink_dry_run_receipt_v1"
|
||||
)
|
||||
_RECEIPT_FILE = "windows99-vmx-source-locator-receipt.snapshot.json"
|
||||
_COLLECTOR_SCRIPT_PATH = "scripts/reboot-recovery/locate-windows99-vmx-source.sh"
|
||||
_LOCATOR_SCRIPT_PATH = "scripts/reboot-recovery/windows99-vmx-source-locator.ps1"
|
||||
@@ -93,6 +102,34 @@ _CANDIDATE_CONFIRMATION_FORBIDDEN_FRAGMENTS = [
|
||||
"token=",
|
||||
"secret_value",
|
||||
]
|
||||
_SCOPED_RELINK_DRY_RUN_REQUIRED_FIELDS = [
|
||||
"schema_version",
|
||||
"plan_id",
|
||||
"target_host_alias",
|
||||
"target_vm_aliases",
|
||||
"candidate_confirmation_accepted",
|
||||
"candidate_identity_evidence_ref",
|
||||
"source_fingerprint_ref",
|
||||
"backup_ref",
|
||||
"source_of_truth_diff",
|
||||
"pre_apply_backup_plan",
|
||||
"rollback_ref",
|
||||
"post_verifier",
|
||||
"redacted_metadata_only",
|
||||
"bounded_identity_metadata_only",
|
||||
"path_fingerprint_only",
|
||||
"raw_path_output",
|
||||
"vmx_file_content_read",
|
||||
"secret_value_read",
|
||||
"remote_write_performed",
|
||||
"runtime_write_performed",
|
||||
"host_reboot_performed",
|
||||
"vm_power_change_performed",
|
||||
"windows_registry_apply_performed",
|
||||
"scheduled_task_write_performed",
|
||||
"apply_requested",
|
||||
"vm_power_change_requested",
|
||||
]
|
||||
|
||||
|
||||
def _dict(value: Any) -> dict[str, Any]:
|
||||
@@ -292,6 +329,10 @@ def _build_candidate_confirmation_gate(
|
||||
}
|
||||
|
||||
|
||||
def _scoped_relink_plan_id(target_aliases: list[str]) -> str:
|
||||
return f"windows99-vmx-source-relink-dry-run-{','.join(target_aliases) or 'none'}"
|
||||
|
||||
|
||||
def _build_projected_scoped_relink_dry_run_plan(
|
||||
*,
|
||||
accepted: bool,
|
||||
@@ -321,10 +362,7 @@ def _build_projected_scoped_relink_dry_run_plan(
|
||||
|
||||
return {
|
||||
"schema_version": _SCOPED_RELINK_DRY_RUN_SCHEMA_VERSION,
|
||||
"plan_id": (
|
||||
"windows99-vmx-source-relink-dry-run-"
|
||||
f"{','.join(missing_aliases) or 'none'}"
|
||||
),
|
||||
"plan_id": _scoped_relink_plan_id(missing_aliases),
|
||||
"status": (
|
||||
"ready_for_no_write_review"
|
||||
if ready
|
||||
@@ -550,6 +588,234 @@ def validate_windows99_vmx_source_candidate_confirmation_packet(
|
||||
}
|
||||
|
||||
|
||||
def validate_windows99_vmx_source_scoped_relink_dry_run_packet(
|
||||
packet: dict[str, Any],
|
||||
*,
|
||||
locator_readback: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate one redacted scoped relink dry-run packet.
|
||||
|
||||
This no-persist validator proves the dry-run is reviewable and bounded. It
|
||||
does not save payloads, reveal raw VMX paths, read VMX contents, read
|
||||
secrets, SSH, change VM power state, write Windows state, or open apply.
|
||||
"""
|
||||
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"))
|
||||
allowed_target_aliases = current_missing_aliases or current_target_aliases
|
||||
submitted_aliases = _strings(packet.get("target_vm_aliases"))
|
||||
submitted_aliases_valid = bool(
|
||||
submitted_aliases
|
||||
and allowed_target_aliases
|
||||
and set(submitted_aliases).issubset(set(allowed_target_aliases))
|
||||
)
|
||||
expected_plan_id = _scoped_relink_plan_id(submitted_aliases)
|
||||
source_of_truth_diff = _dict(packet.get("source_of_truth_diff"))
|
||||
pre_apply_backup_plan = _dict(packet.get("pre_apply_backup_plan"))
|
||||
post_verifier = _strings(packet.get("post_verifier"))
|
||||
|
||||
missing_fields = [
|
||||
field
|
||||
for field in _SCOPED_RELINK_DRY_RUN_REQUIRED_FIELDS
|
||||
if field not in packet
|
||||
]
|
||||
rejected_fields: list[str] = []
|
||||
forbidden_fragments = _forbidden_fragments(packet)
|
||||
|
||||
if packet.get("schema_version") != _SCOPED_RELINK_DRY_RUN_PACKET_SCHEMA_VERSION:
|
||||
rejected_fields.append("schema_version")
|
||||
if str(packet.get("plan_id") or "") != expected_plan_id:
|
||||
rejected_fields.append("plan_id")
|
||||
if str(packet.get("target_host_alias") or "") != "99":
|
||||
rejected_fields.append("target_host_alias")
|
||||
if not submitted_aliases_valid:
|
||||
rejected_fields.append("target_vm_aliases")
|
||||
if packet.get("candidate_confirmation_accepted") is not True:
|
||||
rejected_fields.append("candidate_confirmation_accepted")
|
||||
if str(packet.get("candidate_identity_evidence_ref") or "").strip() == "":
|
||||
rejected_fields.append("candidate_identity_evidence_ref")
|
||||
if str(packet.get("source_fingerprint_ref") or "").strip() == "":
|
||||
rejected_fields.append("source_fingerprint_ref")
|
||||
if str(packet.get("backup_ref") or "").strip() == "":
|
||||
rejected_fields.append("backup_ref")
|
||||
if not source_of_truth_diff:
|
||||
rejected_fields.append("source_of_truth_diff")
|
||||
if source_of_truth_diff.get("raw_path_output", False) is not False:
|
||||
rejected_fields.append("source_of_truth_diff.raw_path_output")
|
||||
if source_of_truth_diff.get("path_fingerprint_only") is not True:
|
||||
rejected_fields.append("source_of_truth_diff.path_fingerprint_only")
|
||||
if source_of_truth_diff.get("bounded_identity_metadata_only") is not True:
|
||||
rejected_fields.append("source_of_truth_diff.bounded_identity_metadata_only")
|
||||
if not pre_apply_backup_plan:
|
||||
rejected_fields.append("pre_apply_backup_plan")
|
||||
if pre_apply_backup_plan.get("ready") is not True:
|
||||
rejected_fields.append("pre_apply_backup_plan.ready")
|
||||
if str(pre_apply_backup_plan.get("backup_ref") or "").strip() == "":
|
||||
rejected_fields.append("pre_apply_backup_plan.backup_ref")
|
||||
if str(packet.get("rollback_ref") or "").startswith("rollback://") is False:
|
||||
rejected_fields.append("rollback_ref")
|
||||
if not post_verifier:
|
||||
rejected_fields.append("post_verifier")
|
||||
if "GET /api/v1/agents/windows99-vmx-source-locator-readback" not in post_verifier:
|
||||
rejected_fields.append("post_verifier.locator_readback")
|
||||
|
||||
required_true_flags = [
|
||||
"redacted_metadata_only",
|
||||
"bounded_identity_metadata_only",
|
||||
"path_fingerprint_only",
|
||||
]
|
||||
required_false_flags = [
|
||||
"raw_path_output",
|
||||
"vmx_file_content_read",
|
||||
"secret_value_read",
|
||||
"remote_write_performed",
|
||||
"runtime_write_performed",
|
||||
"host_reboot_performed",
|
||||
"vm_power_change_performed",
|
||||
"windows_registry_apply_performed",
|
||||
"scheduled_task_write_performed",
|
||||
"apply_requested",
|
||||
"vm_power_change_requested",
|
||||
]
|
||||
for field in required_true_flags:
|
||||
if packet.get(field) is not True:
|
||||
rejected_fields.append(field)
|
||||
for field in required_false_flags:
|
||||
if packet.get(field, False) is not False:
|
||||
rejected_fields.append(field)
|
||||
if forbidden_fragments:
|
||||
rejected_fields.append("forbidden_fragments")
|
||||
|
||||
accepted = not missing_fields and not rejected_fields
|
||||
status = (
|
||||
"accepted_for_reviewer_validation_passed_no_write"
|
||||
if accepted
|
||||
else "rejected_scoped_relink_dry_run_packet"
|
||||
)
|
||||
next_safe_step = (
|
||||
"commit_or_attach_no_secret_dry_run_receipt_then_rerun_no_secret_"
|
||||
"collector_and_scorecard_no_vm_power_change"
|
||||
if accepted
|
||||
else "resubmit_redacted_scoped_relink_dry_run_packet_without_raw_paths_or_runtime_actions"
|
||||
)
|
||||
plan_id = expected_plan_id if accepted else str(packet.get("plan_id") or "")
|
||||
source_fingerprint_ref = (
|
||||
str(packet.get("source_fingerprint_ref") or "").strip() if accepted else ""
|
||||
)
|
||||
backup_ref = str(packet.get("backup_ref") or "").strip() if accepted else ""
|
||||
candidate_identity_evidence_ref = (
|
||||
str(packet.get("candidate_identity_evidence_ref") or "").strip()
|
||||
if accepted
|
||||
else ""
|
||||
)
|
||||
rollback_ref = str(packet.get("rollback_ref") or "").strip() if accepted else ""
|
||||
projected_source_diff = {
|
||||
"expected_vmx_path_count": locator.get("expected_vmx_path_count"),
|
||||
"expected_vmx_path_present_count": source_of_truth_diff.get(
|
||||
"expected_vmx_path_present_count"
|
||||
)
|
||||
if accepted
|
||||
else None,
|
||||
"candidate_source_count": source_of_truth_diff.get("candidate_source_count")
|
||||
if accepted
|
||||
else None,
|
||||
"candidate_source_fingerprint_count": source_of_truth_diff.get(
|
||||
"candidate_source_fingerprint_count"
|
||||
)
|
||||
if accepted
|
||||
else None,
|
||||
"candidate_identity_evidence_ref": candidate_identity_evidence_ref,
|
||||
"source_fingerprint_ref": source_fingerprint_ref,
|
||||
"backup_ref": backup_ref,
|
||||
"raw_path_output": False,
|
||||
"path_fingerprint_only": True,
|
||||
"bounded_identity_metadata_only": True,
|
||||
}
|
||||
operation_boundaries = {
|
||||
"no_persist_validator": True,
|
||||
"request_payload_saved": False,
|
||||
"secret_value_read": False,
|
||||
"raw_path_output": False,
|
||||
"vmx_file_content_read": False,
|
||||
"password_prompt_allowed": False,
|
||||
"remote_write_performed": False,
|
||||
"runtime_write_performed": False,
|
||||
"host_reboot_performed": False,
|
||||
"vm_power_change_performed": False,
|
||||
"windows_registry_apply_performed": False,
|
||||
"scheduled_task_write_performed": False,
|
||||
"apply_gate_opened": False,
|
||||
}
|
||||
|
||||
return {
|
||||
"schema_version": _SCOPED_RELINK_DRY_RUN_VALIDATION_SCHEMA_VERSION,
|
||||
"status": status,
|
||||
"accepted": accepted,
|
||||
"reviewer_validation_passed": accepted,
|
||||
"controlled_relink_dry_run_ready": 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 ""
|
||||
),
|
||||
"plan_id": plan_id,
|
||||
"expected_plan_id": expected_plan_id,
|
||||
"source_fingerprint_ref": source_fingerprint_ref,
|
||||
"backup_ref": backup_ref,
|
||||
"candidate_identity_evidence_ref": candidate_identity_evidence_ref,
|
||||
"missing_fields": missing_fields,
|
||||
"rejected_fields": rejected_fields,
|
||||
"forbidden_fragment_count": len(forbidden_fragments),
|
||||
"rollback_ready": accepted,
|
||||
"post_verifier_ready": accepted,
|
||||
"pre_apply_backup_plan_ready": accepted,
|
||||
"km_writeback_ready": accepted,
|
||||
"playbook_trust_writeback_ready": accepted,
|
||||
"request_payload_saved": False,
|
||||
"apply_allowed_by_this_validation": False,
|
||||
"runtime_write_authorized": False,
|
||||
"remote_write_performed": False,
|
||||
"vm_power_change_performed": False,
|
||||
"secret_value_read": False,
|
||||
"projected_relink_dry_run_receipt": {
|
||||
"schema_version": _SCOPED_RELINK_DRY_RUN_RECEIPT_SCHEMA_VERSION,
|
||||
"receipt_ref": (
|
||||
f"receipt://awoooi/windows99/{plan_id}" if accepted else ""
|
||||
),
|
||||
"ready": accepted,
|
||||
"status": (
|
||||
"reviewer_validation_passed_no_write"
|
||||
if accepted
|
||||
else "blocked_reviewer_validation_not_passed"
|
||||
),
|
||||
"plan_id": plan_id,
|
||||
"target_selector": {
|
||||
"host_alias": "99",
|
||||
"vm_aliases": submitted_aliases if accepted else [],
|
||||
"missing_vmx_aliases": submitted_aliases if accepted else [],
|
||||
"secret_collection_allowed": False,
|
||||
"vm_power_change_allowed": False,
|
||||
"runtime_write_allowed": False,
|
||||
},
|
||||
"source_of_truth_diff": projected_source_diff,
|
||||
"pre_apply_backup_plan": {
|
||||
"ready": accepted,
|
||||
"backup_ref": backup_ref,
|
||||
"destructive_restore_allowed": False,
|
||||
},
|
||||
"rollback_ref": rollback_ref,
|
||||
"post_verifier": post_verifier if accepted else [],
|
||||
"operation_boundaries": operation_boundaries,
|
||||
},
|
||||
"operation_boundaries": operation_boundaries,
|
||||
"next_safe_step": next_safe_step,
|
||||
}
|
||||
|
||||
|
||||
def build_windows99_vmx_source_locator_readback(
|
||||
scorecard: dict[str, Any],
|
||||
*,
|
||||
|
||||
@@ -14,6 +14,7 @@ from src.api.v1.agents import router
|
||||
from src.services.windows99_vmx_source_locator_readback import (
|
||||
build_windows99_vmx_source_locator_readback,
|
||||
validate_windows99_vmx_source_candidate_confirmation_packet,
|
||||
validate_windows99_vmx_source_scoped_relink_dry_run_packet,
|
||||
)
|
||||
from src.services.windows99_vmx_source_repair_package import (
|
||||
build_windows99_vmx_source_repair_package,
|
||||
@@ -53,6 +54,85 @@ def _scorecard() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _locator_ready_for_scoped_relink_dry_run() -> dict:
|
||||
repair_package = build_windows99_vmx_source_repair_package(
|
||||
_scorecard(),
|
||||
generated_at="2026-07-03T11:21:00+08:00",
|
||||
)
|
||||
return 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,
|
||||
"collector_collection_status": (
|
||||
"collected_windows99_vmx_source_locator_stdout"
|
||||
),
|
||||
"target_host_alias": "99",
|
||||
"target_vm_aliases": ["111"],
|
||||
"expected_vmx_path_present_count": 0,
|
||||
"candidate_source_count": 1,
|
||||
"candidate_source_fingerprint_count": 1,
|
||||
"alias_hint_candidate_count": 0,
|
||||
"unassigned_ubuntu_candidate_count": 0,
|
||||
"identity_metadata_read": True,
|
||||
"bounded_identity_metadata_only": True,
|
||||
"identity_target_hint_count": 1,
|
||||
"identity_unassigned_ubuntu_candidate_count": 0,
|
||||
"locate_status": "single_identity_candidate_requires_confirmation",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _valid_scoped_relink_dry_run_packet() -> dict:
|
||||
return {
|
||||
"schema_version": "windows99_vmx_source_scoped_relink_dry_run_packet_v1",
|
||||
"plan_id": "windows99-vmx-source-relink-dry-run-111",
|
||||
"target_host_alias": "99",
|
||||
"target_vm_aliases": ["111"],
|
||||
"candidate_confirmation_accepted": True,
|
||||
"candidate_identity_evidence_ref": (
|
||||
"console://awoooi/windows99/candidate-identity-redacted-111"
|
||||
),
|
||||
"source_fingerprint_ref": "fingerprint://awoooi/windows99/vmx-source-111",
|
||||
"backup_ref": "backup://awoooi/windows99/pre-relink-redacted-111",
|
||||
"source_of_truth_diff": {
|
||||
"expected_vmx_path_present_count": 0,
|
||||
"candidate_source_count": 1,
|
||||
"candidate_source_fingerprint_count": 1,
|
||||
"raw_path_output": False,
|
||||
"path_fingerprint_only": True,
|
||||
"bounded_identity_metadata_only": True,
|
||||
},
|
||||
"pre_apply_backup_plan": {
|
||||
"ready": True,
|
||||
"backup_ref": "backup://awoooi/windows99/pre-relink-redacted-111",
|
||||
"destructive_restore_allowed": False,
|
||||
},
|
||||
"rollback_ref": "rollback://awoooi/windows99-vmx-source-relink-dry-run",
|
||||
"post_verifier": [
|
||||
"bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect",
|
||||
"GET /api/v1/agents/reboot-auto-recovery-slo-scorecard",
|
||||
"GET /api/v1/agents/windows99-vmx-source-repair-package",
|
||||
"GET /api/v1/agents/windows99-vmx-source-locator-readback",
|
||||
],
|
||||
"redacted_metadata_only": True,
|
||||
"bounded_identity_metadata_only": True,
|
||||
"path_fingerprint_only": True,
|
||||
"raw_path_output": False,
|
||||
"vmx_file_content_read": False,
|
||||
"secret_value_read": False,
|
||||
"remote_write_performed": False,
|
||||
"runtime_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,
|
||||
}
|
||||
|
||||
|
||||
def test_windows99_vmx_source_locator_builder_is_no_secret_check_mode() -> None:
|
||||
repair_package = build_windows99_vmx_source_repair_package(
|
||||
_scorecard(),
|
||||
@@ -442,6 +522,78 @@ def test_windows99_candidate_confirmation_validator_rejects_sensitive_payload()
|
||||
assert payload["operation_boundaries"]["apply_gate_opened"] is False
|
||||
|
||||
|
||||
def test_windows99_scoped_relink_dry_run_validator_accepts_redacted_packet() -> None:
|
||||
payload = validate_windows99_vmx_source_scoped_relink_dry_run_packet(
|
||||
_valid_scoped_relink_dry_run_packet(),
|
||||
locator_readback=_locator_ready_for_scoped_relink_dry_run(),
|
||||
)
|
||||
|
||||
assert payload["schema_version"] == (
|
||||
"windows99_vmx_source_scoped_relink_dry_run_validation_v1"
|
||||
)
|
||||
assert payload["status"] == "accepted_for_reviewer_validation_passed_no_write"
|
||||
assert payload["accepted"] is True
|
||||
assert payload["reviewer_validation_passed"] is True
|
||||
assert payload["controlled_relink_dry_run_ready"] is True
|
||||
assert payload["validated_target_vm_aliases"] == ["111"]
|
||||
assert payload["plan_id"] == "windows99-vmx-source-relink-dry-run-111"
|
||||
assert payload["rollback_ready"] is True
|
||||
assert payload["post_verifier_ready"] is True
|
||||
assert payload["pre_apply_backup_plan_ready"] is True
|
||||
assert payload["apply_allowed_by_this_validation"] is False
|
||||
assert payload["runtime_write_authorized"] is False
|
||||
assert payload["request_payload_saved"] is False
|
||||
assert payload["remote_write_performed"] is False
|
||||
assert payload["vm_power_change_performed"] is False
|
||||
assert payload["secret_value_read"] is False
|
||||
receipt = payload["projected_relink_dry_run_receipt"]
|
||||
assert receipt["schema_version"] == (
|
||||
"windows99_vmx_source_scoped_relink_dry_run_receipt_v1"
|
||||
)
|
||||
assert receipt["ready"] is True
|
||||
assert receipt["target_selector"]["host_alias"] == "99"
|
||||
assert receipt["target_selector"]["vm_aliases"] == ["111"]
|
||||
assert receipt["target_selector"]["runtime_write_allowed"] is False
|
||||
assert receipt["source_of_truth_diff"]["raw_path_output"] is False
|
||||
assert receipt["source_of_truth_diff"]["path_fingerprint_only"] is True
|
||||
assert receipt["pre_apply_backup_plan"]["ready"] is True
|
||||
assert receipt["pre_apply_backup_plan"]["destructive_restore_allowed"] is False
|
||||
assert "GET /api/v1/agents/windows99-vmx-source-locator-readback" in receipt[
|
||||
"post_verifier"
|
||||
]
|
||||
assert receipt["operation_boundaries"]["request_payload_saved"] is False
|
||||
assert receipt["operation_boundaries"]["remote_write_performed"] is False
|
||||
assert receipt["operation_boundaries"]["vm_power_change_performed"] is False
|
||||
|
||||
|
||||
def test_windows99_scoped_relink_dry_run_validator_rejects_sensitive_payload() -> None:
|
||||
packet = _valid_scoped_relink_dry_run_packet()
|
||||
packet["source_fingerprint_ref"] = "D:\\raw\\path\\vm.vmx"
|
||||
packet["source_of_truth_diff"]["raw_path_output"] = True
|
||||
packet["vmx_file_content_read"] = True
|
||||
|
||||
payload = validate_windows99_vmx_source_scoped_relink_dry_run_packet(
|
||||
packet,
|
||||
locator_readback=_locator_ready_for_scoped_relink_dry_run(),
|
||||
)
|
||||
|
||||
assert payload["accepted"] is False
|
||||
assert payload["status"] == "rejected_scoped_relink_dry_run_packet"
|
||||
assert payload["reviewer_validation_passed"] is False
|
||||
assert payload["controlled_relink_dry_run_ready"] is False
|
||||
assert "source_of_truth_diff.raw_path_output" in payload["rejected_fields"]
|
||||
assert "vmx_file_content_read" in payload["rejected_fields"]
|
||||
assert "forbidden_fragments" in payload["rejected_fields"]
|
||||
assert payload["source_fingerprint_ref"] == ""
|
||||
assert payload["backup_ref"] == ""
|
||||
assert payload["projected_relink_dry_run_receipt"]["ready"] is False
|
||||
assert payload["projected_relink_dry_run_receipt"]["post_verifier"] == []
|
||||
serialized = json.dumps(payload)
|
||||
assert "D:\\" not in serialized
|
||||
assert "192.168.0." not in serialized
|
||||
assert payload["operation_boundaries"]["apply_gate_opened"] is False
|
||||
|
||||
|
||||
def test_windows99_vmx_source_locator_endpoint_returns_public_safe_readback(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -578,3 +730,47 @@ def test_windows99_candidate_confirmation_endpoint_is_no_persist_validator(
|
||||
assert plan["request_payload_saved"] is False
|
||||
assert "D:\\" not in response.text
|
||||
assert "192.168.0." not in response.text
|
||||
|
||||
|
||||
def test_windows99_scoped_relink_dry_run_endpoint_is_no_persist_validator(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
locator = _locator_ready_for_scoped_relink_dry_run()
|
||||
monkeypatch.setattr(
|
||||
agents,
|
||||
"validate_windows99_vmx_source_scoped_relink_dry_run_packet",
|
||||
lambda packet: validate_windows99_vmx_source_scoped_relink_dry_run_packet(
|
||||
packet,
|
||||
locator_readback=locator,
|
||||
),
|
||||
)
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/agents/windows99-vmx-source-locator-readback/"
|
||||
"validate-scoped-relink-dry-run",
|
||||
json=_valid_scoped_relink_dry_run_packet(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["schema_version"] == (
|
||||
"windows99_vmx_source_scoped_relink_dry_run_validation_v1"
|
||||
)
|
||||
assert payload["accepted"] is True
|
||||
assert payload["reviewer_validation_passed"] is True
|
||||
assert payload["controlled_relink_dry_run_ready"] is True
|
||||
assert payload["operation_boundaries"]["no_persist_validator"] is True
|
||||
assert payload["operation_boundaries"]["request_payload_saved"] is False
|
||||
assert payload["operation_boundaries"]["apply_gate_opened"] is False
|
||||
assert payload["operation_boundaries"]["vm_power_change_performed"] is False
|
||||
receipt = payload["projected_relink_dry_run_receipt"]
|
||||
assert receipt["ready"] is True
|
||||
assert receipt["target_selector"]["vm_aliases"] == ["111"]
|
||||
assert receipt["pre_apply_backup_plan"]["destructive_restore_allowed"] is False
|
||||
assert payload["apply_allowed_by_this_validation"] is False
|
||||
assert payload["runtime_write_authorized"] is False
|
||||
assert "D:\\" not in response.text
|
||||
assert "192.168.0." not in response.text
|
||||
|
||||
Reference in New Issue
Block a user