Skip to content

Commit e889c58

Browse files
authored
fix: add CLI edge case tests and input validation for agent-compliance (#234)
* fix: centralize hardcoded ring thresholds and constants into constants.py Fixes #171 Ring thresholds and constants were hardcoded with duplicate values across multiple files (rate_limiter.py, enforcer.py, models.py, vouching.py, orchestrator.py). This commit introduces a single constants.py module under hypervisor/ and updates all source files to import from it. Constants centralized: - Ring trust-score thresholds (0.95, 0.70, 0.60) - Rate-limiter defaults per ring (req/s and burst capacity) - Vouching/sponsorship thresholds (score scale, bond pct, max exposure) - Saga orchestrator defaults (retries, delay, step timeout) - Validation limits (agent ID, name, API path, participants, duration) - Risk-weight ranges by reversibility level - Session default min_eff_score All 562 existing tests pass with zero failures. * fix: resolve F401 lint errors in agent-compliance package * fix: resolve lint errors in agent-mesh package (W293, F401, F821, E402) * fix: resolve lint errors in agent-sre package (F401, E741) * fix: resolve lint errors in agent-os package (F401, F541, noqa directives) * fix: skip gRPC transport tests when grpcio is not installed * fix: add CLI edge case tests and validation for issue #159 - Add test for mutually exclusive --manifest and --generate flags - Add test for non-existent manifest file path - Add test for read-only output directory (Linux/macOS only) - Fix CLI to reject both flags simultaneously with clear error - Fix CLI to validate manifest file exists before proceeding Closes #159
1 parent b123f6a commit e889c58

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed

packages/agent-compliance/src/agent_compliance/cli/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from __future__ import annotations
1313

1414
import argparse
15+
import os
1516
import sys
1617

1718

@@ -37,6 +38,13 @@ def cmd_integrity(args: argparse.Namespace) -> int:
3738
from agent_compliance.integrity import IntegrityVerifier
3839

3940
try:
41+
if args.generate and args.manifest:
42+
print(
43+
"Error: --manifest and --generate are mutually exclusive",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
4048
if args.generate:
4149
verifier = IntegrityVerifier()
4250
manifest = verifier.generate_manifest(args.generate)
@@ -45,6 +53,13 @@ def cmd_integrity(args: argparse.Namespace) -> int:
4553
print(f" Functions hashed: {len(manifest['functions'])}")
4654
return 0
4755

56+
if args.manifest and not os.path.exists(args.manifest):
57+
print(
58+
f"Error: manifest file not found: {args.manifest}",
59+
file=sys.stderr,
60+
)
61+
return 1
62+
4863
verifier = IntegrityVerifier(manifest_path=args.manifest)
4964
report = verifier.verify()
5065

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""Tests for CLI edge cases (GitHub issue #159).
4+
5+
Covers:
6+
- Providing both --manifest and --generate simultaneously
7+
- Non-existent manifest file path
8+
- Read-only output directory
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import os
14+
import stat
15+
import sys
16+
17+
import pytest
18+
19+
from agent_compliance.cli.main import main
20+
21+
22+
def run_cli(*args: str) -> int:
23+
"""Run the CLI with the given arguments and return the exit code."""
24+
old_argv = sys.argv
25+
sys.argv = ["agent-compliance", *args]
26+
try:
27+
return main()
28+
finally:
29+
sys.argv = old_argv
30+
31+
32+
class TestIntegrityBothFlags:
33+
"""--manifest and --generate are mutually exclusive."""
34+
35+
def test_both_manifest_and_generate_errors(self, tmp_path, capsys):
36+
manifest_file = str(tmp_path / "existing.json")
37+
output_file = str(tmp_path / "generated.json")
38+
39+
with open(manifest_file, "w") as f:
40+
f.write("{}")
41+
42+
rc = run_cli(
43+
"integrity",
44+
"--manifest", manifest_file,
45+
"--generate", output_file,
46+
)
47+
48+
assert rc == 1
49+
captured = capsys.readouterr()
50+
assert "mutually exclusive" in captured.err
51+
52+
53+
class TestNonExistentManifest:
54+
"""Passing a manifest path that does not exist should fail gracefully."""
55+
56+
def test_nonexistent_manifest_returns_error(self, capsys):
57+
rc = run_cli(
58+
"integrity",
59+
"--manifest", "/absolutely/does/not/exist/integrity.json",
60+
)
61+
62+
assert rc == 1
63+
captured = capsys.readouterr()
64+
assert "not found" in captured.err
65+
66+
def test_nonexistent_manifest_no_traceback(self, capsys):
67+
"""The error should be user-friendly, not a raw traceback."""
68+
run_cli(
69+
"integrity",
70+
"--manifest", "/absolutely/does/not/exist/integrity.json",
71+
)
72+
73+
captured = capsys.readouterr()
74+
assert "Traceback" not in captured.err
75+
assert "Traceback" not in captured.out
76+
77+
78+
class TestReadOnlyOutputDirectory:
79+
"""Generating a manifest into a read-only directory should fail
80+
gracefully with a non-zero exit code."""
81+
82+
@pytest.mark.skipif(
83+
sys.platform == "win32",
84+
reason="chmod-based read-only directories are not enforced on Windows",
85+
)
86+
def test_readonly_output_dir(self, tmp_path):
87+
readonly_dir = tmp_path / "locked"
88+
readonly_dir.mkdir()
89+
90+
output_file = str(readonly_dir / "integrity.json")
91+
92+
# Make the directory read-only
93+
readonly_dir.chmod(stat.S_IRUSR | stat.S_IXUSR)
94+
95+
try:
96+
rc = run_cli("integrity", "--generate", output_file)
97+
98+
assert rc == 1
99+
assert not os.path.exists(output_file)
100+
finally:
101+
# Restore permissions for cleanup
102+
readonly_dir.chmod(stat.S_IRWXU)
103+
104+
@pytest.mark.skipif(
105+
sys.platform == "win32",
106+
reason="chmod-based read-only directories are not enforced on Windows",
107+
)
108+
def test_readonly_output_dir_error_message(self, tmp_path, capsys):
109+
readonly_dir = tmp_path / "locked"
110+
readonly_dir.mkdir()
111+
112+
output_file = str(readonly_dir / "integrity.json")
113+
114+
readonly_dir.chmod(stat.S_IRUSR | stat.S_IXUSR)
115+
116+
try:
117+
run_cli("integrity", "--generate", output_file)
118+
119+
captured = capsys.readouterr()
120+
assert "Error" in captured.err or "error" in captured.err.lower()
121+
assert "Traceback" not in captured.err
122+
assert "Traceback" not in captured.out
123+
finally:
124+
readonly_dir.chmod(stat.S_IRWXU)

0 commit comments

Comments
 (0)