"""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