pytest-skill-engineering 1.0.0__tar.gz → 1.0.1__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.0 → pytest_skill_engineering-1.0.1}/PKG-INFO +1 -1
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/pyproject.toml +1 -1
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/__init__.py +3 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/eval.py +17 -1
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/fixtures.py +1 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/result.py +8 -1
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/runner.py +29 -2
- pytest_skill_engineering-1.0.1/src/pytest_skill_engineering/copilot/skills.py +104 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/__init__.py +4 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/result.py +26 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/serialization.py +23 -1
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/.gitignore +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/LICENSE +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/README.md +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/__init__.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/api.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/client.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/config.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/contracts.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/controls.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/events.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/personas.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/copilot/requests.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/errors.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/evals.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/plugin.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/prompt.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/skill.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/skill_evals.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/core/skill_grading.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/execution/__init__.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/execution/cost.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/execution/rate_limiter.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/execution/servers.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/execution/skill_tools.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/fixtures/__init__.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/fixtures/factories.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/fixtures/iteration.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/onboarding.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/plugin.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/plugin_options.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/plugin_report.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/reporting/__init__.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/reporting/collector.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/reporting/generator.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/reporting/schema.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/testing/__init__.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/testing/banking.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/testing/banking_mcp.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/testing/todo.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/testing/todo_mcp.py +0 -0
- {pytest_skill_engineering-1.0.0 → pytest_skill_engineering-1.0.1}/src/pytest_skill_engineering/testing/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pytest-skill-engineering
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Reliable Copilot execution and evidence for coding-agent-led testing of MCP servers, CLI tools, skills, and custom agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sbroenne/pytest-skill-engineering
|
|
6
6
|
Project-URL: Repository, https://github.com/sbroenne/pytest-skill-engineering
|
|
@@ -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.1"
|
|
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" }
|
|
@@ -22,11 +22,14 @@ from pytest_skill_engineering.copilot.personas import (
|
|
|
22
22
|
from pytest_skill_engineering.copilot.requests import RequestAudit
|
|
23
23
|
from pytest_skill_engineering.copilot.result import CopilotResult, UsageInfo
|
|
24
24
|
from pytest_skill_engineering.core.evals import load_custom_agent, load_custom_agents
|
|
25
|
+
from pytest_skill_engineering.core.result import DiscoveredSkill, SkillDiscovery
|
|
25
26
|
|
|
26
27
|
__all__ = [
|
|
27
28
|
"CopilotEval",
|
|
28
29
|
"CopilotResult",
|
|
30
|
+
"DiscoveredSkill",
|
|
29
31
|
"RequestAudit",
|
|
32
|
+
"SkillDiscovery",
|
|
30
33
|
"UsageInfo",
|
|
31
34
|
"ClaudeCodePersona",
|
|
32
35
|
"CopilotCLIPersona",
|
|
@@ -139,7 +139,7 @@ class CopilotEval:
|
|
|
139
139
|
# display_name, tools, mcp_servers, infer)
|
|
140
140
|
custom_agents: list[CopilotCustomAgentConfig] = field(default_factory=list)
|
|
141
141
|
|
|
142
|
-
#
|
|
142
|
+
# Explicit skills are enabled and checked before model execution.
|
|
143
143
|
skill_directories: list[str] = field(default_factory=list)
|
|
144
144
|
disabled_skills: list[str] = field(default_factory=list)
|
|
145
145
|
|
|
@@ -235,8 +235,24 @@ class CopilotEval:
|
|
|
235
235
|
config["hooks"] = self.hooks
|
|
236
236
|
|
|
237
237
|
# Apply extra_config passthrough
|
|
238
|
+
if (
|
|
239
|
+
self.skill_directories
|
|
240
|
+
and "skill_directories" in self.extra_config
|
|
241
|
+
and self.extra_config["skill_directories"] != self.skill_directories
|
|
242
|
+
):
|
|
243
|
+
raise ValueError("extra_config must not override explicitly supplied skill_directories")
|
|
238
244
|
config.update(self.extra_config)
|
|
239
245
|
|
|
246
|
+
if config.get("skill_directories"):
|
|
247
|
+
if config.get("enable_skills") is False:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
"Explicit skill_directories conflict with enable_skills=False; "
|
|
250
|
+
"remove the directories for a no-skill baseline, or enable skills"
|
|
251
|
+
)
|
|
252
|
+
if config.get("enable_skills") is not None and config["enable_skills"] is not True:
|
|
253
|
+
raise ValueError("enable_skills must be a boolean or None")
|
|
254
|
+
config["enable_skills"] = True
|
|
255
|
+
|
|
240
256
|
if self.client_mode == "empty":
|
|
241
257
|
isolated = {
|
|
242
258
|
"enable_config_discovery": False,
|
|
@@ -12,7 +12,13 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
from pytest_skill_engineering.copilot.contracts import CopilotResultAgent, SubagentInvocation
|
|
14
14
|
from pytest_skill_engineering.copilot.requests import RequestAudit
|
|
15
|
-
from pytest_skill_engineering.core.result import
|
|
15
|
+
from pytest_skill_engineering.core.result import (
|
|
16
|
+
SkillDiscovery,
|
|
17
|
+
StopReason,
|
|
18
|
+
ToolCall,
|
|
19
|
+
Turn,
|
|
20
|
+
UsageInfo,
|
|
21
|
+
)
|
|
16
22
|
|
|
17
23
|
__all__ = [
|
|
18
24
|
"CopilotResult",
|
|
@@ -68,6 +74,7 @@ class CopilotResult:
|
|
|
68
74
|
request_audit: list[RequestAudit] = field(default_factory=list)
|
|
69
75
|
stop_reason: StopReason | None = None
|
|
70
76
|
tool_calls_admitted: int = 0
|
|
77
|
+
skill_discovery: SkillDiscovery | None = None
|
|
71
78
|
|
|
72
79
|
@property
|
|
73
80
|
def evidence_complete(self) -> bool:
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import logging
|
|
17
|
+
from pathlib import Path
|
|
17
18
|
from tempfile import TemporaryDirectory
|
|
18
19
|
from typing import TYPE_CHECKING
|
|
19
20
|
|
|
@@ -27,6 +28,8 @@ from pytest_skill_engineering.copilot.controls import RunControls
|
|
|
27
28
|
from pytest_skill_engineering.copilot.events import EventMapper
|
|
28
29
|
from pytest_skill_engineering.copilot.requests import RequestAuditHandler
|
|
29
30
|
from pytest_skill_engineering.copilot.result import CopilotResult, StopReason
|
|
31
|
+
from pytest_skill_engineering.copilot.skills import discover_requested_skills, requested_skill_files
|
|
32
|
+
from pytest_skill_engineering.core.result import SkillDiscovery
|
|
30
33
|
|
|
31
34
|
if TYPE_CHECKING:
|
|
32
35
|
from copilot.client import CopilotClient
|
|
@@ -80,8 +83,20 @@ async def run_copilot(agent: CopilotEvalConfig, prompt: str) -> CopilotResult:
|
|
|
80
83
|
reason: StopReason = "completed"
|
|
81
84
|
error: str | None = None
|
|
82
85
|
cleanup_errors: list[str] = []
|
|
86
|
+
skill_discovery: SkillDiscovery | None = None
|
|
87
|
+
send_started = False
|
|
83
88
|
try:
|
|
84
89
|
session_config = agent.build_session_config()
|
|
90
|
+
skill_directories = session_config.get("skill_directories") or []
|
|
91
|
+
requested_skills: dict[Path, str] = {}
|
|
92
|
+
if skill_directories:
|
|
93
|
+
skill_discovery = SkillDiscovery()
|
|
94
|
+
if not isinstance(skill_directories, list):
|
|
95
|
+
raise ValueError("skill_directories must be a list of directory paths")
|
|
96
|
+
requested_skills = requested_skill_files(skill_directories)
|
|
97
|
+
session_config["skill_directories"] = [
|
|
98
|
+
str(Path(directory).resolve()) for directory in skill_directories
|
|
99
|
+
]
|
|
85
100
|
caller_hooks = dict(session_config.get("hooks") or {})
|
|
86
101
|
session_config["hooks"] = dict(caller_hooks)
|
|
87
102
|
# Empty mode must not read persona instruction files or inject tools.
|
|
@@ -123,12 +138,20 @@ async def run_copilot(agent: CopilotEvalConfig, prompt: str) -> CopilotResult:
|
|
|
123
138
|
)
|
|
124
139
|
|
|
125
140
|
async def execute() -> None:
|
|
126
|
-
nonlocal session
|
|
141
|
+
nonlocal session, send_started
|
|
127
142
|
assert client is not None
|
|
128
143
|
async with asyncio.timeout(min(60, agent.timeout_s)):
|
|
129
144
|
await client.start()
|
|
130
145
|
async with asyncio.timeout(min(30, agent.timeout_s)):
|
|
131
146
|
session = await client.create_session(**session_config)
|
|
147
|
+
if skill_discovery is not None:
|
|
148
|
+
await discover_requested_skills(
|
|
149
|
+
session,
|
|
150
|
+
requested_skills,
|
|
151
|
+
session_config.get("disabled_skills") or [],
|
|
152
|
+
skill_discovery,
|
|
153
|
+
)
|
|
154
|
+
send_started = True
|
|
132
155
|
await session.send_and_wait(prompt, timeout=agent.timeout_s)
|
|
133
156
|
|
|
134
157
|
execution = asyncio.create_task(execute())
|
|
@@ -158,6 +181,9 @@ async def run_copilot(agent: CopilotEvalConfig, prompt: str) -> CopilotResult:
|
|
|
158
181
|
except Exception as exc:
|
|
159
182
|
logger.error("Copilot execution failed: %s", exc)
|
|
160
183
|
reason, error = "execution_error", str(exc)
|
|
184
|
+
if skill_discovery is not None and not skill_discovery.complete:
|
|
185
|
+
error = f"Skill setup failed before model execution: {exc}"
|
|
186
|
+
skill_discovery.errors.append(error)
|
|
161
187
|
finally:
|
|
162
188
|
controls.closed = True
|
|
163
189
|
for watcher in watchers:
|
|
@@ -210,12 +236,13 @@ async def run_copilot(agent: CopilotEvalConfig, prompt: str) -> CopilotResult:
|
|
|
210
236
|
|
|
211
237
|
result = mapper.build()
|
|
212
238
|
result.agent = agent
|
|
239
|
+
result.skill_discovery = skill_discovery
|
|
213
240
|
result.tool_calls_admitted = controls.admitted
|
|
214
241
|
if result.capture_errors:
|
|
215
242
|
result.capture_errors.extend(controls.incomplete_diagnostics(abort_phase=reason))
|
|
216
243
|
if audit is not None and capture_requests:
|
|
217
244
|
result.request_audit = list(audit.records)
|
|
218
|
-
if audit.error or (
|
|
245
|
+
if audit.error or (send_started and not audit.observed_requests):
|
|
219
246
|
audit_error = audit.error or "No outbound model requests captured; audit unsupported"
|
|
220
247
|
result.capture_errors.append(audit_error)
|
|
221
248
|
if reason == "completed" or audit.error:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Validate explicit skill inputs and verify native SDK availability before sending."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from pytest_skill_engineering.core.result import DiscoveredSkill, SkillDiscovery
|
|
10
|
+
from pytest_skill_engineering.core.skill import Skill, SkillError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from copilot.session import CopilotSession
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def requested_skill_files(directories: list[str]) -> dict[Path, str]:
|
|
19
|
+
"""Validate individual skill directories or parents of immediate skill directories.
|
|
20
|
+
|
|
21
|
+
Relative directories are resolved from the Python caller's current directory,
|
|
22
|
+
then passed to the SDK as absolute paths.
|
|
23
|
+
"""
|
|
24
|
+
requested: dict[Path, str] = {}
|
|
25
|
+
names: dict[str, Path] = {}
|
|
26
|
+
for raw_directory in directories:
|
|
27
|
+
if not isinstance(raw_directory, str) or not raw_directory.strip():
|
|
28
|
+
raise SkillError("skill_directories entries must be non-empty directory paths")
|
|
29
|
+
directory = Path(raw_directory).resolve()
|
|
30
|
+
if not directory.is_dir():
|
|
31
|
+
raise SkillError(
|
|
32
|
+
f"Requested skill directory does not exist or is not a directory: {directory}"
|
|
33
|
+
)
|
|
34
|
+
try:
|
|
35
|
+
candidates = (
|
|
36
|
+
[directory]
|
|
37
|
+
if (directory / "SKILL.md").exists()
|
|
38
|
+
else [
|
|
39
|
+
child
|
|
40
|
+
for child in sorted(directory.iterdir())
|
|
41
|
+
if child.is_dir() and (child / "SKILL.md").exists()
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
if not candidates:
|
|
45
|
+
raise SkillError(f"No SKILL.md found in requested skill directory: {directory}")
|
|
46
|
+
for candidate in candidates:
|
|
47
|
+
skill = Skill.from_path(candidate)
|
|
48
|
+
path = (skill.path / "SKILL.md").resolve()
|
|
49
|
+
if skill.name in names and names[skill.name] != path:
|
|
50
|
+
raise SkillError(
|
|
51
|
+
f"Duplicate requested skill name '{skill.name}': "
|
|
52
|
+
f"{names[skill.name]} and {path}"
|
|
53
|
+
)
|
|
54
|
+
names[skill.name] = path
|
|
55
|
+
requested[path] = skill.name
|
|
56
|
+
except OSError as exc:
|
|
57
|
+
raise SkillError(f"Cannot read requested skill directory {directory}: {exc}") from exc
|
|
58
|
+
return requested
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def discover_requested_skills(
|
|
62
|
+
session: CopilotSession,
|
|
63
|
+
requested: dict[Path, str],
|
|
64
|
+
disabled: list[str],
|
|
65
|
+
evidence: SkillDiscovery,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Record SDK discovery and reject missing or unexpectedly disabled requested skills."""
|
|
68
|
+
await session.rpc.skills.ensure_loaded()
|
|
69
|
+
diagnostics = await session.rpc.skills.reload()
|
|
70
|
+
evidence.warnings = list(diagnostics.warnings)
|
|
71
|
+
evidence.errors = list(diagnostics.errors)
|
|
72
|
+
for warning in evidence.warnings:
|
|
73
|
+
logger.warning("SDK skill discovery: %s", warning)
|
|
74
|
+
listed = await session.rpc.skills.list()
|
|
75
|
+
evidence.skills = [
|
|
76
|
+
DiscoveredSkill(
|
|
77
|
+
name=skill.name,
|
|
78
|
+
description=skill.description,
|
|
79
|
+
source=skill.source.value,
|
|
80
|
+
enabled=skill.enabled,
|
|
81
|
+
user_invocable=skill.user_invocable,
|
|
82
|
+
path=skill.path,
|
|
83
|
+
plugin_name=skill.plugin_name,
|
|
84
|
+
command_name=skill.command_name,
|
|
85
|
+
argument_hint=skill.argument_hint,
|
|
86
|
+
)
|
|
87
|
+
for skill in listed.skills
|
|
88
|
+
]
|
|
89
|
+
evidence.complete = True
|
|
90
|
+
available = {
|
|
91
|
+
(Path(skill.path).resolve(), skill.name): skill
|
|
92
|
+
for skill in evidence.skills
|
|
93
|
+
if skill.path is not None
|
|
94
|
+
}
|
|
95
|
+
for path, name in requested.items():
|
|
96
|
+
skill = available.get((path, name))
|
|
97
|
+
if skill is None:
|
|
98
|
+
evidence.errors.append(
|
|
99
|
+
f"Requested skill '{name}' was not discovered by the SDK: {path}"
|
|
100
|
+
)
|
|
101
|
+
elif not skill.enabled and name not in disabled:
|
|
102
|
+
evidence.errors.append(f"Requested skill '{name}' is unexpectedly disabled: {path}")
|
|
103
|
+
if evidence.errors:
|
|
104
|
+
raise SkillError("Skill setup failed before model execution: " + "; ".join(evidence.errors))
|
|
@@ -17,10 +17,12 @@ from pytest_skill_engineering.core.prompt import (
|
|
|
17
17
|
load_system_prompts,
|
|
18
18
|
)
|
|
19
19
|
from pytest_skill_engineering.core.result import (
|
|
20
|
+
DiscoveredSkill,
|
|
20
21
|
EvalResult,
|
|
21
22
|
ImageContent,
|
|
22
23
|
MCPPrompt,
|
|
23
24
|
MCPPromptArgument,
|
|
25
|
+
SkillDiscovery,
|
|
24
26
|
SkillInfo,
|
|
25
27
|
SubagentInvocation,
|
|
26
28
|
ToolCall,
|
|
@@ -38,6 +40,7 @@ from pytest_skill_engineering.core.skill_grading import export_grading
|
|
|
38
40
|
__all__ = [
|
|
39
41
|
"AITestError",
|
|
40
42
|
"EvalResult",
|
|
43
|
+
"DiscoveredSkill",
|
|
41
44
|
"EngineTimeoutError",
|
|
42
45
|
"HookDefinition",
|
|
43
46
|
"ImageContent",
|
|
@@ -48,6 +51,7 @@ __all__ = [
|
|
|
48
51
|
"Prompt",
|
|
49
52
|
"ServerStartError",
|
|
50
53
|
"Skill",
|
|
54
|
+
"SkillDiscovery",
|
|
51
55
|
"SkillError",
|
|
52
56
|
"SkillInfo",
|
|
53
57
|
"SkillMetadata",
|
|
@@ -181,6 +181,31 @@ class SkillInfo:
|
|
|
181
181
|
return f"SkillInfo({self.name}{refs})"
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
@dataclass(slots=True, frozen=True)
|
|
185
|
+
class DiscoveredSkill:
|
|
186
|
+
"""Skill metadata actually returned by the public SDK, not observed use."""
|
|
187
|
+
|
|
188
|
+
name: str
|
|
189
|
+
description: str
|
|
190
|
+
source: str
|
|
191
|
+
enabled: bool
|
|
192
|
+
user_invocable: bool
|
|
193
|
+
path: str | None = None
|
|
194
|
+
plugin_name: str | None = None
|
|
195
|
+
command_name: str | None = None
|
|
196
|
+
argument_hint: str | None = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(slots=True)
|
|
200
|
+
class SkillDiscovery:
|
|
201
|
+
"""Pre-execution discovery evidence, including incomplete or failed checks."""
|
|
202
|
+
|
|
203
|
+
skills: list[DiscoveredSkill] = field(default_factory=list)
|
|
204
|
+
warnings: list[str] = field(default_factory=list)
|
|
205
|
+
errors: list[str] = field(default_factory=list)
|
|
206
|
+
complete: bool = False
|
|
207
|
+
|
|
208
|
+
|
|
184
209
|
@dataclass(slots=True)
|
|
185
210
|
class ClarificationStats:
|
|
186
211
|
"""Statistics about clarification requests detected during execution.
|
|
@@ -280,6 +305,7 @@ class EvalResult:
|
|
|
280
305
|
# Recorded interface metadata.
|
|
281
306
|
available_tools: list[ToolInfo] = field(default_factory=list)
|
|
282
307
|
skill_info: SkillInfo | None = None
|
|
308
|
+
skill_discovery: SkillDiscovery | None = None
|
|
283
309
|
effective_system_prompt: str = ""
|
|
284
310
|
mcp_prompts: list[MCPPrompt] = field(default_factory=list)
|
|
285
311
|
prompt_name: str | None = None
|
|
@@ -56,7 +56,14 @@ def deserialize_suite_report(data: dict[str, Any]) -> SuiteReport:
|
|
|
56
56
|
Reconstructs the full dataclass hierarchy from the serialized format.
|
|
57
57
|
"""
|
|
58
58
|
from pytest_skill_engineering.copilot.requests import RequestAudit
|
|
59
|
-
from pytest_skill_engineering.core.result import
|
|
59
|
+
from pytest_skill_engineering.core.result import (
|
|
60
|
+
DiscoveredSkill,
|
|
61
|
+
EvalResult,
|
|
62
|
+
SkillDiscovery,
|
|
63
|
+
ToolCall,
|
|
64
|
+
Turn,
|
|
65
|
+
UsageInfo,
|
|
66
|
+
)
|
|
60
67
|
from pytest_skill_engineering.reporting.collector import SuiteReport, TestReport
|
|
61
68
|
|
|
62
69
|
# Reconstruct tests
|
|
@@ -214,6 +221,20 @@ def deserialize_suite_report(data: dict[str, Any]) -> SuiteReport:
|
|
|
214
221
|
)
|
|
215
222
|
|
|
216
223
|
# Reconstruct agent result
|
|
224
|
+
discovery_data = _require_key(ar_data, "skill_discovery", context="EvalResult")
|
|
225
|
+
skill_discovery = None
|
|
226
|
+
if discovery_data is not None:
|
|
227
|
+
skill_discovery = SkillDiscovery(
|
|
228
|
+
skills=[
|
|
229
|
+
DiscoveredSkill(**skill)
|
|
230
|
+
for skill in _require_key(
|
|
231
|
+
discovery_data, "skills", context="SkillDiscovery"
|
|
232
|
+
)
|
|
233
|
+
],
|
|
234
|
+
warnings=_require_key(discovery_data, "warnings", context="SkillDiscovery"),
|
|
235
|
+
errors=_require_key(discovery_data, "errors", context="SkillDiscovery"),
|
|
236
|
+
complete=_require_key(discovery_data, "complete", context="SkillDiscovery"),
|
|
237
|
+
)
|
|
217
238
|
eval_result = EvalResult(
|
|
218
239
|
turns=turns,
|
|
219
240
|
success=_require_key(ar_data, "success", context="EvalResult"),
|
|
@@ -228,6 +249,7 @@ def deserialize_suite_report(data: dict[str, Any]) -> SuiteReport:
|
|
|
228
249
|
assertions=assertions,
|
|
229
250
|
available_tools=available_tools,
|
|
230
251
|
skill_info=skill_info,
|
|
252
|
+
skill_discovery=skill_discovery,
|
|
231
253
|
effective_system_prompt=_require_key(
|
|
232
254
|
ar_data, "effective_system_prompt", context="EvalResult"
|
|
233
255
|
),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|