feat(devkit): add resumable workspace automation and UI review tooling
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.
This commit is contained in:
Executable
+524
@@ -0,0 +1,524 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user