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.
165 lines
5.6 KiB
Python
Executable File
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
|