Files
govoplan/tests/test_devkit_doctor.py
zemion 2ffdb23f69
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s
feat(devkit): add resumable workspace automation and UI review tooling
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:17 +02:00

165 lines
5.6 KiB
Python
Executable File

"""Portable preflight inference uses fixtures only and never installs or starts tools."""
from argparse import Namespace
from pathlib import Path
import sys
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import doctor
from govoplan_devkit.workspace import Project, Repository
def project(tmp_path, checks, *, tools=None, profiles=None):
return Project(
"Fixture",
(Repository("app", tmp_path / "app"), Repository("other", tmp_path / "other")),
{
"checks": checks,
"profiles": profiles
if profiles is not None
else {"quick": [item["id"] for item in checks]},
"tools": tools or {},
},
)
def check(identity, executable, *, repos=None, deps=None):
return {
"id": identity,
"argv": [executable, "--version"],
"cwd": ".",
"repos": repos or [],
"deps": deps or [],
}
def diagnose(tmp_path, configured, *, profile=None, repos=None, versions=None):
args = Namespace(
workspace_root=tmp_path,
project=tmp_path / "not-read.json",
repo=repos or [],
profile=profile,
)
tools = {"python": sys.executable, "node": "fixture-node", "npm": "fixture-npm"}
version_map = versions or {
sys.executable: "Python 3",
"fixture-node": "unavailable",
"fixture-npm": "unavailable",
}
with (
patch.object(doctor, "load_project", return_value=configured),
patch.object(doctor, "resolve_tools", return_value=tools),
patch.object(
doctor,
"tool_version",
side_effect=lambda executable, _env: version_map[executable],
) as probe,
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
):
result = doctor.diagnose(args)
return result, probe
def test_python_only_portable_project_does_not_probe_or_block_on_unused_node(tmp_path):
configured = project(tmp_path, [check("python-test", "{python}")])
result, probe = diagnose(tmp_path, configured)
assert result["_exit_code"] == 0
assert result["required_tools"] == ["python"]
assert probe.call_count == 1
assert {
item["id"] for item in result["checks"] if item["status"] == "not_required"
} == {"node", "npm"}
def test_selected_profile_excludes_other_profile_tool_requirements(tmp_path):
configured = project(
tmp_path,
[check("py", "{python}"), check("js", "{npm}")],
profiles={"quick": ["py"], "ui": ["js"]},
)
result, _ = diagnose(tmp_path, configured, profile="quick")
assert result["required_tools"] == ["python"]
result, _ = diagnose(tmp_path, configured)
assert result["required_tools"] == ["node", "npm", "python"]
assert result["_exit_code"] == 1
def test_selected_repository_includes_dependency_tool_requirements(tmp_path):
configured = project(
tmp_path,
[
check("build", "{npm}", repos=["other"]),
check("test", "{python}", repos=["app"], deps=["build"]),
],
)
result, _ = diagnose(tmp_path, configured, repos=["app"])
assert result["required_tools"] == ["node", "npm", "python"]
def test_unselected_repository_does_not_require_its_tool(tmp_path):
configured = project(
tmp_path,
[check("js", "{npm}", repos=["other"]), check("py", "{python}", repos=["app"])],
)
result, _ = diagnose(tmp_path, configured, repos=["app"])
assert result["required_tools"] == ["python"]
def test_explicit_tool_configuration_declares_indirect_script_dependency(tmp_path):
configured = project(tmp_path, [check("shell", "sh")], tools={"npm": "fixture-npm"})
result, _ = diagnose(tmp_path, configured)
assert result["required_tools"] == ["node", "npm", "python"]
assert result["_exit_code"] == 1
@pytest.mark.parametrize(
"executable,expected",
[
("/opt/node/bin/node", ["node", "python"]),
("npm", ["node", "npm", "python"]),
("npx", ["node", "npm", "python"]),
],
)
def test_direct_tool_names_are_inferred(tmp_path, executable, expected):
result, _ = diagnose(tmp_path, project(tmp_path, [check("test", executable)]))
assert result["required_tools"] == expected
def test_context_only_project_needs_no_node_and_makes_no_files(tmp_path):
before = set(tmp_path.iterdir())
result, _ = diagnose(tmp_path, project(tmp_path, [], profiles={}))
assert result["required_tools"] == ["python"]
assert set(tmp_path.iterdir()) == before
def test_missing_profile_is_not_silently_widened(tmp_path):
with pytest.raises(ValueError, match="does not declare profile"):
diagnose(tmp_path, project(tmp_path, []), profile="ui")
def test_native_govoplan_still_requires_all_three_tools(tmp_path):
configured = project(tmp_path, [])
args = Namespace(workspace_root=tmp_path, project=None, repo=[], profile=None)
with (
patch.object(doctor, "load_project", return_value=configured),
patch.object(
doctor,
"resolve_tools",
return_value={"python": sys.executable, "node": "node", "npm": "npm"},
),
patch.object(
doctor,
"tool_version",
side_effect=lambda executable, _env: (
"Python" if executable == sys.executable else "unavailable"
),
),
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
):
result = doctor.diagnose(args)
assert result["required_tools"] == ["node", "npm", "python"]
assert result["_exit_code"] == 1