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.
This commit is contained in:
Executable
+164
@@ -0,0 +1,164 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user