from __future__ import annotations import contextlib import copy import hashlib import importlib.util import io import json import os import sys import threading import unittest from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from tempfile import TemporaryDirectory from typing import Any from unittest import mock SCRIPT_PATH = Path(__file__).with_name("probe-nas-preview-g8-rollback.py") SPEC = importlib.util.spec_from_file_location("g8_rollback_probe", SCRIPT_PATH) assert SPEC is not None and SPEC.loader is not None MODULE = importlib.util.module_from_spec(SPEC) sys.modules[SPEC.name] = MODULE SPEC.loader.exec_module(MODULE) EXECUTOR_PATH = Path(__file__).with_name("serve-nas-preview-rollback-executor.py") EXECUTOR_SPEC = importlib.util.spec_from_file_location( "nas_preview_rollback_executor_contract", EXECUTOR_PATH ) assert EXECUTOR_SPEC is not None and EXECUTOR_SPEC.loader is not None EXECUTOR_MODULE = importlib.util.module_from_spec(EXECUTOR_SPEC) sys.modules[EXECUTOR_SPEC.name] = EXECUTOR_MODULE EXECUTOR_SPEC.loader.exec_module(EXECUTOR_MODULE) INTERNAL_TOKEN = "internal-probe-token-" + "x" * 32 ADMIN_EMAIL = "g8-control-plane-admin@hs.ac.kr" COOKIE_VALUE = "secret-cookie-value-must-never-be-printed" def sample_plan(suffix: str = "proof-20260807") -> dict[str, Any]: return MODULE.build_plan( suffix=suffix, artifact_id=f"nas-preview-rollback-{suffix}", artifact_sha256="a" * 64, artifact_provenance_uri=( f"repo://vignette/releases/nas-preview-rollback-{suffix}" ), evidence_refs=[ f"audit://vignette-nas-preview/g8-rollback/{suffix}/authorization", f"repo://vignette/releases/{suffix}/manifest", ], compose_sha256="b" * 64, env_sha256="c" * 64, resolved_config_sha256="d" * 64, api_image="sha256:" + "e" * 64, web_image="sha256:" + "f" * 64, ) class FakeState: def __init__( self, plan: dict[str, Any], *, release_failure_status: int | None = None, lifecycle_status: str = "executed", mismatch_artifact: bool = False, ) -> None: self.plan = plan self.release_failure_status = release_failure_status self.lifecycle_status = lifecycle_status self.mismatch_artifact = mismatch_artifact self.requests: list[dict[str, Any]] = [] self.release_calls = 0 self.approval_calls = 0 def read_model(self) -> dict[str, Any]: release = self.plan["release_gate"]["payload"] approval = self.plan["approval"]["payload"] expected = self.plan["expected_executor_request"] artifact_sha256 = expected["artifact_sha256"] if self.mismatch_artifact: artifact_sha256 = "0" * 64 executed = self.lifecycle_status == "executed" executor_refs = ( ["audit://vignette-nas-preview/rollback-executions/nas-g8-proof"] if executed else [] ) receipt_id = "nas-g8-0123456789abcdef01234567" if executed else None lifecycle_refs = [*approval["evidence_refs"], *executor_refs] return { "content_qualifications": [], "model_change_gates": [], "release_gates": [ { "gate_id": release["gate_id"], "release_id": release["manifest"]["release_id"], "qualified": True, "state": "pending_human_approval", "created_at": "2026-08-07T00:00:00Z", } ], "gate_artifacts": [ { "artifact_record_id": expected["artifact_record_id"], "owner_kind": "release_gate", "owner_id": release["gate_id"], "artifact_kind": "rollback", "artifact_id": expected["artifact_id"], "content_sha256": artifact_sha256, "provenance_uri": expected["artifact_provenance_uri"], "created_at": "2026-08-07T00:00:00Z", } ], "approvals": [ { "approval_event_id": approval["approval_event_id"], "target_kind": "release_gate", "target_id": release["gate_id"], "decision": "authorize_rollback", "reason_code": approval["reason_code"], "evidence_refs": approval["evidence_refs"], "created_at": "2026-08-07T00:00:00Z", } ], "catalog_entries": [], "lifecycle_events": [ { "lifecycle_event_id": approval["effect_record_id"], "target_kind": "release_gate", "target_id": release["gate_id"], "event_type": "rollback", "event_status": self.lifecycle_status, "approval_event_id": approval["approval_event_id"], "artifact_record_id": expected["artifact_record_id"], "evidence_refs": lifecycle_refs, "executor_receipt_id": receipt_id, "executor_evidence_refs": executor_refs, "created_at": "2026-08-07T00:00:00Z", } ], "incidents": [], "regression_dag_nodes": [], "data_classification": MODULE.DATA_CLASSIFICATION, "silent_auto_promotion_allowed": False, "raw_transcript_included": False, "pii_included": False, "clinical_claim_allowed": False, } def make_handler(state: FakeState) -> type[BaseHTTPRequestHandler]: class Handler(BaseHTTPRequestHandler): def log_message(self, format: str, *args: object) -> None: return def _body(self) -> dict[str, Any]: length = int(self.headers.get("Content-Length", "0")) raw = self.rfile.read(length) return json.loads(raw) if raw else {} def _reply( self, status: int, body: dict[str, Any], *, cookie: bool = False, ) -> None: raw = json.dumps(body).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(raw))) if cookie: self.send_header("Set-Cookie", f"sid={COOKIE_VALUE}; HttpOnly; Path=/") self.end_headers() self.wfile.write(raw) def do_POST(self) -> None: body = self._body() state.requests.append( { "method": "POST", "path": self.path, "body": body, "internal_token": self.headers.get(MODULE.INTERNAL_TOKEN_HEADER), "cookie": self.headers.get("Cookie"), "origin": self.headers.get("Origin"), } ) if self.path == MODULE.RELEASE_GATE_PATH: if state.release_failure_status is not None: self._reply( state.release_failure_status, {"detail": f"sensitive-{INTERNAL_TOKEN}-{ADMIN_EMAIL}"}, ) return state.release_calls += 1 payload = state.plan["release_gate"]["payload"] self._reply( 201, { "submission_id": payload["submission_id"], "gate_id": payload["gate_id"], "qualified": True, "state": "pending_human_approval", "human_approval_required": True, "promotion_executed": False, "idempotent_replay": state.release_calls > 1, }, ) return if self.path == MODULE.DEV_LOGIN_PATH: self._reply( 200, {"user_id": "not-printed", "role": "admin"}, cookie=True, ) return if self.path == MODULE.APPROVAL_PATH: if self.headers.get("Cookie") != f"sid={COOKIE_VALUE}": self._reply(403, {"detail": "cookie required"}) return state.approval_calls += 1 payload = state.plan["approval"]["payload"] self._reply( 201, { "submission_id": payload["submission_id"], "approval_event_id": payload["approval_event_id"], "target_kind": payload["target_kind"], "target_id": payload["target_id"], "decision": payload["decision"], "effect_record_id": payload["effect_record_id"], "idempotent_replay": state.approval_calls > 1, }, ) return self._reply(404, {"detail": "not found"}) def do_GET(self) -> None: state.requests.append( { "method": "GET", "path": self.path, "body": None, "internal_token": self.headers.get(MODULE.INTERNAL_TOKEN_HEADER), "cookie": self.headers.get("Cookie"), } ) if self.path == MODULE.READ_MODEL_PATH: self._reply(200, state.read_model()) return self._reply(404, {"detail": "not found"}) return Handler @contextlib.contextmanager def fake_api(state: FakeState): server = ThreadingHTTPServer(("127.0.0.1", 0), make_handler(state)) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: yield f"http://127.0.0.1:{server.server_port}" finally: server.shutdown() server.server_close() thread.join(timeout=5) class G8RollbackProbeTest(unittest.TestCase): def test_prepare_is_deterministic_and_exactly_binds_executor_manifest(self) -> None: first = sample_plan() second = sample_plan() self.assertEqual(first, second) self.assertEqual(MODULE.PLAN_FIELDS, set(first)) self.assertEqual( first["approval"]["payload"]["effect_record_id"], first["expected_executor_request"]["idempotency_key"], ) self.assertEqual( first["approval"]["payload"]["approval_event_id"], first["expected_executor_request"]["approval_event_id"], ) self.assertEqual( first["release_gate"]["payload"]["gate_id"], first["expected_executor_request"]["target_id"], ) self.assertEqual( first["release_gate"]["payload"]["manifest"]["release_id"], first["expected_executor_request"]["subject_id"], ) manifest = first["executor_manifest"]["payload"] self.assertEqual(MODULE.EXECUTOR_MANIFEST_SCHEMA, manifest["schema_version"]) self.assertEqual(1, len(manifest["artifacts"])) self.assertEqual( first["expected_executor_request"]["artifact_record_id"], manifest["artifacts"][0]["artifact_record_id"], ) self.assertFalse(first["safety"]["contains_secrets"]) self.assertFalse(first["safety"]["contains_pii"]) def test_prepared_manifest_loads_in_current_d31b0d_executor_contract(self) -> None: plan = sample_plan("executor-contract") with TemporaryDirectory(prefix="g8-probe-executor-contract-") as temp: manifest_path = Path(temp) / "manifest.json" manifest_path.write_bytes( MODULE._manifest_bytes(plan["executor_manifest"]["payload"]) ) loaded = EXECUTOR_MODULE.load_manifest( manifest_path, plan["executor_manifest"]["file_sha256"], ) artifact = next(iter(loaded.artifacts.values())) expected = plan["expected_executor_request"] self.assertEqual(expected["idempotency_key"], artifact.idempotency_key) self.assertEqual(expected["approval_event_id"], artifact.approval_event_id) self.assertEqual(expected["artifact_record_id"], artifact.artifact_record_id) self.assertEqual(expected["artifact_sha256"], artifact.artifact_sha256) self.assertEqual(expected["target_id"], artifact.target_id) self.assertEqual(expected["subject_id"], artifact.subject_id) self.assertEqual(expected["rollback_target_id"], artifact.rollback_target_id) def test_prepare_cli_emits_repeatable_plan_and_exact_manifest_bytes(self) -> None: arguments = [ "prepare", "--suffix", "prepare-cli", "--artifact-id", "nas-preview-rollback-prepare-cli", "--artifact-sha256", "a" * 64, "--artifact-provenance-uri", "repo://vignette/releases/nas-preview-rollback-prepare-cli", "--evidence-ref", "audit://vignette-nas-preview/g8-rollback/prepare-cli/authorization", "--compose-sha256", "b" * 64, "--env-sha256", "c" * 64, "--resolved-config-sha256", "d" * 64, "--api-image", "sha256:" + "e" * 64, "--web-image", "sha256:" + "f" * 64, ] with TemporaryDirectory(prefix="g8-probe-prepare-cli-") as temp: plan_path = Path(temp) / "plan.json" manifest_path = Path(temp) / "manifest.json" full_arguments = [ *arguments, "--output", str(plan_path), "--executor-manifest-output", str(manifest_path), ] first_stdout = io.StringIO() with contextlib.redirect_stdout(first_stdout): first_exit = MODULE.main(full_arguments) first_plan_bytes = plan_path.read_bytes() first_manifest_bytes = manifest_path.read_bytes() second_stdout = io.StringIO() with contextlib.redirect_stdout(second_stdout): second_exit = MODULE.main(full_arguments) self.assertEqual(0, first_exit) self.assertEqual(0, second_exit) self.assertEqual(first_stdout.getvalue(), second_stdout.getvalue()) plan = json.loads(first_plan_bytes) self.assertEqual(plan, json.loads(first_stdout.getvalue())) self.assertEqual( plan["executor_manifest"]["file_sha256"], hashlib.sha256(first_manifest_bytes).hexdigest(), ) def test_plan_extra_field_and_semantic_tampering_fail_closed(self) -> None: extra = sample_plan() extra["unexpected"] = True with self.assertRaisesRegex(MODULE.ProbeError, "invalid_plan_shape"): MODULE.validate_plan(extra) changed = copy.deepcopy(sample_plan()) changed["expected_executor_request"]["artifact_sha256"] = "0" * 64 core = {key: value for key, value in changed.items() if key != "plan_sha256"} changed["plan_sha256"] = MODULE._sha256_json(core) with self.assertRaisesRegex( MODULE.ProbeError, "executor_request_binding_mismatch" ): MODULE.validate_plan(changed) def test_prepare_rejects_pii_or_query_material_in_evidence_refs(self) -> None: common = { "suffix": "unsafe-evidence", "artifact_id": "nas-preview-rollback-unsafe-evidence", "artifact_sha256": "a" * 64, "artifact_provenance_uri": ( "repo://vignette/releases/nas-preview-rollback-unsafe-evidence" ), "compose_sha256": "b" * 64, "env_sha256": "c" * 64, "resolved_config_sha256": "d" * 64, "api_image": "sha256:" + "e" * 64, "web_image": "sha256:" + "f" * 64, } for evidence_ref in ( "audit://vignette/users/person@example.com/authorization", "https://evidence.invalid/proof?token=secret", ): with self.subTest(evidence_ref=evidence_ref): with self.assertRaisesRegex(MODULE.ProbeError, "invalid_evidence_ref"): MODULE.build_plan(evidence_refs=[evidence_ref], **common) def test_execute_uses_current_shapes_and_verifies_idempotent_receipt(self) -> None: plan = sample_plan() state = FakeState(plan) with fake_api(state) as control_url: result = MODULE.execute_plan( plan=plan, control_plane_base_url=control_url, preview_base_url="http://100.116.83.60:8088/api", internal_token=INTERNAL_TOKEN, admin_email=ADMIN_EMAIL, timeout=5, ) self.assertEqual(MODULE.RESULT_FIELDS, set(result)) self.assertEqual("executed", result["lifecycle_status"]) self.assertTrue(result["binding_verified"]) self.assertTrue(result["release_gate_idempotent_replay"]) self.assertTrue(result["approval_idempotent_replay"]) self.assertFalse(result["contains_secrets"]) self.assertFalse(result["contains_pii"]) release_requests = [ item for item in state.requests if item["path"] == MODULE.RELEASE_GATE_PATH ] approval_requests = [ item for item in state.requests if item["path"] == MODULE.APPROVAL_PATH ] self.assertEqual(2, len(release_requests)) self.assertEqual(2, len(approval_requests)) self.assertTrue( all( item["body"] == plan["release_gate"]["payload"] for item in release_requests ) ) self.assertTrue( all(item["internal_token"] == INTERNAL_TOKEN for item in release_requests) ) self.assertTrue( all( item["body"] == plan["approval"]["payload"] for item in approval_requests ) ) self.assertTrue( all(item["cookie"] == f"sid={COOKIE_VALUE}" for item in approval_requests) ) def test_equal_control_and_preview_origin_is_rejected_before_http(self) -> None: with self.assertRaisesRegex( MODULE.ProbeError, "main_preview_self_rollback_forbidden" ): MODULE.execute_plan( plan=sample_plan(), control_plane_base_url="http://127.0.0.1:8010/api", preview_base_url="http://127.0.0.1:8010", internal_token=INTERNAL_TOKEN, admin_email=ADMIN_EMAIL, timeout=5, ) def test_read_model_artifact_mismatch_and_failed_receipt_fail_closed(self) -> None: for state, expected in ( (FakeState(sample_plan("mismatch"), mismatch_artifact=True), "artifact"), (FakeState(sample_plan("failed"), lifecycle_status="failed"), "receipt"), ): with self.subTest(expected=expected): with fake_api(state) as control_url: with self.assertRaisesRegex(MODULE.ProbeError, expected): MODULE.execute_plan( plan=state.plan, control_plane_base_url=control_url, preview_base_url="http://127.0.0.1:9", internal_token=INTERNAL_TOKEN, admin_email=ADMIN_EMAIL, timeout=5, ) def test_401_403_409_never_expose_response_body_or_secrets(self) -> None: for status in (401, 403, 409): state = FakeState( sample_plan(f"http-{status}"), release_failure_status=status ) with self.subTest(status=status): with fake_api(state) as control_url: with self.assertRaisesRegex( MODULE.ProbeError, f"http_{status}_post" ) as raised: MODULE.execute_plan( plan=state.plan, control_plane_base_url=control_url, preview_base_url="http://127.0.0.1:9", internal_token=INTERNAL_TOKEN, admin_email=ADMIN_EMAIL, timeout=5, ) message = str(raised.exception) self.assertNotIn(INTERNAL_TOKEN, message) self.assertNotIn(ADMIN_EMAIL, message) self.assertNotIn(COOKIE_VALUE, message) self.assertNotIn("sensitive", message) def test_cli_output_contains_no_token_cookie_or_admin_identity(self) -> None: plan = sample_plan("cli-output") state = FakeState(plan) with TemporaryDirectory(prefix="g8-probe-test-") as temp: plan_path = Path(temp) / "plan.json" MODULE._write_json(plan_path, plan) with fake_api(state) as control_url: stdout = io.StringIO() stderr = io.StringIO() with mock.patch.dict( os.environ, { "PROBE_TEST_INTERNAL_TOKEN": INTERNAL_TOKEN, "PROBE_TEST_ADMIN_EMAIL": ADMIN_EMAIL, }, clear=False, ): with ( contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr), ): exit_code = MODULE.main( [ "execute", "--plan", str(plan_path), "--control-plane-base-url", control_url, "--preview-base-url", "http://127.0.0.1:9", "--internal-token-env", "PROBE_TEST_INTERNAL_TOKEN", "--admin-email-env", "PROBE_TEST_ADMIN_EMAIL", ] ) self.assertEqual(0, exit_code) combined = stdout.getvalue() + stderr.getvalue() self.assertNotIn(INTERNAL_TOKEN, combined) self.assertNotIn(ADMIN_EMAIL, combined) self.assertNotIn(COOKIE_VALUE, combined) self.assertEqual(MODULE.RESULT_FIELDS, set(json.loads(stdout.getvalue()))) def test_help_states_separate_control_plane_prerequisite(self) -> None: help_text = MODULE._build_parser().format_help() self.assertIn("main preview API must not self-authorize rollback", help_text) self.assertIn("separately deployed control-plane API", help_text) if __name__ == "__main__": unittest.main()