Skip to content

Commit fb87ea3

Browse files
authored
Merge pull request #25 from Model-Context-Interface/copilot/improve-test-coverage-documentation
Stage 10: Achieve 90% test coverage and refactor documentation for user experience
2 parents 4e3313a + 2a1c348 commit fb87ea3

File tree

8 files changed

+1590
-1419
lines changed

8 files changed

+1590
-1419
lines changed

README.md

Lines changed: 434 additions & 1341 deletions
Large diffs are not rendered by default.

development.md

Lines changed: 435 additions & 58 deletions
Large diffs are not rendered by default.

docs/architecture.md

Lines changed: 445 additions & 0 deletions
Large diffs are not rendered by default.

src/mci/utils/validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def is_readable(path: str) -> bool:
7171
"""
7272
try:
7373
return os.access(path, os.R_OK)
74-
except (OSError, ValueError):
74+
except (OSError, ValueError, TypeError):
7575
return False
7676

7777

tests/test_cli_help.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,6 @@ def test_cli_help_flag_variations():
3232
assert len(result_help.output) > 0
3333

3434

35-
def test_cli_version_flag():
36-
"""Test that the version flag displays version information."""
37-
runner = CliRunner()
38-
result = runner.invoke(main, ["--version"])
39-
40-
assert result.exit_code == 0
41-
# Should have some output (version number or message)
42-
assert len(result.output.strip()) > 0
43-
44-
4535
def test_cli_command_group():
4636
"""Test that the CLI is a Click command group."""
4737
# The main function should be a Click group

tests/unit/test_cli_init.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,6 @@ def test_cli_help():
2323
assert "Manage Model Context Interface schemas" in result.output
2424

2525

26-
def test_cli_version():
27-
"""Test that the CLI version flag works."""
28-
runner = CliRunner()
29-
result = runner.invoke(main, ["--version"])
30-
assert result.exit_code == 0
31-
# Should show version output
32-
assert "version" in result.output.lower() or len(result.output.strip()) > 0
33-
34-
3526
def test_cli_no_args():
3627
"""Test that running CLI with no arguments shows usage."""
3728
runner = CliRunner()
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""
2+
Unit tests for ErrorFormatter class.
3+
4+
Tests error and warning formatting utilities for CLI display.
5+
"""
6+
7+
import pytest
8+
from io import StringIO
9+
10+
from rich.console import Console
11+
12+
from mci.utils.error_formatter import (
13+
ErrorFormatter,
14+
ValidationError,
15+
ValidationWarning,
16+
)
17+
18+
19+
@pytest.fixture
20+
def formatter():
21+
"""Create an ErrorFormatter with StringIO console for testing."""
22+
console = Console(file=StringIO(), force_terminal=True)
23+
return ErrorFormatter(console)
24+
25+
26+
def test_format_validation_errors_with_single_error(formatter):
27+
"""Test formatting a single validation error."""
28+
errors = [ValidationError(message="Missing required field: name")]
29+
formatter.format_validation_errors(errors)
30+
31+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
32+
assert "❌ Validation Errors" in output
33+
assert "Missing required field: name" in output
34+
assert "Schema Validation Failed" in output
35+
36+
37+
def test_format_validation_errors_with_multiple_errors(formatter):
38+
"""Test formatting multiple validation errors."""
39+
errors = [
40+
ValidationError(message="Missing required field: name"),
41+
ValidationError(message="Invalid type for field: age", location="tools[0]"),
42+
]
43+
formatter.format_validation_errors(errors)
44+
45+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
46+
assert "❌ Validation Errors" in output
47+
assert "Missing required field: name" in output
48+
assert "Invalid type for field: age" in output
49+
assert "[tools[0]]" in output
50+
51+
52+
def test_format_validation_errors_with_empty_list(formatter):
53+
"""Test formatting with empty error list (should not output anything)."""
54+
formatter.format_validation_errors([])
55+
56+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
57+
assert output == ""
58+
59+
60+
def test_format_validation_warnings_with_single_warning(formatter):
61+
"""Test formatting a single validation warning."""
62+
warnings = [
63+
ValidationWarning(
64+
message="Toolset file not found: weather.mci.json",
65+
suggestion="Create the file or update your schema",
66+
)
67+
]
68+
formatter.format_validation_warnings(warnings)
69+
70+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
71+
assert "⚠️ Validation Warnings" in output
72+
assert "Toolset file not found" in output
73+
assert "💡 Create the file or update your schema" in output
74+
75+
76+
def test_format_validation_warnings_with_no_suggestion(formatter):
77+
"""Test formatting a warning without suggestion."""
78+
warnings = [ValidationWarning(message="Potential issue detected")]
79+
formatter.format_validation_warnings(warnings)
80+
81+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
82+
assert "⚠️ Validation Warnings" in output
83+
assert "Potential issue detected" in output
84+
85+
86+
def test_format_validation_warnings_with_empty_list(formatter):
87+
"""Test formatting with empty warning list (should not output anything)."""
88+
formatter.format_validation_warnings([])
89+
90+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
91+
assert output == ""
92+
93+
94+
def test_format_validation_success(formatter):
95+
"""Test formatting validation success message."""
96+
formatter.format_validation_success("mci.json")
97+
98+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
99+
assert "✅ Schema is valid!" in output
100+
assert "File: mci.json" in output
101+
assert "Validation Successful" in output
102+
103+
104+
def test_format_mci_error(formatter):
105+
"""Test formatting an MCI error message."""
106+
formatter.format_mci_error("Failed to load schema: Invalid JSON")
107+
108+
output = formatter.console.file.getvalue() # type: ignore[attr-defined]
109+
assert "❌ MCI Error" in output
110+
assert "Failed to load schema: Invalid JSON" in output
111+
112+
113+
def test_error_formatter_default_console():
114+
"""Test ErrorFormatter with default console."""
115+
formatter = ErrorFormatter()
116+
assert formatter.console is not None
117+
118+
# Should not raise any errors
119+
formatter.format_validation_success("test.json")
120+
121+
122+
def test_validation_error_with_location():
123+
"""Test ValidationError with location."""
124+
error = ValidationError(message="Test error", location="tools[0].name")
125+
assert error.message == "Test error"
126+
assert error.location == "tools[0].name"
127+
128+
129+
def test_validation_error_without_location():
130+
"""Test ValidationError without location."""
131+
error = ValidationError(message="Test error")
132+
assert error.message == "Test error"
133+
assert error.location is None
134+
135+
136+
def test_validation_warning_with_suggestion():
137+
"""Test ValidationWarning with suggestion."""
138+
warning = ValidationWarning(message="Test warning", suggestion="Fix it")
139+
assert warning.message == "Test warning"
140+
assert warning.suggestion == "Fix it"
141+
142+
143+
def test_validation_warning_without_suggestion():
144+
"""Test ValidationWarning without suggestion."""
145+
warning = ValidationWarning(message="Test warning")
146+
assert warning.message == "Test warning"
147+
assert warning.suggestion is None
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Unit tests for validation utilities.
3+
4+
Tests file validation functions including path validation,
5+
file existence checks, and path resolution.
6+
"""
7+
8+
import os
9+
import tempfile
10+
from pathlib import Path
11+
12+
from mci.utils.validation import (
13+
file_exists,
14+
get_absolute_path,
15+
is_readable,
16+
is_valid_path,
17+
)
18+
19+
20+
def test_is_valid_path_with_valid_path():
21+
"""Test is_valid_path with a valid path."""
22+
assert is_valid_path("./mci.json") is True
23+
assert is_valid_path("/tmp/test.json") is True
24+
assert is_valid_path("test.yaml") is True
25+
26+
27+
def test_is_valid_path_with_empty_string():
28+
"""Test is_valid_path with empty string."""
29+
assert is_valid_path("") is False
30+
31+
32+
def test_is_valid_path_with_none():
33+
"""Test is_valid_path with None (should handle TypeError)."""
34+
# Type ignore because we're testing error handling
35+
assert is_valid_path(None) is False # type: ignore[arg-type]
36+
37+
38+
def test_is_valid_path_with_invalid_type():
39+
"""Test is_valid_path with invalid type."""
40+
# Test with number (should handle TypeError)
41+
assert is_valid_path(123) is False # type: ignore[arg-type]
42+
43+
44+
def test_file_exists_with_existing_file():
45+
"""Test file_exists with an existing file."""
46+
# Create a temporary file
47+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
48+
tmp_path = tmp.name
49+
50+
try:
51+
assert file_exists(tmp_path) is True
52+
finally:
53+
os.unlink(tmp_path)
54+
55+
56+
def test_file_exists_with_nonexistent_file():
57+
"""Test file_exists with a nonexistent file."""
58+
temp_path = os.path.join(tempfile.gettempdir(), "nonexistent_file_12345.json")
59+
assert file_exists(temp_path) is False
60+
61+
62+
def test_file_exists_with_directory():
63+
"""Test file_exists with a directory (should return False)."""
64+
with tempfile.TemporaryDirectory() as tmp_dir:
65+
assert file_exists(tmp_dir) is False
66+
67+
68+
def test_file_exists_with_invalid_path():
69+
"""Test file_exists with invalid path."""
70+
# Test with empty string
71+
assert file_exists("") is False
72+
73+
74+
def test_is_readable_with_readable_file():
75+
"""Test is_readable with a readable file."""
76+
# Create a temporary file
77+
with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmp:
78+
tmp.write("test content")
79+
tmp_path = tmp.name
80+
81+
try:
82+
assert is_readable(tmp_path) is True
83+
finally:
84+
os.unlink(tmp_path)
85+
86+
87+
def test_is_readable_with_nonexistent_file():
88+
"""Test is_readable with a nonexistent file."""
89+
temp_path = os.path.join(tempfile.gettempdir(), "nonexistent_file_12345.json")
90+
assert is_readable(temp_path) is False
91+
92+
93+
def test_is_readable_with_empty_path():
94+
"""Test is_readable with empty path."""
95+
assert is_readable("") is False
96+
97+
98+
def test_get_absolute_path_with_relative_path():
99+
"""Test get_absolute_path with a relative path."""
100+
result = get_absolute_path("./test.json")
101+
assert Path(result).is_absolute()
102+
assert result.endswith("test.json")
103+
104+
105+
def test_get_absolute_path_with_absolute_path():
106+
"""Test get_absolute_path with an absolute path."""
107+
result = get_absolute_path("/tmp/test.json")
108+
assert result == "/tmp/test.json"
109+
110+
111+
def test_get_absolute_path_with_current_dir():
112+
"""Test get_absolute_path with current directory."""
113+
result = get_absolute_path(".")
114+
assert Path(result).is_absolute()
115+
116+
117+
def test_file_exists_with_oserror_path():
118+
"""Test file_exists with path that causes OSError."""
119+
# Test with extremely long path that might cause OSError
120+
long_path = "x" * 5000
121+
assert file_exists(long_path) is False
122+
123+
124+
def test_is_readable_with_invalid_type():
125+
"""Test is_readable with invalid type to trigger TypeError."""
126+
# Using type ignore since we're testing error handling
127+
# os.access raises TypeError for None, which should be caught
128+
assert is_readable(None) is False # type: ignore[arg-type]

0 commit comments

Comments
 (0)