Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
525 lines
27 KiB
Python
Executable File
525 lines
27 KiB
Python
Executable File
"""Fixture-only devkit coverage of the real durable release API/store boundary."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from copy import deepcopy
|
|
import json
|
|
from pathlib import Path
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
for path in (ROOT / "tools/devkit", ROOT / "tools/release"):
|
|
if str(path) not in sys.path:
|
|
sys.path.insert(0, str(path))
|
|
|
|
from govoplan_devkit import release # noqa: E402
|
|
from govoplan_release.release_execution import ReleaseExecutionAmbiguous, ReleaseExecutionBlocked # noqa: E402
|
|
from govoplan_release.release_run import ReleaseRunCorrupt, ReleaseRunStore # noqa: E402
|
|
from govoplan_release.candidate_artifact import ( # noqa: E402
|
|
candidate_output_path, harden_private_candidate_tree, issue_candidate_receipt,
|
|
)
|
|
from server import app as api # noqa: E402
|
|
|
|
|
|
def receipt(repo="govoplan-core", *, target_tag="v1.2.3"):
|
|
return {
|
|
"kind": "repository_state", "repo": repo, "head": "a" * 40,
|
|
"branch": "main", "remote": "origin", "remote_sha256": "b" * 64,
|
|
"worktree_clean": True, "target_tag": target_tag, "tag_object": None,
|
|
}
|
|
|
|
|
|
def plan(*, catalog=False):
|
|
steps = [
|
|
("core:preflight", "govoplan-core", False),
|
|
("core:tag", "govoplan-core", True),
|
|
] if not catalog else [
|
|
("catalog:selective-generator", None, True),
|
|
("catalog:validate-sign-publish", None, True),
|
|
]
|
|
return {
|
|
"generated_at": "2026-09-08T00:00:00Z", "target_channel": "stable",
|
|
"status": "attention", "units": [{"repo": "govoplan-core", "target_version": "1.2.3"}],
|
|
"compatibility": [], "gate_findings": [], "recommended_action": {},
|
|
"source_preflight_ready": True, "notes": [],
|
|
"dry_run_steps": [
|
|
{"id": identity, "repo": repo, "mutating": mutating, "title": identity,
|
|
"detail": "Fixture-only operation.", "command": "fixture never executed",
|
|
"cwd": "/fixture", "status": "planned"}
|
|
for identity, repo, mutating in steps
|
|
],
|
|
}
|
|
|
|
|
|
def namespace(workspace, state, *arguments):
|
|
parser = argparse.ArgumentParser()
|
|
parser.set_defaults(workspace_root=workspace, state_dir=state, format="json", project=None)
|
|
release.register(parser.add_subparsers(required=True))
|
|
return parser.parse_args(["release", *arguments])
|
|
|
|
|
|
@pytest.fixture
|
|
def environment(tmp_path, monkeypatch):
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
state = tmp_path / "private-state"
|
|
plans = [plan()]
|
|
dashboard = Mock(return_value={"summary": {"status": "ready"}, "repositories": []})
|
|
planner = Mock(side_effect=lambda *args, **kwargs: deepcopy(plans[0]))
|
|
execute = Mock(return_value=({"status": "inspected"}, receipt()))
|
|
|
|
def bind(**kwargs):
|
|
result = kwargs["plan"]
|
|
for step in result["dry_run_steps"]:
|
|
if step.get("repo"):
|
|
step["source_binding"] = receipt(step["repo"])
|
|
elif step["id"] == "catalog:validate-sign-publish":
|
|
step["source_binding"] = receipt("addideas-govoplan-website", target_tag="")
|
|
return result
|
|
|
|
monkeypatch.setattr(api, "build_dashboard", dashboard)
|
|
monkeypatch.setattr(api, "build_selective_release_plan", planner)
|
|
monkeypatch.setattr(api, "require_trusted_release_runtime", Mock())
|
|
monkeypatch.setattr(api, "verify_release_runtime_binding", Mock())
|
|
monkeypatch.setattr(api, "bind_plan_source_states", bind)
|
|
monkeypatch.setattr(api, "verify_repository_preflight_binding", Mock(return_value=receipt()))
|
|
monkeypatch.setattr(api, "verify_repository_step_precondition", Mock(return_value=receipt()))
|
|
monkeypatch.setattr(api, "execute_repository_step", execute)
|
|
monkeypatch.setattr(api, "default_signing_keys", lambda: ())
|
|
|
|
def forbidden(*args, **kwargs):
|
|
raise AssertionError("Fixture test attempted a real subprocess or network connection")
|
|
monkeypatch.setattr(subprocess, "run", forbidden)
|
|
monkeypatch.setattr(socket, "create_connection", forbidden)
|
|
|
|
def call(*args, state_dir=state, workspace_root=workspace):
|
|
values = namespace(workspace_root, state_dir, *args)
|
|
return values.handler(values)
|
|
|
|
def create(request="devkit-create-request-0001"):
|
|
result = call("create", "--repo-version", "govoplan-core=1.2.3", "--request-id", request, "--apply")
|
|
assert result["_exit_code"] == 0, result
|
|
return result
|
|
|
|
return SimpleNamespace(
|
|
workspace=workspace, state=state, plans=plans, dashboard=dashboard,
|
|
planner=planner, executor=execute, call=call, create=create,
|
|
)
|
|
|
|
|
|
def test_registration_does_not_import_heavy_dependencies():
|
|
script = """
|
|
import argparse, builtins, sys
|
|
sys.path.insert(0, sys.argv[1])
|
|
original = builtins.__import__
|
|
def guarded(name, *args, **kwargs):
|
|
if name.split('.')[0] in {'httpx', 'fastapi', 'govoplan_core', 'govoplan_release'}:
|
|
raise AssertionError('Heavy import during help registration: ' + name)
|
|
return original(name, *args, **kwargs)
|
|
builtins.__import__ = guarded
|
|
from govoplan_devkit.release import register
|
|
parser = argparse.ArgumentParser()
|
|
register(parser.add_subparsers())
|
|
assert 'release' in parser.format_help()
|
|
"""
|
|
result = subprocess.run([sys.executable, "-c", script, str(ROOT / "tools/devkit")], capture_output=True, text=True, check=False)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
def test_plan_is_selective_offline_and_does_not_create_run(environment):
|
|
result = environment.call("plan", "--repo-version", "govoplan-core=1.2.3")
|
|
assert result["_exit_code"] == 0
|
|
assert result["dry_run"] is True
|
|
assert environment.planner.call_args.kwargs["selected_repos"] == ("govoplan-core",)
|
|
arguments = environment.dashboard.call_args.kwargs
|
|
assert arguments["online"] is False
|
|
assert arguments["check_remote_tags"] is False
|
|
assert arguments["check_public_catalog"] is False
|
|
assert arguments["include_migrations"] is False
|
|
assert not list(environment.state.rglob("rr-*.json"))
|
|
assert "release-console/workspace-" in result["state_location"]
|
|
environment.executor.assert_not_called()
|
|
|
|
|
|
def test_status_reports_blocked_exit_and_explicit_check_flags(environment):
|
|
environment.dashboard.return_value = {"summary": {"status": "blocked"}}
|
|
result = environment.call("status", "--online", "--include-migrations", "--include-website")
|
|
assert result["_exit_code"] == 1
|
|
assert environment.dashboard.call_args.kwargs["check_remote_tags"] is True
|
|
assert environment.dashboard.call_args.kwargs["check_public_catalog"] is True
|
|
assert environment.dashboard.call_args.kwargs["include_migrations"] is True
|
|
|
|
|
|
def test_plan_summary_exposes_attention_readiness_gates_and_next_action(environment):
|
|
fixture_plan = environment.plans[0]
|
|
fixture_plan["source_preflight_ready"] = False
|
|
fixture_plan["units"][0]["status"] = "attention"
|
|
fixture_plan["gate_findings"] = [{"code": "worktree_dirty", "repo": "govoplan-core", "message": "Uncommitted source changes need review."}]
|
|
fixture_plan["recommended_action"] = {"id": "prepare_changes", "title": "Prepare source changes", "remediation": "Review and commit the selected changes before preparing the release."}
|
|
result = environment.call("plan", "--repo-version", "govoplan-core=1.2.3")
|
|
summary = "\n".join(result["summary"])
|
|
assert "Release plan: attention." in summary
|
|
assert "Source preflight ready: false." in summary
|
|
assert "govoplan-core: attention; target 1.2.3." in summary
|
|
assert "Gate worktree_dirty (govoplan-core)" in summary
|
|
assert "Next: prepare_changes" in summary
|
|
assert "no run was created" in summary
|
|
assert "fixture never executed" not in summary
|
|
|
|
|
|
def test_summary_is_bounded_and_redacts_display_only_fields(monkeypatch):
|
|
monkeypatch.setenv("DEVKIT_FIXTURE_SECRET", "fixture-value-not-for-display")
|
|
fixture_plan = plan()
|
|
fixture_plan["gate_findings"] = [{"code": "fixture", "message": "fixture-value-not-for-display " + "x" * 1000}] * 20
|
|
fixture_plan["units"] = [{"repo": f"repo-{index}", "target_version": "1.2.3", "status": "ready"} for index in range(50)]
|
|
summary = "\n".join(release._summary_lines("plan", fixture_plan))
|
|
assert "38 more selected repositories" in summary
|
|
assert "16 more gate findings" in summary
|
|
assert "fixture-value-not-for-display" not in summary
|
|
assert "[redacted]" in summary
|
|
assert len(summary) < 3000
|
|
|
|
|
|
def test_run_and_preview_summary_exposes_steps_and_next_action(environment):
|
|
run = environment.create()["result"]
|
|
shown = environment.call("show", run["run_id"])
|
|
summary = "\n".join(shown["summary"])
|
|
assert "Steps: 2 pending." in summary
|
|
assert "Step core:preflight: pending." in summary
|
|
assert "Next: execute_step [core:preflight]" in summary
|
|
preview = environment.call("preview", run["run_id"], "core:tag")
|
|
assert "Release preview: pending." in preview["summary"]
|
|
assert any("Complete core:preflight first." in line for line in preview["summary"])
|
|
|
|
|
|
def test_status_summary_exposes_repository_counts(environment):
|
|
environment.dashboard.return_value = {"summary": {"status": "attention", "repository_count": 77, "dirty_count": 42, "ahead_count": 1, "behind_count": 0, "error_count": 0}}
|
|
result = environment.call("status")
|
|
assert "Release status: attention." in result["summary"]
|
|
assert any("77 repositories, 42 dirty, 1 ahead, 0 behind, 0 errors" in line for line in result["summary"])
|
|
|
|
|
|
def test_display_redaction_cannot_change_semantic_failure_exit(environment, monkeypatch):
|
|
monkeypatch.setenv("DEVKIT_FIXTURE_SECRET", "blocked")
|
|
environment.dashboard.return_value = {"summary": {"status": "blocked"}}
|
|
result = environment.call("status")
|
|
assert result["_exit_code"] == 1
|
|
assert "Release status: [redacted]." in result["summary"]
|
|
|
|
|
|
def test_create_defaults_to_preview_without_run_record(environment):
|
|
result = environment.call("create", "--repo", "govoplan-core", "--target-version", "1.2.3", "--request-id", "preview-create-request-0001")
|
|
assert result["_exit_code"] == 0 and result["dry_run"]
|
|
assert not list(environment.state.rglob("rr-*.json"))
|
|
environment.executor.assert_not_called()
|
|
|
|
|
|
def test_create_show_and_same_id_replay_use_durable_service(environment):
|
|
created = environment.create()
|
|
run_id = created["result"]["run_id"]
|
|
repeated = environment.create()
|
|
assert repeated["result"]["run_id"] == run_id
|
|
environment.planner.assert_called_once()
|
|
shown = environment.call("show", run_id)
|
|
assert shown["result"]["immutable"] == created["result"]["immutable"]
|
|
assert shown["result"]["state"]["steps"][0]["executor"]["confirmation"] == ""
|
|
assert len(list(environment.state.rglob("rr-*.json"))) == 1
|
|
|
|
|
|
def test_create_replay_rejects_changed_inputs(environment):
|
|
environment.create()
|
|
result = environment.call("create", "--repo-version", "govoplan-core=1.2.4", "--request-id", "devkit-create-request-0001", "--apply")
|
|
assert result["http_status"] == 409
|
|
environment.planner.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize("arguments", [
|
|
("plan",),
|
|
("create", "--repo", "govoplan-core", "--request-id", "missing-version-request"),
|
|
("plan", "--repo-version", "govoplan-core=1.2.3", "--repo-version", "govoplan-core=1.2.4"),
|
|
("plan", "--repo-version", "govoplan-core=not-a-version"),
|
|
])
|
|
def test_invalid_selection_is_rejected_before_service_collection(environment, arguments):
|
|
result = environment.call(*arguments)
|
|
assert result["_exit_code"] == 2
|
|
environment.dashboard.assert_not_called()
|
|
environment.executor.assert_not_called()
|
|
|
|
|
|
def test_runtime_trust_guard_cannot_be_bypassed_by_cli(environment, monkeypatch):
|
|
monkeypatch.setattr(api, "require_trusted_release_runtime", Mock(side_effect=ReleaseExecutionBlocked("Fixture untrusted runtime")))
|
|
result = environment.call("create", "--repo-version", "govoplan-core=1.2.3", "--request-id", "trust-create-request-0001", "--apply")
|
|
assert result["http_status"] == 409
|
|
assert not list(environment.state.rglob("rr-*.json"))
|
|
environment.planner.assert_not_called()
|
|
|
|
|
|
def test_dry_execute_and_generic_preview_never_claim_or_execute(environment):
|
|
run = environment.create()["result"]
|
|
dry = environment.call("execute", run["run_id"], "core:preflight", "--request-id", "dry-execute-request-0001")
|
|
preview = environment.call("preview", run["run_id"], "core:tag")
|
|
assert dry["dry_run"] and preview["dry_run"]
|
|
assert preview["result"]["state_step"]["executor"]["confirmation"] == "TAG"
|
|
assert preview["result"]["state_step"]["available"] is False
|
|
environment.executor.assert_not_called()
|
|
assert environment.call("show", run["run_id"])["result"]["state"]["steps"][0]["attempt_count"] == 0
|
|
|
|
|
|
def test_prerequisite_and_confirmation_guards_remain_enforced(environment):
|
|
run_id = environment.create()["result"]["run_id"]
|
|
blocked = environment.call("execute", run_id, "core:tag", "--request-id", "ordered-tag-request-0001", "--confirm", "TAG", "--apply")
|
|
assert blocked["http_status"] == 409
|
|
environment.executor.assert_not_called()
|
|
assert environment.call("execute", run_id, "core:preflight", "--request-id", "preflight-request-0001", "--apply")["_exit_code"] == 0
|
|
missing = environment.call("execute", run_id, "core:tag", "--request-id", "confirmed-tag-request-0001", "--apply")
|
|
assert missing["http_status"] == 409
|
|
assert environment.executor.call_count == 1
|
|
|
|
|
|
def test_execute_same_attempt_replays_without_repeating_effect(environment):
|
|
run_id = environment.create()["result"]["run_id"]
|
|
arguments = ("execute", run_id, "core:preflight", "--request-id", "exact-attempt-request-0001", "--apply")
|
|
first = environment.call(*arguments)
|
|
second = environment.call(*arguments)
|
|
assert first["_exit_code"] == second["_exit_code"] == 0
|
|
assert second["result"]["execution_result"]["status"] == "replayed"
|
|
environment.executor.assert_called_once()
|
|
assert environment.executor.call_args.kwargs["remote"] == "origin"
|
|
|
|
|
|
def test_lost_finish_write_is_interrupted_without_reexecuting_effect(environment, monkeypatch):
|
|
run_id = environment.create()["result"]["run_id"]
|
|
finish = ReleaseRunStore.finish_step
|
|
monkeypatch.setattr(ReleaseRunStore, "finish_step", Mock(side_effect=ReleaseRunCorrupt("Fixture durable write failure")))
|
|
arguments = ("execute", run_id, "core:preflight", "--request-id", "lost-finish-request-0001", "--apply")
|
|
interrupted = environment.call(*arguments)
|
|
assert interrupted["http_status"] == 409
|
|
monkeypatch.setattr(ReleaseRunStore, "finish_step", finish)
|
|
assert environment.call(*arguments)["http_status"] == 409
|
|
environment.executor.assert_called_once()
|
|
shown = environment.call("show", run_id)["result"]
|
|
assert shown["state"]["steps"][0]["state"] == "interrupted"
|
|
|
|
|
|
def test_source_guard_failure_does_not_call_executor(environment, monkeypatch):
|
|
run_id = environment.create()["result"]["run_id"]
|
|
monkeypatch.setattr(api, "verify_repository_preflight_binding", Mock(side_effect=ReleaseExecutionBlocked("Frozen HEAD/remote changed")))
|
|
result = environment.call("execute", run_id, "core:preflight", "--request-id", "changed-source-request-0001", "--apply")
|
|
assert result["_exit_code"] == 1
|
|
environment.executor.assert_not_called()
|
|
|
|
|
|
def test_interrupted_write_requires_reconciliation_not_retry(environment, monkeypatch):
|
|
environment.plans[0]["dry_run_steps"] = [environment.plans[0]["dry_run_steps"][1]]
|
|
environment.executor.side_effect = ReleaseExecutionAmbiguous("Fixture remote outcome uncertain")
|
|
run_id = environment.create()["result"]["run_id"]
|
|
execute = ("execute", run_id, "core:tag", "--request-id", "uncertain-tag-request-0001", "--confirm", "TAG", "--apply")
|
|
uncertain = environment.call(*execute)
|
|
assert uncertain["http_status"] == 409
|
|
assert environment.call(*execute)["http_status"] == 409
|
|
retry = environment.call("retry", run_id, "core:tag", "--request-id", "unsafe-retry-request-0001", "--apply")
|
|
assert retry["http_status"] == 409
|
|
environment.executor.assert_called_once()
|
|
invalid = environment.call("reconcile", run_id, "core:tag", "--request-id", "bad-reconcile-request-0001", "--outcome", "effect_absent", "--apply")
|
|
assert invalid["http_status"] == 409
|
|
reconciled = environment.call("reconcile", run_id, "core:tag", "--request-id", "reconcile-absent-request-0001", "--outcome", "effect_absent", "--confirm", "RECONCILE", "--apply")
|
|
assert reconciled["_exit_code"] == 0
|
|
assert reconciled["result"]["state"]["steps"][0]["state"] == "pending"
|
|
|
|
|
|
def test_effect_succeeded_reconciliation_keeps_independent_receipt_guard(environment, monkeypatch):
|
|
environment.plans[0]["dry_run_steps"] = [environment.plans[0]["dry_run_steps"][1]]
|
|
environment.executor.side_effect = ReleaseExecutionAmbiguous("Fixture interruption")
|
|
run_id = environment.create()["result"]["run_id"]
|
|
environment.call("execute", run_id, "core:tag", "--request-id", "receipt-tag-request-0001", "--confirm", "TAG", "--apply")
|
|
guard = Mock(side_effect=ReleaseExecutionBlocked("Remote annotation mismatch"))
|
|
monkeypatch.setattr(api, "reconciled_repository_receipt", guard)
|
|
result = environment.call("reconcile", run_id, "core:tag", "--request-id", "receipt-success-request-0001", "--outcome", "effect_succeeded", "--confirm", "RECONCILE", "--apply")
|
|
assert result["http_status"] == 409
|
|
guard.assert_called_once()
|
|
environment.executor.assert_called_once()
|
|
|
|
|
|
def test_resume_and_retry_reuse_the_existing_running_attempt_rules(environment):
|
|
created = environment.create()
|
|
run_id = created["result"]["run_id"]
|
|
store = ReleaseRunStore(Path(created["state_location"]), expected_workspace_fingerprint=api.release_workspace_fingerprint(environment.workspace))
|
|
store.claim_step(run_id, "core:preflight", attempt_id="lost-process-request-0001")
|
|
dry = environment.call("resume", run_id, "--request-id", "resume-process-request-0001")
|
|
assert dry["dry_run"]
|
|
assert store.get(run_id)["state"]["steps"][0]["state"] == "running"
|
|
resumed = environment.call("resume", run_id, "--request-id", "resume-process-request-0001", "--apply")
|
|
assert resumed["result"]["state"]["steps"][0]["state"] == "interrupted"
|
|
retried = environment.call("retry", run_id, "core:preflight", "--request-id", "retry-readonly-request-0001", "--apply")
|
|
assert retried["result"]["state"]["steps"][0]["state"] == "pending"
|
|
environment.executor.assert_not_called()
|
|
|
|
|
|
def test_workspace_scoping_and_corrupt_records_fail_closed(environment, tmp_path):
|
|
created = environment.create()
|
|
run_id = created["result"]["run_id"]
|
|
another = tmp_path / "another-workspace"
|
|
another.mkdir()
|
|
foreign = environment.call("show", run_id, workspace_root=another)
|
|
assert foreign["http_status"] == 404
|
|
path = Path(created["state_location"]) / f"{run_id}.json"
|
|
record = json.loads(path.read_text())
|
|
record["immutable"]["input"]["repo_versions"]["govoplan-core"] = "9.9.9"
|
|
path.write_text(json.dumps(record))
|
|
assert environment.call("show", run_id)["http_status"] == 409
|
|
|
|
|
|
def test_run_list_keeps_cursor_pagination(environment):
|
|
environment.create("page-create-request-0001")
|
|
environment.create("page-create-request-0002")
|
|
first = environment.call("list", "--limit", "1")["result"]
|
|
second = environment.call("list", "--limit", "1", "--cursor", first["next_cursor"])["result"]
|
|
assert len(first["runs"]) == len(second["runs"]) == 1
|
|
assert first["runs"][0]["run_id"] != second["runs"][0]["run_id"]
|
|
assert second["next_cursor"] is None
|
|
|
|
|
|
def test_catalog_preview_calls_only_existing_receipt_bound_preview(environment, monkeypatch, tmp_path):
|
|
environment.plans[0] = plan(catalog=True)
|
|
run_id = environment.create()["result"]["run_id"]
|
|
candidate = tmp_path / "candidate"
|
|
verify = Mock(return_value=candidate)
|
|
publish = Mock(return_value={"status": "planned", "apply": False})
|
|
monkeypatch.setattr(api, "verified_run_candidate", verify)
|
|
monkeypatch.setattr(api, "publish_catalog_candidate", publish)
|
|
result = environment.call("preview", run_id, "catalog:validate-sign-publish")
|
|
assert result["_exit_code"] == 0
|
|
verify.assert_called_once()
|
|
assert publish.call_args.kwargs["apply"] is False
|
|
assert publish.call_args.kwargs["candidate_dir"] == candidate
|
|
assert publish.call_args.kwargs["remote"] == "origin"
|
|
environment.executor.assert_not_called()
|
|
|
|
|
|
def test_catalog_generation_and_publication_use_exact_durable_receipts(environment, monkeypatch):
|
|
environment.plans[0] = plan(catalog=True)
|
|
created = environment.create()
|
|
run_id = created["result"]["run_id"]
|
|
candidate_root = Path(created["candidate_location"])
|
|
|
|
def generated(**kwargs):
|
|
candidate_id = kwargs["candidate_id"]
|
|
candidate = candidate_output_path(candidate_root, candidate_id)
|
|
channels = candidate / "channels"
|
|
channels.mkdir(parents=True)
|
|
(channels / "stable.json").write_text(json.dumps({"channel": "stable", "signatures": [{}]}))
|
|
harden_private_candidate_tree(candidate)
|
|
return {"status": "ready"}, issue_candidate_receipt(root=candidate_root, candidate_id=candidate_id, channel="stable")
|
|
|
|
def published(**kwargs):
|
|
candidate = kwargs["candidate_receipt"]
|
|
website = kwargs["expected_website_receipt"]
|
|
return {"status": "published"}, {
|
|
"kind": "catalog_publication", "candidate_id": candidate["candidate_id"],
|
|
"catalog_sha256": candidate["catalog_sha256"], "keyring_sha256": "c" * 64,
|
|
"publication_commit_sha": "d" * 40, "publication_tag_object_sha": "e" * 40,
|
|
"publication_tag_commit_sha": "d" * 40, "branch": website["branch"],
|
|
"tag_name": "catalog-stable-1", "remote": "origin", "remote_sha256": website["remote_sha256"],
|
|
}
|
|
|
|
generator = Mock(side_effect=generated)
|
|
publisher = Mock(side_effect=published)
|
|
website = receipt("addideas-govoplan-website", target_tag="")
|
|
monkeypatch.setattr(api, "generate_catalog_candidate", generator)
|
|
monkeypatch.setattr(api, "publish_received_candidate", publisher)
|
|
monkeypatch.setattr(api, "verify_catalog_publication_precondition", Mock(return_value=website))
|
|
generate = ("execute", run_id, "catalog:selective-generator", "--request-id", "generate-candidate-request-0001", "--confirm", "GENERATE", "--apply")
|
|
first = environment.call(*generate, "--signing-key", "fixture-key=/private/fixture-key.pem")
|
|
assert first["_exit_code"] == 0, first
|
|
replayed = environment.call(*generate)
|
|
assert replayed["result"]["execution_result"]["status"] == "replayed"
|
|
generator.assert_called_once()
|
|
assert generator.call_args.kwargs["signing_keys"] == ("fixture-key=/private/fixture-key.pem",)
|
|
assert "/private/fixture-key.pem" not in json.dumps(first)
|
|
record_text = next(environment.state.rglob("rr-*.json")).read_text()
|
|
assert "/private/fixture-key.pem" not in record_text
|
|
missing_confirmation = environment.call("execute", run_id, "catalog:validate-sign-publish", "--request-id", "publish-candidate-request-0001", "--apply")
|
|
assert missing_confirmation["http_status"] == 409
|
|
publisher.assert_not_called()
|
|
publish = ("execute", run_id, "catalog:validate-sign-publish", "--request-id", "publish-candidate-request-0001", "--confirm", "PUSH", "--apply")
|
|
complete = environment.call(*publish)
|
|
assert complete["_exit_code"] == 0, complete
|
|
assert complete["result"]["state"]["status"] == "completed"
|
|
frozen = first["result"]["state"]["steps"][0]["result_receipt"]
|
|
assert publisher.call_args.kwargs["candidate_path"] == candidate_root / frozen["candidate_id"]
|
|
assert publisher.call_args.kwargs["candidate_receipt"] == frozen
|
|
assert publisher.call_args.kwargs["expected_website_receipt"] == website
|
|
assert publisher.call_args.kwargs["remote"] == "origin"
|
|
assert environment.call(*publish)["result"]["execution_result"]["status"] == "replayed"
|
|
publisher.assert_called_once()
|
|
|
|
|
|
def test_portable_project_cannot_override_release_catalog(environment):
|
|
args = namespace(environment.workspace, environment.state, "plan", "--repo-version", "govoplan-core=1.2.3")
|
|
args.project = environment.workspace / "custom-project.json"
|
|
result = release.handle(args)
|
|
assert result["_exit_code"] == 2
|
|
assert "does not accept --project" in result["summary"][0]
|
|
environment.dashboard.assert_not_called()
|
|
|
|
|
|
def test_foreign_cached_release_package_is_rejected_before_import(environment, monkeypatch, tmp_path):
|
|
monkeypatch.setitem(sys.modules, "server.app", SimpleNamespace(__file__=str(tmp_path / "foreign/app.py")))
|
|
importer = Mock(side_effect=AssertionError("Foreign module import must not occur"))
|
|
monkeypatch.setattr(release.importlib, "import_module", importer)
|
|
result = environment.call("plan", "--repo-version", "govoplan-core=1.2.3")
|
|
assert result["_exit_code"] == 2
|
|
assert "foreign module" in result["summary"][0]
|
|
importer.assert_not_called()
|
|
environment.dashboard.assert_not_called()
|
|
|
|
|
|
def test_no_arbitrary_remote_or_legacy_publication_flags():
|
|
run_id = "rr-request-" + "a" * 64
|
|
with pytest.raises(SystemExit):
|
|
namespace(Path("/fixture"), None, "execute", run_id, "core:tag", "--request-id", "arbitrary-remote-request", "--remote", "untrusted")
|
|
with pytest.raises(SystemExit):
|
|
namespace(Path("/fixture"), None, "publish-candidate", "--candidate-dir", "/untrusted")
|
|
|
|
|
|
def test_unknown_step_and_malformed_paths_do_not_become_other_routes(environment):
|
|
run_id = environment.create()["result"]["run_id"]
|
|
assert environment.call("preview", run_id, "unknown:step")["http_status"] == 404
|
|
with pytest.raises(SystemExit):
|
|
namespace(environment.workspace, environment.state, "preview", run_id, "../repositories/push")
|
|
|
|
|
|
def test_signing_material_is_not_echoed_by_validation(environment):
|
|
run_id = environment.create()["result"]["run_id"]
|
|
secret = "PRIVATE-KEY-MATERIAL\nDO-NOT-ECHO"
|
|
result = environment.call("execute", run_id, "core:preflight", "--request-id", "secret-input-request-0001", "--signing-key", secret, "--apply")
|
|
assert result["_exit_code"] == 2
|
|
assert secret not in json.dumps(result)
|
|
assert "SECRET" not in release._error_detail({"detail": [{"loc": ["body", "signing_keys"], "msg": "Invalid input", "input": "SECRET"}]})
|
|
environment.executor.assert_not_called()
|
|
|
|
|
|
def test_default_state_matches_console_and_token_never_appears_in_output(environment, monkeypatch, tmp_path):
|
|
monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "xdg-state"))
|
|
monkeypatch.setattr(release.secrets, "token_urlsafe", lambda size: "ephemeral-do-not-print-token")
|
|
result = environment.call("plan", "--repo-version", "govoplan-core=1.2.3", state_dir=None)
|
|
assert result["state_location"] == str(api.default_release_run_root(environment.workspace))
|
|
assert "ephemeral-do-not-print-token" not in json.dumps(result)
|
|
|
|
|
|
def test_documentation_keeps_disabled_generic_mutations_and_recovery_explicit():
|
|
console = (ROOT / "docs/operations/RELEASE_CONSOLE.md").read_text()
|
|
usage = (ROOT / "docs/operations/DEVKIT_RELEASE.md").read_text()
|
|
assert "generic push, sync and prepare\nmutation endpoints are disabled" in console
|
|
assert "ASGI application inside" in usage
|
|
assert "effect_absent" in usage and "effect_succeeded" in usage and "unresolved" in usage
|
|
assert "does **not** stage arbitrary source" in usage
|