From 83ad9631938887879e0b978a01494b50e2c842a4 Mon Sep 17 00:00:00 2001 From: emil Date: Mon, 24 Aug 2026 20:04:14 +0200 Subject: [PATCH] fix: skip spec-driven workflow tests when sibling repos not checked out Add _skip_if_repo_missing guard to skill tests that reference sibling repos (infra, grm, sso-bridge). In CI, only devx is checked out, so these tests skip instead of failing with FileNotFoundError. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/unit/test_spec_driven_workflows.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/unit/test_spec_driven_workflows.py b/tests/unit/test_spec_driven_workflows.py index 6f862e8..3af7894 100644 --- a/tests/unit/test_spec_driven_workflows.py +++ b/tests/unit/test_spec_driven_workflows.py @@ -36,6 +36,22 @@ def _load_workflow(repo_path: Path, filename: str) -> dict: return yaml.safe_load(f) +def _skip_if_repo_missing(repo_name: str) -> None: + """Skip test if the sibling repo directory doesn't exist (CI only checks out one repo).""" + repo_path = _OBLACHNO_ROOT / repo_name + if not repo_path.is_dir(): + pytest.skip(f"Repo {repo_name} not found at {repo_path} (CI only checks out devx)") + + +def _read_skill(repo_name: str, skill_name: str) -> str: + """Read a skill file from a repo, skipping if the repo or file doesn't exist.""" + _skip_if_repo_missing(repo_name) + skill_path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / skill_name / "SKILL.md" + if not skill_path.exists(): + pytest.skip(f"SKILL.md not found in {repo_name}/{skill_name}") + return skill_path.read_text(encoding="utf-8") + + def _get_step_names(job: dict) -> list[str]: """Extract step names from a job dict.""" names = [] @@ -426,11 +442,13 @@ class TestSpecDrivenDevelopmentSkill: @pytest.mark.parametrize("repo_name", ["infra", "grm", "sso-bridge", "devx"]) def test_skill_exists_in_repo(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) skill_path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "spec-driven-development" / "SKILL.md" assert skill_path.exists(), f"SKILL.md not found in {repo_name}" @pytest.mark.parametrize("repo_name", ["infra", "grm", "sso-bridge", "devx"]) def test_skill_has_required_sections(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) skill_path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "spec-driven-development" / "SKILL.md" content = skill_path.read_text(encoding="utf-8") for section in self.REQUIRED_SECTIONS: @@ -438,18 +456,21 @@ class TestSpecDrivenDevelopmentSkill: @pytest.mark.parametrize("repo_name", ["infra", "grm", "sso-bridge", "devx"]) def test_skill_mentions_req_ids(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) skill_path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "spec-driven-development" / "SKILL.md" content = skill_path.read_text(encoding="utf-8") assert "REQ-" in content, f"SKILL.md in {repo_name} must mention REQ-ID format" @pytest.mark.parametrize("repo_name", ["infra", "grm", "sso-bridge", "devx"]) def test_skill_mentions_pr_size_limit(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) skill_path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "spec-driven-development" / "SKILL.md" content = skill_path.read_text(encoding="utf-8") assert "500" in content, f"SKILL.md in {repo_name} must mention 500 line PR size limit" @pytest.mark.parametrize("repo_name", ["infra", "grm", "sso-bridge", "devx"]) def test_skill_mentions_nightly_gate(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) skill_path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "spec-driven-development" / "SKILL.md" content = skill_path.read_text(encoding="utf-8") assert "nightly" in content.lower(), f"SKILL.md in {repo_name} must mention nightly gate" @@ -470,23 +491,27 @@ class TestDevxWorkflowSkill: @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_skill_exists(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "devx-workflow" / "SKILL.md" assert path.exists(), f"devx-workflow SKILL.md not found in {repo_name}" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_spec_validation(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "devx-workflow" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "validate_spec" in content, f"devx-workflow skill in {repo_name} must mention validate_spec" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_pr_size_check(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "devx-workflow" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "check_pr_size" in content, f"devx-workflow skill in {repo_name} must mention check_pr_size" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_pr_workflow_commands(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "devx-workflow" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "make create-pr" in content or "make push-with-pr" in content, ( @@ -495,6 +520,7 @@ class TestDevxWorkflowSkill: @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_auto_merge(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "devx-workflow" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "auto-merge" in content.lower() or "ready-to-merge" in content, ( @@ -503,6 +529,7 @@ class TestDevxWorkflowSkill: @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_has_correct_task_prefix(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) """Each repo's devx-workflow skill must mention its correct task prefix.""" path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "devx-workflow" / "SKILL.md" content = path.read_text(encoding="utf-8") @@ -559,11 +586,13 @@ class TestTestingAndDebuggingSkill: @pytest.mark.parametrize("repo_name", ALL_REPOS) def test_skill_exists(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "testing-and-debugging" / "SKILL.md" assert path.exists(), f"testing-and-debugging SKILL.md not found in {repo_name}" @pytest.mark.parametrize("repo_name", ALL_REPOS) def test_has_required_sections(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "testing-and-debugging" / "SKILL.md" content = path.read_text(encoding="utf-8") # All testing-and-debugging skills should have a CI failure investigation section @@ -574,6 +603,7 @@ class TestTestingAndDebuggingSkill: # Repos with PR workflow should mention spec-driven workflow @pytest.mark.parametrize("repo_name", ["infra", "grm", "sso-bridge", "devx"]) def test_mentions_spec_driven_workflow(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "testing-and-debugging" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "spec" in content.lower(), ( @@ -612,11 +642,13 @@ class TestPrReviewSkill: @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_skill_exists(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" assert path.exists(), f"pr-review SKILL.md not found in {repo_name}" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_all_review_categories(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" content = path.read_text(encoding="utf-8") required_categories = [ @@ -634,6 +666,7 @@ class TestPrReviewSkill: @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_auto_fix(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "auto-fix" in content.lower() or "auto fix" in content.lower(), ( @@ -642,30 +675,35 @@ class TestPrReviewSkill: @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_gitea_mcp(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "mcp" in content.lower(), f"pr-review skill in {repo_name} must mention Gitea MCP" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_inline_comments(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "inline" in content.lower(), f"pr-review skill in {repo_name} must mention inline comments" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_ready_to_merge(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "ready-to-merge" in content, f"pr-review skill in {repo_name} must mention ready-to-merge label" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_resolve_discussion(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "resolve" in content.lower(), f"pr-review skill in {repo_name} must mention resolving discussions" @pytest.mark.parametrize("repo_name", REPOS_WITH_PR_WORKFLOW) def test_mentions_summary(self, repo_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / "pr-review" / "SKILL.md" content = path.read_text(encoding="utf-8") assert "summary" in content.lower(), f"pr-review skill in {repo_name} must mention posting a summary" @@ -726,6 +764,7 @@ class TestSkillConsistency: @pytest.mark.parametrize("repo_name, skill_name", ALL_SKILLS) def test_skill_has_title(self, repo_name: str, skill_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / skill_name / "SKILL.md" content = path.read_text(encoding="utf-8") first_line = content.strip().split("\n")[0] @@ -733,12 +772,14 @@ class TestSkillConsistency: @pytest.mark.parametrize("repo_name, skill_name", ALL_SKILLS) def test_skill_not_empty(self, repo_name: str, skill_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / skill_name / "SKILL.md" content = path.read_text(encoding="utf-8").strip() assert len(content) > 100, f"{repo_name}/{skill_name}: SKILL.md is too short ({len(content)} chars)" @pytest.mark.parametrize("repo_name, skill_name", ALL_SKILLS) def test_skill_has_sections(self, repo_name: str, skill_name: str) -> None: + _skip_if_repo_missing(repo_name) path = _OBLACHNO_ROOT / repo_name / ".devin" / "skills" / skill_name / "SKILL.md" content = path.read_text(encoding="utf-8") # Must have at least 2 ## sections