lllin000_PaperForge/tests/cli/test_error_codes.py
Research Assistant 6840d324fe feat(52-golden-datasets-cli-contracts): add CLI contract tests with snapshot integration
- tests/cli/ with conftest.py (cli_invoker, vault_builder, mock_ocr_backend fixtures)
- test_contract_helpers.py with normalize_snapshot, assert_valid_json, assert_json_shape
- 3 test files covering all 8 CLI commands (paths, status, sync, ocr, doctor,
  repair, context, setup) with exit code, output shape, and error contracts
- fixtures/ocr/mock_ocr_backend.py with 4 mock modes (success, pending, error, timeout)
- pyproject.toml: added pytest-snapshot, responses, pytest-timeout, pytest-mock,
  coverage; added testpaths and layer markers
- All 27 tests pass (CLI-01, CLI-02, CLI-03 satisfied)
2026-05-08 23:32:03 +08:00

97 lines
3.8 KiB
Python

"""CLI contract tests for error handling and exit codes."""
from __future__ import annotations
import pytest
class TestExitCodes:
"""Contract: CLI commands exit with correct exit codes."""
def test_invalid_command_exits_nonzero(self, cli_invoker):
"""Unknown command -> exit code 2 (argparse standard)."""
result = cli_invoker(["nonexistent-command"])
assert result.returncode != 0, f"Expected non-zero, got {result.returncode}"
assert len(result.stderr) > 0, "Expected stderr output for unknown command"
def test_no_command_shows_help(self, cli_invoker):
"""No arguments -> exit code 2 with help text."""
result = cli_invoker([])
assert result.returncode != 0
assert (
"usage:" in result.stdout.lower()
or "usage:" in result.stderr.lower()
)
def test_help_flag(self, cli_invoker):
"""--help -> exit code 0 with help text."""
result = cli_invoker(["--help"])
assert result.returncode == 0
assert "usage:" in result.stdout.lower()
def test_version_flag(self, cli_invoker):
"""--version -> exit code 0 with version string."""
result = cli_invoker(["--version"])
assert result.returncode == 0
assert "paperforge" in result.stdout.lower()
class TestErrorOutput:
"""Contract: Error output follows stable text patterns."""
def test_missing_vault_error_message(self, cli_invoker):
"""Missing vault produces error message (not traceback)."""
result = cli_invoker(["status", "--vault", "/tmp/nonexistent_vault_xyz"])
output = result.stdout + result.stderr
assert "Traceback" not in output, "Error should not contain traceback"
assert (
"Error" in output or "error" in output.lower()
), "Error should contain error message"
def test_no_vault_flag_no_env(self, cli_invoker):
"""Running without --vault and without env produces error message."""
# Unset PAPERFORGE_VAULT to simulate no vault context
import os
old = os.environ.pop("PAPERFORGE_VAULT", None)
try:
result = cli_invoker(["status", "--vault", "/nonexistent_vault_check"])
output = result.stdout + result.stderr
assert "Traceback" not in output, "Should not show Python traceback"
finally:
if old is not None:
os.environ["PAPERFORGE_VAULT"] = old
class TestCli02ErrorContract:
"""CLI-02: Error responses use standard text format with actionable info.
Note: Full JSON error schema (ok, error_code, message, details, suggestions)
is planned for all commands. Currently, errors use human-readable text.
These tests validate the TEXT contract — JSON error contract will be
validated once --json error output is implemented across all commands.
"""
def test_error_contains_actionable_info(self, cli_invoker):
"""Errors mention what went wrong (not just 'error')."""
result = cli_invoker(
["status", "--vault", "/tmp/nonexistent_vault_xyz_98765"]
)
output = (result.stdout + result.stderr).lower()
# Should contain descriptive terms
descriptive_terms = [
"not found", "does not exist", "no such", "error",
"invalid", "cannot",
]
has_description = any(t in output for t in descriptive_terms)
assert has_description, (
f"Error should contain descriptive info, got: {output[:200]}"
)
def test_stable_error_output(self, cli_invoker):
"""Same error produces same output (deterministic)."""
vault_arg = ["status", "--vault", "/tmp/nonexistent_contract_test_12345"]
r1 = cli_invoker(vault_arg)
r2 = cli_invoker(vault_arg)
assert r1.returncode == r2.returncode
assert r1.stderr.strip() == r2.stderr.strip()