"""Tests for ``.opencode/scripts/project_contract.py`` — single source of truth for project types, stacks, structure, frontend markers, no_db. The contract module is imported via ``importlib.util`` (no ``sys.path`` mutation), mirroring how ``spec-status.py`` and ``project-status.py`` load it. """ from __future__ import annotations import importlib.util import sys from pathlib import Path SCRIPT_PATH = ( Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "project_contract.py" ) _spec = importlib.util.spec_from_file_location("project_contract", SCRIPT_PATH) pc = importlib.util.module_from_spec(_spec) # type: ignore[arg-type] _spec.loader.exec_module(pc) # type: ignore[union-attr] # ── ProjectType enum ──────────────────────────────────────────────────────── def test_project_type_enum_values(): """ProjectType has exactly 7 members including MCP_SERVER and UNKNOWN.""" members = {t.name for t in pc.ProjectType} assert members == { "FULLSTACK", "BACKEND", "CLI", "BOT", "WORKER", "MCP_SERVER", "UNKNOWN", }, f"unexpected members: {members}" # Values are lowercase strings. values = {t.value for t in pc.ProjectType} assert values == { "fullstack", "backend", "cli", "bot", "worker", "mcp-server", "unknown", } def test_valid_types_excludes_unknown(): """VALID_TYPES excludes ``"unknown"`` (used by spec-status PROJECT_TYPE).""" assert "unknown" not in pc.VALID_TYPES assert "mcp-server" in pc.VALID_TYPES assert { "fullstack", "backend", "cli", "bot", "worker", "mcp-server", } == pc.VALID_TYPES def test_stack_required_keys_match_valid_types(): """STACK_REQUIRED keys == VALID_TYPES (all 6 types including mcp-server).""" assert set(pc.STACK_REQUIRED.keys()) == pc.VALID_TYPES def test_stack_required_uv_in_all_types(): """``uv`` stays in ALL stacks (build-tool, mentioned in stack.md).""" for ptype, required in pc.STACK_REQUIRED.items(): assert "uv" in required, f"missing 'uv' in {ptype} stack: {required}" def test_structure_expected_keys_subset(): """STRUCTURE_EXPECTED keys ⊆ VALID_TYPES ∪ {"unknown"}. BACKEND is resolved dynamically (not in dict); mcp-server TBD out of scope. """ keys = set(pc.STRUCTURE_EXPECTED.keys()) valid_plus_unknown = pc.VALID_TYPES | {"unknown"} assert keys.issubset(valid_plus_unknown), f"unexpected keys: {keys - valid_plus_unknown}" # Backend NOT in dict (resolved dynamically via _expected_backend_paths) assert "backend" not in keys # mcp-server NOT in dict (out of scope) assert "mcp-server" not in keys def test_no_db_supported_subset(): """NO_DB_SUPPORTED ⊆ VALID_TYPES (backend + fullstack only).""" assert pc.NO_DB_SUPPORTED.issubset(pc.VALID_TYPES) assert {"backend", "fullstack"} == pc.NO_DB_SUPPORTED def test_frontend_stack_markers_keys(): """FRONTEND_STACK_MARKERS has the expected keys with correct marker lists.""" assert set(pc.FRONTEND_STACK_MARKERS.keys()) == { "fullstack_package_deps", "fullstack_files", } assert pc.FRONTEND_STACK_MARKERS["fullstack_package_deps"] == [ "tailwindcss", "bits-ui", ] assert pc.FRONTEND_STACK_MARKERS["fullstack_files"] == [ "frontend/components.json", "frontend/tsconfig.json", ] # ── re-export sanity (backward compat) ───────────────────────────────────── def test_spec_status_reexports_match_contract(): """``spec-status.py`` re-exports VALID_TYPES / STACK_REQUIRED from contract.""" spec_status_path = ( Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "spec-status.py" ) ss_spec = importlib.util.spec_from_file_location("spec_status", spec_status_path) ss = importlib.util.module_from_spec(ss_spec) # type: ignore[arg-type] sys.modules["spec_status"] = ss ss_spec.loader.exec_module(ss) # type: ignore[union-attr] assert ss.VALID_TYPES is pc.VALID_TYPES or ss.VALID_TYPES == pc.VALID_TYPES assert ss.STACK_REQUIRED == pc.STACK_REQUIRED def test_project_status_reexports_match_contract(): """``project-status.py`` re-exports ProjectType / STRUCTURE_EXPECTED.""" project_status_path = ( Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "project-status.py" ) ps_spec = importlib.util.spec_from_file_location("project_status", project_status_path) ps = importlib.util.module_from_spec(ps_spec) # type: ignore[arg-type] sys.modules["project_status"] = ps ps_spec.loader.exec_module(ps) # type: ignore[union-attr] # Note: ProjectType is loaded twice (once here, once inside project-status # via importlib.util with a different module name) so identity differs, # but the enum members compare equal by value. assert list(ps.ProjectType) == list(pc.ProjectType) assert ps.STRUCTURE_EXPECTED == pc.STRUCTURE_EXPECTED