import argparse import json from pathlib import Path import subprocess import sys import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit")) from govoplan_devkit import review from govoplan_devkit.cli import main @pytest.fixture def args(tmp_path): repo = tmp_path / "optional-feature" repo.mkdir() subprocess.run(["git", "init", "-q", str(repo)], check=True) paths = ["webui/src/pages/CampaignPage.tsx", "webui/src/dialogs/SettingsDialog.tsx", "webui/src/settings/AdminSettings.tsx", "webui/src/widgets/SummaryWidget.tsx", "webui/src/module.ts"] for relative in paths: path = repo / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_text("// fixture only\n") manifest = repo / "src/optional_feature/backend/manifest.py" manifest.parent.mkdir(parents=True) manifest.write_text("raise RuntimeError('MUST NEVER IMPORT MODULE CODE')\n") (tmp_path / "principles.md").write_text("# Principles\nRevision **UI-2026-09-08**\n\n## UI-01 — Help beside text\n\n## UI-02 — Display first; edit deliberately\n") inventory = {"schema_version": 1, "epic": {"repository": "meta", "number": 4, "url": "https://gitea.invalid/team/meta/issues/4"}, "issues": [{"scope_id": "campaigns", "name": "Campaign", "kind": "manifest", "repository": "optional-feature", "number": 8, "url": "https://gitea.invalid/team/optional-feature/issues/8", "state_at_verification": "closed", "operation": "created"}]} (tmp_path / "issues.json").write_text(json.dumps(inventory)) project = tmp_path / "project.json" project.write_text(json.dumps({"schema_version": 1, "name": "Portable project", "repositories": [ {"name": "optional-feature", "path": "optional-feature", "aliases": ["campaign"]}, {"name": "absent-optional", "path": "absent-optional", "aliases": ["absent"]}], "review": {"issue_inventory": "issues.json", "principles": "principles.md"}, "checks": [{"id": "ui-check", "argv": ["never-execute-this"], "repos": ["optional-feature"], "cwd": "optional-feature"}], "profiles": {"ui": ["ui-check"]}})) return argparse.Namespace(workspace_root=tmp_path, project=project, state_dir=tmp_path / "state", module="campaign", bundle_module=None, profile="ui", evidence=None, output=None) def test_bundle_contains_guidance_links_revision_and_unexecuted_check_plan(args): result = review.handle_review(args) assert result["module"] == "optional-feature" assert result["inventory"]["ui_source_count"] == 5 assert not result["inventory"]["module_code_imported"] assert result["inventory"]["manifest_paths"] == ["src/optional_feature/backend/manifest.py"] assert result["principles"]["revision"] == "UI-2026-09-08" assert [rule["id"] for rule in result["principles"]["rules"]] == ["UI-01", "UI-02"] assert result["check_plan"]["stages"][0]["argv"] == ["never-execute-this"] assert not result["check_plan"]["executed"] assert result["review_completion"].startswith("Not assessed") assert len(result["manual_checklist"]) >= 9 assert all("checked" not in item for item in result["manual_checklist"]) assert "state_at_verification" not in json.dumps(result) assert "operation" not in result["issue_links"][0] assert result["central_issue"]["number"] == 4 assert not args.state_dir.exists() @pytest.mark.parametrize("name", ["campaign", "campaigns", "optional-feature"]) def test_alias_scope_id_and_repository_resolve_to_same_registered_module(args, name): args.module = name assert review.handle_review(args)["module"] == "optional-feature" def test_bundle_alias_and_direct_cli_work_with_global_options_anywhere(args, capsys): assert main(["--workspace-root", str(args.workspace_root), "review", "campaign", "--project", str(args.project), "--json"]) == 0 direct = json.loads(capsys.readouterr().out) assert direct["module"] == "optional-feature" assert main(["review", "bundle", "campaigns", "--workspace-root", str(args.workspace_root), "--project", str(args.project), "--format", "json"]) == 0 assert json.loads(capsys.readouterr().out)["module"] == direct["module"] def test_absent_optional_repository_and_no_ui_do_not_complete_review(args): args.module = "absent" result = review.handle_review(args) assert not result["repository"]["exists"] assert result["repository"]["errors"] assert result["inventory"]["ui_source_count"] == 0 assert result["check_plan"]["stages"] == [] assert result["review_completion"].startswith("Not assessed") assert any("not automatic N/A" in warning for warning in result["warnings"]) assert any("not a passing" in warning for warning in result["warnings"]) def test_portable_project_does_not_inherit_govoplan_links_or_principles(args): payload = json.loads(args.project.read_text()) payload.pop("review") args.project.write_text(json.dumps(payload)) result = review.handle_review(args) assert result["issue_links"] == [] and result["central_issue"] is None assert not result["principles"]["available"] assert "git.add-ideas.de" not in json.dumps(result) @pytest.mark.parametrize("kind", ["symlink", "outside", "bad-url", "duplicate-scope", "foreign-repository"]) def test_review_inputs_cannot_escape_or_silently_misbind_scope(args, kind): if kind in {"symlink", "outside"}: payload = json.loads(args.project.read_text()) if kind == "symlink": alias = args.workspace_root / "alias.json" alias.symlink_to(args.workspace_root / "issues.json") payload["review"]["issue_inventory"] = "alias.json" else: payload["review"]["principles"] = "../outside.md" args.project.write_text(json.dumps(payload)) else: path = args.workspace_root / "issues.json" payload = json.loads(path.read_text()) if kind == "bad-url": payload["issues"][0]["url"] = "https://gitea.invalid/team/wrong/issues/8" elif kind == "duplicate-scope": payload["issues"].append(payload["issues"][0]) else: payload["issues"][0].update(repository="foreign", url="https://gitea.invalid/team/foreign/issues/8") args.module = "campaigns" path.write_text(json.dumps(payload)) with pytest.raises(ValueError): review.handle_review(args) def test_source_inventory_never_follows_symlinked_ancestors(args): root = args.workspace_root / "indirect" root.mkdir() (root / "webui").symlink_to(args.workspace_root / "optional-feature/webui", target_is_directory=True) result = review.source_inventory(root) assert result["ui_source_count"] == 0 assert result["skipped_symlinks"] == ["webui/src"] def test_explicit_artifact_is_only_local_and_preserves_issue_discovery_snapshot(args): inventory = args.workspace_root / "issues.json" original = inventory.read_bytes() args.output = args.workspace_root / "artifacts/review.json" result = review.handle_review(args) assert json.loads(args.output.read_text()) == result assert inventory.read_bytes() == original assert args.output.stat().st_mode & 0o777 == 0o600 assert not args.state_dir.exists() def test_attached_historical_receipt_stays_separate_from_current_review(args): path = args.workspace_root / "historical.json" path.write_text(json.dumps({"schema_version": 1, "run_id": "historical", "workspace_root": str(args.workspace_root), "project_file": str(args.project), "source_fingerprint": "a" * 64, "status": "passed", "snapshot_verified": True, "generated_at": "2026-09-08", "finished_at": "2026-09-08", "stages": [{"id": "old-check", "status": "passed", "exit_code": 0, "duration_seconds": 1, "log_path": "/not-read"}]})) args.evidence = str(path) result = review.handle_review(args) assert result["evidence"]["origin"] == "external-unverified" assert result["evidence"]["source_state"] == "historical-source-differs" assert result["review_completion"].startswith("Not assessed") assert not result["check_plan"]["executed"] def test_unknown_module_and_extra_positionals_are_errors(args): args.module = "typo" with pytest.raises(ValueError, match="Unknown repository"): review.handle_review(args) args.module, args.bundle_module = "campaign", "extra" with pytest.raises(ValueError, match="one module"): review.handle_review(args) def test_compact_review_exposes_scoped_coverage_limits_without_hiding_full_list(args, monkeypatch): notes = [f"Separate suite {number} was not included." for number in range(12)] monkeypatch.setattr("govoplan_devkit.catalog.build_stages", lambda *_args, **_kwargs: [ {"id": "source-only", "argv": ["never-execute"], "coverage_notes": notes}]) result = review.handle_review(args) assert result["coverage_notes"] == notes assert sum(line.startswith("Coverage limitation:") for line in result["summary"]) == 8 assert any("4 additional coverage limitations" in line for line in result["summary"]) assert not result["check_plan"]["executed"]