diff --git a/Makefile b/Makefile index f2abcfb97..90ee3b878 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,13 @@ e2e-run: ## Run chart-8 E2E for an explicit lab target (requires a mode-0600 LAB @test "$(TARGET)" = "lab" || { echo "TARGET=lab is required"; exit 2; } @test -n "$(LAB_PROFILE)" -a -f "$(LAB_PROFILE)" || { echo "LAB_PROFILE must name a mode-0600 explicit target/credentials-path profile"; exit 2; } @test -x "$(E2E_PYTHON)" || { echo "E2E_PYTHON must name an executable Python with backend dependencies (default: $(BACKEND)/.venv/bin/python)"; exit 2; } - @cd $(BACKEND) && "$(E2E_PYTHON)" ../scripts/e2e-scripts/native_chart8_prepare.py --profile "$(LAB_PROFILE)" $(if $(filter 1,$(E2E_VALIDATE_ONLY)),--validate-only,--run-lifecycle) + @case "$(E2E_MODE)" in \ + resume-published) mode=--resume-published ;; \ + fresh|'') mode=--run-lifecycle ;; \ + *) echo "E2E_MODE must be fresh or resume-published"; exit 2 ;; \ + esac; \ + if [ "$(E2E_VALIDATE_ONLY)" = "1" ]; then mode=--validate-only; fi; \ + cd $(BACKEND) && "$(E2E_PYTHON)" ../scripts/e2e-scripts/native_chart8_prepare.py --profile "$(LAB_PROFILE)" "$$mode" lab-reset: ## Prepare/archive or explicitly apply the exact approved chart-8 row reset (PHASE=prepare|apply|verify) @test -n "$(SCOPE)" || { echo "SCOPE must name the mode-0600 exact lab reset scope JSON"; exit 2; } diff --git a/scripts/e2e-scripts/native_chart8_prepare.py b/scripts/e2e-scripts/native_chart8_prepare.py index 5bbbeaf89..6f66ed155 100644 --- a/scripts/e2e-scripts/native_chart8_prepare.py +++ b/scripts/e2e-scripts/native_chart8_prepare.py @@ -161,6 +161,7 @@ def validate_target_descriptor(target: dict, seed: dict, release: dict) -> None: raise Blocked("NATIVE_CAPTURE_SCENARIO_CASES_REQUIRED") elif capture_scenario.get("mode") == "existing_mcp_revision": validate_existing_mcp_revision(capture_scenario) + validate_published_resume_target(target) else: raise Blocked("NATIVE_CAPTURE_SCENARIO_MODE_UNSUPPORTED") numeric = target.get("numeric_verification") @@ -286,6 +287,20 @@ def validate_existing_mcp_revision(selection: dict) -> None: raise Blocked("NATIVE_CAPTURE_SCENARIO_REVISION_REQUIRED") +def validate_published_resume_target(target: dict) -> None: + resume = target.get("published_resume") if isinstance(target, dict) else None + selection = target.get("catalog_selection") if isinstance(target, dict) else None + if (target.get("catalog_mode") != "exact_revision" + or not isinstance(selection, dict) or selection.get("kind") != "revision" + or not _is_uuid(selection.get("catalog_id")) + or not _is_uuid(selection.get("catalog_revision_id")) + or not isinstance(resume, dict) + or set(resume) != {"capture_artifact_id", "publication_operation_id"} + or not _is_uuid(resume.get("capture_artifact_id")) + or not _is_uuid(resume.get("publication_operation_id"))): + raise Blocked("NATIVE_PUBLISHED_RESUME_INPUT_INVALID") + + def _is_uuid(value: object) -> bool: if type(value) is not str: return False @@ -489,8 +504,6 @@ def register_mcp_owned_scenario(driver: Driver, dashboard_id: int, model: dict, def select_m01_profile_coordinate(profile: dict, target: dict, dashboard_id: int, model: dict) -> dict: if not isinstance(profile, dict) or not isinstance(profile.get("coordinates"), list): raise Blocked("NATIVE_M01_PROFILE_COORDINATES_UNAVAILABLE") - selection = target["capture_scenario"] - metric_name = selection["metric_name"] charts = [chart for chart in model.get("charts", []) if chart.get("chart_uuid") == target["chart_uuid"]] if len(charts) != 1: @@ -501,15 +514,103 @@ def select_m01_profile_coordinate(profile: dict, target: dict, dashboard_id: int if isinstance(item, dict) and item.get("chart_id") == expected_chart_id and item.get("dataset_id") == expected_dataset_id - and item.get("metric_name") == metric_name + and item.get("metric_name") is None and item.get("reference_coordinate") == { - "kind": "metric", "chart_uuid": target["chart_uuid"], "metric": metric_name, + "kind": "chart", "chart_uuid": target["chart_uuid"], "metric": None, }] if type(expected_chart_id) is not int or len(coordinates) != 1: raise Blocked("NATIVE_M01_PROFILE_COORDINATE_AMBIGUOUS") return coordinates[0] +def read_published_capture(driver: Driver, target: dict, model: dict) -> dict: + """Resume M01 only from an explicitly named, publicly read-back publication.""" + resume = target.get("published_resume") + selection = target.get("catalog_selection") + expected_capture = resume.get("capture_artifact_id") if isinstance(resume, dict) else None + operation_id = resume.get("publication_operation_id") if isinstance(resume, dict) else None + if (not isinstance(resume, dict) or set(resume) != {"capture_artifact_id", "publication_operation_id"} + or not _is_uuid(expected_capture) or not _is_uuid(operation_id) + or target.get("catalog_mode") != "exact_revision" + or not isinstance(selection, dict) or selection.get("kind") != "revision"): + raise Blocked("NATIVE_PUBLISHED_RESUME_INPUT_INVALID") + + operation = driver.call("published-resume-operation-readback", "GET", + f"/api/catalog-publications/{operation_id}") + revision_response = driver.call("published-resume-revision-readback", "GET", + "/api/dashboard-testing/baselines?" + urlencode({ + "catalog_id": selection["catalog_id"], + "catalog_revision_id": selection["catalog_revision_id"], + })) + revision = revision_response.get("revision", {}) + if (operation.get("operation_id") != operation_id or operation.get("state") != "published" + or operation.get("catalog_revision_id") != selection["catalog_revision_id"] + or operation.get("catalog_digest") != revision.get("catalog_digest") + or not operation.get("published_receipt") + or operation.get("repository") != target["catalog_repository"] + or operation.get("branch") != target["catalog_branch"] + or revision.get("catalog_id") != selection["catalog_id"] + or revision.get("catalog_revision_id") != selection["catalog_revision_id"] + or revision.get("release_id") != target["release_identity"].get("id")): + raise Blocked("NATIVE_PUBLISHED_RESUME_IDENTITY_MISMATCH") + + wrappers = revision.get("entry_revisions") + matches = [wrapper for wrapper in wrappers if isinstance(wrapper, dict) + and wrapper.get("capture_artifact_id") == expected_capture + and isinstance(wrapper.get("entry"), dict)] + if len(matches) != 1: + raise Blocked("NATIVE_PUBLISHED_RESUME_CAPTURE_MISSING") + wrapper = matches[0] + entry = wrapper["entry"] + expected = entry.get("expected") or {} + source = wrapper.get("reference_source") or {} + native = entry.get("native_result") or {} + authority = native.get("authority_record") or {} + descriptor = authority.get("descriptor") or {} + reference = descriptor.get("reference_coordinate") or {} + chart = next((item for item in model.get("charts", []) + if item.get("chart_uuid") == target["chart_uuid"]), None) + if (wrapper.get("status") != "approved" or entry.get("status") != "approved" + or expected.get("kind") != "chart_response" + or entry.get("result_key", "missing") is not None + or entry.get("chart_id") != descriptor.get("chart_id") + or entry.get("dataset_id") != descriptor.get("dataset_id") + or not isinstance(chart, dict) + or descriptor.get("chart_id") != chart.get("chart_id") + or descriptor.get("dataset_id") != chart.get("dataset_id") + or descriptor.get("kind") != "chart_response" + or authority.get("capture_artifact_id") != expected_capture + or descriptor.get("selected_metric", "missing") is not None + or reference != {"kind": "chart", "chart_uuid": target["chart_uuid"], "metric": None} + or source.get("environment_id") != target["target_environment_id"] + or source.get("dashboard_id") != target["resolved_target_dashboard_id"] + or source.get("filters_hash") != target["expected_filters_hash"] + or source.get("query_model_fingerprint") != model.get("query_model_fingerprint") + or entry.get("release_version") != target["release_identity"].get("version") + or entry.get("release_commit_hash") != target["release_identity"].get("commit_hash")): + raise Blocked("NATIVE_PUBLISHED_RESUME_CAPTURE_IDENTITY_MISMATCH") + + driver.ids.update(capture_artifact_id=expected_capture, + published_catalog_revision_id=selection["catalog_revision_id"], + publication_operation_id=operation_id) + driver.record("published-capture-exact-resume", "PASS", + catalog_id=selection["catalog_id"], + catalog_revision_id=selection["catalog_revision_id"], + operation_id=operation_id, + capture_count=1, publication_state="published") + return {"capture": {"release_id": target["release_identity"]["id"], + "capture_artifact_ids": [expected_capture]}, + "catalog_revision": revision, + "publication": operation, + "catalog_selection": selection, + "coordinate": {"chart_id": descriptor["chart_id"], + "dataset_id": descriptor["dataset_id"], + "chart_uuid": target["chart_uuid"], + "dataset_uuid": target["chart_contract"]["dataset_uuid"]}, + "query_model_fingerprint": source["query_model_fingerprint"], + "filters_hash": source["filters_hash"]} + + def register_m01_capture_scenario(driver: Driver, dashboard_id: int, model: dict, environment_id: str, target: dict) -> dict: """Resolve the published chart-response source through the owner-scoped M01 MCP workflow.""" @@ -1330,8 +1431,11 @@ def main() -> int: parser.add_argument("--output", type=Path) parser.add_argument("--validate-only", action="store_true", help="Validate explicit inputs without making API calls") - parser.add_argument("--run-lifecycle", action="store_true", - help="Run chart-8 capture, publication, pin, and three-run acceptance") + lifecycle = parser.add_mutually_exclusive_group() + lifecycle.add_argument("--run-lifecycle", action="store_true", + help="Run chart-8 capture, publication, pin, and three-run acceptance") + lifecycle.add_argument("--resume-published", action="store_true", + help="Resume only M01 and numeric acceptance from one exact published capture") args = parser.parse_args() try: env_file, seed_path, release_path, target_path, output = explicit_inputs(args) @@ -1414,8 +1518,12 @@ def main() -> int: "target_dashboard_uuid": target["target_dashboard_uuid"], "chart_uuid": target["chart_uuid"]}}, indent=2, ensure_ascii=False) + "\n") - if args.run_lifecycle: - captured = capture_approve_publish(driver, target, scenario, catalog, model) + if args.run_lifecycle or args.resume_published: + if args.resume_published: + captured = read_published_capture(driver, target, model) + pin_target = target + else: + captured = capture_approve_publish(driver, target, scenario, catalog, model) driver.ids.update( capture_artifact_id=(captured["capture"].get("capture_artifact_ids") or [None])[0], published_catalog_revision_id=captured["catalog_revision"].get("catalog_revision_id"), @@ -1426,7 +1534,8 @@ def main() -> int: dataset_id=captured["coordinate"]["dataset_id"], query_model_fingerprint=captured["query_model_fingerprint"], filters_hash=captured["filters_hash"], capture_count=1, publication_state="published") - pin_target = target_for_published_revision(target, captured) + if not args.resume_published: + pin_target = target_for_published_revision(target, captured) pin_scenario = register_m01_capture_scenario(driver, dashboard_id, model, target["target_environment_id"], pin_target) driver.ids.update(scenario_id=pin_scenario["scenario_id"], @@ -1467,7 +1576,7 @@ def main() -> int: driver.flush() driver.client.close() stopped = any(step["status"] in {"BLOCKED", "NEEDS_REVIEW"} for step in driver.steps) - if args.run_lifecycle: + if args.run_lifecycle or args.resume_published: if not stopped and acceptance_completed: driver.flush() return 0 diff --git a/scripts/e2e-scripts/test_native_chart8_prepare.py b/scripts/e2e-scripts/test_native_chart8_prepare.py index a12d22667..b29e86cee 100644 --- a/scripts/e2e-scripts/test_native_chart8_prepare.py +++ b/scripts/e2e-scripts/test_native_chart8_prepare.py @@ -14,8 +14,10 @@ from common import Blocked # noqa: E402 from native_chart8_prepare import ( # noqa: E402 bind_seeded_catalog_selection, target_for_published_revision, + select_m01_profile_coordinate, validate_lab_bootstrap_mode, validate_existing_mcp_revision, + validate_published_resume_target, ) @@ -42,6 +44,78 @@ def test_existing_mcp_revision_identity_requires_exact_uuid_values() -> None: validate_existing_mcp_revision({"scenario_id": "first", "revision_id": "latest"}) +def test_published_resume_requires_explicit_exact_revision_and_producer_ids() -> None: + valid = { + "catalog_mode": "exact_revision", + "catalog_selection": { + "kind": "revision", + "catalog_id": "67ec18a8-3d66-443e-8324-cdb8476d8f16", + "catalog_revision_id": "9ffd1709-6aa8-46ad-b997-447338116df1", + }, + "published_resume": { + "capture_artifact_id": "306a684b-f8dd-4bec-b90d-48ff75ac4f5d", + "publication_operation_id": "58bd49d1-7845-405b-a43c-bcc44f7fdcf4", + }, + } + validate_published_resume_target(valid) + invalid = {**valid, "catalog_selection": {**valid["catalog_selection"], "kind": "current"}} + with pytest.raises(Blocked, match="NATIVE_PUBLISHED_RESUME_INPUT_INVALID"): + validate_published_resume_target(invalid) + + +def _m01_profile_coordinate(*, chart_id=8, dataset_id=4, kind="chart", chart_uuid="chart-uuid", + metric=None) -> dict: + return { + "coordinate_id": f"coordinate-{chart_id}-{dataset_id}-{kind}-{metric}", + "chart_id": chart_id, + "dataset_id": dataset_id, + "metric_name": metric, + "reference_coordinate": {"kind": kind, "chart_uuid": chart_uuid, "metric": metric}, + } + + +def _m01_profile(coordinates: list[dict]) -> tuple[dict, dict]: + target = {"chart_uuid": "chart-uuid", "capture_scenario": {"metric_name": "selected_value_cents"}} + return {"coordinates": coordinates}, target + + +def test_m01_selects_exact_published_whole_chart_coordinate() -> None: + metric = _m01_profile_coordinate(kind="metric", metric="selected_value_cents") + chart = _m01_profile_coordinate() + profile, target = _m01_profile([metric, chart]) + + selected = select_m01_profile_coordinate(profile, target, 2, { + "charts": [{"chart_id": 8, "dataset_id": 4, "chart_uuid": "chart-uuid"}], + }) + + assert selected == chart + + +@pytest.mark.parametrize("coordinate", [ + _m01_profile_coordinate(chart_id=9), + _m01_profile_coordinate(dataset_id=5), + _m01_profile_coordinate(kind="metric", metric="selected_value_cents"), + _m01_profile_coordinate(chart_uuid="other-chart"), +]) +def test_m01_refuses_wrong_published_chart_coordinate(coordinate: dict) -> None: + profile, target = _m01_profile([coordinate]) + + with pytest.raises(Blocked, match="NATIVE_M01_PROFILE_COORDINATE_AMBIGUOUS"): + select_m01_profile_coordinate(profile, target, 2, { + "charts": [{"chart_id": 8, "dataset_id": 4, "chart_uuid": "chart-uuid"}], + }) + + +def test_m01_refuses_ambiguous_whole_chart_coordinates() -> None: + chart = _m01_profile_coordinate() + profile, target = _m01_profile([chart, chart.copy()]) + + with pytest.raises(Blocked, match="NATIVE_M01_PROFILE_COORDINATE_AMBIGUOUS"): + select_m01_profile_coordinate(profile, target, 2, { + "charts": [{"chart_id": 8, "dataset_id": 4, "chart_uuid": "chart-uuid"}], + }) + + def test_seeded_catalog_selection_is_bound_from_exact_official_result() -> None: target = {"catalog_mode": "seed_empty", "source_dashboard_id": 2} catalog = {"source_dashboard_id": 2, "selection": {