Files
semantica/tests/test_cli_foundation.py
T
Sameer6305 b54d885bf2 cli: harden config parsing and logging override handling
- keep command-level config from overriding logging unless --log-level is set

- validate YAML/JSON config roots and surface parse failures as Click errors

- tighten CLI tests around isolation and cleanup
2026-05-26 23:06:00 +05:30

221 lines
6.3 KiB
Python

import pytest
from click.testing import CliRunner
import semantica.cli as cli_module
@pytest.fixture
def runner():
return CliRunner()
def test_root_help_shows_expected_groups(runner):
result = runner.invoke(cli_module.main, ["--help"])
assert result.exit_code == 0
assert (
"Semantica - Semantic Layer & Knowledge Engineering Framework"
in result.output
)
assert "kg" in result.output
assert "pipeline" in result.output
assert "serve" in result.output
def test_kg_group_help_shows_build_command(runner):
result = runner.invoke(cli_module.main, ["kg", "--help"])
assert result.exit_code == 0
assert "Knowledge graph and semantic build commands." in result.output
assert "build" in result.output
def test_kg_build_help_shows_source_and_config_flags(runner):
result = runner.invoke(cli_module.main, ["kg", "build", "--help"])
assert result.exit_code == 0
assert "--source" in result.output
assert "-s" in result.output
assert "--config" in result.output
assert "-c" in result.output
def test_command_config_keeps_own_logging_without_global_override(runner, monkeypatch):
captured = {}
def fake_run_build(cli_ctx, sources):
captured["logging_level"] = cli_ctx.config.get("logging.level")
captured["sources"] = list(sources)
monkeypatch.setattr(cli_module, "_run_build", fake_run_build)
with runner.isolated_filesystem():
with open("cfg.yml", "w", encoding="utf-8") as handle:
handle.write("logging:\n level: DEBUG\n")
result = runner.invoke(
cli_module.main,
["kg", "build", "-s", "README.md", "-c", "cfg.yml"],
)
assert result.exit_code == 0
assert captured["sources"] == ["README.md"]
assert captured["logging_level"] == "DEBUG"
@pytest.mark.parametrize(
"argv",
[
["kg", "build", "-s", "README.md"],
["build", "-s", "README.md"],
],
)
def test_build_paths_invoke_shared_wrapper(runner, monkeypatch, argv):
captured = {}
def fake_run_build(cli_ctx, sources):
captured["config_path"] = cli_ctx.config_path
captured["sources"] = list(sources)
monkeypatch.setattr(cli_module, "_run_build", fake_run_build)
result = runner.invoke(cli_module.main, argv)
assert result.exit_code == 0
assert captured["sources"] == ["README.md"]
@pytest.mark.parametrize(
"argv",
[
["kg", "build", "-s", "README.md", "-c", "cfg.yml"],
["kg", "build", "-s", "README.md", "--config", "cfg.yml"],
["build", "-s", "README.md", "-c", "cfg.yml"],
["build", "-s", "README.md", "--config", "cfg.yml"],
],
)
def test_build_config_short_and_long_flags_are_compatible(runner, monkeypatch, argv):
captured = {}
def fake_run_build(cli_ctx, sources):
captured["config_path"] = cli_ctx.config_path
captured["sources"] = list(sources)
monkeypatch.setattr(cli_module, "_run_build", fake_run_build)
with runner.isolated_filesystem():
with open("cfg.yml", "w", encoding="utf-8") as handle:
handle.write("logging:\n level: INFO\n")
result = runner.invoke(cli_module.main, argv)
assert result.exit_code == 0
assert captured["sources"] == ["README.md"]
assert captured["config_path"] is not None
@pytest.mark.parametrize(
"argv",
[
["kg", "build"],
["build"],
],
)
def test_missing_input_errors_are_clean_and_click_safe(runner, argv):
result = runner.invoke(cli_module.main, argv)
assert result.exit_code != 0
assert "At least one source is required" in result.output
assert "Traceback" not in result.output
def test_invalid_root_config_error_is_clean_and_click_safe(runner):
with runner.isolated_filesystem():
with open("bad.md", "w", encoding="utf-8") as handle:
handle.write("not-a-config")
result = runner.invoke(cli_module.main, ["--config", "bad.md", "info"])
assert result.exit_code != 0
assert "Unsupported configuration file format" in result.output
assert "Traceback" not in result.output
def test_invalid_command_config_error_is_clean_and_click_safe(runner):
with runner.isolated_filesystem():
with open("bad.md", "w", encoding="utf-8") as handle:
handle.write("not-a-config")
result = runner.invoke(
cli_module.main,
["kg", "build", "-s", "README.md", "-c", "bad.md"],
)
assert result.exit_code != 0
assert "Unsupported configuration file format" in result.output
assert "Traceback" not in result.output
@pytest.mark.parametrize(
"file_name, config_text, expected_error",
[
("cfg.json", "{not-json", "Failed to parse configuration file"),
(
"cfg.yml",
"- item\n- item2\n",
"Configuration file must contain a mapping/object",
),
],
)
def test_command_config_parse_errors_are_clean_and_click_safe(
runner,
file_name,
config_text,
expected_error,
):
with runner.isolated_filesystem():
with open(file_name, "w", encoding="utf-8") as handle:
handle.write(config_text)
result = runner.invoke(
cli_module.main,
["kg", "build", "-s", "README.md", "-c", file_name],
)
assert result.exit_code != 0
assert expected_error in result.output
assert "Traceback" not in result.output
@pytest.mark.parametrize(
"argv",
[
["--help"],
["kg", "--help"],
["kg", "build", "--help"],
["build", "--help"],
],
)
def test_help_calls_do_not_initialize_framework(runner, monkeypatch, argv):
def fail_get_framework(_):
raise AssertionError("framework initialization must not happen on help")
monkeypatch.setattr(cli_module, "_get_framework", fail_get_framework)
result = runner.invoke(cli_module.main, argv)
assert result.exit_code == 0
def test_runtime_errors_are_click_safe_without_traceback(runner, monkeypatch):
def boom(_cli_ctx, _sources):
raise RuntimeError("boom")
monkeypatch.setattr(cli_module, "_run_build", boom)
result = runner.invoke(cli_module.main, ["kg", "build", "-s", "README.md"])
assert result.exit_code != 0
assert "Unexpected error: boom" in result.output
assert "Traceback" not in result.output