pytest-skill-engineering 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_skill_engineering-1.0.4/PKG-INFO +175 -0
- pytest_skill_engineering-1.0.4/README.md +127 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/pyproject.toml +14 -14
- pytest_skill_engineering-1.0.4/src/pytest_skill_engineering/copilot/config.py +133 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/contracts.py +8 -21
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/eval.py +15 -28
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/events.py +72 -3
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/fixtures.py +43 -43
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/personas.py +23 -25
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/result.py +57 -5
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/runner.py +6 -1
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/evals.py +34 -5
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/plugin.py +2 -85
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/result.py +36 -8
- pytest_skill_engineering-1.0.4/src/pytest_skill_engineering/core/serialization.py +128 -0
- pytest_skill_engineering-1.0.4/src/pytest_skill_engineering/core/validation.py +83 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/cost.py +5 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/servers.py +9 -9
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/plugin.py +57 -29
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/collector.py +10 -4
- pytest_skill_engineering-1.0.2/PKG-INFO +0 -182
- pytest_skill_engineering-1.0.2/README.md +0 -134
- pytest_skill_engineering-1.0.2/src/pytest_skill_engineering/copilot/config.py +0 -57
- pytest_skill_engineering-1.0.2/src/pytest_skill_engineering/core/serialization.py +0 -309
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/.gitignore +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/LICENSE +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/__init__.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/__init__.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/api.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/client.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/controls.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/requests.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/copilot/skills.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/__init__.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/errors.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/prompt.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/skill.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/skill_evals.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/core/skill_grading.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/__init__.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/rate_limiter.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/execution/skill_tools.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/fixtures/__init__.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/fixtures/factories.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/fixtures/iteration.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/onboarding.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/plugin_options.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/plugin_report.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/__init__.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/generator.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/reporting/schema.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/__init__.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/banking.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/banking_mcp.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/todo.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/todo_mcp.py +0 -0
- {pytest_skill_engineering-1.0.2 → pytest_skill_engineering-1.0.4}/src/pytest_skill_engineering/testing/types.py +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pytest-skill-engineering
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Reliable Copilot execution and evidence for coding-agent-led testing of MCP servers, CLI tools, skills, and custom agents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sbroenne/pytest-skill-engineering
|
|
6
|
+
Project-URL: Repository, https://github.com/sbroenne/pytest-skill-engineering
|
|
7
|
+
Author: Stefan Brunner
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,custom-agents,llm,mcp,pytest,skill-engineering,skills,testing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: Pytest
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: github-copilot-sdk>=1.0.28
|
|
23
|
+
Requires-Dist: httpx>=0.28.1
|
|
24
|
+
Requires-Dist: mcp>=2.3.0
|
|
25
|
+
Requires-Dist: pydantic>=2.14.1
|
|
26
|
+
Requires-Dist: pytest-asyncio>=1.4.1
|
|
27
|
+
Requires-Dist: pytest>=9.2.0
|
|
28
|
+
Requires-Dist: python-frontmatter>=1.3.0
|
|
29
|
+
Requires-Dist: tomlkit>=0.15.2
|
|
30
|
+
Requires-Dist: trio>=0.34.0
|
|
31
|
+
Requires-Dist: websockets>=17.3
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pre-commit>=4.7.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pyright>=1.1.414; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: python-dotenv>=1.2.4; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.17.3; extra == 'dev'
|
|
38
|
+
Requires-Dist: typeguard>=4.6.0; extra == 'dev'
|
|
39
|
+
Provides-Extra: docs
|
|
40
|
+
Requires-Dist: cairosvg>=2.9.1; extra == 'docs'
|
|
41
|
+
Requires-Dist: mkdocs-material<9.8,>=9.7.7; extra == 'docs'
|
|
42
|
+
Requires-Dist: mkdocs>=1.6.1; extra == 'docs'
|
|
43
|
+
Requires-Dist: mkdocstrings[python]>=1.0.8; extra == 'docs'
|
|
44
|
+
Requires-Dist: pillow>=13.0.0; extra == 'docs'
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
Requires-Dist: syrupy<7,>=6.1.1; extra == 'test'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# pytest-skill-engineering
|
|
50
|
+
|
|
51
|
+
[](https://pypi.org/project/pytest-skill-engineering/)
|
|
52
|
+
[](https://github.com/sbroenne/pytest-skill-engineering/actions/workflows/ci.yml)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
|
|
55
|
+
**Test whether GitHub Copilot can complete tasks with your tools and instructions.**
|
|
56
|
+
|
|
57
|
+
pytest-skill-engineering runs real Copilot sessions from pytest. Give Copilot a
|
|
58
|
+
task, check the actual output with ordinary assertions, and save the execution
|
|
59
|
+
as JSON.
|
|
60
|
+
|
|
61
|
+
Use it to test MCP servers, CLI tools, skills, system prompts, plugins, and custom
|
|
62
|
+
agents. It answers a different question from your tool's unit tests: **can
|
|
63
|
+
Copilot use the interface correctly?** A working tool can still have confusing
|
|
64
|
+
descriptions or arguments that lead the agent to the wrong result.
|
|
65
|
+
|
|
66
|
+
You define what correctness means. The framework manages the Copilot session,
|
|
67
|
+
execution limits, usage, and evidence; it does not grade answers with another AI.
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
You need Python 3.11+, [uv](https://docs.astral.sh/uv/), and a GitHub account with
|
|
72
|
+
Copilot access.
|
|
73
|
+
|
|
74
|
+
Run these commands in an existing Python project with a `pyproject.toml`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv add pytest-skill-engineering
|
|
78
|
+
gh auth login --hostname github.com
|
|
79
|
+
uv run pytest-skill-engineering init
|
|
80
|
+
uv run pytest-skill-engineering doctor
|
|
81
|
+
uv run python -m pytest tests/test_copilot_eval.py -v
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`init` creates a test using the bundled Todo MCP server, configures JSON output,
|
|
85
|
+
and adds starter-model rates to `pricing.toml`. It reports conflicts rather than
|
|
86
|
+
overwriting existing files or settings. `doctor` checks setup and Copilot access.
|
|
87
|
+
Real task execution may consume Copilot premium requests.
|
|
88
|
+
|
|
89
|
+
The test checks the tool's arguments and returned task state. pytest prints the
|
|
90
|
+
result; `aitest-reports/results.json` preserves the execution. See the
|
|
91
|
+
[setup guide](docs/getting-started/index.md) or the standalone
|
|
92
|
+
[quickstart project](examples/quickstart/).
|
|
93
|
+
|
|
94
|
+
## A test that checks the result
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from __future__ import annotations
|
|
98
|
+
|
|
99
|
+
import json
|
|
100
|
+
|
|
101
|
+
from pytest_skill_engineering.copilot import CopilotEval
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def test_order_total(copilot_eval, tmp_path):
|
|
105
|
+
agent = CopilotEval(
|
|
106
|
+
name="order-total",
|
|
107
|
+
model="gpt-5.6-sol",
|
|
108
|
+
instructions="Save the requested output as JSON. Use integer cents.",
|
|
109
|
+
working_directory=str(tmp_path),
|
|
110
|
+
)
|
|
111
|
+
result = await copilot_eval(
|
|
112
|
+
agent,
|
|
113
|
+
"Save order.json for two items at 1250 cents each plus 20% tax. "
|
|
114
|
+
"Include subtotal_cents, tax_cents, and total_cents.",
|
|
115
|
+
)
|
|
116
|
+
assert result.success, result.error
|
|
117
|
+
output = tmp_path / "order.json"
|
|
118
|
+
assert output.is_file()
|
|
119
|
+
assert json.loads(output.read_text(encoding="utf-8")) == {
|
|
120
|
+
"subtotal_cents": 2500,
|
|
121
|
+
"tax_cents": 500,
|
|
122
|
+
"total_cents": 3000,
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`CopilotEval` configures the session. The second argument to `copilot_eval` is
|
|
127
|
+
the user task. `result.success` checks execution completion; the file assertions
|
|
128
|
+
check that the task produced the right result.
|
|
129
|
+
|
|
130
|
+
For your own tools, check the resulting application state, saved files, or
|
|
131
|
+
operation output. A tool call or a model's claim is not proof of task success.
|
|
132
|
+
Use approved isolation for live tests: a temporary directory is not a sandbox,
|
|
133
|
+
and generated code or tool calls can act with your user's access.
|
|
134
|
+
|
|
135
|
+
## What the framework provides
|
|
136
|
+
|
|
137
|
+
| You need | Use |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| Real task execution | `CopilotEval` and the `copilot_eval` pytest fixture |
|
|
140
|
+
| Controlled runs | Timeouts, tool budgets, request controls, and permission settings |
|
|
141
|
+
| Debugging evidence | Configuration, calls, arguments, outputs, errors, and completion records |
|
|
142
|
+
| Repeatable comparisons | `ab_run` and pytest repetitions |
|
|
143
|
+
| Usage visibility | Captured tokens, Copilot premium requests, and explicit price estimates |
|
|
144
|
+
| Saved results | Native JSON with execution evidence and ordinary pytest outcomes |
|
|
145
|
+
|
|
146
|
+
Your tests supply application fixtures, guarded tool adapters, and independent
|
|
147
|
+
output checks. The framework makes one execution attempt per call; it does not
|
|
148
|
+
retry failures automatically. Missing usage, prices, and incomplete evidence
|
|
149
|
+
remain visible.
|
|
150
|
+
|
|
151
|
+
## Investigate and compare
|
|
152
|
+
|
|
153
|
+
Give your coding agent a failed test, its JSON evidence, the requirements, and
|
|
154
|
+
the relevant source. It can investigate the failure and help you change the
|
|
155
|
+
interface. Keep the success criteria fixed when rerunning a comparison.
|
|
156
|
+
|
|
157
|
+
For A/B tests, record verification separately for each side: both saved
|
|
158
|
+
executions share one pytest outcome. That outcome alone cannot establish
|
|
159
|
+
independent pass rates or a causal improvement. See
|
|
160
|
+
[comparing configurations](docs/getting-started/comparing.md).
|
|
161
|
+
|
|
162
|
+
## Documentation
|
|
163
|
+
|
|
164
|
+
- [Getting started](https://sbroenne.github.io/pytest-skill-engineering/getting-started/)
|
|
165
|
+
- [Task guides](docs/how-to/index.md) and [API reference](docs/reference/api.md)
|
|
166
|
+
- [Examples](examples/README.md), including historical skill comparisons
|
|
167
|
+
- [Upgrading from 0.6.x](docs/migration.md): 1.0 removed AI judging, renderers,
|
|
168
|
+
rankings, and skill refinement
|
|
169
|
+
|
|
170
|
+
No framework companion skill is required or distributed. Testing your own
|
|
171
|
+
domain skills is supported.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT. Inspired by [agent-benchmark](https://github.com/mykhaliev/agent-benchmark).
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# pytest-skill-engineering
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pytest-skill-engineering/)
|
|
4
|
+
[](https://github.com/sbroenne/pytest-skill-engineering/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Test whether GitHub Copilot can complete tasks with your tools and instructions.**
|
|
8
|
+
|
|
9
|
+
pytest-skill-engineering runs real Copilot sessions from pytest. Give Copilot a
|
|
10
|
+
task, check the actual output with ordinary assertions, and save the execution
|
|
11
|
+
as JSON.
|
|
12
|
+
|
|
13
|
+
Use it to test MCP servers, CLI tools, skills, system prompts, plugins, and custom
|
|
14
|
+
agents. It answers a different question from your tool's unit tests: **can
|
|
15
|
+
Copilot use the interface correctly?** A working tool can still have confusing
|
|
16
|
+
descriptions or arguments that lead the agent to the wrong result.
|
|
17
|
+
|
|
18
|
+
You define what correctness means. The framework manages the Copilot session,
|
|
19
|
+
execution limits, usage, and evidence; it does not grade answers with another AI.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
You need Python 3.11+, [uv](https://docs.astral.sh/uv/), and a GitHub account with
|
|
24
|
+
Copilot access.
|
|
25
|
+
|
|
26
|
+
Run these commands in an existing Python project with a `pyproject.toml`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv add pytest-skill-engineering
|
|
30
|
+
gh auth login --hostname github.com
|
|
31
|
+
uv run pytest-skill-engineering init
|
|
32
|
+
uv run pytest-skill-engineering doctor
|
|
33
|
+
uv run python -m pytest tests/test_copilot_eval.py -v
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`init` creates a test using the bundled Todo MCP server, configures JSON output,
|
|
37
|
+
and adds starter-model rates to `pricing.toml`. It reports conflicts rather than
|
|
38
|
+
overwriting existing files or settings. `doctor` checks setup and Copilot access.
|
|
39
|
+
Real task execution may consume Copilot premium requests.
|
|
40
|
+
|
|
41
|
+
The test checks the tool's arguments and returned task state. pytest prints the
|
|
42
|
+
result; `aitest-reports/results.json` preserves the execution. See the
|
|
43
|
+
[setup guide](docs/getting-started/index.md) or the standalone
|
|
44
|
+
[quickstart project](examples/quickstart/).
|
|
45
|
+
|
|
46
|
+
## A test that checks the result
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import json
|
|
52
|
+
|
|
53
|
+
from pytest_skill_engineering.copilot import CopilotEval
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def test_order_total(copilot_eval, tmp_path):
|
|
57
|
+
agent = CopilotEval(
|
|
58
|
+
name="order-total",
|
|
59
|
+
model="gpt-5.6-sol",
|
|
60
|
+
instructions="Save the requested output as JSON. Use integer cents.",
|
|
61
|
+
working_directory=str(tmp_path),
|
|
62
|
+
)
|
|
63
|
+
result = await copilot_eval(
|
|
64
|
+
agent,
|
|
65
|
+
"Save order.json for two items at 1250 cents each plus 20% tax. "
|
|
66
|
+
"Include subtotal_cents, tax_cents, and total_cents.",
|
|
67
|
+
)
|
|
68
|
+
assert result.success, result.error
|
|
69
|
+
output = tmp_path / "order.json"
|
|
70
|
+
assert output.is_file()
|
|
71
|
+
assert json.loads(output.read_text(encoding="utf-8")) == {
|
|
72
|
+
"subtotal_cents": 2500,
|
|
73
|
+
"tax_cents": 500,
|
|
74
|
+
"total_cents": 3000,
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`CopilotEval` configures the session. The second argument to `copilot_eval` is
|
|
79
|
+
the user task. `result.success` checks execution completion; the file assertions
|
|
80
|
+
check that the task produced the right result.
|
|
81
|
+
|
|
82
|
+
For your own tools, check the resulting application state, saved files, or
|
|
83
|
+
operation output. A tool call or a model's claim is not proof of task success.
|
|
84
|
+
Use approved isolation for live tests: a temporary directory is not a sandbox,
|
|
85
|
+
and generated code or tool calls can act with your user's access.
|
|
86
|
+
|
|
87
|
+
## What the framework provides
|
|
88
|
+
|
|
89
|
+
| You need | Use |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| Real task execution | `CopilotEval` and the `copilot_eval` pytest fixture |
|
|
92
|
+
| Controlled runs | Timeouts, tool budgets, request controls, and permission settings |
|
|
93
|
+
| Debugging evidence | Configuration, calls, arguments, outputs, errors, and completion records |
|
|
94
|
+
| Repeatable comparisons | `ab_run` and pytest repetitions |
|
|
95
|
+
| Usage visibility | Captured tokens, Copilot premium requests, and explicit price estimates |
|
|
96
|
+
| Saved results | Native JSON with execution evidence and ordinary pytest outcomes |
|
|
97
|
+
|
|
98
|
+
Your tests supply application fixtures, guarded tool adapters, and independent
|
|
99
|
+
output checks. The framework makes one execution attempt per call; it does not
|
|
100
|
+
retry failures automatically. Missing usage, prices, and incomplete evidence
|
|
101
|
+
remain visible.
|
|
102
|
+
|
|
103
|
+
## Investigate and compare
|
|
104
|
+
|
|
105
|
+
Give your coding agent a failed test, its JSON evidence, the requirements, and
|
|
106
|
+
the relevant source. It can investigate the failure and help you change the
|
|
107
|
+
interface. Keep the success criteria fixed when rerunning a comparison.
|
|
108
|
+
|
|
109
|
+
For A/B tests, record verification separately for each side: both saved
|
|
110
|
+
executions share one pytest outcome. That outcome alone cannot establish
|
|
111
|
+
independent pass rates or a causal improvement. See
|
|
112
|
+
[comparing configurations](docs/getting-started/comparing.md).
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
- [Getting started](https://sbroenne.github.io/pytest-skill-engineering/getting-started/)
|
|
117
|
+
- [Task guides](docs/how-to/index.md) and [API reference](docs/reference/api.md)
|
|
118
|
+
- [Examples](examples/README.md), including historical skill comparisons
|
|
119
|
+
- [Upgrading from 0.6.x](docs/migration.md): 1.0 removed AI judging, renderers,
|
|
120
|
+
rankings, and skill refinement
|
|
121
|
+
|
|
122
|
+
No framework companion skill is required or distributed. Testing your own
|
|
123
|
+
domain skills is supported.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT. Inspired by [agent-benchmark](https://github.com/mykhaliev/agent-benchmark).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-skill-engineering"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Reliable Copilot execution and evidence for coding-agent-led testing of MCP servers, CLI tools, skills, and custom agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -26,25 +26,25 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Software Development :: Testing",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"pytest>=9.
|
|
30
|
-
"mcp>=2.
|
|
31
|
-
"pydantic>=2.
|
|
29
|
+
"pytest>=9.2.0",
|
|
30
|
+
"mcp>=2.3.0",
|
|
31
|
+
"pydantic>=2.14.1",
|
|
32
32
|
"python-frontmatter>=1.3.0",
|
|
33
|
-
"github-copilot-sdk>=1.0.
|
|
33
|
+
"github-copilot-sdk>=1.0.28",
|
|
34
34
|
"httpx>=0.28.1",
|
|
35
35
|
"trio>=0.34.0",
|
|
36
|
-
"tomlkit>=0.15.
|
|
37
|
-
"pytest-asyncio>=1.4.
|
|
38
|
-
"websockets>=17.
|
|
36
|
+
"tomlkit>=0.15.2",
|
|
37
|
+
"pytest-asyncio>=1.4.1",
|
|
38
|
+
"websockets>=17.3",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
41
|
[project.optional-dependencies]
|
|
42
42
|
dev = [
|
|
43
43
|
"pytest-cov>=7.1.0",
|
|
44
|
-
"python-dotenv>=1.2.
|
|
45
|
-
"ruff>=0.
|
|
44
|
+
"python-dotenv>=1.2.4",
|
|
45
|
+
"ruff>=0.17.3",
|
|
46
46
|
"pyright>=1.1.414",
|
|
47
|
-
"pre-commit>=4.
|
|
47
|
+
"pre-commit>=4.7.0",
|
|
48
48
|
"typeguard>=4.6.0",
|
|
49
49
|
]
|
|
50
50
|
test = [
|
|
@@ -53,8 +53,8 @@ test = [
|
|
|
53
53
|
docs = [
|
|
54
54
|
"mkdocs>=1.6.1",
|
|
55
55
|
"mkdocs-material>=9.7.7,<9.8",
|
|
56
|
-
"mkdocstrings[python]>=1.0.
|
|
57
|
-
"pillow>=
|
|
56
|
+
"mkdocstrings[python]>=1.0.8",
|
|
57
|
+
"pillow>=13.0.0",
|
|
58
58
|
"cairosvg>=2.9.1",
|
|
59
59
|
]
|
|
60
60
|
|
|
@@ -98,7 +98,7 @@ quote-style = "double"
|
|
|
98
98
|
indent-style = "space"
|
|
99
99
|
|
|
100
100
|
[tool.pyright]
|
|
101
|
-
include = ["src", "tests", "examples/skill-dogfood"]
|
|
101
|
+
include = ["src", "tests", "examples/skill-dogfood", "examples/excel-skill-value"]
|
|
102
102
|
pythonVersion = "3.11"
|
|
103
103
|
typeCheckingMode = "basic"
|
|
104
104
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Configuration file loaders for Copilot and Claude Code projects.
|
|
2
|
+
|
|
3
|
+
Provides utilities for loading MCP server configurations from standard
|
|
4
|
+
config files (``.mcp.json``, ``.vscode/mcp.json``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from pytest_skill_engineering.core.plugin import _read_json
|
|
14
|
+
from pytest_skill_engineering.core.validation import _validate_mcp_servers
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pytest_skill_engineering.copilot.contracts import CopilotEvalConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def snapshot_session_configuration(
|
|
21
|
+
agent: CopilotEvalConfig, session_config: dict[str, Any]
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Capture prepared settings without handlers, credentials, or server environments."""
|
|
24
|
+
system = session_config.get("system_message") or {}
|
|
25
|
+
snapshot = {
|
|
26
|
+
"name": agent.name,
|
|
27
|
+
"model": session_config.get("model"),
|
|
28
|
+
"instructions": system.get("content"),
|
|
29
|
+
"system_message_mode": system.get("mode"),
|
|
30
|
+
"reasoning_effort": session_config.get("reasoning_effort"),
|
|
31
|
+
"client_mode": agent.client_mode,
|
|
32
|
+
"image_detail": agent.image_detail,
|
|
33
|
+
"audit_requests": agent.audit_requests,
|
|
34
|
+
"max_tool_calls": agent.max_tool_calls,
|
|
35
|
+
"max_turns": agent.max_turns,
|
|
36
|
+
"timeout_s": agent.timeout_s,
|
|
37
|
+
"auto_confirm": agent.auto_confirm,
|
|
38
|
+
"allowed_tools": session_config.get("available_tools"),
|
|
39
|
+
"excluded_tools": session_config.get("excluded_tools"),
|
|
40
|
+
"skill_directories": session_config.get("skill_directories", []),
|
|
41
|
+
"disabled_skills": session_config.get("disabled_skills", []),
|
|
42
|
+
"active_agent": session_config.get("agent"),
|
|
43
|
+
"working_directory": session_config.get("working_directory"),
|
|
44
|
+
"persona": type(agent.persona).__name__,
|
|
45
|
+
"tools": [
|
|
46
|
+
{
|
|
47
|
+
"name": tool.name,
|
|
48
|
+
"description": tool.description,
|
|
49
|
+
"parameters": tool.parameters,
|
|
50
|
+
"metadata": tool.metadata,
|
|
51
|
+
}
|
|
52
|
+
for tool in session_config.get("tools") or []
|
|
53
|
+
],
|
|
54
|
+
"mcp_servers": {
|
|
55
|
+
name: {"type": config.get("type"), "tools": config.get("tools")}
|
|
56
|
+
for name, config in (session_config.get("mcp_servers") or {}).items()
|
|
57
|
+
},
|
|
58
|
+
"custom_agents": [
|
|
59
|
+
{
|
|
60
|
+
key: value
|
|
61
|
+
for key, value in custom.items()
|
|
62
|
+
if key
|
|
63
|
+
in {
|
|
64
|
+
"name",
|
|
65
|
+
"prompt",
|
|
66
|
+
"description",
|
|
67
|
+
"display_name",
|
|
68
|
+
"model",
|
|
69
|
+
"reasoning_effort",
|
|
70
|
+
"tools",
|
|
71
|
+
"infer",
|
|
72
|
+
"skills",
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for custom in session_config.get("custom_agents") or []
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
for key in (
|
|
79
|
+
"enable_config_discovery",
|
|
80
|
+
"enable_file_hooks",
|
|
81
|
+
"enable_on_demand_instruction_discovery",
|
|
82
|
+
"skip_custom_instructions",
|
|
83
|
+
"enable_managed_settings",
|
|
84
|
+
"enable_skills",
|
|
85
|
+
):
|
|
86
|
+
if key in session_config:
|
|
87
|
+
snapshot[key] = session_config[key]
|
|
88
|
+
return deepcopy(snapshot)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_mcp_config(path: str | Path) -> dict[str, dict[str, Any]]:
|
|
92
|
+
"""Load MCP server configs from a ``.mcp.json`` file.
|
|
93
|
+
|
|
94
|
+
Supports both Claude Code / root-level ``.mcp.json`` and VS Code
|
|
95
|
+
``.vscode/mcp.json`` formats. Handles two common top-level keys:
|
|
96
|
+
|
|
97
|
+
* ``mcpServers`` — Claude Code / standard MCP convention
|
|
98
|
+
* ``servers`` — VS Code ``mcp.json`` convention
|
|
99
|
+
|
|
100
|
+
Exactly one supported key must be present. An explicit empty mapping is valid;
|
|
101
|
+
missing keys, conflicting keys, and malformed server definitions raise errors.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dict of ``server_name → config`` compatible with
|
|
105
|
+
``CopilotEval(mcp_servers=...)``.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
FileNotFoundError: If the file does not exist.
|
|
109
|
+
ValueError: If the file is not valid JSON or has unexpected structure.
|
|
110
|
+
|
|
111
|
+
Example::
|
|
112
|
+
|
|
113
|
+
from pytest_skill_engineering.copilot.config import load_mcp_config
|
|
114
|
+
|
|
115
|
+
servers = load_mcp_config(".mcp.json")
|
|
116
|
+
agent = CopilotEval(mcp_servers=servers)
|
|
117
|
+
"""
|
|
118
|
+
path = Path(path)
|
|
119
|
+
if not path.is_file():
|
|
120
|
+
msg = f"MCP config file not found: {path}"
|
|
121
|
+
raise FileNotFoundError(msg)
|
|
122
|
+
|
|
123
|
+
raw = _read_json(path)
|
|
124
|
+
|
|
125
|
+
if not isinstance(raw, dict):
|
|
126
|
+
msg = f"{path}: MCP config must be a JSON object, got {type(raw).__name__}"
|
|
127
|
+
raise ValueError(msg)
|
|
128
|
+
|
|
129
|
+
fields = [key for key in ("mcpServers", "servers") if key in raw]
|
|
130
|
+
if len(fields) != 1:
|
|
131
|
+
raise ValueError(f"{path}: specify exactly one of 'mcpServers' and 'servers'")
|
|
132
|
+
|
|
133
|
+
return _validate_mcp_servers(raw[fields[0]], path, field=fields[0])
|
|
@@ -3,24 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Awaitable, Callable, Mapping
|
|
6
|
-
from dataclasses import dataclass
|
|
7
6
|
from typing import Any, Literal, Protocol, TypeAlias
|
|
8
7
|
|
|
9
8
|
from copilot.session import CustomAgentConfig, MCPServerConfig, ReasoningEffort, SessionHooks
|
|
10
9
|
|
|
10
|
+
from pytest_skill_engineering.copilot.result import CopilotResult, CopilotResultAgent
|
|
11
|
+
|
|
11
12
|
CopilotCustomAgentConfig: TypeAlias = CustomAgentConfig
|
|
12
13
|
CopilotMCPServerConfig: TypeAlias = MCPServerConfig
|
|
13
14
|
CopilotReasoningEffort: TypeAlias = ReasoningEffort
|
|
14
15
|
CopilotSessionHooks: TypeAlias = SessionHooks
|
|
15
|
-
SubagentStatus: TypeAlias = Literal["selected", "started", "completed", "failed"]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class CopilotResultAgent(Protocol):
|
|
19
|
-
"""Minimal agent surface stored on :class:`CopilotResult`."""
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def working_directory(self) -> str | None:
|
|
23
|
-
raise NotImplementedError
|
|
24
16
|
|
|
25
17
|
|
|
26
18
|
class CopilotRunResult(Protocol):
|
|
@@ -53,6 +45,11 @@ class CopilotEventMapper(Protocol):
|
|
|
53
45
|
def record_subagent_failed(self, *, invocation_id: str, name: str) -> None:
|
|
54
46
|
raise NotImplementedError
|
|
55
47
|
|
|
48
|
+
def record_subagent_result(
|
|
49
|
+
self, *, invocation_id: str, name: str, result: CopilotResult
|
|
50
|
+
) -> None:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
56
53
|
|
|
57
54
|
class CopilotEvalConfig(CopilotResultAgent, Protocol):
|
|
58
55
|
"""Configuration surface consumed by the runner and personas."""
|
|
@@ -141,7 +138,7 @@ class CopilotEvalConfig(CopilotResultAgent, Protocol):
|
|
|
141
138
|
|
|
142
139
|
CopilotNestedRunner: TypeAlias = Callable[
|
|
143
140
|
[CopilotEvalConfig, str],
|
|
144
|
-
Awaitable[
|
|
141
|
+
Awaitable["CopilotResult"],
|
|
145
142
|
]
|
|
146
143
|
|
|
147
144
|
|
|
@@ -158,16 +155,6 @@ class CopilotPersona(Protocol):
|
|
|
158
155
|
raise NotImplementedError
|
|
159
156
|
|
|
160
157
|
|
|
161
|
-
@dataclass(slots=True)
|
|
162
|
-
class SubagentInvocation:
|
|
163
|
-
"""A single custom-agent dispatch observed during a run."""
|
|
164
|
-
|
|
165
|
-
invocation_id: str
|
|
166
|
-
name: str
|
|
167
|
-
status: SubagentStatus
|
|
168
|
-
duration_ms: float | None = None
|
|
169
|
-
|
|
170
|
-
|
|
171
158
|
def declared_agent_tools(
|
|
172
159
|
agent_config: CopilotCustomAgentConfig,
|
|
173
160
|
) -> list[str] | None:
|
|
@@ -18,7 +18,6 @@ from pytest_skill_engineering.copilot.personas import ClaudeCodePersona, VSCodeP
|
|
|
18
18
|
from pytest_skill_engineering.core.evals import load_custom_agent, load_instruction_file
|
|
19
19
|
from pytest_skill_engineering.core.plugin import (
|
|
20
20
|
_discover_skills,
|
|
21
|
-
_validate_mcp_servers,
|
|
22
21
|
_validate_skill_reference_names,
|
|
23
22
|
)
|
|
24
23
|
|
|
@@ -34,30 +33,20 @@ def _parse_agent_file(path: Path) -> CopilotCustomAgentConfig:
|
|
|
34
33
|
fields and uses the Markdown body as the custom agent's system prompt.
|
|
35
34
|
Parsing and metadata errors retain the definition's file path.
|
|
36
35
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
agent: CopilotCustomAgentConfig = {
|
|
40
|
-
"name": frontmatter.get("name", loaded["name"]),
|
|
41
|
-
"prompt": loaded["prompt"],
|
|
42
|
-
}
|
|
43
|
-
for key in (
|
|
44
|
-
"description",
|
|
45
|
-
"display_name",
|
|
46
|
-
"infer",
|
|
47
|
-
"skills",
|
|
48
|
-
"model",
|
|
49
|
-
"reasoning_effort",
|
|
50
|
-
"tools",
|
|
51
|
-
):
|
|
52
|
-
if key in frontmatter:
|
|
53
|
-
agent[key] = frontmatter[key]
|
|
54
|
-
if "mcp-servers" in frontmatter:
|
|
55
|
-
agent["mcp_servers"] = cast(
|
|
56
|
-
dict[str, CopilotMCPServerConfig],
|
|
57
|
-
_validate_mcp_servers(frontmatter["mcp-servers"], path, field="mcp-servers"),
|
|
58
|
-
)
|
|
36
|
+
return cast(CopilotCustomAgentConfig, load_custom_agent(path))
|
|
37
|
+
|
|
59
38
|
|
|
60
|
-
|
|
39
|
+
def _parse_agent_files(paths: list[Path]) -> list[CopilotCustomAgentConfig]:
|
|
40
|
+
agents: list[CopilotCustomAgentConfig] = []
|
|
41
|
+
names: set[str] = set()
|
|
42
|
+
for path in paths:
|
|
43
|
+
agent = _parse_agent_file(path)
|
|
44
|
+
name = require_custom_agent_name(agent)
|
|
45
|
+
if name in names:
|
|
46
|
+
raise ValueError(f"{path}: Duplicate custom agent name '{name}'")
|
|
47
|
+
names.add(name)
|
|
48
|
+
agents.append(agent)
|
|
49
|
+
return agents
|
|
61
50
|
|
|
62
51
|
|
|
63
52
|
def _default_persona() -> CopilotPersona:
|
|
@@ -361,8 +350,7 @@ class CopilotEval:
|
|
|
361
350
|
if agents_dir.exists():
|
|
362
351
|
if not agents_dir.is_dir():
|
|
363
352
|
raise ValueError(f"{agents_dir}: custom agents path must be a directory")
|
|
364
|
-
|
|
365
|
-
agents.append(_parse_agent_file(agent_file))
|
|
353
|
+
agents = _parse_agent_files(sorted(agents_dir.rglob("*.agent.md")))
|
|
366
354
|
|
|
367
355
|
config: dict[str, Any] = {
|
|
368
356
|
"instructions": instructions,
|
|
@@ -527,8 +515,7 @@ class CopilotEval:
|
|
|
527
515
|
if not agents_dir.is_dir():
|
|
528
516
|
raise ValueError(f"{agents_dir}: custom agents path must be a directory")
|
|
529
517
|
# Claude Code uses plain .md files for agents
|
|
530
|
-
|
|
531
|
-
agents.append(_parse_agent_file(agent_file))
|
|
518
|
+
agents = _parse_agent_files(sorted(agents_dir.glob("*.md")))
|
|
532
519
|
|
|
533
520
|
# 3. Discover skill directories from .claude/skills/
|
|
534
521
|
skills = _discover_skills(claude_dir)
|