diff --git a/cli/python/base_projects/tests/test_engine.py b/cli/python/base_projects/tests/test_engine.py index ba7ca405..57440669 100644 --- a/cli/python/base_projects/tests/test_engine.py +++ b/cli/python/base_projects/tests/test_engine.py @@ -9,6 +9,7 @@ import unittest from contextlib import redirect_stderr, redirect_stdout from pathlib import Path +from typing import Any from unittest import mock from base_cli_adapters.history import build_finished_record @@ -16,48 +17,6 @@ from base_projects import engine, project_discovery -def write_manifest(project_root: Path, name: str) -> None: - project_root.mkdir(parents=True) - (project_root / "base_manifest.yaml").write_text( - f"project:\n name: {name}\npython: {{}}\nartifacts: []\n", - encoding="utf-8", - ) - - -def write_shell_manifest(project_root: Path, name: str) -> None: - project_root.mkdir(parents=True) - (project_root / "base_manifest.yaml").write_text( - f"project:\n name: {name}\nartifacts: []\n", - encoding="utf-8", - ) - - -def write_uv_manifest(project_root: Path, name: str) -> None: - project_root.mkdir(parents=True) - (project_root / "base_manifest.yaml").write_text( - f"project:\n name: {name}\npython:\n manager: uv\n", - encoding="utf-8", - ) - python_bin = project_root / ".venv" / "bin" / "python" - write_ready_python_bin(python_bin) - - -def write_inline_uv_manifest(project_root: Path, name: str) -> None: - project_root.mkdir(parents=True) - (project_root / "base_manifest.yaml").write_text( - f"project:\n name: {name}\npython: {{manager: uv}}\nartifacts: []\n", - encoding="utf-8", - ) - python_bin = project_root / ".venv" / "bin" / "python" - write_ready_python_bin(python_bin) - - -def write_ready_python_bin(python_bin: Path) -> None: - python_bin.parent.mkdir(parents=True) - python_bin.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") - python_bin.chmod(0o755) - - def write_versioned_python_bin(python_bin: Path, version: str) -> None: python_bin.parent.mkdir(parents=True, exist_ok=True) python_bin.write_text(f"#!/bin/sh\nprintf '{version}\\n'\n", encoding="utf-8") @@ -251,6 +210,8 @@ def run_engine_with_home(args: list[str], base_home: Path, home: Path) -> tuple[ class ProjectDiscoveryTests(unittest.TestCase): + manifest_factory: Any + # pylint: disable=too-many-statements def test_command_protocol_covers_project_command_bridge_records(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: @@ -390,8 +351,8 @@ def test_main_reports_config_errors_without_traceback(self) -> None: def test_cached_project_discovery_sorts_projects(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: workspace = Path(tmpdir) - write_manifest(workspace / "zeta", "zeta") - write_manifest(workspace / "alpha", "alpha") + self.manifest_factory.write_project(workspace / "zeta", "zeta") + self.manifest_factory.write_project(workspace / "alpha", "alpha") (workspace / "notes").mkdir() ctx = mock.Mock() @@ -404,8 +365,8 @@ def test_projects_list_defaults_to_base_home_parent(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: workspace = Path(tmpdir) base_home = workspace / "base" - write_manifest(base_home, "base") - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(base_home, "base") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine(["list"], base_home) @@ -419,7 +380,7 @@ def test_projects_list_supports_workspace_override(self) -> None: workspace = Path(tmpdir) / "custom" base_home = Path(tmpdir) / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine(["list", "--workspace", str(workspace)], base_home) @@ -433,7 +394,7 @@ def test_projects_list_prefers_configured_workspace_root(self) -> None: workspace = root / "configured-workspace" base_home = root / "homebrew" / "base" / "libexec" base_home.mkdir(parents=True) - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine( ["list"], @@ -452,8 +413,8 @@ def test_projects_list_workspace_override_wins_over_config(self) -> None: explicit_workspace = root / "explicit" base_home = root / "homebrew" / "base" / "libexec" base_home.mkdir(parents=True) - write_manifest(configured_workspace / "configured-demo", "configured-demo") - write_manifest(explicit_workspace / "explicit-demo", "explicit-demo") + self.manifest_factory.write_project(configured_workspace / "configured-demo", "configured-demo") + self.manifest_factory.write_project(explicit_workspace / "explicit-demo", "explicit-demo") status, stdout, stderr = run_engine( ["list", "--workspace", str(explicit_workspace)], @@ -470,7 +431,7 @@ def test_projects_list_supports_json_format(self) -> None: workspace = Path(tmpdir) / "custom" base_home = Path(tmpdir) / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine( ["list", "--workspace", str(workspace), "--format", "json"], @@ -486,7 +447,7 @@ def test_projects_list_supports_csv_format(self) -> None: workspace = Path(tmpdir) / "custom" base_home = Path(tmpdir) / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine( ["list", "--workspace", str(workspace), "--format", "csv"], @@ -504,7 +465,7 @@ def test_projects_list_supports_yaml_format(self) -> None: workspace = Path(tmpdir) / "custom" base_home = Path(tmpdir) / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine( ["list", "--workspace", str(workspace), "--format", "yaml"], @@ -523,10 +484,10 @@ def test_workspace_status_reports_manifest_and_venv_state(self) -> None: base_home = root / "base" home.mkdir() base_home.mkdir() - write_manifest(workspace / "base", "base") - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "base", "base") + self.manifest_factory.write_project(workspace / "demo", "demo") python_bin = home / ".base.d" / "base" / ".venv" / "bin" / "python" - write_ready_python_bin(python_bin) + self.manifest_factory.write_ready_python_bin(python_bin) write_last_check(home, "base", "2026-06-17T14:30:00Z") status, stdout, stderr = invoke_engine(["status", "--workspace", str(workspace)], base_home, home) @@ -546,7 +507,7 @@ def test_workspace_status_reports_uv_project_venv_ready_without_base_project_ven base_home = root / "base" home.mkdir() base_home.mkdir() - write_uv_manifest(workspace / "bankbuddy", "bankbuddy") + self.manifest_factory.write_python(workspace / "bankbuddy", "bankbuddy") status, stdout, stderr = invoke_engine(["status", "--workspace", str(workspace)], base_home, home) @@ -564,7 +525,7 @@ def test_workspace_status_reports_shell_only_project_venv_not_applicable(self) - home.mkdir() base_home.mkdir() project_root = workspace / "shell-only" - write_shell_manifest(project_root, "shell-only") + self.manifest_factory.write_shell(project_root, "shell-only") status, stdout, stderr = invoke_engine( ["status", "--workspace", str(workspace), "--format", "json"], @@ -589,7 +550,7 @@ def test_workspace_status_text_aligns_shell_only_project_columns(self) -> None: home.mkdir() base_home.mkdir() project_root = workspace / "shell-only" - write_shell_manifest(project_root, "shell-only") + self.manifest_factory.write_shell(project_root, "shell-only") status, stdout, stderr = invoke_engine(["status", "--workspace", str(workspace)], base_home, home) @@ -615,7 +576,7 @@ def test_workspace_status_supports_json_format(self) -> None: base_home = root / "base" home.mkdir() base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") write_last_check(home, "demo", "2026-06-17T14:30:00Z", status="error") status, stdout, stderr = invoke_engine( @@ -651,8 +612,8 @@ def test_workspace_status_debug_logs_default_discovery_scan_without_manifest(sel workspace = root / "workspace" base_home = workspace / "base" home.mkdir() - write_manifest(base_home, "base") - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(base_home, "base") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = invoke_engine( ["--debug", "status", "--format", "json"], @@ -679,7 +640,7 @@ def test_workspace_status_json_reports_uv_project_python_runtime(self) -> None: project_root = workspace / "bankbuddy" home.mkdir() base_home.mkdir() - write_uv_manifest(project_root, "bankbuddy") + self.manifest_factory.write_python(project_root, "bankbuddy") python_bin = project_root / ".venv" / "bin" / "python" write_versioned_python_bin(python_bin, "3.12") @@ -706,7 +667,7 @@ def test_workspace_status_reports_invalid_manifest_without_stopping_scan(self) - base_home = root / "base" home.mkdir() base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") broken_root = workspace / "broken" broken_root.mkdir(parents=True) (broken_root / "base_manifest.yaml").write_text("project: [", encoding="utf-8") @@ -727,7 +688,7 @@ def test_projects_list_reuses_project_cache_when_manifests_are_unchanged(self) - base_home = root / "base" home.mkdir() base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") with mock.patch( "base_projects.project_discovery.read_project", wraps=project_discovery.read_project @@ -753,7 +714,7 @@ def test_projects_list_invalidates_project_cache_when_manifest_changes(self) -> home.mkdir() base_home.mkdir() project_root = workspace / "demo" - write_manifest(project_root, "demo") + self.manifest_factory.write_project(project_root, "demo") with mock.patch( "base_projects.project_discovery.read_project", wraps=project_discovery.read_project @@ -779,7 +740,7 @@ def test_projects_list_dry_run_does_not_write_discovery_cache(self) -> None: workspace = Path(tmpdir) base_home = workspace / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") with mock.patch("base_projects.project_discovery.write_project_cache") as write_cache: status, stdout, stderr = run_engine( @@ -808,7 +769,7 @@ def test_projects_resolve_prints_project_details(self) -> None: base_home = workspace / "base" base_home.mkdir() project_root = workspace / "demo" - write_manifest(project_root, "demo") + self.manifest_factory.write_project(project_root, "demo") status, stdout, stderr = run_engine(["resolve", "demo"], base_home) @@ -826,7 +787,7 @@ def test_projects_resolve_populates_history_project_metadata(self) -> None: base_home = workspace / "base" base_home.mkdir() project_root = workspace / "demo" - write_manifest(project_root, "demo") + self.manifest_factory.write_project(project_root, "demo") outside = workspace / "outside" outside.mkdir() captured: list[tuple[object, ...]] = [] @@ -854,7 +815,7 @@ def test_projects_resolve_prints_python_route_metadata_for_inline_uv_manager(sel base_home = workspace / "base" base_home.mkdir() project_root = workspace / "demo" - write_inline_uv_manifest(project_root, "demo") + self.manifest_factory.write_inline_python(project_root, "demo") status, stdout, stderr = run_engine(["resolve", "demo"], base_home) @@ -873,7 +834,7 @@ def test_projects_resolve_uses_active_project_manifest_without_workspace_scan(se base_home.mkdir() project_root = root / "active" / "demo" manifest_path = project_root / "base_manifest.yaml" - write_manifest(project_root, "demo") + self.manifest_factory.write_project(project_root, "demo") with mock.patch( "base_projects.engine.discover_projects_cached", @@ -910,8 +871,8 @@ def test_projects_resolve_explicit_workspace_wins_over_active_project(self) -> N active_root = root / "active" / "demo" explicit_workspace = root / "explicit" explicit_root = explicit_workspace / "demo" - write_manifest(active_root, "demo") - write_manifest(explicit_root, "demo") + self.manifest_factory.write_project(active_root, "demo") + self.manifest_factory.write_project(explicit_root, "demo") status, stdout, stderr = run_engine( ["resolve", "demo", "--workspace", str(explicit_workspace)], @@ -942,7 +903,7 @@ def test_projects_resolve_rejects_active_project_manifest_mismatch(self) -> None base_home = root / "base" base_home.mkdir() project_root = root / "active" / "demo" - write_manifest(project_root, "other") + self.manifest_factory.write_project(project_root, "other") status, _stdout, stderr = run_engine( ["resolve", "demo"], @@ -963,7 +924,7 @@ def test_projects_resolve_prefers_configured_workspace_root(self) -> None: base_home = root / "homebrew" / "base" / "libexec" base_home.mkdir(parents=True) project_root = workspace / "demo" - write_manifest(project_root, "demo") + self.manifest_factory.write_project(project_root, "demo") status, stdout, stderr = run_engine( ["resolve", "demo"], @@ -990,8 +951,8 @@ def test_projects_resolve_base_uses_base_home_without_workspace_scan(self) -> No workspace = Path(tmpdir) base_home = workspace / "base" other_base = workspace / "base-worktree" - write_manifest(base_home, "base") - write_manifest(other_base, "base") + self.manifest_factory.write_project(base_home, "base") + self.manifest_factory.write_project(other_base, "base") status, stdout, stderr = run_engine(["resolve", "base"], base_home) @@ -1009,8 +970,8 @@ def test_projects_resolve_base_uses_base_home_with_configured_workspace(self) -> workspace = root / "configured-workspace" base_home = root / "homebrew" / "base" / "libexec" other_base = workspace / "base" - write_manifest(base_home, "base") - write_manifest(other_base, "base") + self.manifest_factory.write_project(base_home, "base") + self.manifest_factory.write_project(other_base, "base") status, stdout, stderr = run_engine( ["resolve", "base"], @@ -1180,7 +1141,7 @@ def test_projects_activation_sources_supports_empty_manifest_section(self) -> No workspace = Path(tmpdir) base_home = workspace / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine(["activation-sources", "demo"], base_home) @@ -1209,7 +1170,7 @@ def test_projects_resolve_marks_activation_trust_required_only_when_sources_decl workspace = Path(tmpdir) base_home = workspace / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, stdout, stderr = run_engine(["resolve", "demo"], base_home) @@ -1284,7 +1245,7 @@ def test_projects_test_command_requires_manifest_test_command(self) -> None: workspace = Path(tmpdir) base_home = workspace / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, _stdout, stderr = run_engine(["test-command", "demo"], base_home) @@ -1523,7 +1484,7 @@ def test_projects_demo_script_requires_demo_declaration(self) -> None: workspace = Path(tmpdir) base_home = workspace / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, _stdout, stderr = run_engine(["demo-script", "demo"], base_home) @@ -1748,7 +1709,7 @@ def test_projects_run_commands_requires_commands(self) -> None: workspace = Path(tmpdir) base_home = workspace / "base" base_home.mkdir() - write_manifest(workspace / "demo", "demo") + self.manifest_factory.write_project(workspace / "demo", "demo") status, _stdout, stderr = run_engine(["run-commands", "demo"], base_home) @@ -1762,7 +1723,7 @@ def test_projects_manifest_prints_project_details(self) -> None: base_home.mkdir() project_root = workspace / "demo" manifest_path = project_root / "base_manifest.yaml" - write_manifest(project_root, "demo") + self.manifest_factory.write_project(project_root, "demo") status, stdout, stderr = run_engine(["manifest", str(manifest_path)], base_home) @@ -1787,7 +1748,7 @@ def test_projects_current_prints_nearest_project_details(self) -> None: base_home.mkdir() project_root = workspace / "demo" nested = project_root / "docs" / "notes" - write_manifest(project_root, "demo") + self.manifest_factory.write_project(project_root, "demo") nested.mkdir(parents=True) old_cwd = Path.cwd() @@ -1847,8 +1808,8 @@ def test_projects_list_reports_invalid_manifest(self) -> None: def test_cached_project_discovery_rejects_duplicate_project_names(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: workspace = Path(tmpdir) - write_manifest(workspace / "one", "demo") - write_manifest(workspace / "two", "demo") + self.manifest_factory.write_project(workspace / "one", "demo") + self.manifest_factory.write_project(workspace / "two", "demo") with self.assertRaisesRegex(engine.ProjectDiscoveryError, "Duplicate project names"): ctx = mock.Mock() diff --git a/cli/python/base_trust/tests/test_engine.py b/cli/python/base_trust/tests/test_engine.py index 7ad89ab7..0f7aa610 100644 --- a/cli/python/base_trust/tests/test_engine.py +++ b/cli/python/base_trust/tests/test_engine.py @@ -9,56 +9,13 @@ import unittest from contextlib import redirect_stderr, redirect_stdout from pathlib import Path +from typing import Any from unittest import mock from base_cli_adapters.history import build_finished_record from base_cli.testing import invoke -def write_manifest(project_root: Path, name: str = "demo", command: str | None = "pytest tests/") -> Path: - project_root.mkdir(parents=True, exist_ok=True) - manifest_path = project_root / "base_manifest.yaml" - lines = ["project:", f" name: {name}"] - if command is not None: - lines.extend(["test:", f" command: {command}"]) - lines.append("artifacts: []") - manifest_path.write_text( - "\n".join(lines) + "\n", - encoding="utf-8", - ) - return manifest_path - - -def write_all_command_surfaces_manifest(project_root: Path, name: str = "demo") -> Path: - project_root.mkdir(parents=True, exist_ok=True) - manifest_path = project_root / "base_manifest.yaml" - manifest_path.write_text( - "\n".join( - [ - "project:", - f" name: {name}", - "test:", - " command: pytest tests/", - "commands:", - " lint: ruff check .", - "build:", - " targets:", - " api:", - " command: go build ./...", - "demo:", - " script: demo.sh", - "activate:", - " source:", - " - .base/activate.sh", - "artifacts: []", - ] - ) - + "\n", - encoding="utf-8", - ) - return manifest_path - - def init_git_repo(project_root: Path, origin: str) -> str: subprocess.run(["git", "init"], cwd=project_root, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run( @@ -96,6 +53,8 @@ def init_git_repo(project_root: Path, origin: str) -> str: class ManifestCommandTrustTests(unittest.TestCase): + manifest_factory: Any + def test_require_explicit_manifest_populates_history_project_metadata(self) -> None: from base_trust import engine @@ -103,7 +62,7 @@ def test_require_explicit_manifest_populates_history_project_metadata(self) -> N root = Path(tmpdir) home = root / "home" project_root = root / "work" / "demo" - manifest_path = write_manifest(project_root) + manifest_path = self.manifest_factory.write(project_root) outside = root / "outside" outside.mkdir() captured: list[tuple[object, ...]] = [] @@ -159,7 +118,7 @@ def test_compute_trust_identity_includes_manifest_digest_and_sanitized_git_metad with tempfile.TemporaryDirectory() as tmpdir: project_root = Path(tmpdir) / "work" / "demo" - manifest_path = write_manifest(project_root) + manifest_path = self.manifest_factory.write(project_root) head = init_git_repo(project_root, "https://user:secret@github.com/example/demo.git") expected_digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest() @@ -179,8 +138,8 @@ def test_manifest_command_surfaces_classifies_only_executable_manifest_fields(se with tempfile.TemporaryDirectory() as tmpdir: root = Path(tmpdir) - all_surfaces = write_all_command_surfaces_manifest(root / "all") - no_surfaces = write_manifest(root / "none", name="none", command=None) + all_surfaces = self.manifest_factory.write_command_surfaces(root / "all") + no_surfaces = self.manifest_factory.write(root / "none", name="none", test_command=None) self.assertEqual( engine.manifest_command_surfaces(all_surfaces), @@ -195,7 +154,7 @@ def test_trust_store_writes_allow_record_under_base_state_with_schema_version_on root = Path(tmpdir) home = root / "home" project_root = root / "work" / "demo" - manifest_path = write_manifest(project_root) + manifest_path = self.manifest_factory.write(project_root) identity = engine.compute_trust_identity_for_manifest(manifest_path) store = engine.ManifestCommandTrustStore(home=home) @@ -223,7 +182,7 @@ def test_status_json_reports_blocked_project_and_allow_command(self) -> None: root = Path(tmpdir) home = root / "home" workspace = root / "work" - manifest_path = write_manifest(workspace / "demo") + manifest_path = self.manifest_factory.write(workspace / "demo") expected_digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest() result = invoke( @@ -252,10 +211,10 @@ def test_workspace_status_reports_only_projects_with_executable_manifest_surface root = Path(tmpdir) home = root / "home" workspace = root / "work" - write_manifest(workspace / "fresh", name="fresh") - allowed_manifest = write_manifest(workspace / "allowed", name="allowed") - changed_manifest = write_manifest(workspace / "changed", name="changed") - write_manifest(workspace / "metadata-only", name="metadata-only", command=None) + self.manifest_factory.write(workspace / "fresh", name="fresh") + allowed_manifest = self.manifest_factory.write(workspace / "allowed", name="allowed") + changed_manifest = self.manifest_factory.write(workspace / "changed", name="changed") + self.manifest_factory.write(workspace / "metadata-only", name="metadata-only", test_command=None) store = engine.ManifestCommandTrustStore(home=home) store.allow( engine.compute_trust_identity_for_manifest(allowed_manifest), @@ -311,7 +270,7 @@ def test_status_for_project_without_commands_keeps_json_v1_and_omits_text_guidan root = Path(tmpdir) home = root / "home" workspace = root / "work" - write_manifest(workspace / "docs", name="docs", command=None) + self.manifest_factory.write(workspace / "docs", name="docs", test_command=None) result = invoke( engine.app, @@ -344,9 +303,9 @@ def test_workspace_status_adds_context_projects_only_for_implicit_workspace(self workspace = root / "configured-workspace" base_home = root / "installed-base" active_root = root / "active" - write_manifest(workspace / "scoped", name="scoped") - write_manifest(base_home, name="base") - active_manifest = write_manifest(active_root, name="active") + self.manifest_factory.write(workspace / "scoped", name="scoped") + self.manifest_factory.write(base_home, name="base") + active_manifest = self.manifest_factory.write(active_root, name="active") config_path = home / ".base.d" / "config.yaml" config_path.parent.mkdir(parents=True) config_path.write_text(f"workspace:\n root: {workspace}\n", encoding="utf-8") @@ -396,7 +355,7 @@ def test_require_blocks_unapproved_manifest_with_review_guidance(self) -> None: root = Path(tmpdir) home = root / "home" workspace = root / "work" - manifest_path = write_manifest(workspace / "demo") + manifest_path = self.manifest_factory.write(workspace / "demo") expected_digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest() stdout = io.StringIO() @@ -429,7 +388,7 @@ def test_status_guidance_covers_demo_and_manifest_backed_activation(self) -> Non root = Path(tmpdir) home = root / "home" workspace = root / "work" - manifest_path = write_all_command_surfaces_manifest(workspace / "demo") + manifest_path = self.manifest_factory.write_command_surfaces(workspace / "demo") with mock.patch("base_cli.is_terminal", return_value=True): result = invoke( @@ -455,7 +414,7 @@ def test_changed_manifest_status_shows_recorded_digest_and_reapproval_guidance(s root = Path(tmpdir) home = root / "home" workspace = root / "work" - manifest_path = write_manifest(workspace / "demo") + manifest_path = self.manifest_factory.write(workspace / "demo") identity = engine.compute_trust_identity_for_manifest(manifest_path) engine.ManifestCommandTrustStore(home=home).allow(identity, base_version="9.9.9") manifest_path.write_text( @@ -489,7 +448,7 @@ def test_require_allows_matching_trust_record_for_manifest_path(self) -> None: root = Path(tmpdir) home = root / "home" workspace = root / "work" - manifest_path = write_manifest(workspace / "demo") + manifest_path = self.manifest_factory.write(workspace / "demo") identity = engine.compute_trust_identity_for_manifest(manifest_path) engine.ManifestCommandTrustStore(home=home).allow(identity, base_version="9.9.9") @@ -511,7 +470,7 @@ def test_allow_rejects_manifest_sha256_mismatch_without_writing_record(self) -> root = Path(tmpdir) home = root / "home" workspace = root / "work" - write_manifest(workspace / "demo") + self.manifest_factory.write(workspace / "demo") stdout = io.StringIO() stderr = io.StringIO() @@ -549,7 +508,7 @@ def test_allow_and_revoke_update_status(self) -> None: root = Path(tmpdir) home = root / "home" workspace = root / "work" - manifest_path = write_manifest(workspace / "demo") + manifest_path = self.manifest_factory.write(workspace / "demo") expected_digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest() env = {"BASE_HOME": str(workspace / "base")} diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..6f931300 --- /dev/null +++ b/conftest.py @@ -0,0 +1,120 @@ +"""Shared pytest setup helpers for Base's Python tests.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + + +class ManifestFactory: + """Build the small manifest variants used by package-level tests.""" + + @staticmethod + def _write(root: Path, lines: list[str]) -> Path: + root.mkdir(parents=True, exist_ok=True) + manifest_path = root / "base_manifest.yaml" + manifest_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return manifest_path + + def write( + self, + root: Path, + name: str = "demo", + *, + test_command: str | None = "pytest tests/", + ) -> Path: + """Write the standard manifest used by command-trust tests.""" + + lines = ["project:", f" name: {name}"] + if test_command is not None: + lines.extend(["test:", f" command: {test_command}"]) + lines.append("artifacts: []") + return self._write(root, lines) + + def write_project(self, root: Path, name: str = "demo") -> Path: + """Write a minimal Python-project manifest.""" + + return self._write( + root, + ["project:", f" name: {name}", "python: {}", "artifacts: []"], + ) + + def write_shell(self, root: Path, name: str = "demo") -> Path: + """Write a minimal shell-project manifest.""" + + return self._write(root, ["project:", f" name: {name}", "artifacts: []"]) + + def write_python(self, root: Path, name: str = "demo") -> Path: + """Write a uv-managed Python-project manifest with a ready venv.""" + + manifest_path = self._write( + root, + ["project:", f" name: {name}", "python:", " manager: uv"], + ) + self.write_ready_python_bin(root / ".venv" / "bin" / "python") + return manifest_path + + def write_inline_python(self, root: Path, name: str = "demo") -> Path: + """Write an inline-uv Python-project manifest with a ready venv.""" + + manifest_path = self._write( + root, + ["project:", f" name: {name}", "python: {manager: uv}", "artifacts: []"], + ) + self.write_ready_python_bin(root / ".venv" / "bin" / "python") + return manifest_path + + def write_command_surfaces(self, root: Path, name: str = "demo") -> Path: + """Write a manifest containing every executable command surface.""" + + return self._write( + root, + [ + "project:", + f" name: {name}", + "test:", + " command: pytest tests/", + "commands:", + " lint: ruff check .", + "build:", + " targets:", + " api:", + " command: go build ./...", + "demo:", + " script: demo.sh", + "activate:", + " source:", + " - .base/activate.sh", + "artifacts: []", + ], + ) + + @staticmethod + def write_ready_python_bin(python_bin: Path) -> None: + python_bin.parent.mkdir(parents=True, exist_ok=True) + python_bin.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + python_bin.chmod(0o755) + + +@pytest.fixture(name="manifest_factory") +def manifest_factory_fixture() -> ManifestFactory: + """Return shared manifest writers for pytest-style tests.""" + + return ManifestFactory() + + +@pytest.fixture(name="project_root") +def project_root_fixture(tmp_path: Path) -> Path: + """Return an isolated project root for tests that need one project.""" + + return tmp_path / "project" + + +@pytest.fixture(autouse=True) +def attach_shared_test_fixtures(request: pytest.FixtureRequest, manifest_factory: ManifestFactory) -> None: + """Expose fixture values on legacy unittest test cases during migration.""" + + instance = getattr(request.node, "instance", None) + if instance is not None: + instance.manifest_factory = manifest_factory diff --git a/tests/test_shared_fixtures.py b/tests/test_shared_fixtures.py new file mode 100644 index 00000000..6cd61f56 --- /dev/null +++ b/tests/test_shared_fixtures.py @@ -0,0 +1,25 @@ +from pathlib import Path + + +def test_project_root_fixture_is_isolated(project_root: Path) -> None: + assert project_root.name == "project" + assert not project_root.exists() + + +def test_manifest_factory_writes_standard_manifest(project_root: Path, manifest_factory) -> None: + manifest_path = manifest_factory.write(project_root) + + assert manifest_path == project_root / "base_manifest.yaml" + assert manifest_path.read_text(encoding="utf-8") == ( + "project:\n name: demo\ntest:\n command: pytest tests/\nartifacts: []\n" + ) + + +def test_manifest_factory_writes_command_surfaces(project_root: Path, manifest_factory) -> None: + manifest_path = manifest_factory.write_command_surfaces(project_root) + + manifest = manifest_path.read_text(encoding="utf-8") + assert "commands:" in manifest + assert "build:" in manifest + assert "demo:" in manifest + assert "activate:" in manifest