diff --git a/src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/skill_ledger/cli.py b/src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/skill_ledger/cli.py index e12f33a1b6..06f15b103d 100644 --- a/src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/skill_ledger/cli.py +++ b/src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/skill_ledger/cli.py @@ -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" ), add_completion=True, ) @@ -186,6 +189,10 @@ def cmd_check( warn Signature valid, but scan found low-risk issues 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 diff --git a/src/agent-sec-core/tests/unit-test/skill_ledger/test_cli_help.py b/src/agent-sec-core/tests/unit-test/skill_ledger/test_cli_help.py new file mode 100644 index 0000000000..f10444d21e --- /dev/null +++ b/src/agent-sec-core/tests/unit-test/skill_ledger/test_cli_help.py @@ -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: + 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"]