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
+175
@@ -0,0 +1,175 @@
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit import review
|
||||
from govoplan_devkit.cli import main
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def args(tmp_path):
|
||||
repo = tmp_path / "optional-feature"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q", str(repo)], check=True)
|
||||
paths = ["webui/src/pages/CampaignPage.tsx", "webui/src/dialogs/SettingsDialog.tsx",
|
||||
"webui/src/settings/AdminSettings.tsx", "webui/src/widgets/SummaryWidget.tsx", "webui/src/module.ts"]
|
||||
for relative in paths:
|
||||
path = repo / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("// fixture only\n")
|
||||
manifest = repo / "src/optional_feature/backend/manifest.py"
|
||||
manifest.parent.mkdir(parents=True)
|
||||
manifest.write_text("raise RuntimeError('MUST NEVER IMPORT MODULE CODE')\n")
|
||||
(tmp_path / "principles.md").write_text("# Principles\nRevision **UI-2026-09-08**\n\n## UI-01 — Help beside text\n\n## UI-02 — Display first; edit deliberately\n")
|
||||
inventory = {"schema_version": 1, "epic": {"repository": "meta", "number": 4, "url": "https://gitea.invalid/team/meta/issues/4"},
|
||||
"issues": [{"scope_id": "campaigns", "name": "Campaign", "kind": "manifest", "repository": "optional-feature", "number": 8,
|
||||
"url": "https://gitea.invalid/team/optional-feature/issues/8", "state_at_verification": "closed", "operation": "created"}]}
|
||||
(tmp_path / "issues.json").write_text(json.dumps(inventory))
|
||||
project = tmp_path / "project.json"
|
||||
project.write_text(json.dumps({"schema_version": 1, "name": "Portable project", "repositories": [
|
||||
{"name": "optional-feature", "path": "optional-feature", "aliases": ["campaign"]},
|
||||
{"name": "absent-optional", "path": "absent-optional", "aliases": ["absent"]}],
|
||||
"review": {"issue_inventory": "issues.json", "principles": "principles.md"},
|
||||
"checks": [{"id": "ui-check", "argv": ["never-execute-this"], "repos": ["optional-feature"], "cwd": "optional-feature"}],
|
||||
"profiles": {"ui": ["ui-check"]}}))
|
||||
return argparse.Namespace(workspace_root=tmp_path, project=project, state_dir=tmp_path / "state",
|
||||
module="campaign", bundle_module=None, profile="ui", evidence=None, output=None)
|
||||
|
||||
|
||||
def test_bundle_contains_guidance_links_revision_and_unexecuted_check_plan(args):
|
||||
result = review.handle_review(args)
|
||||
assert result["module"] == "optional-feature"
|
||||
assert result["inventory"]["ui_source_count"] == 5
|
||||
assert not result["inventory"]["module_code_imported"]
|
||||
assert result["inventory"]["manifest_paths"] == ["src/optional_feature/backend/manifest.py"]
|
||||
assert result["principles"]["revision"] == "UI-2026-09-08"
|
||||
assert [rule["id"] for rule in result["principles"]["rules"]] == ["UI-01", "UI-02"]
|
||||
assert result["check_plan"]["stages"][0]["argv"] == ["never-execute-this"]
|
||||
assert not result["check_plan"]["executed"]
|
||||
assert result["review_completion"].startswith("Not assessed")
|
||||
assert len(result["manual_checklist"]) >= 9
|
||||
assert all("checked" not in item for item in result["manual_checklist"])
|
||||
assert "state_at_verification" not in json.dumps(result)
|
||||
assert "operation" not in result["issue_links"][0]
|
||||
assert result["central_issue"]["number"] == 4
|
||||
assert not args.state_dir.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["campaign", "campaigns", "optional-feature"])
|
||||
def test_alias_scope_id_and_repository_resolve_to_same_registered_module(args, name):
|
||||
args.module = name
|
||||
assert review.handle_review(args)["module"] == "optional-feature"
|
||||
|
||||
|
||||
def test_bundle_alias_and_direct_cli_work_with_global_options_anywhere(args, capsys):
|
||||
assert main(["--workspace-root", str(args.workspace_root), "review", "campaign", "--project", str(args.project), "--json"]) == 0
|
||||
direct = json.loads(capsys.readouterr().out)
|
||||
assert direct["module"] == "optional-feature"
|
||||
assert main(["review", "bundle", "campaigns", "--workspace-root", str(args.workspace_root), "--project", str(args.project), "--format", "json"]) == 0
|
||||
assert json.loads(capsys.readouterr().out)["module"] == direct["module"]
|
||||
|
||||
|
||||
def test_absent_optional_repository_and_no_ui_do_not_complete_review(args):
|
||||
args.module = "absent"
|
||||
result = review.handle_review(args)
|
||||
assert not result["repository"]["exists"]
|
||||
assert result["repository"]["errors"]
|
||||
assert result["inventory"]["ui_source_count"] == 0
|
||||
assert result["check_plan"]["stages"] == []
|
||||
assert result["review_completion"].startswith("Not assessed")
|
||||
assert any("not automatic N/A" in warning for warning in result["warnings"])
|
||||
assert any("not a passing" in warning for warning in result["warnings"])
|
||||
|
||||
|
||||
def test_portable_project_does_not_inherit_govoplan_links_or_principles(args):
|
||||
payload = json.loads(args.project.read_text())
|
||||
payload.pop("review")
|
||||
args.project.write_text(json.dumps(payload))
|
||||
result = review.handle_review(args)
|
||||
assert result["issue_links"] == [] and result["central_issue"] is None
|
||||
assert not result["principles"]["available"]
|
||||
assert "git.add-ideas.de" not in json.dumps(result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["symlink", "outside", "bad-url", "duplicate-scope", "foreign-repository"])
|
||||
def test_review_inputs_cannot_escape_or_silently_misbind_scope(args, kind):
|
||||
if kind in {"symlink", "outside"}:
|
||||
payload = json.loads(args.project.read_text())
|
||||
if kind == "symlink":
|
||||
alias = args.workspace_root / "alias.json"
|
||||
alias.symlink_to(args.workspace_root / "issues.json")
|
||||
payload["review"]["issue_inventory"] = "alias.json"
|
||||
else:
|
||||
payload["review"]["principles"] = "../outside.md"
|
||||
args.project.write_text(json.dumps(payload))
|
||||
else:
|
||||
path = args.workspace_root / "issues.json"
|
||||
payload = json.loads(path.read_text())
|
||||
if kind == "bad-url":
|
||||
payload["issues"][0]["url"] = "https://gitea.invalid/team/wrong/issues/8"
|
||||
elif kind == "duplicate-scope":
|
||||
payload["issues"].append(payload["issues"][0])
|
||||
else:
|
||||
payload["issues"][0].update(repository="foreign", url="https://gitea.invalid/team/foreign/issues/8")
|
||||
args.module = "campaigns"
|
||||
path.write_text(json.dumps(payload))
|
||||
with pytest.raises(ValueError):
|
||||
review.handle_review(args)
|
||||
|
||||
|
||||
def test_source_inventory_never_follows_symlinked_ancestors(args):
|
||||
root = args.workspace_root / "indirect"
|
||||
root.mkdir()
|
||||
(root / "webui").symlink_to(args.workspace_root / "optional-feature/webui", target_is_directory=True)
|
||||
result = review.source_inventory(root)
|
||||
assert result["ui_source_count"] == 0
|
||||
assert result["skipped_symlinks"] == ["webui/src"]
|
||||
|
||||
|
||||
def test_explicit_artifact_is_only_local_and_preserves_issue_discovery_snapshot(args):
|
||||
inventory = args.workspace_root / "issues.json"
|
||||
original = inventory.read_bytes()
|
||||
args.output = args.workspace_root / "artifacts/review.json"
|
||||
result = review.handle_review(args)
|
||||
assert json.loads(args.output.read_text()) == result
|
||||
assert inventory.read_bytes() == original
|
||||
assert args.output.stat().st_mode & 0o777 == 0o600
|
||||
assert not args.state_dir.exists()
|
||||
|
||||
|
||||
def test_attached_historical_receipt_stays_separate_from_current_review(args):
|
||||
path = args.workspace_root / "historical.json"
|
||||
path.write_text(json.dumps({"schema_version": 1, "run_id": "historical", "workspace_root": str(args.workspace_root),
|
||||
"project_file": str(args.project), "source_fingerprint": "a" * 64, "status": "passed", "snapshot_verified": True, "generated_at": "2026-09-08",
|
||||
"finished_at": "2026-09-08", "stages": [{"id": "old-check", "status": "passed", "exit_code": 0, "duration_seconds": 1, "log_path": "/not-read"}]}))
|
||||
args.evidence = str(path)
|
||||
result = review.handle_review(args)
|
||||
assert result["evidence"]["origin"] == "external-unverified"
|
||||
assert result["evidence"]["source_state"] == "historical-source-differs"
|
||||
assert result["review_completion"].startswith("Not assessed")
|
||||
assert not result["check_plan"]["executed"]
|
||||
|
||||
|
||||
def test_unknown_module_and_extra_positionals_are_errors(args):
|
||||
args.module = "typo"
|
||||
with pytest.raises(ValueError, match="Unknown repository"):
|
||||
review.handle_review(args)
|
||||
args.module, args.bundle_module = "campaign", "extra"
|
||||
with pytest.raises(ValueError, match="one module"):
|
||||
review.handle_review(args)
|
||||
|
||||
|
||||
def test_compact_review_exposes_scoped_coverage_limits_without_hiding_full_list(args, monkeypatch):
|
||||
notes = [f"Separate suite {number} was not included." for number in range(12)]
|
||||
monkeypatch.setattr("govoplan_devkit.catalog.build_stages", lambda *_args, **_kwargs: [
|
||||
{"id": "source-only", "argv": ["never-execute"], "coverage_notes": notes}])
|
||||
result = review.handle_review(args)
|
||||
assert result["coverage_notes"] == notes
|
||||
assert sum(line.startswith("Coverage limitation:") for line in result["summary"]) == 8
|
||||
assert any("4 additional coverage limitations" in line for line in result["summary"])
|
||||
assert not result["check_plan"]["executed"]
|
||||
Reference in New Issue
Block a user