pytest-skill-engineering 1.0.1__tar.gz → 1.0.2__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.1 → pytest_skill_engineering-1.0.2}/PKG-INFO +1 -1
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/pyproject.toml +1 -1
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/plugin.py +1 -1
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/skill.py +53 -18
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/.gitignore +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/LICENSE +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/README.md +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/__init__.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/__init__.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/api.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/client.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/config.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/contracts.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/controls.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/eval.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/events.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/fixtures.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/personas.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/requests.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/result.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/runner.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/copilot/skills.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/__init__.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/errors.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/evals.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/prompt.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/result.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/serialization.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/skill_evals.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/core/skill_grading.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/execution/__init__.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/execution/cost.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/execution/rate_limiter.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/execution/servers.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/execution/skill_tools.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/fixtures/__init__.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/fixtures/factories.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/fixtures/iteration.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/onboarding.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/plugin.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/plugin_options.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/plugin_report.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/reporting/__init__.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/reporting/collector.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/reporting/generator.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/reporting/schema.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/testing/__init__.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/testing/banking.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/testing/banking_mcp.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/testing/todo.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/src/pytest_skill_engineering/testing/todo_mcp.py +0 -0
- {pytest_skill_engineering-1.0.1 → pytest_skill_engineering-1.0.2}/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.2
|
|
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.2"
|
|
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" }
|
|
@@ -341,7 +341,7 @@ def _discover_skills(plugin_dir: Path) -> list[Skill]:
|
|
|
341
341
|
|
|
342
342
|
|
|
343
343
|
def _validate_skill_reference_names(skills: list[Skill]) -> None:
|
|
344
|
-
"""Reject duplicate reference
|
|
344
|
+
"""Reject duplicate relative reference paths across loaded skills."""
|
|
345
345
|
owners: dict[str, str] = {}
|
|
346
346
|
for skill in skills:
|
|
347
347
|
for reference_name in skill.references:
|
|
@@ -28,6 +28,7 @@ Example SKILL.md:
|
|
|
28
28
|
from __future__ import annotations
|
|
29
29
|
|
|
30
30
|
import re
|
|
31
|
+
import stat
|
|
31
32
|
from dataclasses import dataclass, field
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
|
|
@@ -163,7 +164,8 @@ class Skill:
|
|
|
163
164
|
path: Path to skill directory or SKILL.md file
|
|
164
165
|
|
|
165
166
|
Returns:
|
|
166
|
-
Loaded Skill instance
|
|
167
|
+
Loaded Skill instance. Reference keys are slash-separated paths
|
|
168
|
+
relative to references/, including any nested subdirectories.
|
|
167
169
|
|
|
168
170
|
Raises:
|
|
169
171
|
SkillError: If skill cannot be loaded or is invalid
|
|
@@ -197,7 +199,7 @@ class Skill:
|
|
|
197
199
|
|
|
198
200
|
for name in ("references", "scripts", "assets"):
|
|
199
201
|
component = skill_dir / name
|
|
200
|
-
if component.exists() and not component.is_dir():
|
|
202
|
+
if (component.exists() or component.is_symlink()) and not component.is_dir():
|
|
201
203
|
raise SkillError(f"{component}: skill component must be a directory")
|
|
202
204
|
|
|
203
205
|
# Load references if directory exists
|
|
@@ -320,27 +322,59 @@ def _parse_skill_md(content: str) -> tuple[SkillMetadata, str]:
|
|
|
320
322
|
|
|
321
323
|
|
|
322
324
|
def _load_references(refs_dir: Path) -> dict[str, str]:
|
|
323
|
-
"""Load
|
|
325
|
+
"""Load Markdown files recursively, without escaping references/ or following cycles.
|
|
324
326
|
|
|
325
327
|
Returns:
|
|
326
|
-
Dict mapping
|
|
328
|
+
Dict mapping slash-separated paths relative to references/ to content
|
|
327
329
|
"""
|
|
330
|
+
root = _resolve_reference_path(refs_dir, refs_dir.parent.resolve())
|
|
328
331
|
references: dict[str, str] = {}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
raise SkillError(f"Invalid references entry (must be a file): {file_path}")
|
|
333
|
-
if file_path.suffix.lower() != ".md":
|
|
334
|
-
raise SkillError(f"Invalid reference file '{file_path}': only .md files are allowed")
|
|
332
|
+
pending = [(refs_dir, frozenset({root}))]
|
|
333
|
+
while pending:
|
|
334
|
+
directory, ancestors = pending.pop()
|
|
335
335
|
try:
|
|
336
|
-
|
|
337
|
-
except
|
|
338
|
-
raise SkillError(f"
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
336
|
+
entries = sorted(directory.iterdir())
|
|
337
|
+
except OSError as exc:
|
|
338
|
+
raise SkillError(f"Cannot read references directory {directory}: {exc}") from exc
|
|
339
|
+
for file_path in entries:
|
|
340
|
+
resolved = _resolve_reference_path(file_path, root)
|
|
341
|
+
try:
|
|
342
|
+
mode = resolved.stat().st_mode
|
|
343
|
+
except OSError as exc:
|
|
344
|
+
raise SkillError(f"Cannot inspect reference entry {file_path}: {exc}") from exc
|
|
345
|
+
if stat.S_ISDIR(mode):
|
|
346
|
+
if resolved in ancestors:
|
|
347
|
+
raise SkillError(f"Reference directory cycle: {file_path}")
|
|
348
|
+
pending.append((file_path, ancestors | {resolved}))
|
|
349
|
+
continue
|
|
350
|
+
if not stat.S_ISREG(mode):
|
|
351
|
+
raise SkillError(f"Invalid references entry (must be a file): {file_path}")
|
|
352
|
+
if file_path.suffix.lower() != ".md":
|
|
353
|
+
raise SkillError(
|
|
354
|
+
f"Invalid reference file '{file_path}': only .md files are allowed"
|
|
355
|
+
)
|
|
356
|
+
try:
|
|
357
|
+
content = resolved.read_text(encoding="utf-8")
|
|
358
|
+
except UnicodeDecodeError as exc:
|
|
359
|
+
raise SkillError(f"Reference file must be valid UTF-8 text: {file_path}") from exc
|
|
360
|
+
except OSError as exc:
|
|
361
|
+
raise SkillError(f"Cannot read reference file {file_path}: {exc}") from exc
|
|
362
|
+
if not content.strip():
|
|
363
|
+
raise SkillError(f"Reference file must not be empty: {file_path}")
|
|
364
|
+
references[file_path.relative_to(refs_dir).as_posix()] = content
|
|
365
|
+
|
|
366
|
+
return dict(sorted(references.items()))
|
|
342
367
|
|
|
343
|
-
|
|
368
|
+
|
|
369
|
+
def _resolve_reference_path(path: Path, root: Path) -> Path:
|
|
370
|
+
"""Resolve links before traversal or reads, keeping their targets within the root."""
|
|
371
|
+
try:
|
|
372
|
+
resolved = path.resolve(strict=True)
|
|
373
|
+
except (OSError, RuntimeError) as exc:
|
|
374
|
+
raise SkillError(f"Cannot resolve reference path {path}: {exc}") from exc
|
|
375
|
+
if not resolved.is_relative_to(root):
|
|
376
|
+
raise SkillError(f"Reference path escapes its root {root}: {path}")
|
|
377
|
+
return resolved
|
|
344
378
|
|
|
345
379
|
|
|
346
380
|
_SCRIPT_EXTENSIONS = frozenset({".py", ".sh", ".js", ".bash"})
|
|
@@ -375,7 +409,8 @@ def _load_scripts(scripts_dir: Path) -> dict[str, str]:
|
|
|
375
409
|
def load_skill(path: Path | str) -> Skill:
|
|
376
410
|
"""Load a skill from a path.
|
|
377
411
|
|
|
378
|
-
Convenience function wrapping Skill.from_path().
|
|
412
|
+
Convenience function wrapping Skill.from_path(). This is the runner's local
|
|
413
|
+
skill validation, without SDK startup or model execution.
|
|
379
414
|
|
|
380
415
|
Args:
|
|
381
416
|
path: Path to skill directory or SKILL.md file
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|