chore: run repository skill tests in dedicated CI (#4881) · openai/openai-agents-python@36b38b8 · GitHub
Skip to content

Commit 36b38b8

Browse files
authored
chore: run repository skill tests in dedicated CI (#4881)
1 parent f1ffb3d commit 36b38b8

6 files changed

Lines changed: 277 additions & 1 deletion

File tree

‎.agents/skills/implementation-final-review/scripts/test_review_state.py‎

Lines changed: 2 additions & 1 deletion
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Repository skill tests
2+
3+
Run `make tests-repo-skills` from the repository root. This command uses `uv run --no-project --python 3.11` and needs Git and make for disposable repository fixtures. It installs no SDK dependencies and does not collect `tests/` or pytest configuration. With an existing Python 3.11+ interpreter, use `python .github/scripts/run_repo_skill_tests.py` directly.
4+
5+
Use `uv run --no-project --python 3.11 python .github/scripts/run_repo_skill_tests.py --list` to print the discovered inventory without executing tests. The runner discovers `.agents/skills/*/scripts/test_*.py` and its own `.github/scripts/test_run_repo_skill_tests.py` regression suite. Add skill helper tests under that pattern using standard-library `unittest` and sibling imports.
6+
7+
The current skill inventory contains 159 tests across five modules:
8+
9+
| Skill | Module | Tests |
10+
| --- | --- | --- |
11+
| implementation-final-review | test_review_protocol.py | 77 |
12+
| implementation-final-review | test_review_state.py | 43 |
13+
| implementation-kickoff | test_validate_handoff.py | 12 |
14+
| release-candidate-prep | test_prepare.py | 17 |
15+
| sensitive-logging-audit | test_inventory.py | 10 |
16+
17+
Each module runs in a separate interpreter from its own scripts directory so sibling imports cannot collide across suites. Child processes receive only process essentials and fixed test settings; inherited API keys, tokens, Python import overrides, and user Git configuration are excluded. Git transport is limited to local files. Tests must use disposable local Git repositories and local bare origins, with no live service calls.
18+
19+
The runner executes every discovered module and reports all failed modules before returning a nonzero exit code. An empty skill inventory also fails. The dedicated `repo-skills.yml` workflow runs the same Make target when skills or the command's owning files change. Existing review tiers, budgets, and validator behavior are unchanged.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
"""Run repository skill unittest modules without collecting the SDK suite."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import os
8+
import subprocess
9+
import sys
10+
from pathlib import Path
11+
12+
13+
def test_environment() -> dict[str, str]:
14+
"""Keep process essentials, excluding credentials and user Git configuration."""
15+
env = {
16+
name: os.environ[name]
17+
for name in ("PATH", "SYSTEMROOT", "WINDIR", "TMPDIR", "TEMP", "TMP", "LANG", "LC_ALL")
18+
if name in os.environ
19+
}
20+
# Release fixtures invoke python through make, so use this interpreter's directory first.
21+
env["PATH"] = os.pathsep.join((str(Path(sys.executable).parent), env.get("PATH", os.defpath)))
22+
env.update(
23+
GIT_CONFIG_GLOBAL=os.devnull,
24+
GIT_CONFIG_NOSYSTEM="1",
25+
GIT_ALLOW_PROTOCOL="file",
26+
GIT_TERMINAL_PROMPT="0",
27+
UV_DEFAULT_INDEX="https://pypi.org/simple",
28+
)
29+
return env
30+
31+
32+
def main() -> int:
33+
parser = argparse.ArgumentParser(description=__doc__)
34+
parser.add_argument(
35+
"--list", action="store_true", help="List discovered modules without running."
36+
)
37+
args = parser.parse_args()
38+
repo = Path(__file__).resolve().parents[2]
39+
skill_tests = sorted(repo.glob(".agents/skills/*/scripts/test_*.py"))
40+
if not skill_tests:
41+
parser.error("No repository skill test modules found.")
42+
tests = sorted([*skill_tests, *repo.glob(".github/scripts/test_run_repo_skill_tests.py")])
43+
print(f"Discovered {len(tests)} repository skill test modules:", flush=True)
44+
for path in tests:
45+
print(f" {path.relative_to(repo).as_posix()}", flush=True)
46+
if args.list:
47+
return 0
48+
49+
env = test_environment()
50+
failed = []
51+
for path in tests:
52+
print(f"\nRunning {path.relative_to(repo).as_posix()}", flush=True)
53+
result = subprocess.run(
54+
[sys.executable, "-m", "unittest", "discover", "-s", ".", "-p", path.name, "-v"],
55+
cwd=path.parent,
56+
env=env,
57+
check=False,
58+
)
59+
if result.returncode:
60+
failed.append(path.relative_to(repo).as_posix())
61+
62+
print(f"\nCompleted {len(tests)} modules; {len(failed)} failed.", flush=True)
63+
for path in failed:
64+
print(f" FAILED: {path}", flush=True)
65+
return 1 if failed else 0
66+
67+
68+
if __name__ == "__main__":
69+
raise SystemExit(main())
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Exercise the runner CLI in disposable repositories without loading the SDK."""
2+
3+
from __future__ import annotations
4+
5+
import shutil
6+
import subprocess
7+
import sys
8+
import tempfile
9+
import unittest
10+
from pathlib import Path
11+
12+
from run_repo_skill_tests import test_environment
13+
14+
15+
class RepoSkillRunnerTests(unittest.TestCase):
16+
def setUp(self) -> None:
17+
directory = tempfile.TemporaryDirectory()
18+
self.addCleanup(directory.cleanup)
19+
self.repo = Path(directory.name)
20+
self.runner = self.repo / ".github/scripts/run_repo_skill_tests.py"
21+
self.runner.parent.mkdir(parents=True)
22+
shutil.copyfile(Path(__file__).with_name(self.runner.name), self.runner)
23+
24+
def write_suite(self, skill: str, source: str) -> Path:
25+
path = self.repo / ".agents/skills" / skill / "scripts/test_fixture.py"
26+
path.parent.mkdir(parents=True)
27+
path.write_text(source, encoding="utf-8")
28+
return path
29+
30+
def run_cli(
31+
self, *args: str, env: dict[str, str] | None = None
32+
) -> subprocess.CompletedProcess[str]:
33+
return subprocess.run(
34+
[sys.executable, str(self.runner), *args],
35+
cwd=self.repo.parent,
36+
env=test_environment() if env is None else env,
37+
capture_output=True,
38+
text=True,
39+
timeout=30,
40+
check=False,
41+
)
42+
43+
def test_inventory_is_sorted_and_listing_does_not_execute_tests(self) -> None:
44+
for name in ("z-last", "a-first"):
45+
self.write_suite(name, "raise RuntimeError('must not execute during listing')\n")
46+
result = self.run_cli("--list")
47+
self.assertEqual(result.returncode, 0, result.stderr)
48+
self.assertEqual(
49+
result.stdout.splitlines(),
50+
[
51+
"Discovered 2 repository skill test modules:",
52+
" .agents/skills/a-first/scripts/test_fixture.py",
53+
" .agents/skills/z-last/scripts/test_fixture.py",
54+
],
55+
)
56+
57+
def test_isolated_modules_keep_sibling_imports_and_exclude_sdk_tests(self) -> None:
58+
for name in ("first", "second"):
59+
suite = self.write_suite(
60+
name,
61+
"import unittest\nfrom helper import VALUE\n"
62+
"class Fixture(unittest.TestCase):\n"
63+
f" def test_value(self): self.assertEqual(VALUE, {name!r})\n",
64+
)
65+
suite.with_name("helper.py").write_text(f"VALUE = {name!r}\n", encoding="utf-8")
66+
sdk_suite = self.repo / "tests/test_sdk.py"
67+
sdk_suite.parent.mkdir()
68+
sdk_suite.write_text("raise RuntimeError('SDK suite must not be collected')\n")
69+
result = self.run_cli()
70+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
71+
self.assertIn("Completed 2 modules; 0 failed.", result.stdout)
72+
self.assertEqual(result.stderr.count("Ran 1 test"), 2)
73+
74+
def test_failure_propagates_and_later_modules_still_execute(self) -> None:
75+
self.write_suite(
76+
"a-failing",
77+
"import unittest\nclass Fixture(unittest.TestCase):\n"
78+
" def test_failure(self): self.fail('deliberate fixture failure')\n",
79+
)
80+
self.write_suite(
81+
"z-passing",
82+
"import unittest\nfrom pathlib import Path\nclass Fixture(unittest.TestCase):\n"
83+
" def test_later(self): Path('executed.txt').write_text('ran')\n",
84+
)
85+
result = self.run_cli()
86+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
87+
self.assertIn("deliberate fixture failure", result.stderr)
88+
self.assertIn("Completed 2 modules; 1 failed.", result.stdout)
89+
self.assertTrue((self.repo / ".agents/skills/z-passing/scripts/executed.txt").is_file())
90+
91+
def test_import_failure_propagates(self) -> None:
92+
self.write_suite("broken", "raise ImportError('broken helper import')\n")
93+
result = self.run_cli()
94+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
95+
self.assertIn("broken helper import", result.stderr)
96+
97+
def test_empty_skill_inventory_fails(self) -> None:
98+
result = self.run_cli()
99+
self.assertNotEqual(result.returncode, 0)
100+
self.assertIn("No repository skill test modules found", result.stderr)
101+
102+
def test_children_drop_credentials_and_git_only_allows_local_remotes(self) -> None:
103+
self.write_suite(
104+
"environment",
105+
"""import os
106+
import subprocess
107+
import sys
108+
import unittest
109+
110+
class Fixture(unittest.TestCase):
111+
def test_environment(self):
112+
for name in ('OPENAI_API_KEY', 'OPENAI_API_KEY_SOURCE', 'GH_TOKEN', 'GITHUB_TOKEN',
113+
'AZURE_OPENAI_API_KEY', 'AWS_SECRET_ACCESS_KEY', 'PYTHONPATH'):
114+
self.assertNotIn(name, os.environ)
115+
subprocess.run([sys.executable, '-c',
116+
"import os; assert 'OPENAI_API_KEY' not in os.environ"], check=True)
117+
subprocess.run(['git', 'init', '--bare', 'origin.git'], check=True, capture_output=True)
118+
local = subprocess.run(['git', 'ls-remote', 'origin.git'], capture_output=True)
119+
self.assertEqual(local.returncode, 0, local.stderr)
120+
remote = subprocess.run(['git', 'ls-remote', 'https://example.invalid/repo.git'],
121+
capture_output=True, text=True)
122+
self.assertNotEqual(remote.returncode, 0)
123+
self.assertIn("transport 'https' not allowed", remote.stderr)
124+
""",
125+
)
126+
env = test_environment()
127+
# Only synthetic values enter this fixture; inherited credentials are never forwarded.
128+
env.update(
129+
OPENAI_API_KEY="fixture-only",
130+
OPENAI_API_KEY_SOURCE="fixture-only",
131+
GH_TOKEN="fixture-only",
132+
GITHUB_TOKEN="fixture-only",
133+
AZURE_OPENAI_API_KEY="fixture-only",
134+
AWS_SECRET_ACCESS_KEY="fixture-only",
135+
PYTHONPATH=str(self.repo / "unused"),
136+
)
137+
result = self.run_cli(env=env)
138+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
139+
140+
141+
if __name__ == "__main__":
142+
unittest.main()

‎.github/workflows/repo-skills.yml‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Repository skills
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- .agents/**
9+
- .github/scripts/*repo_skill_tests.py
10+
- .github/scripts/repo-skill-tests.md
11+
- .github/workflows/repo-skills.yml
12+
- Makefile
13+
pull_request:
14+
paths:
15+
- .agents/**
16+
- .github/scripts/*repo_skill_tests.py
17+
- .github/scripts/repo-skill-tests.md
18+
- .github/workflows/repo-skills.yml
19+
- Makefile
20+
workflow_dispatch:
21+
22+
permissions:
23+
contents: read
24+
25+
jobs:
26+
repo-skills:
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 5
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
32+
with:
33+
persist-credentials: false
34+
- name: Setup uv
35+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # setup-uv v9.0.0; uv 0.11.14
36+
with:
37+
version: "0.11.14"
38+
python-version: "3.11"
39+
enable-cache: false
40+
- name: Run repository skill tests
41+
run: make tests-repo-skills

‎Makefile‎

Lines changed: 4 additions & 0 deletions

0 commit comments

Comments
 (0)