Files
govoplan/tests/test_devkit_validation.py
T
zemion 2ffdb23f69
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s
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.
2026-09-09 02:03:17 +02:00

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