diff --git a/CHANGELOG.md b/CHANGELOG.md index b53847a3..113560f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,13 @@ ## Version 2 - MARM Protocol to Universal MCP Server Evolution
-Unreleased: Concept Store Module Split (v2.29.1) +Unreleased: Product CLI Module Split (v2.29.1) ### Internal +- Split `cli.py`'s remaining output-formatting and argparse-construction concerns into `services/cli_output.py` and `services/cli_parser.py`, continuing the v2.28.0 CLI service-module split. No behavior change; `_dispatch_product`, `main`, and runtime-preset application stay in `cli.py` as the orchestration owner. `cli.py` drops from 786 to 509 lines. +Unreleased: Concept Store Module Split (v2.29.1) + - Split MARM Console's `concept_store.py` graph-atlas and single-entity-neighborhood queries into their own modules: `console/concept_graph_overview.py` (`graph_overview`, the full-vs-sampled visual atlas with its degree-ranked BFS tree-sampling) and `console/concept_neighborhood.py` (`neighborhood`, the bounded single-entity BFS traversal). No behavior change; shared low-level helpers (`_connect`, `_schema_status`, `_entity`) and the smaller query functions (`summary`, `search`, `get_entity`, `build_runs`, `get_build_run`, `duplicates`) stay in `concept_store.py`. `concept_store.py` drops from 676 to 334 lines.
diff --git a/marm-mcp-server/marm_mcp_server/cli.py b/marm-mcp-server/marm_mcp_server/cli.py index 68982394..ba609d85 100644 --- a/marm-mcp-server/marm_mcp_server/cli.py +++ b/marm-mcp-server/marm_mcp_server/cli.py @@ -4,7 +4,6 @@ import argparse import asyncio -import json import os import sys import urllib.error @@ -26,6 +25,13 @@ SERVER_VERSION, ) from .core.rate_limiter import rate_limiter +from .services.cli_output import ( + _print_doctor, + _print_maintenance, + _print_payload, + _print_status, +) +from .services.cli_parser import _compatibility_parser, _product_parser from .utils.dependency_check import check_dependencies from .utils.security import generate_api_key @@ -128,182 +134,6 @@ def apply_runtime_preset( } -def _add_profile_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - "--profile", - choices=("standard", "swarm", "swarm-max", "trusted"), - default="standard", - ) - parser.add_argument( - "--rate-limit-rpm", - type=int, - help="Override HTTP rate limit RPM; 0 disables rate limiting", - ) - - -def _add_docker_run_arguments(parser: argparse.ArgumentParser) -> None: - _add_profile_arguments(parser) - parser.add_argument("--port", type=int, default=8001) - parser.add_argument("--data-dir", type=Path, default=Path.home() / ".marm") - parser.add_argument("--name", default="marm-mcp-server") - parser.add_argument("--tag", default="latest") - parser.add_argument("--repo", type=Path, action="append", default=[]) - parser.add_argument("--pull", action="store_true") - parser.add_argument("--expose-network", action="store_true") - parser.add_argument("--env-file", type=Path) - parser.add_argument("--memory") - parser.add_argument("--cpus") - - -def _product_help() -> str: - """Return the stable, human-oriented root help for the product CLI.""" - from .services.product_help import render_product_help - - return render_product_help(SERVER_VERSION) - - -class _ProductArgumentParser(argparse.ArgumentParser): - """Keep root help stable while retaining argparse for all subcommands.""" - - def format_help(self) -> str: - return _product_help() - - -def _product_parser() -> argparse.ArgumentParser: - parser = _ProductArgumentParser( - prog="marm-memory", - description="Run and manage marm-memory locally", - add_help=False, - ) - parser.add_argument("-h", "--help", action="help", help="Show help") - parser.add_argument( - "-V", - "--version", - action="version", - version=SERVER_VERSION, - help="Show installed version", - ) - subparsers = parser.add_subparsers( - dest="command", required=True, parser_class=argparse.ArgumentParser - ) - - start = subparsers.add_parser("start", help="Start the local MARM runtime") - _add_profile_arguments(start) - start.add_argument("--foreground", action="store_true") - start.add_argument("--runtime-id", help=argparse.SUPPRESS) - - fast_start = subparsers.add_parser( - "fast-start-http", help="Start HTTP, Console, and optional client setup" - ) - _add_profile_arguments(fast_start) - fast_start.add_argument("--client", help="Configure a supported MCP client") - fast_start.add_argument("--no-console", action="store_true") - fast_start.add_argument("--no-browser", action="store_true") - - http = subparsers.add_parser( - "http", help="Run the HTTP transport in the foreground" - ) - _add_profile_arguments(http) - http.add_argument("--runtime-id", help=argparse.SUPPRESS) - http.set_defaults(foreground=True) - - subparsers.add_parser("stdio", help="Run the MCP STDIO transport") - - stop = subparsers.add_parser("stop", help="Stop the managed MARM runtime") - stop.add_argument("--force", action="store_true") - restart = subparsers.add_parser("restart", help="Restart the managed runtime") - restart.add_argument("--force", action="store_true") - status = subparsers.add_parser("status", help="Show local MARM status") - status.add_argument("--json", action="store_true", dest="as_json") - console = subparsers.add_parser("console", help="Launch MARM Console") - browser = console.add_mutually_exclusive_group() - browser.add_argument("--open", action="store_true", dest="open_browser") - browser.add_argument("--no-open", action="store_false", dest="open_browser") - console.set_defaults(open_browser=True) - console.add_argument("--foreground", action="store_true") - console.add_argument( - "--import-key", - action="store_true", - help="Create a managed authenticated Console browser session", - ) - logs = subparsers.add_parser("logs", help="Read managed runtime logs") - logs.add_argument("--follow", action="store_true") - logs.add_argument("--lines", type=int, default=100) - doctor = subparsers.add_parser("doctor", help="Diagnose the local install") - doctor.add_argument("--json", action="store_true", dest="as_json") - - knowledge = subparsers.add_parser("knowledge", help="Manage concept extraction") - knowledge_sub = knowledge.add_subparsers(dest="knowledge_command", required=True) - knowledge_sub.add_parser("status") - build = knowledge_sub.add_parser("build") - scope = build.add_mutually_exclusive_group(required=True) - scope.add_argument("--all", action="store_true", dest="search_all") - scope.add_argument("--session") - scope.add_argument("--project") - - projects = subparsers.add_parser("projects", help="Manage code indexes") - projects_sub = projects.add_subparsers(dest="projects_command", required=True) - projects_sub.add_parser("list") - index = projects_sub.add_parser("index") - index.add_argument("path") - index.add_argument( - "--mode", choices=("fast", "moderate", "full"), default="moderate" - ) - project_status = projects_sub.add_parser("status") - project_status.add_argument("project", nargs="?") - remove = projects_sub.add_parser("remove") - remove.add_argument("project") - remove.add_argument("--confirm", required=True) - - maintenance = subparsers.add_parser("maintenance") - maintenance_sub = maintenance.add_subparsers( - dest="maintenance_command", required=True - ) - maintenance_status = maintenance_sub.add_parser("status") - maintenance_status.add_argument("--json", action="store_true", dest="as_json") - embeddings = maintenance_sub.add_parser("embeddings") - embeddings_sub = embeddings.add_subparsers(dest="embeddings_command", required=True) - embeddings_sub.add_parser("migrate") - - key = subparsers.add_parser("key", help="Manage local bearer authentication") - key_sub = key.add_subparsers(dest="key_command", required=True) - key_sub.add_parser("generate", help="Generate and display an ephemeral key") - key_sub.add_parser("init", help="Create or reuse the managed local key file") - key_sub.add_parser("path", help="Print the managed local key-file path") - key_sub.add_parser("reveal", help="Display the managed local key") - - from .services.docker_cli import add_docker_commands - - add_docker_commands(subparsers, _add_docker_run_arguments) - upgrade = subparsers.add_parser( - "upgrade", aliases=["update"], help="Check for and install a newer MARM release" - ) - upgrade.add_argument("--check", action="store_true") - upgrade.add_argument("--version") - upgrade.add_argument("--yes", action="store_true") - upgrade.add_argument("--json", action="store_true", dest="as_json") - - uninstall = subparsers.add_parser( - "uninstall", help="Remove MARM while preserving user data" - ) - uninstall.add_argument("--yes", action="store_true") - - subparsers.add_parser("version", help="Show installed version") - return parser - - -def _compatibility_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="MARM MCP Server") - parser.add_argument("--check-deps", action="store_true") - parser.add_argument("--generate-key", action="store_true") - parser.add_argument("--swarm", action="store_true") - parser.add_argument("--swarm-max", action="store_true") - parser.add_argument("--trusted", action="store_true") - parser.add_argument("--rate-limit-rpm", type=int) - parser.add_argument("--migrate-embeddings", action="store_true") - return parser - - def _profile_flags(profile: str) -> dict[str, bool]: return { "swarm": profile == "swarm", @@ -363,113 +193,6 @@ def _log_startup(runtime_config: dict) -> None: ) -def _print_payload(payload: dict[str, Any], *, as_json: bool = False) -> None: - if as_json: - print(json.dumps(payload, indent=2, sort_keys=True)) - return - print(json.dumps(payload, indent=2)) - - -def _format_size(size_bytes: int | None) -> str: - if size_bytes is None: - return "unknown" - size = float(size_bytes) - for unit in ("B", "KB", "MB", "GB"): - if size < 1024 or unit == "GB": - return f"{size:.1f} {unit}" - size /= 1024 - return f"{size_bytes} B" - - -def _queue_state(queue: dict[str, Any]) -> str: - if not queue.get("enabled", True): - return "disabled" - if queue.get("stopping"): - return "stopping" - return "healthy" if queue.get("running") else "starting" - - -def _print_status(payload: dict[str, Any]) -> None: - runtime = payload.get("runtime", {}) - metadata = runtime.get("metadata", {}) - mcp = payload.get("mcp", {}) - console = payload.get("console", {}) - memory = payload.get("memory", {}) - knowledge = payload.get("knowledge", {}) - projects = payload.get("projects", {}) - queue = payload.get("write_queue") - print(f"MARM Memory {payload.get('version', SERVER_VERSION)}") - print( - f"Runtime: {runtime.get('state', 'unknown')}" - f" | profile: {metadata.get('profile', 'standard')}" - ) - print( - f"MCP: {mcp.get('state', 'unknown')}" - f" | http://127.0.0.1:{mcp.get('port', SERVER_PORT)}/mcp" - ) - print( - f"Console: {console.get('state', 'unknown')}" - f" | http://127.0.0.1:{console.get('port', 8002)}" - ) - if memory.get("error"): - print(f"Memory: unavailable | {memory['error']}") - elif memory.get("exists"): - print( - f"Memory: {memory.get('memories', 0)} records" - f" | {memory.get('sessions', 0)} sessions" - f" | WAL: {memory.get('wal_mode', 'unknown')}" - f" | {_format_size(memory.get('size_bytes'))}" - ) - else: - print(f"Memory: no database at {memory.get('path', DEFAULT_DB_PATH)}") - if isinstance(queue, dict): - print( - f"Write queue: {_queue_state(queue)}" - f" | depth: {queue.get('depth', queue.get('queue_depth', 0))}" - ) - else: - print("Write queue: runtime stopped") - print( - f"Knowledge: {knowledge.get('state', 'unknown')}" - f" | schema: {knowledge.get('schema', 'unknown')}" - ) - print(f"Projects: {projects.get('state', projects.get('status', 'unknown'))}") - - -def _print_doctor(payload: dict[str, Any]) -> None: - print("MARM Doctor") - for check in payload.get("checks", []): - marker = ( - "OK" if check.get("ok") else "WARN" if check.get("optional") else "FAIL" - ) - print(f"[{marker}] {check.get('name')}: {check.get('detail')}") - print() - _print_status(payload.get("status", {})) - - -def _print_maintenance(payload: dict[str, Any]) -> None: - runtime = payload.get("runtime", {}) - memory = payload.get("memory_database", {}) - embedding = payload.get("embedding", {}) - print(f"MARM Maintenance {payload.get('version', SERVER_VERSION)}") - print(f"Runtime: {runtime.get('state', 'unknown')}") - queue = runtime.get("write_queue") - print( - "Write queue: runtime stopped" - if not isinstance(queue, dict) - else f"Write queue: {_queue_state(queue)}" - ) - print( - f"Memory DB: {memory.get('path', DEFAULT_DB_PATH)}" - f" | WAL: {memory.get('wal_mode', 'unknown')}" - f" | {_format_size(memory.get('size_bytes'))}" - ) - print( - f"Embeddings: {'compatible' if embedding.get('compatible') else 'migration required'}" - f" | {embedding.get('model', 'unknown')}" - ) - - def _ensure_runtime() -> dict[str, Any]: from .core.runtime_manager import inspect_runtime, start_background diff --git a/marm-mcp-server/marm_mcp_server/services/cli_output.py b/marm-mcp-server/marm_mcp_server/services/cli_output.py new file mode 100644 index 00000000..4787a727 --- /dev/null +++ b/marm-mcp-server/marm_mcp_server/services/cli_output.py @@ -0,0 +1,115 @@ +"""Human-readable status/doctor/maintenance rendering for the product CLI.""" + +from __future__ import annotations + +import json +from typing import Any + +from ..config.settings import DEFAULT_DB_PATH, SERVER_PORT, SERVER_VERSION + + +def _print_payload(payload: dict[str, Any], *, as_json: bool = False) -> None: + if as_json: + print(json.dumps(payload, indent=2, sort_keys=True)) + return + print(json.dumps(payload, indent=2)) + + +def _format_size(size_bytes: int | None) -> str: + if size_bytes is None: + return "unknown" + size = float(size_bytes) + for unit in ("B", "KB", "MB", "GB"): + if size < 1024 or unit == "GB": + return f"{size:.1f} {unit}" + size /= 1024 + return f"{size_bytes} B" + + +def _queue_state(queue: dict[str, Any]) -> str: + if not queue.get("enabled", True): + return "disabled" + if queue.get("stopping"): + return "stopping" + return "healthy" if queue.get("running") else "starting" + + +def _print_status(payload: dict[str, Any]) -> None: + runtime = payload.get("runtime", {}) + metadata = runtime.get("metadata", {}) + mcp = payload.get("mcp", {}) + console = payload.get("console", {}) + memory = payload.get("memory", {}) + knowledge = payload.get("knowledge", {}) + projects = payload.get("projects", {}) + queue = payload.get("write_queue") + print(f"MARM Memory {payload.get('version', SERVER_VERSION)}") + print( + f"Runtime: {runtime.get('state', 'unknown')}" + f" | profile: {metadata.get('profile', 'standard')}" + ) + print( + f"MCP: {mcp.get('state', 'unknown')}" + f" | http://127.0.0.1:{mcp.get('port', SERVER_PORT)}/mcp" + ) + print( + f"Console: {console.get('state', 'unknown')}" + f" | http://127.0.0.1:{console.get('port', 8002)}" + ) + if memory.get("error"): + print(f"Memory: unavailable | {memory['error']}") + elif memory.get("exists"): + print( + f"Memory: {memory.get('memories', 0)} records" + f" | {memory.get('sessions', 0)} sessions" + f" | WAL: {memory.get('wal_mode', 'unknown')}" + f" | {_format_size(memory.get('size_bytes'))}" + ) + else: + print(f"Memory: no database at {memory.get('path', DEFAULT_DB_PATH)}") + if isinstance(queue, dict): + print( + f"Write queue: {_queue_state(queue)}" + f" | depth: {queue.get('depth', queue.get('queue_depth', 0))}" + ) + else: + print("Write queue: runtime stopped") + print( + f"Knowledge: {knowledge.get('state', 'unknown')}" + f" | schema: {knowledge.get('schema', 'unknown')}" + ) + print(f"Projects: {projects.get('state', projects.get('status', 'unknown'))}") + + +def _print_doctor(payload: dict[str, Any]) -> None: + print("MARM Doctor") + for check in payload.get("checks", []): + marker = ( + "OK" if check.get("ok") else "WARN" if check.get("optional") else "FAIL" + ) + print(f"[{marker}] {check.get('name')}: {check.get('detail')}") + print() + _print_status(payload.get("status", {})) + + +def _print_maintenance(payload: dict[str, Any]) -> None: + runtime = payload.get("runtime", {}) + memory = payload.get("memory_database", {}) + embedding = payload.get("embedding", {}) + print(f"MARM Maintenance {payload.get('version', SERVER_VERSION)}") + print(f"Runtime: {runtime.get('state', 'unknown')}") + queue = runtime.get("write_queue") + print( + "Write queue: runtime stopped" + if not isinstance(queue, dict) + else f"Write queue: {_queue_state(queue)}" + ) + print( + f"Memory DB: {memory.get('path', DEFAULT_DB_PATH)}" + f" | WAL: {memory.get('wal_mode', 'unknown')}" + f" | {_format_size(memory.get('size_bytes'))}" + ) + print( + f"Embeddings: {'compatible' if embedding.get('compatible') else 'migration required'}" + f" | {embedding.get('model', 'unknown')}" + ) diff --git a/marm-mcp-server/marm_mcp_server/services/cli_parser.py b/marm-mcp-server/marm_mcp_server/services/cli_parser.py new file mode 100644 index 00000000..fa62d281 --- /dev/null +++ b/marm-mcp-server/marm_mcp_server/services/cli_parser.py @@ -0,0 +1,184 @@ +"""Argument parser construction for the product and compatibility CLIs.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from ..config.settings import SERVER_VERSION + + +def _add_profile_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--profile", + choices=("standard", "swarm", "swarm-max", "trusted"), + default="standard", + ) + parser.add_argument( + "--rate-limit-rpm", + type=int, + help="Override HTTP rate limit RPM; 0 disables rate limiting", + ) + + +def _add_docker_run_arguments(parser: argparse.ArgumentParser) -> None: + _add_profile_arguments(parser) + parser.add_argument("--port", type=int, default=8001) + parser.add_argument("--data-dir", type=Path, default=Path.home() / ".marm") + parser.add_argument("--name", default="marm-mcp-server") + parser.add_argument("--tag", default="latest") + parser.add_argument("--repo", type=Path, action="append", default=[]) + parser.add_argument("--pull", action="store_true") + parser.add_argument("--expose-network", action="store_true") + parser.add_argument("--env-file", type=Path) + parser.add_argument("--memory") + parser.add_argument("--cpus") + + +def _product_help() -> str: + """Return the stable, human-oriented root help for the product CLI.""" + from .product_help import render_product_help + + return render_product_help(SERVER_VERSION) + + +class _ProductArgumentParser(argparse.ArgumentParser): + """Keep root help stable while retaining argparse for all subcommands.""" + + def format_help(self) -> str: + return _product_help() + + +def _product_parser() -> argparse.ArgumentParser: + parser = _ProductArgumentParser( + prog="marm-memory", + description="Run and manage marm-memory locally", + add_help=False, + ) + parser.add_argument("-h", "--help", action="help", help="Show help") + parser.add_argument( + "-V", + "--version", + action="version", + version=SERVER_VERSION, + help="Show installed version", + ) + subparsers = parser.add_subparsers( + dest="command", required=True, parser_class=argparse.ArgumentParser + ) + + start = subparsers.add_parser("start", help="Start the local MARM runtime") + _add_profile_arguments(start) + start.add_argument("--foreground", action="store_true") + start.add_argument("--runtime-id", help=argparse.SUPPRESS) + + fast_start = subparsers.add_parser( + "fast-start-http", help="Start HTTP, Console, and optional client setup" + ) + _add_profile_arguments(fast_start) + fast_start.add_argument("--client", help="Configure a supported MCP client") + fast_start.add_argument("--no-console", action="store_true") + fast_start.add_argument("--no-browser", action="store_true") + + http = subparsers.add_parser( + "http", help="Run the HTTP transport in the foreground" + ) + _add_profile_arguments(http) + http.add_argument("--runtime-id", help=argparse.SUPPRESS) + http.set_defaults(foreground=True) + + subparsers.add_parser("stdio", help="Run the MCP STDIO transport") + + stop = subparsers.add_parser("stop", help="Stop the managed MARM runtime") + stop.add_argument("--force", action="store_true") + restart = subparsers.add_parser("restart", help="Restart the managed runtime") + restart.add_argument("--force", action="store_true") + status = subparsers.add_parser("status", help="Show local MARM status") + status.add_argument("--json", action="store_true", dest="as_json") + console = subparsers.add_parser("console", help="Launch MARM Console") + browser = console.add_mutually_exclusive_group() + browser.add_argument("--open", action="store_true", dest="open_browser") + browser.add_argument("--no-open", action="store_false", dest="open_browser") + console.set_defaults(open_browser=True) + console.add_argument("--foreground", action="store_true") + console.add_argument( + "--import-key", + action="store_true", + help="Create a managed authenticated Console browser session", + ) + logs = subparsers.add_parser("logs", help="Read managed runtime logs") + logs.add_argument("--follow", action="store_true") + logs.add_argument("--lines", type=int, default=100) + doctor = subparsers.add_parser("doctor", help="Diagnose the local install") + doctor.add_argument("--json", action="store_true", dest="as_json") + + knowledge = subparsers.add_parser("knowledge", help="Manage concept extraction") + knowledge_sub = knowledge.add_subparsers(dest="knowledge_command", required=True) + knowledge_sub.add_parser("status") + build = knowledge_sub.add_parser("build") + scope = build.add_mutually_exclusive_group(required=True) + scope.add_argument("--all", action="store_true", dest="search_all") + scope.add_argument("--session") + scope.add_argument("--project") + + projects = subparsers.add_parser("projects", help="Manage code indexes") + projects_sub = projects.add_subparsers(dest="projects_command", required=True) + projects_sub.add_parser("list") + index = projects_sub.add_parser("index") + index.add_argument("path") + index.add_argument( + "--mode", choices=("fast", "moderate", "full"), default="moderate" + ) + project_status = projects_sub.add_parser("status") + project_status.add_argument("project", nargs="?") + remove = projects_sub.add_parser("remove") + remove.add_argument("project") + remove.add_argument("--confirm", required=True) + + maintenance = subparsers.add_parser("maintenance") + maintenance_sub = maintenance.add_subparsers( + dest="maintenance_command", required=True + ) + maintenance_status = maintenance_sub.add_parser("status") + maintenance_status.add_argument("--json", action="store_true", dest="as_json") + embeddings = maintenance_sub.add_parser("embeddings") + embeddings_sub = embeddings.add_subparsers(dest="embeddings_command", required=True) + embeddings_sub.add_parser("migrate") + + key = subparsers.add_parser("key", help="Manage local bearer authentication") + key_sub = key.add_subparsers(dest="key_command", required=True) + key_sub.add_parser("generate", help="Generate and display an ephemeral key") + key_sub.add_parser("init", help="Create or reuse the managed local key file") + key_sub.add_parser("path", help="Print the managed local key-file path") + key_sub.add_parser("reveal", help="Display the managed local key") + + from .docker_cli import add_docker_commands + + add_docker_commands(subparsers, _add_docker_run_arguments) + upgrade = subparsers.add_parser( + "upgrade", aliases=["update"], help="Check for and install a newer MARM release" + ) + upgrade.add_argument("--check", action="store_true") + upgrade.add_argument("--version") + upgrade.add_argument("--yes", action="store_true") + upgrade.add_argument("--json", action="store_true", dest="as_json") + + uninstall = subparsers.add_parser( + "uninstall", help="Remove MARM while preserving user data" + ) + uninstall.add_argument("--yes", action="store_true") + + subparsers.add_parser("version", help="Show installed version") + return parser + + +def _compatibility_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="MARM MCP Server") + parser.add_argument("--check-deps", action="store_true") + parser.add_argument("--generate-key", action="store_true") + parser.add_argument("--swarm", action="store_true") + parser.add_argument("--swarm-max", action="store_true") + parser.add_argument("--trusted", action="store_true") + parser.add_argument("--rate-limit-rpm", type=int) + parser.add_argument("--migrate-embeddings", action="store_true") + return parser diff --git a/marm-mcp-server/tests/test_cli_output.py b/marm-mcp-server/tests/test_cli_output.py new file mode 100644 index 00000000..461537df --- /dev/null +++ b/marm-mcp-server/tests/test_cli_output.py @@ -0,0 +1,49 @@ +"""Direct unit tests for the pure formatting helpers in services/cli_output.py.""" + +from marm_mcp_server.services.cli_output import _format_size, _queue_state + + +def test_format_size_none_is_unknown(): + assert _format_size(None) == "unknown" + + +def test_format_size_stays_in_bytes_under_1024(): + assert _format_size(512) == "512.0 B" + + +def test_format_size_converts_to_kb(): + assert _format_size(2048) == "2.0 KB" + + +def test_format_size_converts_to_mb(): + assert _format_size(5 * 1024 * 1024) == "5.0 MB" + + +def test_format_size_caps_at_gb_for_very_large_values(): + # 3 TB worth of bytes must still render as GB, not overflow the unit list. + huge = 3 * 1024 * 1024 * 1024 * 1024 + result = _format_size(huge) + assert result.endswith(" GB") + assert float(result.split()[0]) == huge / (1024**3) + + +def test_queue_state_disabled_when_not_enabled(): + assert _queue_state({"enabled": False, "running": True}) == "disabled" + + +def test_queue_state_stopping_takes_priority_over_running(): + assert _queue_state({"enabled": True, "running": True, "stopping": True}) == ( + "stopping" + ) + + +def test_queue_state_healthy_when_running(): + assert _queue_state({"enabled": True, "running": True}) == "healthy" + + +def test_queue_state_starting_when_not_yet_running(): + assert _queue_state({"enabled": True, "running": False}) == "starting" + + +def test_queue_state_defaults_enabled_true_when_key_missing(): + assert _queue_state({"running": True}) == "healthy" diff --git a/marm-mcp-server/tests/test_command_smoke.py b/marm-mcp-server/tests/test_command_smoke.py index 61e3680a..edc64677 100644 --- a/marm-mcp-server/tests/test_command_smoke.py +++ b/marm-mcp-server/tests/test_command_smoke.py @@ -170,14 +170,14 @@ def visit(current: argparse.ArgumentParser, prefix: tuple[str, ...]) -> None: @pytest.mark.smoke def test_static_inventory_matches_registered_product_parser(): - from marm_mcp_server.cli import _product_parser + from marm_mcp_server.services.cli_parser import _product_parser assert set(COMMAND_HELP_PATHS) == _parser_command_paths(_product_parser()) @pytest.mark.smoke def test_root_help_lists_every_registered_top_level_command(): - from marm_mcp_server.cli import _product_help, _product_parser + from marm_mcp_server.services.cli_parser import _product_help, _product_parser help_text = _product_help() top_level = {path[0] for path in _parser_command_paths(_product_parser())} diff --git a/marm-mcp-server/tests/test_runtime_cli.py b/marm-mcp-server/tests/test_runtime_cli.py index ab44cbfd..9ee0b162 100644 --- a/marm-mcp-server/tests/test_runtime_cli.py +++ b/marm-mcp-server/tests/test_runtime_cli.py @@ -307,6 +307,7 @@ def test_product_help_uses_grouped_stable_layout(capsys): def test_product_help_wraps_for_narrow_terminals(monkeypatch): from marm_mcp_server.services import product_help + from marm_mcp_server.services.cli_parser import _product_help monkeypatch.setattr( product_help.shutil, @@ -314,7 +315,7 @@ def test_product_help_wraps_for_narrow_terminals(monkeypatch): lambda fallback: os.terminal_size((72, 24)), ) - output = cli._product_help() + output = _product_help() assert all(len(line) <= 72 for line in output.splitlines())