skilltest-pytest 0.5.0__tar.gz → 0.7.0__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.
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/PKG-INFO +43 -18
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/README.md +41 -16
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/pyproject.toml +2 -2
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/skilltest_pytest/__init__.py +32 -8
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/tests/test_plugin.py +66 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/uv.lock +2 -2
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/.gitignore +0 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/project.json +0 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/skilltest_pytest/plugin.py +0 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/tests/collected/deploy.skilltest.yaml +0 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/tests/collected/deployer/SKILL.md +0 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/tests/collected/greet.skilltest.yaml +0 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/tests/collected/greeter/SKILL.md +0 -0
- {skilltest_pytest-0.5.0 → skilltest_pytest-0.7.0}/tests/conftest.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skilltest-pytest
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: pytest integration for skilltest: auto-collect *.skilltest.yaml cases as pytest tests, built on skilltest-sdk.
|
|
5
5
|
Author: Nick DeRobertis
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
8
|
Requires-Dist: pytest>=8
|
|
9
|
-
Requires-Dist: skilltest-sdk==0.
|
|
9
|
+
Requires-Dist: skilltest-sdk==0.7.0
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
|
|
12
12
|
# skilltest-pytest
|
|
@@ -17,10 +17,38 @@ your own deterministic checks. Built on
|
|
|
17
17
|
[`skilltest-sdk`](../../sdks/python/README.md) — the SDK's code API is
|
|
18
18
|
re-exported here, so a pytest suite needs only this one dependency.
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Define the whole case in code (recommended)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
Build the case — skill, input, evals, an optional simulated user, mocks — right
|
|
23
|
+
in the test. Everything the YAML carries has a typed builder, so the case, its
|
|
24
|
+
mocks, and any deterministic transcript checks live in one place:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from skilltest_pytest import TestCase, run_skill, boolean, numeric, describe_failures
|
|
28
|
+
|
|
29
|
+
def test_greeter():
|
|
30
|
+
case = TestCase(
|
|
31
|
+
skill="skills/greeter", # resolved relative to the working dir
|
|
32
|
+
input="Greet Dr. Smith, who has an appointment today.",
|
|
33
|
+
evals=[
|
|
34
|
+
boolean("the reply greets Dr. Smith by name"),
|
|
35
|
+
numeric("how warm is the tone", min=0, max=10, threshold=7),
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
report = run_skill(case, platforms=["claude-code"], models=["claude-opus-4-8"])
|
|
39
|
+
assert report.passed, describe_failures(report)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Multi-turn cases add `user(...)`; deterministic call-count checks use `called` /
|
|
43
|
+
`not_called` taking the `stub`/`spy` object itself or a name you gave it (or
|
|
44
|
+
use the mock objects' own assertions — see below). `run_skill` also takes `platforms=`/`models=` to fan a
|
|
45
|
+
case across a matrix.
|
|
46
|
+
|
|
47
|
+
## Or point at a YAML file
|
|
48
|
+
|
|
49
|
+
`run_skill` accepts a path just as well (`run_skill("cases/greet.yaml")`), and
|
|
50
|
+
**auto-collection** still works: name a case `something.skilltest.yaml` and
|
|
51
|
+
pytest runs it with no test function at all —
|
|
24
52
|
|
|
25
53
|
```yaml
|
|
26
54
|
# greet.skilltest.yaml
|
|
@@ -31,16 +59,7 @@ evals:
|
|
|
31
59
|
criterion: "the reply greets Dr. Smith by name"
|
|
32
60
|
```
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```python
|
|
37
|
-
from skilltest_pytest import run_skill, describe_failures, assistant_text
|
|
38
|
-
|
|
39
|
-
def test_greeter():
|
|
40
|
-
report = run_skill("cases/greet.yaml", platforms=["claude-code"], models=["claude-opus-4-8"])
|
|
41
|
-
assert report.passed, describe_failures(report)
|
|
42
|
-
assert "Dr. Smith" in assistant_text(report.runs[0].transcript)
|
|
43
|
-
```
|
|
62
|
+
The full field reference for both forms is [`docs/schema.md`](../../docs/schema.md).
|
|
44
63
|
|
|
45
64
|
## Assert on tool use, and stream
|
|
46
65
|
|
|
@@ -50,10 +69,16 @@ returns the normalized `tool_call` events a run took (each a `ToolEvent` with
|
|
|
50
69
|
test can **short-circuit** on bad behavior:
|
|
51
70
|
|
|
52
71
|
```python
|
|
53
|
-
from skilltest_pytest import run_skill, tool_calls
|
|
72
|
+
from skilltest_pytest import TestCase, run_skill, tool_calls, boolean
|
|
73
|
+
|
|
74
|
+
EDIT_CASE = TestCase(
|
|
75
|
+
skill="skills/editor",
|
|
76
|
+
input="Update the config and commit it.",
|
|
77
|
+
evals=[boolean("the change was committed")],
|
|
78
|
+
)
|
|
54
79
|
|
|
55
80
|
def test_commits_but_never_deletes():
|
|
56
|
-
report = run_skill(
|
|
81
|
+
report = run_skill(EDIT_CASE)
|
|
57
82
|
calls = tool_calls(report.runs[0].transcript)
|
|
58
83
|
assert any("git commit" in str(c.input) for c in calls)
|
|
59
84
|
assert not any("rm -rf" in str(c.input) for c in calls)
|
|
@@ -65,7 +90,7 @@ from skilltest_pytest import stream_skill
|
|
|
65
90
|
|
|
66
91
|
def test_makes_no_network_call():
|
|
67
92
|
async def go():
|
|
68
|
-
async for ev in stream_skill(
|
|
93
|
+
async for ev in stream_skill(EDIT_CASE):
|
|
69
94
|
assert ev.event.name != "curl", "skill made a network call"
|
|
70
95
|
asyncio.run(go()) # or use pytest-asyncio and `async def test_...`
|
|
71
96
|
```
|
|
@@ -6,10 +6,38 @@ your own deterministic checks. Built on
|
|
|
6
6
|
[`skilltest-sdk`](../../sdks/python/README.md) — the SDK's code API is
|
|
7
7
|
re-exported here, so a pytest suite needs only this one dependency.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Define the whole case in code (recommended)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Build the case — skill, input, evals, an optional simulated user, mocks — right
|
|
12
|
+
in the test. Everything the YAML carries has a typed builder, so the case, its
|
|
13
|
+
mocks, and any deterministic transcript checks live in one place:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from skilltest_pytest import TestCase, run_skill, boolean, numeric, describe_failures
|
|
17
|
+
|
|
18
|
+
def test_greeter():
|
|
19
|
+
case = TestCase(
|
|
20
|
+
skill="skills/greeter", # resolved relative to the working dir
|
|
21
|
+
input="Greet Dr. Smith, who has an appointment today.",
|
|
22
|
+
evals=[
|
|
23
|
+
boolean("the reply greets Dr. Smith by name"),
|
|
24
|
+
numeric("how warm is the tone", min=0, max=10, threshold=7),
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
report = run_skill(case, platforms=["claude-code"], models=["claude-opus-4-8"])
|
|
28
|
+
assert report.passed, describe_failures(report)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Multi-turn cases add `user(...)`; deterministic call-count checks use `called` /
|
|
32
|
+
`not_called` taking the `stub`/`spy` object itself or a name you gave it (or
|
|
33
|
+
use the mock objects' own assertions — see below). `run_skill` also takes `platforms=`/`models=` to fan a
|
|
34
|
+
case across a matrix.
|
|
35
|
+
|
|
36
|
+
## Or point at a YAML file
|
|
37
|
+
|
|
38
|
+
`run_skill` accepts a path just as well (`run_skill("cases/greet.yaml")`), and
|
|
39
|
+
**auto-collection** still works: name a case `something.skilltest.yaml` and
|
|
40
|
+
pytest runs it with no test function at all —
|
|
13
41
|
|
|
14
42
|
```yaml
|
|
15
43
|
# greet.skilltest.yaml
|
|
@@ -20,16 +48,7 @@ evals:
|
|
|
20
48
|
criterion: "the reply greets Dr. Smith by name"
|
|
21
49
|
```
|
|
22
50
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```python
|
|
26
|
-
from skilltest_pytest import run_skill, describe_failures, assistant_text
|
|
27
|
-
|
|
28
|
-
def test_greeter():
|
|
29
|
-
report = run_skill("cases/greet.yaml", platforms=["claude-code"], models=["claude-opus-4-8"])
|
|
30
|
-
assert report.passed, describe_failures(report)
|
|
31
|
-
assert "Dr. Smith" in assistant_text(report.runs[0].transcript)
|
|
32
|
-
```
|
|
51
|
+
The full field reference for both forms is [`docs/schema.md`](../../docs/schema.md).
|
|
33
52
|
|
|
34
53
|
## Assert on tool use, and stream
|
|
35
54
|
|
|
@@ -39,10 +58,16 @@ returns the normalized `tool_call` events a run took (each a `ToolEvent` with
|
|
|
39
58
|
test can **short-circuit** on bad behavior:
|
|
40
59
|
|
|
41
60
|
```python
|
|
42
|
-
from skilltest_pytest import run_skill, tool_calls
|
|
61
|
+
from skilltest_pytest import TestCase, run_skill, tool_calls, boolean
|
|
62
|
+
|
|
63
|
+
EDIT_CASE = TestCase(
|
|
64
|
+
skill="skills/editor",
|
|
65
|
+
input="Update the config and commit it.",
|
|
66
|
+
evals=[boolean("the change was committed")],
|
|
67
|
+
)
|
|
43
68
|
|
|
44
69
|
def test_commits_but_never_deletes():
|
|
45
|
-
report = run_skill(
|
|
70
|
+
report = run_skill(EDIT_CASE)
|
|
46
71
|
calls = tool_calls(report.runs[0].transcript)
|
|
47
72
|
assert any("git commit" in str(c.input) for c in calls)
|
|
48
73
|
assert not any("rm -rf" in str(c.input) for c in calls)
|
|
@@ -54,7 +79,7 @@ from skilltest_pytest import stream_skill
|
|
|
54
79
|
|
|
55
80
|
def test_makes_no_network_call():
|
|
56
81
|
async def go():
|
|
57
|
-
async for ev in stream_skill(
|
|
82
|
+
async for ev in stream_skill(EDIT_CASE):
|
|
58
83
|
assert ev.event.name != "curl", "skill made a network call"
|
|
59
84
|
asyncio.run(go()) # or use pytest-asyncio and `async def test_...`
|
|
60
85
|
```
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "skilltest-pytest"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "pytest integration for skilltest: auto-collect *.skilltest.yaml cases as pytest tests, built on skilltest-sdk."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
authors = [{ name = "Nick DeRobertis" }]
|
|
9
9
|
dependencies = [
|
|
10
|
-
"skilltest-sdk==0.
|
|
10
|
+
"skilltest-sdk==0.7.0",
|
|
11
11
|
"pytest>=8",
|
|
12
12
|
]
|
|
13
13
|
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
"""skilltest-pytest: run AI-skill tests and natural-language evals as pytest.
|
|
2
2
|
|
|
3
|
-
The pytest integration on top of [`skilltest-sdk`][skilltest_sdk]
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
suite only needs one dependency:
|
|
3
|
+
The pytest integration on top of [`skilltest-sdk`][skilltest_sdk], whose API is
|
|
4
|
+
re-exported here so a pytest suite needs only this one dependency. Define the
|
|
5
|
+
whole case in code (the recommended form):
|
|
7
6
|
|
|
8
|
-
from skilltest_pytest import run_skill,
|
|
7
|
+
from skilltest_pytest import TestCase, run_skill, boolean, describe_failures
|
|
9
8
|
|
|
10
9
|
def test_greeter():
|
|
11
|
-
|
|
10
|
+
case = TestCase(
|
|
11
|
+
skill="skills/greeter",
|
|
12
|
+
input="Greet Dr. Smith.",
|
|
13
|
+
evals=[boolean("the reply greets Dr. Smith by name")],
|
|
14
|
+
)
|
|
15
|
+
report = run_skill(case)
|
|
12
16
|
assert report.passed, describe_failures(report)
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
|
|
18
|
+
Existing YAML cases stay first-class: ``run_skill("cases/greet.yaml")`` runs a
|
|
19
|
+
file, and any ``*.skilltest.yaml`` dropped next to your tests is auto-collected
|
|
20
|
+
as a test item with no code at all.
|
|
15
21
|
"""
|
|
16
22
|
|
|
17
23
|
from __future__ import annotations
|
|
@@ -22,18 +28,22 @@ from skilltest_sdk import (
|
|
|
22
28
|
BooleanDetail,
|
|
23
29
|
CallsDetail,
|
|
24
30
|
CaseRun,
|
|
31
|
+
Eval,
|
|
25
32
|
EvalOutcome,
|
|
26
33
|
Matcher,
|
|
27
34
|
Message,
|
|
28
35
|
MockCall,
|
|
36
|
+
MockRefEval,
|
|
29
37
|
NumericDetail,
|
|
30
38
|
Report,
|
|
39
|
+
SimulatedUser,
|
|
31
40
|
SkillStream,
|
|
32
41
|
SkilltestError,
|
|
33
42
|
SkilltestProviderError,
|
|
34
43
|
SkilltestUsageError,
|
|
35
44
|
StreamEvent,
|
|
36
45
|
Summary,
|
|
46
|
+
TestCase,
|
|
37
47
|
ToolCall,
|
|
38
48
|
ToolEvent,
|
|
39
49
|
ToolMock,
|
|
@@ -44,18 +54,23 @@ from skilltest_sdk import (
|
|
|
44
54
|
ValidationReport,
|
|
45
55
|
anything,
|
|
46
56
|
assistant_text,
|
|
57
|
+
boolean,
|
|
58
|
+
called,
|
|
47
59
|
contains,
|
|
48
60
|
deny,
|
|
49
61
|
describe_failures,
|
|
50
62
|
failed_evals,
|
|
51
63
|
failed_runs,
|
|
52
64
|
matching,
|
|
65
|
+
not_called,
|
|
66
|
+
numeric,
|
|
53
67
|
rewrite,
|
|
54
68
|
run_skill,
|
|
55
69
|
spy,
|
|
56
70
|
stream_skill,
|
|
57
71
|
stub,
|
|
58
72
|
tool_calls,
|
|
73
|
+
user,
|
|
59
74
|
validate_skill,
|
|
60
75
|
)
|
|
61
76
|
|
|
@@ -67,12 +82,15 @@ __all__ = [
|
|
|
67
82
|
"BooleanDetail",
|
|
68
83
|
"CallsDetail",
|
|
69
84
|
"CaseRun",
|
|
85
|
+
"Eval",
|
|
70
86
|
"EvalOutcome",
|
|
71
87
|
"Matcher",
|
|
72
88
|
"Message",
|
|
73
89
|
"MockCall",
|
|
90
|
+
"MockRefEval",
|
|
74
91
|
"NumericDetail",
|
|
75
92
|
"Report",
|
|
93
|
+
"SimulatedUser",
|
|
76
94
|
"SkillStream",
|
|
77
95
|
"SkilltestError",
|
|
78
96
|
"SkilltestFailure",
|
|
@@ -80,6 +98,7 @@ __all__ = [
|
|
|
80
98
|
"SkilltestUsageError",
|
|
81
99
|
"StreamEvent",
|
|
82
100
|
"Summary",
|
|
101
|
+
"TestCase",
|
|
83
102
|
"ToolCall",
|
|
84
103
|
"ToolEvent",
|
|
85
104
|
"ToolMock",
|
|
@@ -90,17 +109,22 @@ __all__ = [
|
|
|
90
109
|
"ValidationReport",
|
|
91
110
|
"anything",
|
|
92
111
|
"assistant_text",
|
|
112
|
+
"boolean",
|
|
113
|
+
"called",
|
|
93
114
|
"contains",
|
|
94
115
|
"deny",
|
|
95
116
|
"describe_failures",
|
|
96
117
|
"failed_evals",
|
|
97
118
|
"failed_runs",
|
|
98
119
|
"matching",
|
|
120
|
+
"not_called",
|
|
121
|
+
"numeric",
|
|
99
122
|
"rewrite",
|
|
100
123
|
"run_skill",
|
|
101
124
|
"spy",
|
|
102
125
|
"stream_skill",
|
|
103
126
|
"stub",
|
|
104
127
|
"tool_calls",
|
|
128
|
+
"user",
|
|
105
129
|
"validate_skill",
|
|
106
130
|
]
|
|
@@ -92,6 +92,72 @@ def test_collected_case_failure_reports_judge_reason(pytester: pytest.Pytester)
|
|
|
92
92
|
result.stdout.fnmatch_lines(["*skilltest case failed:*", "*says-goodbye*"])
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def test_code_defined_case_is_reexported_and_runs(fixtures: Path) -> None:
|
|
96
|
+
# The recommended form: build the whole case in code and hand it to
|
|
97
|
+
# run_skill — the case API rides the same one-dependency re-export.
|
|
98
|
+
from skilltest_pytest import TestCase, boolean, run_skill
|
|
99
|
+
|
|
100
|
+
case = TestCase(
|
|
101
|
+
skill=fixtures / "skills" / "greeter",
|
|
102
|
+
input="Greet Dr. Smith, who has an appointment today.",
|
|
103
|
+
evals=[boolean("the reply greets `Dr. Smith` by name")],
|
|
104
|
+
)
|
|
105
|
+
report = run_skill(case)
|
|
106
|
+
assert report.passed, describe_failures(report)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_full_code_defined_case_surface_is_reexported(fixtures: Path) -> None:
|
|
110
|
+
# Every case builder rides the one-dependency re-export: a multi-turn case
|
|
111
|
+
# with judge evals, and a mocked case with a deterministic call eval —
|
|
112
|
+
# streamed and buffered — defined entirely in code through the plugin.
|
|
113
|
+
from skilltest_pytest import (
|
|
114
|
+
TestCase,
|
|
115
|
+
boolean,
|
|
116
|
+
called,
|
|
117
|
+
numeric,
|
|
118
|
+
run_skill,
|
|
119
|
+
stream_skill,
|
|
120
|
+
stub,
|
|
121
|
+
user,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
multi = TestCase(
|
|
125
|
+
skill=fixtures / "skills" / "greeter",
|
|
126
|
+
input="I'd like to confirm my appointment, please.",
|
|
127
|
+
user=user(
|
|
128
|
+
"You are a terse patient.\nsay: Yes, please go ahead.",
|
|
129
|
+
done_when="the conversation has reached turns>=2",
|
|
130
|
+
max_turns=4,
|
|
131
|
+
),
|
|
132
|
+
evals=[
|
|
133
|
+
boolean("the assistant confirmed the appointment (`confirmed`)"),
|
|
134
|
+
numeric("mentions `confirmed`", min=0, max=10, threshold=5, comparator=">"),
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
report = run_skill(multi)
|
|
138
|
+
assert report.passed, describe_failures(report)
|
|
139
|
+
assert report.runs[0].turns == 2
|
|
140
|
+
|
|
141
|
+
# The eval references the stub by object — no name to keep in sync — and
|
|
142
|
+
# the reference resolves through the plugin's re-export and streaming path.
|
|
143
|
+
push = stub(pattern=r"git push( --force)?\b", output="Everything up-to-date")
|
|
144
|
+
mocked = TestCase(
|
|
145
|
+
skill=fixtures / "skills" / "deployer",
|
|
146
|
+
input="Deploy the app",
|
|
147
|
+
mocks=[push],
|
|
148
|
+
evals=[called(push, times=1)],
|
|
149
|
+
)
|
|
150
|
+
stream = stream_skill(mocked)
|
|
151
|
+
|
|
152
|
+
async def drain() -> None:
|
|
153
|
+
async for _ in stream:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
asyncio.run(drain())
|
|
157
|
+
assert stream.report is not None and stream.report.passed
|
|
158
|
+
push.assert_called_once()
|
|
159
|
+
|
|
160
|
+
|
|
95
161
|
def test_mock_api_is_reexported_and_binds(cases: Path) -> None:
|
|
96
162
|
# The mock/spy API rides the one-dependency re-export; code-level mocks
|
|
97
163
|
# intercept and bind through the plugin's SDK exactly as through the SDK.
|
|
@@ -189,7 +189,7 @@ wheels = [
|
|
|
189
189
|
|
|
190
190
|
[[package]]
|
|
191
191
|
name = "skilltest-pytest"
|
|
192
|
-
version = "0.
|
|
192
|
+
version = "0.7.0"
|
|
193
193
|
source = { editable = "." }
|
|
194
194
|
dependencies = [
|
|
195
195
|
{ name = "pytest" },
|
|
@@ -216,7 +216,7 @@ dev = [
|
|
|
216
216
|
|
|
217
217
|
[[package]]
|
|
218
218
|
name = "skilltest-sdk"
|
|
219
|
-
version = "0.
|
|
219
|
+
version = "0.7.0"
|
|
220
220
|
source = { editable = "../../sdks/python" }
|
|
221
221
|
dependencies = [
|
|
222
222
|
{ name = "pydantic" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|