"""Tests for Dockerfile npm install line. Covers issue #56 acceptance criteria: - ``Dockerfile`` installs ``@mathew-cf/opencode-memory`` alongside ``opencode-ai`` and ``repomix`` in the global npm install step, so the memory plugin (``memory_save`` / ``memory_search`` / ``memory_list`` MCP tools) is available inside the container. - ``opencode-ai`` is still present (regression guard — must not be dropped when editing the install line). Checks are raw-text grep assertions (no Docker build needed), mirroring the belt-and-suspenders style of ``test_docker_compose.py``. """ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent DOCKERFILE = REPO_ROOT / "Dockerfile" REQUIRED_PACKAGES = ("opencode-ai", "repomix", "@mathew-cf/opencode-memory") MEMORY_PLUGIN = "@mathew-cf/opencode-memory" OPENCODE_AI = "opencode-ai" def _dockerfile_text() -> str: """Read Dockerfile text (asserts the file exists).""" assert DOCKERFILE.exists(), f"Dockerfile missing: {DOCKERFILE}" return DOCKERFILE.read_text() def _npm_install_lines(text: str) -> list[str]: """Return every ``npm install -g ...`` line from the Dockerfile.""" return [line for line in text.splitlines() if "npm install -g" in line] # ── file exists ───────────────────────────────────────────────────────────── def test_dockerfile_exists(): """Dockerfile exists at repo root.""" assert DOCKERFILE.exists(), f"Dockerfile missing: {DOCKERFILE}" # ── @mathew-cf/opencode-memory installed (issue #56 — the required plugin) ─── def test_memory_plugin_in_npm_install(): """Dockerfile installs ``@mathew-cf/opencode-memory`` via ``npm install -g``. Without the plugin the memory MCP tools (``memory_save`` / ``memory_search`` / ``memory_list``) are unavailable — opencode silently skips the uninstalled plugin referenced in opencode.json. """ lines = _npm_install_lines(_dockerfile_text()) assert lines, "no `npm install -g` line found in Dockerfile" assert any(MEMORY_PLUGIN in line for line in lines), ( f"Dockerfile must install {MEMORY_PLUGIN!r} via `npm install -g` — " "memory plugin MCP tools depend on it" ) def test_memory_plugin_in_npm_install_raw(): """Raw text check: the Dockerfile contains ``@mathew-cf/opencode-memory``. Belt-and-suspenders alongside the parsed check — catches edge cases where the line collector might miss a multi-line or differently-formatted install statement. """ assert MEMORY_PLUGIN in _dockerfile_text(), ( f"Dockerfile must contain {MEMORY_PLUGIN!r} so the memory plugin loads inside the container" ) # ── opencode-ai still present (regression — must not drop the main package) ── def test_opencode_ai_present(): """Dockerfile still installs ``opencode-ai`` (regression guard). Editing the npm install line to add the memory plugin must not accidentally drop the primary ``opencode-ai`` package. """ lines = _npm_install_lines(_dockerfile_text()) assert lines, "no `npm install -g` line found in Dockerfile" assert any(OPENCODE_AI in line for line in lines), ( f"Dockerfile must still install {OPENCODE_AI!r} — dropping it breaks " "the container entrypoint" ) def test_opencode_ai_present_raw(): """Raw text check: the Dockerfile contains ``opencode-ai``.""" assert OPENCODE_AI in _dockerfile_text(), f"Dockerfile must still contain {OPENCODE_AI!r}" # ── single npm install line holds all three packages ──────────────────────── def test_single_npm_install_line_has_all_packages(): """One ``npm install -g`` line installs all three required packages. Guards against splitting the install across multiple RUN layers (which would waste image layers and obscure which package is missing). """ lines = _npm_install_lines(_dockerfile_text()) install_lines = [line for line in lines if OPENCODE_AI in line] assert install_lines, f"no `npm install -g` line with {OPENCODE_AI!r}" main_line = install_lines[0] for pkg in REQUIRED_PACKAGES: assert pkg in main_line, ( f"package {pkg!r} missing from the opencode-ai npm install line: {main_line!r}" ) if __name__ == "__main__": import pytest pytest.main([__file__, "-v"])