From 23645ae81f2b20f9763080bc6aa34250b7eb6da0 Mon Sep 17 00:00:00 2001 From: Ritwij Aryan Parmar Date: Tue, 26 May 2026 17:38:15 -0400 Subject: [PATCH] Validate capy config step payloads --- src/capy_config.py | 34 +++++++++++++++++++++++++++++ src/models.py | 21 ++---------------- tests/test_capy_config.py | 45 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 src/capy_config.py create mode 100644 tests/test_capy_config.py diff --git a/src/capy_config.py b/src/capy_config.py new file mode 100644 index 0000000..a43e5a2 --- /dev/null +++ b/src/capy_config.py @@ -0,0 +1,34 @@ +from typing import List, Literal, Optional + +from pydantic import BaseModel, Field, model_validator + + +class CapyStep(BaseModel): + """capy.yaml step""" + + type: Literal["bash", "create-env", "instruction", "wait"] = Field( + description="Type of step to execute" + ) + command: Optional[str] = None + text: Optional[str] = None + seconds: Optional[int] = None + + @model_validator(mode="after") + def validate_step_payload(self) -> "CapyStep": + """Reject capy.yaml steps that would fail later during setup.""" + if self.type == "bash" and not (self.command and self.command.strip()): + raise ValueError("bash steps require a non-empty command") + + if self.type == "instruction" and not (self.text and self.text.strip()): + raise ValueError("instruction steps require non-empty text") + + if self.type == "wait" and self.seconds is not None and self.seconds <= 0: + raise ValueError("wait steps require seconds to be greater than 0") + + return self + + +class CapyConfig(BaseModel): + """capy.yaml config""" + + steps: List[CapyStep] diff --git a/src/models.py b/src/models.py index 9d7986c..4118fcc 100644 --- a/src/models.py +++ b/src/models.py @@ -1,29 +1,12 @@ from typing import List, Literal, Optional -from pydantic import BaseModel, Field, ConfigDict +from pydantic import BaseModel, ConfigDict from scrapybara.client import Step from .generate.models import TestCase from .execute.models import TestResult as BaseTestResult +from .capy_config import CapyConfig, CapyStep from datetime import datetime -# capy.yaml -class CapyStep(BaseModel): - """capy.yaml step""" - - type: Literal["bash", "create-env", "instruction", "wait"] = Field( - description="Type of step to execute" - ) - command: Optional[str] = None - text: Optional[str] = None - seconds: Optional[int] = None - - -class CapyConfig(BaseModel): - """capy.yaml config""" - - steps: List[CapyStep] - - # Review class TimestampedStep(Step): """Base model for all steps in the review process, extending Scrapybara Step""" diff --git a/tests/test_capy_config.py b/tests/test_capy_config.py new file mode 100644 index 0000000..adf212d --- /dev/null +++ b/tests/test_capy_config.py @@ -0,0 +1,45 @@ +import pytest +from pydantic import ValidationError + +from src.capy_config import CapyConfig + + +def test_accepts_valid_capy_steps(): + config = CapyConfig.model_validate( + { + "steps": [ + {"type": "bash", "command": "npm install"}, + {"type": "create-env"}, + {"type": "instruction", "text": "Open http://localhost:3000"}, + {"type": "wait", "seconds": 5}, + {"type": "wait"}, + ] + } + ) + + assert len(config.steps) == 5 + + +@pytest.mark.parametrize( + ("step", "message"), + [ + ({"type": "bash"}, "bash steps require a non-empty command"), + ({"type": "bash", "command": " "}, "bash steps require a non-empty command"), + ({"type": "instruction"}, "instruction steps require non-empty text"), + ( + {"type": "instruction", "text": ""}, + "instruction steps require non-empty text", + ), + ( + {"type": "wait", "seconds": 0}, + "wait steps require seconds to be greater than 0", + ), + ( + {"type": "wait", "seconds": -1}, + "wait steps require seconds to be greater than 0", + ), + ], +) +def test_rejects_invalid_capy_steps(step, message): + with pytest.raises(ValidationError, match=message): + CapyConfig.model_validate({"steps": [step]})