Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
" drifted Skill files changed since last certification\n"
" warn Scan found low-risk issues\n"
" deny Scan found high-risk issues\n"
" tampered Ledger metadata is incomplete or fails authenticity checks"
" tampered Ledger metadata is incomplete or fails authenticity checks\n"
" unmanaged Skill directory is not covered by any managed skill\n"
" directory, or ledger state is not writable\n"
" error Check could not be completed for the skill"
Comment thread
jfeng18 marked this conversation as resolved.
),
add_completion=True,
)
Expand Down Expand Up @@ -186,6 +189,10 @@ def cmd_check(
warn Signature valid, but scan found low-risk issues
Comment thread
jfeng18 marked this conversation as resolved.
deny Signature valid, but scan found high-risk issues
tampered Ledger metadata is incomplete or fails authenticity checks
error Check could not run for the skill (e.g. missing SKILL.md);
single checks print a JSON object with status and error
keys and exit 1, while --all records it as a per-skill
entry and checks the rest

Use --all to check every registered skill and receive a JSON array of
enriched results. Skill discovery uses built-in default directories plus
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Help-text contract tests for the ``skill-ledger`` command group."""

import json
import re
from pathlib import Path

from agent_sec_cli.security_middleware.backends.skill_ledger import (
_VERDICT_SEVERITY,
)
from agent_sec_cli.skill_ledger.cli import app
from typer.testing import CliRunner

# rich emits ANSI color codes when the environment forces color on (e.g.
# GITHUB_ACTIONS or FORCE_COLOR), so strip them before matching on layout.
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")


def _plain_lines(output: str) -> list[str]:
"""Return help output as ANSI-free lines with indentation stripped."""
return [_ANSI_ESCAPE_RE.sub("", line).strip() for line in output.splitlines()]


def _has_status_line(output: str, status: str) -> bool:
"""True if any line starts with ``status`` followed by a word boundary.

Rich renders help differently per environment (ANSI codes, 2- vs
3-space indent, width padding), so the match is layout-independent.
"""
return any(re.match(rf"^{status}\b", line) for line in _plain_lines(output))


def test_app_help_lists_every_verdict_status() -> None:
result = CliRunner().invoke(app, ["--help"])

assert result.exit_code == 0
for status in _VERDICT_SEVERITY:
assert _has_status_line(
result.output, status
), f"status {status!r} missing from skill-ledger --help"


def test_check_help_lists_error_status() -> None:
result = CliRunner().invoke(app, ["check", "--help"])

assert result.exit_code == 0
assert _has_status_line(
result.output, "error"
), "check --help must document the error status"
# error is produced by both single-check failures and batch entries;
# the old "(--all path only)" qualifier was inaccurate.
assert "(--all path only)" not in result.output
# check never returns unmanaged (only show does, via manageability checks).
assert not _has_status_line(result.output, "unmanaged")


def test_check_missing_dir_prints_error_status_json(tmp_path: Path) -> None:
Comment thread
jfeng18 marked this conversation as resolved.
result = CliRunner().invoke(app, ["check", str(tmp_path / "missing")])

assert result.exit_code == 1
payload = json.loads(result.output)
# Assert only the error envelope contract; the message text is free to
# evolve as new failure types are added.
assert payload["status"] == "error"
assert payload["error"]
Loading