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.
252 lines
7.8 KiB
Python
Executable File
252 lines
7.8 KiB
Python
Executable File
"""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
|