from __future__ import annotations from dataclasses import replace from contextlib import redirect_stdout import io import json from pathlib import Path import runpy import shutil import sys import unittest from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "tools/release")) from govoplan_release import meta_preparation # noqa: E402 from govoplan_release.git_state import collect_repository_snapshot # noqa: E402 from govoplan_release.meta_preparation import ( # noqa: E402 MetaPreparationError, prepare_developer_meta_package, ) from govoplan_release.model import RepositorySpec # noqa: E402 from govoplan_release.selective_planner import build_selective_release_plan # noqa: E402 from govoplan_release.version_metadata import ( # noqa: E402 VersionMetadataError, apply_version_metadata_mutations, version_metadata_mutations, ) import test_release_meta_source_tag as meta_fixture # noqa: E402 from test_release_plan_guidance import dashboard # noqa: E402 from test_release_repository_tag import create_release_repo, git, git_text # noqa: E402 class MetaPreparationTests(unittest.TestCase): synchronize = meta_fixture.MetaSourceTagTests.synchronize def setUp(self): meta_fixture.MetaSourceTagTests.setUp(self) self.operator = self.root / "operator" self.generator = ( self.operator / "tools/release/generate-developer-meta-package.py" ) self.generator.parent.mkdir(parents=True) shutil.copyfile( ROOT / "tools/release/generate-developer-meta-package.py", self.generator ) self.enterContext(patch.object(meta_preparation, "META_ROOT", self.operator)) def prepare_core(self): apply_version_metadata_mutations(self.core, target_version="0.1.11") git(self.core, "add", ".") git(self.core, "commit", "-m", "Prepared synthetic Core target") def preview(self, **kwargs): return prepare_developer_meta_package( repo_path=self.meta, target_version="0.1.11", **kwargs ) def apply(self, preview): return self.preview( apply=True, expected_receipt=preview["receipt"], confirm_out_of_run=True ) def test_full_canonical_preview_apply_and_shared_mutation_discovery(self): extra, remote = create_release_repo( root=self.root, workspace=self.workspace, name="govoplan-workflow-engine", version="0.2.3", ) self.specs.append( { "name": extra.name, "path": extra.name, "category": "module", "subtype": "", "remote": str(remote), } ) self.registry.write_text(json.dumps({"repositories": self.specs})) self.prepare_core() before = self.package.read_bytes() preview = self.preview() self.assertEqual("planned", preview["status"]) self.assertEqual(before, self.package.read_bytes()) mutations = version_metadata_mutations(self.meta, target_version="0.1.11") self.assertEqual([meta_preparation.PACKAGE], [item.path for item in mutations]) self.assertIn(b"govoplan-workflow-engine==0.2.3", mutations[0].after) self.assertIn(b"govoplan-core==0.1.11", mutations[0].after) result = self.apply(preview) self.assertEqual("prepared", result["status"]) self.assertEqual(mutations[0].after, self.package.read_bytes()) self.assertEqual( self.render(workspace=self.workspace, requirements=self.requirements), self.package.read_text(), ) self.assertEqual( f"M {meta_preparation.PACKAGE}", git_text(self.meta, "status", "--porcelain"), ) self.assertFalse(git_text(self.meta, "tag", "--list")) def test_core_target_must_already_be_prepared(self): before = self.package.read_bytes() with self.assertRaisesRegex(MetaPreparationError, "Prepare and commit Core"): self.preview() self.assertEqual(before, self.package.read_bytes()) def test_changed_requirements_receipt_blocks_before_any_output(self): self.prepare_core() preview = self.preview() before = self.package.read_bytes() self.requirements.write_text( self.requirements.read_text() + "# reviewed different inputs\n" ) git(self.meta, "add", ".") git(self.meta, "commit", "-m", "Changed synthetic requirements") with self.assertRaisesRegex(MetaPreparationError, "changed since"): self.apply(preview) self.assertEqual(before, self.package.read_bytes()) def test_source_change_immediately_before_effect_is_rechecked(self): self.prepare_core() preview = self.preview() before = self.package.read_bytes() original = meta_preparation.preview_meta_mutation def changed(**kwargs): result = original(**kwargs) self.requirements.write_text( self.requirements.read_text() + "# concurrent change\n" ) git(self.meta, "add", ".") git(self.meta, "commit", "-m", "Concurrent synthetic change") return result with patch.object( meta_preparation, "preview_meta_mutation", side_effect=changed ): with self.assertRaisesRegex(MetaPreparationError, "changed before"): self.apply(preview) self.assertEqual(before, self.package.read_bytes()) def test_core_full_package_and_operator_generator_are_receipt_bound(self): self.prepare_core() for path, repository in ( (self.core / "pyproject.toml", self.core), (self.access / "pyproject.toml", self.access), (self.generator, None), ): with self.subTest(input=path.name, repo=str(repository)): preview = self.preview() before = self.package.read_bytes() path.write_text(path.read_text() + "\n# changed frozen input\n") if repository is not None: git(repository, "add", ".") git( repository, "commit", "-m", "Changed synthetic composition input", ) with self.assertRaisesRegex(MetaPreparationError, "changed since"): self.apply(preview) self.assertEqual(before, self.package.read_bytes()) def test_post_write_source_change_is_reported_without_retry_or_rollback(self): from govoplan_release import version_metadata self.prepare_core() preview = self.preview() original = version_metadata._atomic_write def changed(path, payload): original(path, payload) self.requirements.write_text( self.requirements.read_text() + "# concurrent after write\n" ) with patch.object( version_metadata, "_atomic_write", side_effect=changed ) as writer: with self.assertRaisesRegex( meta_preparation.MetaPreparationAmbiguous, "write/post-check failed" ): self.apply(preview) self.assertEqual(1, writer.call_count) self.assertIn('version = "0.1.11"', self.package.read_text()) self.assertIn("# concurrent after write", self.requirements.read_text()) def test_write_failure_after_replace_requires_reconciliation(self): from govoplan_release import version_metadata self.prepare_core() preview = self.preview() original = version_metadata._atomic_write def partial(path, payload): original(path, payload) raise OSError("Synthetic directory fsync failure after replacement") with patch.object(version_metadata, "_atomic_write", side_effect=partial) as writer: with self.assertRaisesRegex(meta_preparation.MetaPreparationAmbiguous, "may have been written"): self.apply(preview) self.assertEqual(1, writer.call_count) self.assertIn('version = "0.1.11"', self.package.read_text()) def test_cli_requires_reviewed_receipt_and_explicit_out_of_run_confirmation(self): self.prepare_core() main = runpy.run_path( str(ROOT / "tools/release/prepare-developer-meta-package.py") )["main"] arguments = [ "prepare-developer-meta-package.py", "--workspace", str(self.workspace), "--target-version", "0.1.11", ] output = io.StringIO() with patch.object(sys, "argv", arguments), redirect_stdout(output): self.assertEqual(0, main()) preview = self.root / "meta-preview.json" preview.write_text(output.getvalue()) with ( patch.object(sys, "argv", [*arguments, "--apply"]), redirect_stdout(io.StringIO()), ): self.assertEqual(1, main()) with ( patch.object( sys, "argv", [ *arguments, "--apply", "--receipt", str(preview), "--confirm-out-of-run", ], ), redirect_stdout(io.StringIO()), ): self.assertEqual(0, main()) def test_unknown_full_input_and_unsafe_operator_tooling_fail_closed(self): self.prepare_core() unknown = self.workspace / "govoplan-unknown/pyproject.toml" unknown.parent.mkdir() unknown.write_text('[project]\nname="govoplan-unknown"\nversion="1.0.0"\n') with self.assertRaisesRegex(MetaPreparationError, "unregistered"): self.preview() unknown.unlink() self.generator.chmod(0o666) with self.assertRaisesRegex(MetaPreparationError, "owned, bounded regular"): self.preview() def test_wrong_nested_identity_and_existing_immutable_tag_fail_closed(self): self.prepare_core() original = self.package.read_text() self.package.write_text( original.replace('name = "govoplan"', 'name = "not-govoplan"') ) git(self.meta, "add", ".") git(self.meta, "commit", "-m", "Wrong synthetic package identity") with self.assertRaisesRegex(MetaPreparationError, "identity"): self.preview() self.package.write_text(original) git(self.meta, "add", ".") git(self.meta, "commit", "-m", "Restore synthetic package identity") git(self.meta, "tag", "-a", "v0.1.11", "-m", "Immutable target") with self.assertRaisesRegex(MetaPreparationError, "target Meta tag"): self.preview() def test_no_generic_durable_self_mutation_or_running_tooling_target(self): self.prepare_core() preview = self.preview() with self.assertRaisesRegex(VersionMetadataError, "outside durable runs"): apply_version_metadata_mutations(self.meta, target_version="0.1.11") with self.assertRaisesRegex(MetaPreparationError, "confirm"): self.preview(apply=True, expected_receipt=preview["receipt"]) with patch.object(meta_preparation, "META_ROOT", self.meta): with self.assertRaisesRegex(MetaPreparationError, "running operator"): self.preview() def test_next_version_plan_is_actionable_core_first_without_meta_executor(self): snapshots = tuple( collect_repository_snapshot( RepositorySpec(**spec), workspace_root=self.workspace, target_tag="v0.1.11", ) for spec in self.specs[:2] ) source = replace( dashboard(workspace=self.workspace, version=self.version), repositories=snapshots, ) plan = build_selective_release_plan( source, selected_repos=("govoplan", "govoplan-core"), target_version="0.1.11", ) self.assertEqual( ["govoplan-core", "govoplan"], [unit.repo for unit in plan.units] ) findings = [ finding for finding in plan.gate_findings if finding.repo == "govoplan" ] self.assertEqual( ["developer_meta_core_preparation_required"], [finding.code for finding in findings], ) self.assertIn("prepare-developer-meta-package.py", findings[0].remediation) meta_steps = [step for step in plan.dry_run_steps if step.repo == "govoplan"] self.assertEqual( ["govoplan:prepare-support", "govoplan:publish-support"], [step.id for step in meta_steps], ) self.assertTrue(all(step.status == "needs-executor" for step in meta_steps)) self.prepare_core() prepared = build_selective_release_plan( source, selected_repos=("govoplan",), target_version="0.1.11" ) self.assertEqual("developer_meta_out_of_run", prepared.gate_findings[0].code) if __name__ == "__main__": unittest.main()