diff --git a/ddev/src/ddev/ai/tools/registry.py b/ddev/src/ddev/ai/tools/registry.py index f8249bfb7fd93..3fd255f7bda1b 100644 --- a/ddev/src/ddev/ai/tools/registry.py +++ b/ddev/src/ddev/ai/tools/registry.py @@ -70,6 +70,7 @@ class ToolSpec: "ddev_env_stop": ToolSpec("shell.ddev.env_stop", "DdevEnvStopTool"), "ddev_env_test": ToolSpec("shell.ddev.env_test", "DdevEnvTestTool"), "ddev_release_changelog": ToolSpec("shell.ddev.release_changelog", "DdevReleaseChangelogTool"), + "ddev_validate": ToolSpec("shell.ddev.validate", "DdevValidateTool"), } diff --git a/ddev/src/ddev/ai/tools/shell/ddev/validate.py b/ddev/src/ddev/ai/tools/shell/ddev/validate.py new file mode 100644 index 0000000000000..0f49834fdf384 --- /dev/null +++ b/ddev/src/ddev/ai/tools/shell/ddev/validate.py @@ -0,0 +1,55 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from typing import Annotated, Literal + +from pydantic import Field + +from ddev.ai.tools.core.base import BaseToolInput +from ddev.ai.tools.shell.base import CmdTool + +ValidateSubcommand = Literal["config", "models", "metadata"] + + +class DdevValidateInput(BaseToolInput): + subcommand: Annotated[ + ValidateSubcommand, + Field( + description=( + "Which validator to run. " + "'config' validates assets/configuration/spec.yaml against data/conf.yaml.example. " + "'models' validates spec.yaml against datadog_checks//config_models/. " + "'metadata' validates metadata.csv. " + ) + ), + ] + integration: Annotated[str, Field(description="Integration name to validate")] + sync: Annotated[ + bool, + Field( + description=( + "Regenerate / auto-fix derived files instead of only checking. " + "For 'config', regenerates conf.yaml.example. " + "For 'models', regenerates config_models/. " + "For 'metadata', rewrites metadata.csv into canonical form. " + ) + ), + ] = False + + +class DdevValidateTool(CmdTool[DdevValidateInput]): + """Validates an integration's spec, config example, config models, or metadata.csv. + Set `sync=true` to regenerate the derived files from spec.yaml.""" + + timeout = 120 + + @property + def name(self) -> str: + return "ddev_validate" + + def cmd(self, tool_input: DdevValidateInput) -> list[str]: + cmd = ["ddev", "--no-interactive", "validate", tool_input.subcommand] + if tool_input.sync: + cmd.append("--sync") + cmd.append(tool_input.integration) + return cmd diff --git a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py index cc4eb72a7e182..0f306e51fe7d6 100644 --- a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py +++ b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py @@ -11,6 +11,7 @@ from ddev.ai.tools.shell.ddev.env_stop import DdevEnvStopTool, EnvStopInput from ddev.ai.tools.shell.ddev.env_test import DdevEnvTestTool, EnvTestInput from ddev.ai.tools.shell.ddev.release_changelog import DdevReleaseChangelogTool, ReleaseChangelogInput +from ddev.ai.tools.shell.ddev.validate import DdevValidateInput, DdevValidateTool # --- ddev create --- @@ -163,3 +164,23 @@ def test_release_changelog_cmd_message_placement(): def test_release_changelog_invalid_change_type_raises(): with pytest.raises(ValidationError): ReleaseChangelogInput(change_type="patch", integration="mycheck", message="Some message") + + +# --- ddev validate --- + + +@pytest.mark.parametrize("subcommand", ["config", "models", "metadata"]) +def test_validate_cmd_all_subcommands(subcommand: str): + cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck")) + assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "mycheck"] + + +@pytest.mark.parametrize("subcommand", ["config", "models", "metadata"]) +def test_validate_cmd_sync_flag_per_subcommand(subcommand: str): + cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck", sync=True)) + assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "--sync", "mycheck"] + + +def test_validate_invalid_subcommand_raises(): + with pytest.raises(ValidationError): + DdevValidateInput(subcommand="lint", integration="mycheck")