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
+251
@@ -0,0 +1,251 @@
|
||||
"""Published-schema parity and all-declaration validation, without commands."""
|
||||
|
||||
from copy import deepcopy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools/devkit"))
|
||||
from govoplan_devkit.workspace import load_project # noqa: E402
|
||||
from govoplan_devkit.validation import _schema_value # noqa: E402
|
||||
|
||||
|
||||
def valid():
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"repositories": [{"name": "example", "path": "example"}],
|
||||
"checks": [
|
||||
{"id": "selected", "argv": ["true"]},
|
||||
{"id": "unselected", "argv": ["true"]},
|
||||
],
|
||||
"profiles": {"quick": ["selected"], "full": ["unselected"]},
|
||||
}
|
||||
|
||||
|
||||
def check(tmp_path, value):
|
||||
path = tmp_path / "project.json"
|
||||
path.write_text(json.dumps(value))
|
||||
return load_project(tmp_path, path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,value",
|
||||
[
|
||||
("resource", ["shared"]),
|
||||
("timout_seconds", 20),
|
||||
("cwd", "../escape"),
|
||||
("cwd", "/outside"),
|
||||
("argv", [""]),
|
||||
("argv", ["true", "x" * 8193]),
|
||||
("argv", ["true"] * 257),
|
||||
("argv", ["true", "bad\0value"]),
|
||||
("resources", ["a", "a"]),
|
||||
("resources", [""]),
|
||||
("resources", ["x" * 257]),
|
||||
("resources", ["bad\nname"]),
|
||||
("resources", True),
|
||||
("deps", ["selected", "selected"]),
|
||||
("repos", ["example", "example"]),
|
||||
("title", 10),
|
||||
("title", "x" * 1025),
|
||||
("timeout_seconds", True),
|
||||
("timeout_seconds", 43201),
|
||||
("timeout_seconds", 0),
|
||||
("id", "invalid\n"),
|
||||
],
|
||||
)
|
||||
def test_unselected_checks_obey_the_published_schema(tmp_path, field, value):
|
||||
project = valid()
|
||||
project["checks"][1][field] = value
|
||||
schema = json.loads((ROOT / "tools/devkit/project.schema.json").read_text())
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
jsonschema.validate(project, schema)
|
||||
with pytest.raises(ValueError):
|
||||
check(tmp_path, project)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutation",
|
||||
[
|
||||
lambda value: value["profiles"].update(typo=[]),
|
||||
lambda value: value["profiles"].update(full=["unselected", "unselected"]),
|
||||
lambda value: value["tools"].update(pyton="python"),
|
||||
lambda value: value["tools"].update(python="bad\0tool"),
|
||||
lambda value: value["review"].update(principle="rules.md"),
|
||||
lambda value: value["review"].update(principles="../rules.md"),
|
||||
lambda value: value["repositories"][0].update(alias="other"),
|
||||
lambda value: value["repositories"][0].update(aliases=["alias", "alias"]),
|
||||
lambda value: value.update(checks=value["checks"] * 257),
|
||||
lambda value: value.update(schema_version=True),
|
||||
],
|
||||
)
|
||||
def test_all_nested_shape_constraints_match_schema(tmp_path, mutation):
|
||||
project = valid()
|
||||
project.update(tools={}, review={})
|
||||
mutation(project)
|
||||
schema = json.loads((ROOT / "tools/devkit/project.schema.json").read_text())
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
jsonschema.validate(project, schema)
|
||||
with pytest.raises(ValueError):
|
||||
check(tmp_path, project)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutation,reason",
|
||||
[
|
||||
(
|
||||
lambda value: value["checks"][1].update(deps=["missing"]),
|
||||
"Unknown check dependency",
|
||||
),
|
||||
(lambda value: value["checks"][1].update(deps=["unselected"]), "Cyclic"),
|
||||
(
|
||||
lambda value: value["checks"][1].update(repos=["missing"]),
|
||||
"Unknown repository",
|
||||
),
|
||||
(lambda value: value["profiles"].update(full=["missing"]), "Unknown profile"),
|
||||
(lambda value: value["checks"][1].update(id="selected"), "Duplicate"),
|
||||
(
|
||||
lambda value: value["repositories"].append(
|
||||
{"name": "other", "path": "example"}
|
||||
),
|
||||
"Duplicate project repository path",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_all_semantic_references_are_checked_even_outside_selected_profile(
|
||||
tmp_path, mutation, reason
|
||||
):
|
||||
project = valid()
|
||||
mutation(project)
|
||||
with pytest.raises(ValueError, match=reason):
|
||||
check(tmp_path, project)
|
||||
|
||||
|
||||
def test_valid_defaults_boundaries_and_dependency_order(tmp_path):
|
||||
project = valid()
|
||||
project["checks"][0].update(
|
||||
deps=["unselected"], argv=["true", ""], timeout_seconds=0.1
|
||||
)
|
||||
schema = json.loads((ROOT / "tools/devkit/project.schema.json").read_text())
|
||||
jsonschema.validate(project, schema)
|
||||
assert check(tmp_path, project).config == project
|
||||
|
||||
|
||||
def test_runtime_shape_uses_only_implemented_schema_keywords():
|
||||
schema = json.loads((ROOT / "tools/devkit/project.schema.json").read_text())
|
||||
permitted = {
|
||||
"$schema",
|
||||
"$defs",
|
||||
"$ref",
|
||||
"title",
|
||||
"description",
|
||||
"type",
|
||||
"const",
|
||||
"properties",
|
||||
"additionalProperties",
|
||||
"required",
|
||||
"minItems",
|
||||
"maxItems",
|
||||
"uniqueItems",
|
||||
"prefixItems",
|
||||
"items",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"pattern",
|
||||
"maximum",
|
||||
"exclusiveMinimum",
|
||||
"enum",
|
||||
}
|
||||
|
||||
def visit(node):
|
||||
assert set(node) <= permitted
|
||||
for group in ("properties", "$defs"):
|
||||
for child in node.get(group, {}).values():
|
||||
visit(child)
|
||||
if "items" in node:
|
||||
visit(node["items"])
|
||||
for child in node.get("prefixItems", []):
|
||||
visit(child)
|
||||
|
||||
visit(schema)
|
||||
_schema_value(valid(), schema, schema["$defs"], "project")
|
||||
|
||||
|
||||
def test_huge_numeric_input_is_a_controlled_bound_error(tmp_path):
|
||||
project = deepcopy(valid())
|
||||
project["checks"][1]["timeout_seconds"] = 10**1000
|
||||
with pytest.raises(ValueError):
|
||||
check(tmp_path, project)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,value",
|
||||
[
|
||||
("inputs", {}),
|
||||
("inputs", None),
|
||||
("inputs", {"repos": []}),
|
||||
("inputs", {"paths": ["src/**"]}),
|
||||
("inputs", {"repos": ["example", "example"]}),
|
||||
("inputs", {"repos": [""]}),
|
||||
("inputs", {"repos": ["example"], "glob": "*"}),
|
||||
("after", ["selected", "selected"]),
|
||||
("after", [""]),
|
||||
("after", "selected"),
|
||||
("reuse", True),
|
||||
("reuse", "always"),
|
||||
],
|
||||
)
|
||||
def test_input_order_and_reuse_shapes_apply_to_unselected_checks(
|
||||
tmp_path, field, value
|
||||
):
|
||||
project = valid()
|
||||
project["checks"][1][field] = value
|
||||
schema = json.loads((ROOT / "tools/devkit/project.schema.json").read_text())
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
jsonschema.validate(project, schema)
|
||||
with pytest.raises(ValueError):
|
||||
check(tmp_path, project)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change,reason",
|
||||
[
|
||||
({"inputs": {"repos": ["unknown"]}}, "Unknown input repository"),
|
||||
({"after": ["unknown"]}, "Unknown check ordering"),
|
||||
({"after": ["unselected"]}, "Cyclic"),
|
||||
(
|
||||
{"deps": ["selected"], "after": ["selected"]},
|
||||
"Duplicate dependency/ordering",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_input_and_order_semantics_are_validated_before_selection(
|
||||
tmp_path, change, reason
|
||||
):
|
||||
project = valid()
|
||||
project["checks"][1].update(change)
|
||||
with pytest.raises(ValueError, match=reason):
|
||||
check(tmp_path, project)
|
||||
|
||||
|
||||
def test_dependencies_and_order_only_edges_share_cycle_validation(tmp_path):
|
||||
project = valid()
|
||||
project["checks"][0]["after"] = ["unselected"]
|
||||
project["checks"][1]["deps"] = ["selected"]
|
||||
with pytest.raises(ValueError, match="Cyclic"):
|
||||
check(tmp_path, project)
|
||||
|
||||
|
||||
def test_valid_explicit_inputs_after_and_reuse_match_published_schema(tmp_path):
|
||||
project = valid()
|
||||
project["checks"][0].update(
|
||||
inputs={"repos": ["example"]}, after=["unselected"], reuse="verified"
|
||||
)
|
||||
project["checks"][1]["reuse"] = "never"
|
||||
schema = json.loads((ROOT / "tools/devkit/project.schema.json").read_text())
|
||||
jsonschema.validate(project, schema)
|
||||
assert check(tmp_path, project).config == project
|
||||
Reference in New Issue
Block a user