opencode-config/tests/test_project_contract.py
Sergey c0e8ec816a
refactor(scripts): unify project contract + fix no_db polarity + frontend stack detection (#267)
* refactor(scripts): add project_contract.py single source of truth

* fix(spec-status): word-boundary regex for stack matching (uv!=uvicorn)

* feat(project-status): frontend stack detection + no_db support

* fix(template): cookiecutter use_db=no writes no_db=true marker

* fix(ci): ruff SIM300/PLC0415/W292/E501 in project_contract tests

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-08-04 23:20:11 +03:00

140 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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