Public Access
feat: replace pr_review with spec-driven CI gates and pr-review skill
Add validate_spec, check_pr_size, fast_molecule, nightly_gate, and create_dependency_pr CI modules. Remove the monolithic pr_review module and its tests. Replace pr_review CI steps with validate_spec + check_pr_size + curl-based APPROVE. Add spec-driven-development and pr-review skills. Remove dead get_reviewer_token. Update translations and AGENTS.md. Closes DEVX-155 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
a06caa0e88
commit
aefc22de57
@@ -0,0 +1,223 @@
|
||||
"""Unit tests for devx.ci.validate_spec."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from devx.ci.validate_spec import (
|
||||
AC_CHECKED_RE,
|
||||
AC_UNCHECKED_RE,
|
||||
REQ_ID_RE,
|
||||
cli,
|
||||
find_spec_file,
|
||||
validate_spec_content,
|
||||
)
|
||||
|
||||
VALID_SPEC = """\
|
||||
# OBL-INFRA-531: Fix sso-bridge role for pip install
|
||||
|
||||
## Problem
|
||||
The sso-bridge role uses scripts.sso_bridge.listener but the pip
|
||||
package uses sso_bridge.listener.
|
||||
|
||||
## Approach
|
||||
REQ-1: Update molecule verify.yml to use sso_bridge.listener
|
||||
REQ-2: Add infra repo clone task to sso_bridge role
|
||||
|
||||
## Test Plan
|
||||
- Run molecule test for sso_bridge role
|
||||
- Verify pip package is installed correctly
|
||||
|
||||
## Deploy Plan
|
||||
- Merge PR
|
||||
- Auto-deploy to staging
|
||||
|
||||
## Rollback Plan
|
||||
- Revert PR
|
||||
- Re-deploy previous version
|
||||
|
||||
## Acceptance Criteria
|
||||
- [x] Molecule test passes with sso_bridge.listener
|
||||
- [x] Infra repo is cloned by sso_bridge role
|
||||
"""
|
||||
|
||||
|
||||
SPEC_MISSING_SECTION = """\
|
||||
# OBL-INFRA-531: Fix sso-bridge
|
||||
|
||||
## Problem
|
||||
Something is broken.
|
||||
|
||||
## Approach
|
||||
REQ-1: Fix it
|
||||
|
||||
## Test Plan
|
||||
Run tests
|
||||
"""
|
||||
|
||||
|
||||
SPEC_UNCHECKED_AC = """\
|
||||
# OBL-INFRA-531: Fix sso-bridge
|
||||
|
||||
## Problem
|
||||
Broken.
|
||||
|
||||
## Approach
|
||||
REQ-1: Fix it
|
||||
|
||||
## Test Plan
|
||||
Run tests
|
||||
|
||||
## Deploy Plan
|
||||
Deploy
|
||||
|
||||
## Rollback Plan
|
||||
Revert
|
||||
|
||||
## Acceptance Criteria
|
||||
- [x] Fixed
|
||||
- [ ] Verified in staging
|
||||
"""
|
||||
|
||||
|
||||
SPEC_NO_REQ_IDS = """\
|
||||
# OBL-INFRA-531: Fix sso-bridge
|
||||
|
||||
## Problem
|
||||
Broken.
|
||||
|
||||
## Approach
|
||||
Fix it.
|
||||
|
||||
## Test Plan
|
||||
Run tests
|
||||
|
||||
## Deploy Plan
|
||||
Deploy
|
||||
|
||||
## Rollback Plan
|
||||
Revert
|
||||
|
||||
## Acceptance Criteria
|
||||
- [x] Fixed
|
||||
"""
|
||||
|
||||
|
||||
class TestFindSpecFile:
|
||||
def test_finds_exact_match(self, tmp_path: Path) -> None:
|
||||
specs_dir = tmp_path / "specs"
|
||||
specs_dir.mkdir()
|
||||
(specs_dir / "OBL-INFRA-531.md").write_text("content")
|
||||
result = find_spec_file("OBL-INFRA-531", str(specs_dir))
|
||||
assert result is not None
|
||||
assert result.name == "OBL-INFRA-531.md"
|
||||
|
||||
def test_finds_case_insensitive(self, tmp_path: Path) -> None:
|
||||
specs_dir = tmp_path / "specs"
|
||||
specs_dir.mkdir()
|
||||
(specs_dir / "obl-infra-531.md").write_text("content")
|
||||
result = find_spec_file("OBL-INFRA-531", str(specs_dir))
|
||||
assert result is not None
|
||||
|
||||
def test_returns_none_when_not_found(self, tmp_path: Path) -> None:
|
||||
specs_dir = tmp_path / "specs"
|
||||
specs_dir.mkdir()
|
||||
result = find_spec_file("OBL-INFRA-999", str(specs_dir))
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_when_dir_missing(self, tmp_path: Path) -> None:
|
||||
result = find_spec_file("OBL-INFRA-531", str(tmp_path / "nonexistent"))
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestValidateSpecContent:
|
||||
def test_valid_spec_passes(self) -> None:
|
||||
errors = validate_spec_content(VALID_SPEC)
|
||||
assert errors == []
|
||||
|
||||
def test_missing_sections(self) -> None:
|
||||
errors = validate_spec_content(SPEC_MISSING_SECTION)
|
||||
assert len(errors) >= 3 # Missing Deploy Plan, Rollback Plan, Acceptance Criteria
|
||||
assert any("Deploy Plan" in e for e in errors)
|
||||
assert any("Rollback Plan" in e for e in errors)
|
||||
assert any("Acceptance Criteria" in e for e in errors)
|
||||
|
||||
def test_unchecked_ac_fails(self) -> None:
|
||||
errors = validate_spec_content(SPEC_UNCHECKED_AC)
|
||||
assert len(errors) == 1
|
||||
assert "unchecked" in errors[0].lower()
|
||||
|
||||
def test_no_req_ids_fails(self) -> None:
|
||||
errors = validate_spec_content(SPEC_NO_REQ_IDS)
|
||||
assert any("REQ-ID" in e for e in errors)
|
||||
|
||||
def test_empty_content_fails(self) -> None:
|
||||
errors = validate_spec_content("")
|
||||
assert len(errors) >= 2 # Missing sections + no REQ-IDs
|
||||
|
||||
|
||||
class TestRegexPatterns:
|
||||
def test_req_id_re_matches(self) -> None:
|
||||
assert REQ_ID_RE.search("REQ-1: Do something")
|
||||
assert REQ_ID_RE.search("REQ-42: Another thing")
|
||||
assert not REQ_ID_RE.search("REQ: no number")
|
||||
|
||||
def test_ac_checked_re_matches(self) -> None:
|
||||
assert AC_CHECKED_RE.search("- [x] Done")
|
||||
assert AC_CHECKED_RE.search(" - [x] Indented")
|
||||
assert not AC_CHECKED_RE.search("- [ ] Not done")
|
||||
|
||||
def test_ac_unchecked_re_matches(self) -> None:
|
||||
assert AC_UNCHECKED_RE.search("- [ ] Not done")
|
||||
assert AC_UNCHECKED_RE.search(" - [ ] Indented")
|
||||
assert not AC_UNCHECKED_RE.search("- [x] Done")
|
||||
|
||||
|
||||
class TestCli:
|
||||
def test_fails_without_task_id(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--branch", "no-task-id"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_allow_missing_succeeds_without_task_id(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--branch", "no-task-id", "--allow-missing"])
|
||||
assert result.exit_code == 0
|
||||
assert "WARNING" in result.output
|
||||
|
||||
def test_fails_when_spec_not_found(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-999"):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["--branch", "OBL-INFRA-999-test", "--specs-dir", str(tmp_path / "specs")],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "No spec file found" in result.output
|
||||
|
||||
def test_passes_with_valid_spec(self, tmp_path: Path) -> None:
|
||||
specs_dir = tmp_path / "specs"
|
||||
specs_dir.mkdir()
|
||||
(specs_dir / "OBL-INFRA-531.md").write_text(VALID_SPEC)
|
||||
runner = CliRunner()
|
||||
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-531"):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["--branch", "OBL-INFRA-531-fix-foo", "--specs-dir", str(specs_dir)],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Spec validated" in result.output
|
||||
|
||||
def test_fails_with_unchecked_ac(self, tmp_path: Path) -> None:
|
||||
specs_dir = tmp_path / "specs"
|
||||
specs_dir.mkdir()
|
||||
(specs_dir / "OBL-INFRA-531.md").write_text(SPEC_UNCHECKED_AC)
|
||||
runner = CliRunner()
|
||||
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-531"):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["--branch", "OBL-INFRA-531-fix-foo", "--specs-dir", str(specs_dir)],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "unchecked" in result.output.lower()
|
||||
Reference in New Issue
Block a user