From c4276af1e61e62c0a3919425fc55e82abe414301 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Jan 2026 14:44:46 +0000 Subject: [PATCH 1/7] chore(codex): bootstrap PR for issue #690 --- agents/codex-690.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 agents/codex-690.md diff --git a/agents/codex-690.md b/agents/codex-690.md new file mode 100644 index 000000000..db791e0dd --- /dev/null +++ b/agents/codex-690.md @@ -0,0 +1 @@ + From 47278eb12659a187f9cd4aff491ee8b9c52001d6 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 9 Jan 2026 14:53:08 +0000 Subject: [PATCH 2/7] Add fallback capability checks for blocked tasks --- scripts/langchain/capability_check.py | 156 ++++++++++++++++++++----- scripts/langchain/issue_optimizer.py | 10 +- tests/scripts/test_capability_check.py | 39 ++++++- tests/scripts/test_issue_optimizer.py | 14 +++ 4 files changed, 180 insertions(+), 39 deletions(-) diff --git a/scripts/langchain/capability_check.py b/scripts/langchain/capability_check.py index 2e58ea07f..2efc7999c 100755 --- a/scripts/langchain/capability_check.py +++ b/scripts/langchain/capability_check.py @@ -166,30 +166,132 @@ def _normalize_result(payload: dict[str, Any], provider_used: str | None) -> Cap ) +def _matches_any(patterns: list[str], text: str) -> bool: + return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in patterns) + + +def _is_multi_action_task(task: str) -> bool: + lowered = task.lower() + if len(task.split()) >= 14: + return True + if any(sep in lowered for sep in (" and ", " + ", " & ", " then ", "; ")): + return True + if "," in task or "/" in task: + return True + if re.search(r"\s\+\s", lowered): + return True + return False + + +def _requires_admin_access(task: str) -> bool: + patterns = [ + r"\bgithub\s+secrets?\b", + r"\bsecrets?\b", + r"\brepository\s+settings\b", + r"\brepo\s+settings\b", + r"\bbranch\s+protection\b", + r"\badmin\s+access\b", + r"\badmin\b.*\bpermission\b", + r"\borganization\s+settings\b", + r"\borg\s+settings\b", + r"\bbilling\b", + r"\baccess\s+control\b", + ] + return _matches_any(patterns, task) + + +def _requires_external_dependency(task: str) -> bool: + patterns = [ + r"\bstripe\b", + r"\bpaypal\b", + r"\bbraintree\b", + r"\btwilio\b", + r"\bslack\b", + r"\bsentry\b", + r"\bwebhook\b", + r"\boauth\b", + r"\bapi\s+key\b", + r"\bclient\s+secret\b", + r"\bclient\s+id\b", + r"\bexternal\s+api\b", + r"\bthird-?party\b", + r"\bintegrat(e|ion)\b.*\bapi\b", + ] + return _matches_any(patterns, task) + + +def _fallback_classify( + tasks: list[str], acceptance: str, reason: str | None +) -> CapabilityCheckResult: + actionable: list[str] = [] + partial: list[dict[str, str]] = [] + blocked: list[dict[str, str]] = [] + human_actions: list[str] = [] + + for task in tasks: + if _requires_admin_access(task): + blocked.append( + { + "task": task, + "reason": "Requires admin or repository settings access", + "suggested_action": "Have a repo admin apply the change or grant access.", + } + ) + human_actions.append(f"Admin access needed: {task}") + continue + if _requires_external_dependency(task): + blocked.append( + { + "task": task, + "reason": "Requires external service credentials or configuration", + "suggested_action": "Provide credentials or have a human set up the external service.", + } + ) + human_actions.append(f"External dependency setup required: {task}") + continue + if _is_multi_action_task(task): + partial.append( + { + "task": task, + "limitation": "Task bundles multiple actions; split into smaller tasks.", + } + ) + human_actions.append(f"Split task into smaller steps: {task}") + continue + actionable.append(task) + + if reason: + human_actions.append(reason) + + if blocked: + recommendation = "BLOCKED" + elif partial or not tasks: + recommendation = "REVIEW_NEEDED" + else: + recommendation = "PROCEED" + + return CapabilityCheckResult( + actionable_tasks=actionable, + partial_tasks=partial, + blocked_tasks=blocked, + recommendation=recommendation, + human_actions_needed=human_actions, + provider_used=None, + ) + + def classify_capabilities(tasks: list[str], acceptance: str) -> CapabilityCheckResult: client_info = _get_llm_client() if not client_info: - return CapabilityCheckResult( - actionable_tasks=[], - partial_tasks=[], - blocked_tasks=[], - recommendation="REVIEW_NEEDED", - human_actions_needed=["LLM provider unavailable"], - provider_used=None, - ) + return _fallback_classify(tasks, acceptance, "LLM provider unavailable") client, provider_name = client_info try: from langchain_core.prompts import ChatPromptTemplate except ImportError: - return CapabilityCheckResult( - actionable_tasks=[], - partial_tasks=[], - blocked_tasks=[], - recommendation="REVIEW_NEEDED", - human_actions_needed=["langchain-core not installed"], - provider_used=provider_name, - ) + result = _fallback_classify(tasks, acceptance, "langchain-core not installed") + result.provider_used = provider_name + return result template = ChatPromptTemplate.from_template(AGENT_CAPABILITY_CHECK_PROMPT) chain = template | client @@ -197,25 +299,15 @@ def classify_capabilities(tasks: list[str], acceptance: str) -> CapabilityCheckR content = getattr(response, "content", None) or str(response) payload = _extract_json_payload(content) if not payload: - return CapabilityCheckResult( - actionable_tasks=[], - partial_tasks=[], - blocked_tasks=[], - recommendation="REVIEW_NEEDED", - human_actions_needed=["LLM response missing JSON payload"], - provider_used=provider_name, - ) + result = _fallback_classify(tasks, acceptance, "LLM response missing JSON payload") + result.provider_used = provider_name + return result try: data = json.loads(payload) except json.JSONDecodeError: - return CapabilityCheckResult( - actionable_tasks=[], - partial_tasks=[], - blocked_tasks=[], - recommendation="REVIEW_NEEDED", - human_actions_needed=["LLM response JSON parse failed"], - provider_used=provider_name, - ) + result = _fallback_classify(tasks, acceptance, "LLM response JSON parse failed") + result.provider_used = provider_name + return result return _normalize_result(data, provider_name) diff --git a/scripts/langchain/issue_optimizer.py b/scripts/langchain/issue_optimizer.py index 773e4b589..9b59b8a6e 100644 --- a/scripts/langchain/issue_optimizer.py +++ b/scripts/langchain/issue_optimizer.py @@ -455,7 +455,15 @@ def _coerce_split_suggestions(entry: dict[str, Any]) -> list[str]: def _is_large_task(task: str) -> bool: lowered = task.lower() - return len(task.split()) >= 14 or " and " in lowered or ", " in task or "/" in task + if len(task.split()) >= 14: + return True + if any(sep in lowered for sep in (" and ", " + ", " & ", " then ", "; ")): + return True + if re.search(r"\s\+\s", lowered): + return True + if ", " in task or "/" in task: + return True + return False def _detect_task_splitting(tasks: list[str], *, use_llm: bool = False) -> list[dict[str, Any]]: diff --git a/tests/scripts/test_capability_check.py b/tests/scripts/test_capability_check.py index 5454eadcb..5ccf81135 100644 --- a/tests/scripts/test_capability_check.py +++ b/tests/scripts/test_capability_check.py @@ -275,7 +275,8 @@ def from_template(_: str) -> Any: def test_returns_fallback_when_no_llm_client(self) -> None: with mock.patch("scripts.langchain.capability_check._get_llm_client", return_value=None): result = classify_capabilities(["task1"], "criteria") - assert result.recommendation == "REVIEW_NEEDED" + assert result.recommendation == "PROCEED" + assert result.actionable_tasks == ["task1"] assert "LLM provider unavailable" in result.human_actions_needed assert result.provider_used is None @@ -297,7 +298,8 @@ def mock_import(name: str, *args: Any, **kwargs: Any) -> Any: with mock.patch.object(builtins, "__import__", mock_import): result = classify_capabilities(["task1"], "criteria") - assert result.recommendation == "REVIEW_NEEDED" + assert result.recommendation == "PROCEED" + assert result.actionable_tasks == ["task1"] assert result.provider_used == "github-models" assert "langchain-core not installed" in result.human_actions_needed @@ -345,7 +347,8 @@ def test_returns_fallback_when_response_missing_json( ): result = classify_capabilities(["task1"], "criteria") - assert result.recommendation == "REVIEW_NEEDED" + assert result.recommendation == "PROCEED" + assert result.actionable_tasks == ["task1"] assert "LLM response missing JSON payload" in result.human_actions_needed assert result.provider_used == "github-models" @@ -365,10 +368,32 @@ def test_returns_fallback_when_response_json_invalid( ): result = classify_capabilities(["task1"], "criteria") - assert result.recommendation == "REVIEW_NEEDED" + assert result.recommendation == "PROCEED" + assert result.actionable_tasks == ["task1"] assert "LLM response JSON parse failed" in result.human_actions_needed assert result.provider_used == "github-models" + def test_fallback_flags_external_dependency(self) -> None: + with mock.patch("scripts.langchain.capability_check._get_llm_client", return_value=None): + result = classify_capabilities(["Integrate Stripe payments"], "") + assert result.recommendation == "BLOCKED" + assert result.blocked_tasks[0]["task"] == "Integrate Stripe payments" + assert "external service" in result.blocked_tasks[0]["reason"].lower() + + def test_fallback_flags_admin_requirement(self) -> None: + with mock.patch("scripts.langchain.capability_check._get_llm_client", return_value=None): + result = classify_capabilities(["Update GitHub secrets"], "") + assert result.recommendation == "BLOCKED" + assert result.blocked_tasks[0]["task"] == "Update GitHub secrets" + assert "admin" in result.blocked_tasks[0]["reason"].lower() + + def test_fallback_suggests_decomposition(self) -> None: + with mock.patch("scripts.langchain.capability_check._get_llm_client", return_value=None): + result = classify_capabilities(["Refactor auth + add tests + update docs"], "") + assert result.recommendation == "REVIEW_NEEDED" + assert result.partial_tasks[0]["task"] == "Refactor auth + add tests + update docs" + assert "split" in result.partial_tasks[0]["limitation"].lower() + # The following tests require langchain_core to be installed # They test the LLM response handling paths @@ -407,7 +432,8 @@ def test_returns_fallback_when_no_json_in_response(self) -> None: mock_cpt.from_template.return_value = mock_template result = classify_capabilities(["task1"], "criteria") - assert result.recommendation == "REVIEW_NEEDED" + assert result.recommendation == "PROCEED" + assert result.actionable_tasks == ["task1"] assert "LLM response missing JSON payload" in result.human_actions_needed def test_returns_fallback_when_json_parse_fails(self) -> None: @@ -431,7 +457,8 @@ def test_returns_fallback_when_json_parse_fails(self) -> None: mock_cpt.from_template.return_value = mock_template result = classify_capabilities(["task1"], "criteria") - assert result.recommendation == "REVIEW_NEEDED" + assert result.recommendation == "PROCEED" + assert result.actionable_tasks == ["task1"] assert "LLM response JSON parse failed" in result.human_actions_needed def test_normalizes_valid_llm_response(self) -> None: diff --git a/tests/scripts/test_issue_optimizer.py b/tests/scripts/test_issue_optimizer.py index e0df5485a..747fb724f 100644 --- a/tests/scripts/test_issue_optimizer.py +++ b/tests/scripts/test_issue_optimizer.py @@ -255,6 +255,20 @@ def fake_decompose(task: str, *, use_llm: bool) -> dict[str, list[str]]: assert result[0]["split_suggestions"] == ["Split A", "Split B"] +def test_detect_task_splitting_flags_plus_separated(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_decompose(task: str, *, use_llm: bool) -> dict[str, list[str]]: + assert task == "Refactor auth + add tests + update docs" + return {"sub_tasks": ["Refactor auth", "Add tests", "Update docs"]} + + monkeypatch.setattr( + "scripts.langchain.task_decomposer.decompose_task", + fake_decompose, + ) + tasks = ["Refactor auth + add tests + update docs"] + result = issue_optimizer._detect_task_splitting(tasks, use_llm=False) + assert result[0]["split_suggestions"] == ["Refactor auth", "Add tests", "Update docs"] + + def test_ensure_task_decomposition_fills_missing_suggestions( monkeypatch: pytest.MonkeyPatch, ) -> None: From baf843a484f590384456e41f9e18da1c3760ea36 Mon Sep 17 00:00:00 2001 From: stranske Date: Fri, 9 Jan 2026 15:14:14 +0000 Subject: [PATCH 3/7] fix: Simplify return logic to resolve SIM103 ruff errors - capability_check.py: Consolidate return conditions - issue_optimizer.py: Consolidate return conditions, mark executable Fixes lint-ruff check failures in PR #699 --- scripts/langchain/capability_check.py | 6 +----- scripts/langchain/issue_optimizer.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) mode change 100644 => 100755 scripts/langchain/issue_optimizer.py diff --git a/scripts/langchain/capability_check.py b/scripts/langchain/capability_check.py index 2efc7999c..5417f431a 100755 --- a/scripts/langchain/capability_check.py +++ b/scripts/langchain/capability_check.py @@ -176,11 +176,7 @@ def _is_multi_action_task(task: str) -> bool: return True if any(sep in lowered for sep in (" and ", " + ", " & ", " then ", "; ")): return True - if "," in task or "/" in task: - return True - if re.search(r"\s\+\s", lowered): - return True - return False + return bool("," in task or "/" in task or re.search(r"\s\+\s", lowered)) def _requires_admin_access(task: str) -> bool: diff --git a/scripts/langchain/issue_optimizer.py b/scripts/langchain/issue_optimizer.py old mode 100644 new mode 100755 index 9b59b8a6e..416a01d57 --- a/scripts/langchain/issue_optimizer.py +++ b/scripts/langchain/issue_optimizer.py @@ -459,11 +459,7 @@ def _is_large_task(task: str) -> bool: return True if any(sep in lowered for sep in (" and ", " + ", " & ", " then ", "; ")): return True - if re.search(r"\s\+\s", lowered): - return True - if ", " in task or "/" in task: - return True - return False + return bool(re.search(r"\s\+\s", lowered) or ", " in task or "/" in task) def _detect_task_splitting(tasks: list[str], *, use_llm: bool = False) -> list[dict[str, Any]]: From 49e7287d2a4cea24504be933bd40528080fc1119 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 9 Jan 2026 15:35:25 +0000 Subject: [PATCH 4/7] Add admin/dependency sections for issue parsing --- .github/scripts/parse_chatgpt_topics.py | 5 ++ Issues.txt | 67 +++++++++++++++++++ tests/workflows/test_chatgpt_topics_parser.py | 20 ++++++ 3 files changed, 92 insertions(+) diff --git a/.github/scripts/parse_chatgpt_topics.py b/.github/scripts/parse_chatgpt_topics.py index 6fae0dd7b..42056ffd5 100755 --- a/.github/scripts/parse_chatgpt_topics.py +++ b/.github/scripts/parse_chatgpt_topics.py @@ -119,6 +119,11 @@ def _parse_sections( "tasks": {"tasks"}, "acceptance_criteria": {"acceptance criteria", "acceptance criteria."}, "implementation_notes": { + "admin access", + "admin requirement", + "admin requirements", + "dependencies", + "dependency", "implementation notes", "implementation note", "notes", diff --git a/Issues.txt b/Issues.txt index 8bd90b06d..72e5e99b9 100644 --- a/Issues.txt +++ b/Issues.txt @@ -114,3 +114,70 @@ Manual testing is slowing down releases. An automated testing pipeline will incr - All PRs require passing tests - Code coverage is above 80% - E2E tests cover login, dashboard, and checkout flows + +--- + +Issue 6 — Fix README typo in onboarding section + +## Why +The onboarding instructions include a misspelled command that confuses new contributors. + +## Scope +- Correct the typo in the onboarding README section +- Verify the command matches the repo's actual script name + +## Tasks +- [ ] Locate the onboarding command in README +- [ ] Fix the typo and ensure formatting stays intact + +## Acceptance Criteria +- Onboarding command is spelled correctly +- README formatting remains unchanged elsewhere + +--- + +Issue 7 — Integrate Stripe payments for subscriptions + +## Why +Recurring subscriptions are blocked until Stripe billing is integrated. + +## Scope +- Add Stripe checkout flow for subscription tiers +- Store Stripe customer IDs for existing users + +## Dependencies +- Stripe API access and test keys +- Webhook endpoint configuration in Stripe + +## Tasks +- [ ] Create Stripe customer records for new signups +- [ ] Implement subscription checkout session +- [ ] Handle Stripe webhook events for subscription status + +## Acceptance Criteria +- Users can start a subscription via Stripe checkout +- Subscription status syncs via webhooks +- Billing events are recorded in the database + +--- + +Issue 8 — Rotate GitHub secrets for CI + +## Why +Security policy requires rotating CI secrets every 90 days. + +## Scope +- Rotate CI service account token +- Update secrets referenced by workflows + +## Admin Access +- Requires org admin to update repository secrets + +## Tasks +- [ ] Rotate service account token in secret manager +- [ ] Update GitHub repository secrets with new token +- [ ] Confirm CI runs use updated secrets + +## Acceptance Criteria +- New token is active in GitHub secrets +- CI pipelines succeed with rotated credentials diff --git a/tests/workflows/test_chatgpt_topics_parser.py b/tests/workflows/test_chatgpt_topics_parser.py index 2d343c03a..a90128daa 100644 --- a/tests/workflows/test_chatgpt_topics_parser.py +++ b/tests/workflows/test_chatgpt_topics_parser.py @@ -320,6 +320,26 @@ def test_parse_sections_supports_label_separators() -> None: assert extras == [] +def test_parse_sections_supports_dependencies_and_admin_access() -> None: + labels, sections, extras = parse_module._parse_sections( + [ + "Dependencies", + "- Stripe API access required", + "Admin Access", + "- Rotate GitHub secrets", + "Why", + "Because billing is blocked", + ] + ) + assert labels == [] + assert any( + "Stripe API access required" in line for line in sections["implementation_notes"] + ) + assert any("Rotate GitHub secrets" in line for line in sections["implementation_notes"]) + assert sections["why"] == ["Because billing is blocked"] + assert extras == [] + + def test_parse_text_single_topic_empty_raises_system_exit() -> None: with pytest.raises(SystemExit) as exc: parse_module.parse_text(" \n", allow_single_fallback=True) From c958edc03f8cc714d7a5044f1c416da301743a13 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Jan 2026 15:36:19 +0000 Subject: [PATCH 5/7] chore(autofix): formatting/lint --- tests/workflows/test_chatgpt_topics_parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/workflows/test_chatgpt_topics_parser.py b/tests/workflows/test_chatgpt_topics_parser.py index a90128daa..9c6b390a5 100644 --- a/tests/workflows/test_chatgpt_topics_parser.py +++ b/tests/workflows/test_chatgpt_topics_parser.py @@ -332,9 +332,7 @@ def test_parse_sections_supports_dependencies_and_admin_access() -> None: ] ) assert labels == [] - assert any( - "Stripe API access required" in line for line in sections["implementation_notes"] - ) + assert any("Stripe API access required" in line for line in sections["implementation_notes"]) assert any("Rotate GitHub secrets" in line for line in sections["implementation_notes"]) assert sections["why"] == ["Because billing is blocked"] assert extras == [] From d1b7a15ef268760134b608f896713b91a2d5e89b Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 9 Jan 2026 15:44:25 +0000 Subject: [PATCH 6/7] Add integration consumer test runner --- scripts/run_consumer_repo_tests.py | 104 ++++++++++++++++++ tests/scripts/test_run_consumer_repo_tests.py | 89 +++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 scripts/run_consumer_repo_tests.py create mode 100644 tests/scripts/test_run_consumer_repo_tests.py diff --git a/scripts/run_consumer_repo_tests.py b/scripts/run_consumer_repo_tests.py new file mode 100644 index 000000000..7f5de9b1a --- /dev/null +++ b/scripts/run_consumer_repo_tests.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Sequence + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from tools.integration_repo import DEFAULT_WORKFLOW_REF, render_integration_repo + +DEFAULT_DESTINATION = Path(".consumer-tests") / "integration-repo" + + +def ensure_destination(destination: Path, *, force: bool) -> None: + if destination.exists(): + if force: + shutil.rmtree(destination) + elif any(destination.iterdir()): + raise FileExistsError( + f"Destination {destination} is not empty. Use --force to overwrite." + ) + destination.mkdir(parents=True, exist_ok=True) + + +def build_pytest_command(pytest_args: Sequence[str]) -> list[str]: + return [sys.executable, "-m", "pytest", *pytest_args] + + +def build_pytest_env(destination: Path) -> dict[str, str]: + env = os.environ.copy() + src_path = str(destination.resolve() / "src") + existing = env.get("PYTHONPATH") + env["PYTHONPATH"] = ( + f"{src_path}{os.pathsep}{existing}" if existing else src_path + ) + return env + + +def run_pytest(destination: Path, pytest_args: Sequence[str]) -> int: + command = build_pytest_command(pytest_args) + env = build_pytest_env(destination) + result = subprocess.run(command, cwd=destination, env=env) + return result.returncode + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run tests in a consumer repo (integration template by default)." + ) + parser.add_argument( + "--destination", + type=Path, + default=DEFAULT_DESTINATION, + help="Directory for the rendered integration repo or existing repo path.", + ) + parser.add_argument( + "--workflow-ref", + default=DEFAULT_WORKFLOW_REF, + help="Reusable workflow ref to embed when rendering the integration repo.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Remove existing destination contents before rendering.", + ) + parser.add_argument( + "--skip-render", + action="store_true", + help="Run tests in an existing consumer repo instead of rendering.", + ) + parser.add_argument( + "--pytest-args", + nargs=argparse.REMAINDER, + default=[], + help="Additional pytest args (pass after --pytest-args).", + ) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + destination = args.destination + + try: + if args.skip_render: + if not destination.exists(): + print(f"Destination not found: {destination}", file=sys.stderr) + return 1 + else: + ensure_destination(destination, force=args.force) + render_integration_repo(destination, workflow_ref=args.workflow_ref) + except (FileExistsError, FileNotFoundError) as exc: + print(str(exc), file=sys.stderr) + return 1 + + return run_pytest(destination, args.pytest_args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/scripts/test_run_consumer_repo_tests.py b/tests/scripts/test_run_consumer_repo_tests.py new file mode 100644 index 000000000..ca9c1c5b5 --- /dev/null +++ b/tests/scripts/test_run_consumer_repo_tests.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from pathlib import Path +import sys + +import pytest + +from scripts import run_consumer_repo_tests + + +def test_ensure_destination_existing_non_empty_raises(tmp_path: Path) -> None: + destination = tmp_path / "repo" + destination.mkdir() + (destination / "existing.txt").write_text("data", encoding="utf-8") + + with pytest.raises(FileExistsError): + run_consumer_repo_tests.ensure_destination(destination, force=False) + + +def test_ensure_destination_force_clears(tmp_path: Path) -> None: + destination = tmp_path / "repo" + destination.mkdir() + (destination / "existing.txt").write_text("data", encoding="utf-8") + + run_consumer_repo_tests.ensure_destination(destination, force=True) + + assert destination.exists() + assert list(destination.iterdir()) == [] + + +def test_build_pytest_command_uses_sys_executable() -> None: + command = run_consumer_repo_tests.build_pytest_command(["-q"]) + + assert command[0] == sys.executable + assert command[1:] == ["-m", "pytest", "-q"] + + +def test_build_pytest_env_sets_pythonpath(tmp_path: Path) -> None: + destination = tmp_path / "repo" + env = run_consumer_repo_tests.build_pytest_env(destination) + + assert env["PYTHONPATH"].startswith(str(destination.resolve() / "src")) + + +def test_main_skip_render_missing_destination( + capsys: pytest.CaptureFixture[str], tmp_path: Path +) -> None: + destination = tmp_path / "missing" + + result = run_consumer_repo_tests.main( + ["--skip-render", "--destination", str(destination)] + ) + + captured = capsys.readouterr() + assert result == 1 + assert "Destination not found" in captured.err + + +def test_main_renders_and_runs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + destination = tmp_path / "rendered" + calls: dict[str, object] = {} + + def fake_render(path: Path, workflow_ref: str) -> None: + calls["render"] = (path, workflow_ref) + + def fake_run(path: Path, pytest_args: list[str]) -> int: + calls["run"] = (path, pytest_args) + return 0 + + monkeypatch.setattr(run_consumer_repo_tests, "render_integration_repo", fake_render) + monkeypatch.setattr(run_consumer_repo_tests, "run_pytest", fake_run) + + result = run_consumer_repo_tests.main( + [ + "--destination", + str(destination), + "--workflow-ref", + "owner/repo/.github/workflows/ci.yml@main", + "--pytest-args", + "-q", + ] + ) + + assert result == 0 + assert calls["render"] == ( + destination, + "owner/repo/.github/workflows/ci.yml@main", + ) + assert calls["run"] == (destination, ["-q"]) From 79603d14c2c443babf1b6f9776702fabbac625f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Jan 2026 15:45:24 +0000 Subject: [PATCH 7/7] chore(autofix): formatting/lint --- scripts/run_consumer_repo_tests.py | 6 ++---- tests/scripts/test_run_consumer_repo_tests.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/run_consumer_repo_tests.py b/scripts/run_consumer_repo_tests.py index 7f5de9b1a..999ce4ba4 100644 --- a/scripts/run_consumer_repo_tests.py +++ b/scripts/run_consumer_repo_tests.py @@ -5,8 +5,8 @@ import shutil import subprocess import sys +from collections.abc import Sequence from pathlib import Path -from typing import Sequence sys.path.insert(0, str(Path(__file__).parent.parent)) @@ -34,9 +34,7 @@ def build_pytest_env(destination: Path) -> dict[str, str]: env = os.environ.copy() src_path = str(destination.resolve() / "src") existing = env.get("PYTHONPATH") - env["PYTHONPATH"] = ( - f"{src_path}{os.pathsep}{existing}" if existing else src_path - ) + env["PYTHONPATH"] = f"{src_path}{os.pathsep}{existing}" if existing else src_path return env diff --git a/tests/scripts/test_run_consumer_repo_tests.py b/tests/scripts/test_run_consumer_repo_tests.py index ca9c1c5b5..4b8d94235 100644 --- a/tests/scripts/test_run_consumer_repo_tests.py +++ b/tests/scripts/test_run_consumer_repo_tests.py @@ -1,7 +1,7 @@ from __future__ import annotations -from pathlib import Path import sys +from pathlib import Path import pytest @@ -47,9 +47,7 @@ def test_main_skip_render_missing_destination( ) -> None: destination = tmp_path / "missing" - result = run_consumer_repo_tests.main( - ["--skip-render", "--destination", str(destination)] - ) + result = run_consumer_repo_tests.main(["--skip-render", "--destination", str(destination)]) captured = capsys.readouterr() assert result == 1