opencode-config/tests/test_project_contract.py

156 lines
5.6 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.
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_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",
]
def test_mobile_first_markers_keys():
"""MOBILE_FIRST_MARKERS (issue #278) has the expected keys.
Distinct from FRONTEND_STACK_MARKERS so the stack check stays focused on
the Tailwind/shadcn/TS trio while mobile-first (PWA + mobile Playwright +
a11y) is a separate dict.
"""
assert set(pc.MOBILE_FIRST_MARKERS.keys()) == {
"fullstack_files",
"fullstack_app_html_markers",
}
assert pc.MOBILE_FIRST_MARKERS["fullstack_files"] == [
"frontend/static/manifest.webmanifest",
"frontend/tests/e2e/mobile.spec.ts",
"frontend/tests/e2e/accessibility.spec.ts",
]
assert pc.MOBILE_FIRST_MARKERS["fullstack_app_html_markers"] == [
"viewport",
"manifest",
]
# ── 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