mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
d972fa2090
## What Prepare AgentOps 4.0.0 across the Claude plugin, Codex plugin, skills and CLI. Claude writers capture the supplied check status during its original invocation, and plugin conformance verifies exact skill membership and link destinations. Full release security now scans the repository and blocks on Python collection failures that previously produced a false green result. ## Why The 3.6.0-to-current interval removes published commands and 20 skill names, so this is a major release with migration instructions. Release validation also exposed stale skill assertions and test prerequisites that need to match the current product contracts without weakening acceptance. ## How I tested - Native Claude Opus/Haiku success, failing-check and direct-writer trials: each check ran once, and the direct child returned plain JSON. - Actual fresh installs and upgrades from 3.6.0 in isolated Codex and Claude homes: 34 skills, expected agents, and exact installed package bytes. - Exact candidate `b721d02559e1495be6095ad97b820e88ceb4a049`: all 73 full repository gates, regeneration parity, and the complete local release rehearsal passed. All 12 security tools ran with zero skips, tool errors, critical findings or high-severity security findings. The unchanged advisory policy reports 35 quality-high findings on unchanged files. - Python: 327 tests and 72 subtests passed. Hosted Bats: 1,509 passed, 31 environment-dependent skips, zero failures. Go lint/build/vet/race/shuffle checks and CLI smoke/integration passed. - All 11 hosted checks passed, including Windows correctness, macOS/Linux installation, security, and the six-target no-publish GoReleaser snapshot. Local archive checksums and a real macOS CLI initialization/status/version smoke also passed. - Fresh author-distinct review passed all four acceptance criteria and all 35 changed paths with no unchecked acceptance. Canonical subject and caller-intent verification passed; verdict digest `68af2c935ed0106cd91b3950f5d168e662f4071f660fcbd113c36b7cd0f0426e` binds manifest `7affc77e25eaff69ba36c5ce05582b4f0385c954b76b62c02b97f97041f489b2`. ## Checklist - [x] Breaking changes documented in the migration guide and complete release notes. - [x] No credentials or private runtime proof included. - [x] Final full release checks pass on the exact candidate. - [x] Fresh author-distinct final PASS is recorded before merge. This prepares the release candidate; it does not publish a tag or release. Coverage limits remain explicit: native plugin tests used isolated macOS homes and local marketplaces, guard installation remains opt-in, and reader instructions do not prove sandbox confinement. Semgrep retains pre-existing warning-level parser diagnostics. Snapshot metadata follows the existing 3.6.0 tag; this is a packaging rehearsal, not a published 4.0.0 archive.
199 lines
7.2 KiB
Python
199 lines
7.2 KiB
Python
"""Unit + integration tests for skills/skill-builder/scripts/scan_descriptions.py.
|
|
|
|
Uses vanilla unittest + importlib so it runs without pytest installed.
|
|
Focus: the three trigger-detection forms (matching audit.sh), suggestion
|
|
generation, and the CLI entry point's exit codes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import io
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from contextlib import redirect_stderr, redirect_stdout
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPT = REPO_ROOT / "skills" / "skill-builder" / "scripts" / "scan_descriptions.py"
|
|
|
|
|
|
def _load_module():
|
|
"""Load the scanner module by path (no package install required)."""
|
|
spec = importlib.util.spec_from_file_location("scan_descriptions", SCRIPT)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
# Register before exec so dataclass introspection can resolve the module
|
|
# (required on Python 3.14+ for importlib-loaded modules with dataclasses).
|
|
sys.modules[spec.name] = module
|
|
# Match direct script execution: its sibling modules are importable even
|
|
# when pytest's importlib mode does not modify sys.path for test files.
|
|
with patch.object(sys, "path", [str(SCRIPT.parent), *sys.path]):
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
scan = _load_module()
|
|
|
|
|
|
def _write_skill(root: Path, name: str, frontmatter: str, body: str = "# Title\n") -> Path:
|
|
"""Create root/<name>/SKILL.md with the given frontmatter + body."""
|
|
skill_dir = root / name
|
|
skill_dir.mkdir(parents=True)
|
|
md = skill_dir / "SKILL.md"
|
|
md.write_text(f"---\n{frontmatter}\n---\n{body}", encoding="utf-8")
|
|
return md
|
|
|
|
|
|
class TestTriggerDetection(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self._tmp.name)
|
|
|
|
def tearDown(self):
|
|
self._tmp.cleanup()
|
|
|
|
def test_explicit_marker_in_description_detected(self):
|
|
md = _write_skill(
|
|
self.root,
|
|
"alpha",
|
|
"name: alpha\ndescription: 'Does a thing. Triggers: \"do thing\", \"alpha\".'",
|
|
)
|
|
result = scan.scan_skill(md)
|
|
self.assertTrue(result.has_trigger)
|
|
self.assertIn("inline-marker", result.forms)
|
|
|
|
def test_block_scalar_use_when_detected(self):
|
|
md = _write_skill(
|
|
self.root,
|
|
"beta",
|
|
"name: beta\ndescription: |\n Does a thing.\n **Use when:** you need a thing.",
|
|
)
|
|
result = scan.scan_skill(md)
|
|
self.assertTrue(result.has_trigger)
|
|
self.assertIn("block-marker", result.forms)
|
|
|
|
def test_triggers_list_with_three_items_detected(self):
|
|
md = _write_skill(
|
|
self.root,
|
|
"gamma",
|
|
"name: gamma\ndescription: Does a thing.\nmetadata:\n triggers:\n"
|
|
" - one\n - two\n - three",
|
|
)
|
|
result = scan.scan_skill(md)
|
|
self.assertTrue(result.has_trigger)
|
|
self.assertIn("metadata-list", result.forms)
|
|
|
|
def test_two_item_triggers_list_not_enough(self):
|
|
md = _write_skill(
|
|
self.root,
|
|
"delta",
|
|
"name: delta\ndescription: Does a thing.\nmetadata:\n triggers:\n - one\n - two",
|
|
)
|
|
result = scan.scan_skill(md)
|
|
self.assertFalse(result.has_trigger)
|
|
|
|
def test_plain_description_missing_trigger_gets_suggestion(self):
|
|
md = _write_skill(
|
|
self.root,
|
|
"scope-creep",
|
|
"name: scope-creep\ndescription: Audits the scope of a change.",
|
|
)
|
|
result = scan.scan_skill(md)
|
|
self.assertFalse(result.has_trigger)
|
|
self.assertEqual(result.forms, [])
|
|
self.assertTrue(result.suggestion.startswith("Triggers:"))
|
|
self.assertIn('"scope-creep"', result.suggestion)
|
|
|
|
def test_suggestion_has_no_duplicate_words(self):
|
|
md = _write_skill(
|
|
self.root, "compile", "name: compile\ndescription: Compile the corpus."
|
|
)
|
|
result = scan.scan_skill(md)
|
|
# "compile compile" must not appear — verb equals the only name token.
|
|
self.assertNotIn("compile compile", result.suggestion)
|
|
|
|
def test_invalid_frontmatter_is_rejected(self):
|
|
md = _write_skill(
|
|
self.root,
|
|
"invalid",
|
|
"name: invalid\ndescription: Unquoted colon: is invalid YAML.",
|
|
)
|
|
with self.assertRaisesRegex(scan.ProfileError, "frontmatter configuration error"):
|
|
scan.scan_skill(md)
|
|
|
|
|
|
class TestCli(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self._tmp.name)
|
|
|
|
def tearDown(self):
|
|
self._tmp.cleanup()
|
|
|
|
def test_strict_exits_one_when_a_skill_lacks_trigger(self):
|
|
_write_skill(self.root, "no-trig", "name: no-trig\ndescription: Plain description.")
|
|
with redirect_stdout(io.StringIO()):
|
|
code = scan.main([str(self.root), "--strict", "--quiet"])
|
|
self.assertEqual(code, 1)
|
|
|
|
def test_strict_exits_zero_when_all_have_triggers(self):
|
|
_write_skill(
|
|
self.root,
|
|
"ok",
|
|
"name: ok\ndescription: 'Does X. Triggers: \"ok\", \"do x\".'",
|
|
)
|
|
with redirect_stdout(io.StringIO()):
|
|
code = scan.main([str(self.root), "--strict", "--quiet"])
|
|
self.assertEqual(code, 0)
|
|
|
|
def test_missing_dir_exits_two(self):
|
|
code = scan.main([str(self.root / "does-not-exist")])
|
|
self.assertEqual(code, 2)
|
|
|
|
def test_unknown_profile_exits_two(self):
|
|
_write_skill(self.root, "plain", "name: plain\ndescription: Plain description.")
|
|
error = io.StringIO()
|
|
with patch.dict(scan.os.environ, {"SKILL_CONFORMANCE_PROFILE_ID": "unknown-test-profile"}):
|
|
with redirect_stderr(error):
|
|
code = scan.main([str(self.root), "--strict", "--quiet"])
|
|
self.assertEqual(code, 2)
|
|
self.assertIn("unknown profile", error.getvalue())
|
|
|
|
def test_probe_flow_form_allows_quoted_commas(self):
|
|
_write_skill(
|
|
self.root,
|
|
"deploy",
|
|
'name: deploy\ndescription: Run ci cd deploy.\ntrigger_probes: ["ci, cd deploy"]',
|
|
)
|
|
with redirect_stdout(io.StringIO()):
|
|
code = scan.main([str(self.root), "--probe", "ci, cd deploy"])
|
|
self.assertEqual(code, 0)
|
|
|
|
def test_json_output_reports_counts(self):
|
|
_write_skill(self.root, "a", "name: a\ndescription: Plain.")
|
|
_write_skill(self.root, "b", "name: b\ndescription: 'X. Triggers: \"b\", \"x\".'")
|
|
buf = io.StringIO()
|
|
with redirect_stdout(buf):
|
|
scan.main([str(self.root), "--json"])
|
|
import json
|
|
|
|
payload = json.loads(buf.getvalue())
|
|
self.assertEqual(payload["scanned"], 2)
|
|
self.assertEqual(payload["missing"], 1)
|
|
|
|
|
|
class TestRealCorpus(unittest.TestCase):
|
|
def test_scans_the_live_skills_directory(self):
|
|
skills_dir = REPO_ROOT / "skills"
|
|
results = scan.scan_corpus(skills_dir)
|
|
self.assertGreater(len(results), 0)
|
|
# Every result must carry the name of its directory or frontmatter.
|
|
self.assertTrue(all(r.name for r in results))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|