From b0362a0543dab3ba3acbe1ecacab296376744940 Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Sun, 7 Jun 2026 17:55:17 +0530 Subject: [PATCH 1/3] fix: resolve ollama preflight imports and directory path breakdown in smoke tests --- cli.py | 12 ++++++++++++ tests/test_cli_settings_smoke.py | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index 129104f..a8428e0 100644 --- a/cli.py +++ b/cli.py @@ -306,6 +306,14 @@ def build_parser() -> argparse.ArgumentParser: return parser +def check_ollama_preflight(settings) -> bool: + """ + Runs an optional preflight check for Ollama reachability and model availability + if the default configured model relies on a local Ollama instance. + """ + from rooms import ollama_preflight + return ollama_preflight.run_ollama_preflight(settings) + def main(argv: Optional[List[str]] = None) -> int: parser = build_parser() args = parser.parse_args(argv) @@ -330,6 +338,10 @@ def main(argv: Optional[List[str]] = None) -> int: if found: console.print(f"[dim]Using settings: {found}[/dim]") + # Run the Ollama preflight check. If it fails, exit cleanly without starting the wizard. + if not check_ollama_preflight(settings): + return 1 + main_menu(settings) return 0 diff --git a/tests/test_cli_settings_smoke.py b/tests/test_cli_settings_smoke.py index 35b80aa..fb409ac 100644 --- a/tests/test_cli_settings_smoke.py +++ b/tests/test_cli_settings_smoke.py @@ -56,11 +56,13 @@ def test_cli_main_loads_explicit_config_without_wizard(tmp_path, monkeypatch): encoding="utf-8", ) - with patch.object(cli, "main_menu") as mock_menu: + with patch.object(cli, "main_menu") as mock_menu, \ + patch.object(cli, "check_ollama_preflight", return_value=True) as mock_preflight: rc = cli.main(["--config", str(cfg)]) assert rc == 0 mock_menu.assert_called_once() + mock_preflight.assert_called_once() settings = mock_menu.call_args[0][0] assert settings.defaults.litellm_model == "ollama/smoke:1b" assert settings.defaults.timeout == 99 @@ -88,4 +90,4 @@ def test_settings_error_from_init_guard(tmp_path, monkeypatch): cli.main(["config", "init"]) with pytest.raises(SettingsError): from rooms.settings import init_settings_file - init_settings_file() + init_settings_file() \ No newline at end of file From 1ed20906008a7e0d555af95c767bb0a6f6ba8300 Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Sun, 7 Jun 2026 18:04:51 +0530 Subject: [PATCH 2/3] docs: document --skip-preflight flag in EXAMPLES.md --- docs/EXAMPLES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 2601778..a90d483 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -219,3 +219,14 @@ The quality of your agents is entirely determined by the quality of their system | Agent addresses you directly | Session auto-triggers HITL early | This is by design — respond or type `continue` | | Agent has nothing to add | Returns `PASS` | Turn is silently skipped; visible in logs only | | Topic is very broad | Agents go off-scope | Add Orchestrator with *"steer back to [topic] if agents drift"* | + +--- + +## Advanced CLI Reference + +### Skipping Preflight Checks +If you are running the application in a CI/CD automation environment, running automated test configurations, or simply wish to bypass the local Ollama connectivity and model verification sequence, append the `--skip-preflight` flag alongside your execution statement: + +```bash +python cli.py --skip-preflight +``` From e26364170fb2d6657e76d0f9e59d8e892a2e377c Mon Sep 17 00:00:00 2001 From: Aainee Sinha Date: Mon, 8 Jun 2026 14:46:13 +0530 Subject: [PATCH 3/3] feat: implement preflight validation check module, wire up cli parser flag, and add comprehensive unit tests --- cli.py | 13 +++++-- rooms/ollama_preflight.py | 69 ++++++++++++++++++++++++++++++++++ tests/test_ollama_preflight.py | 48 +++++++++++++++++++++++ 3 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 rooms/ollama_preflight.py create mode 100644 tests/test_ollama_preflight.py diff --git a/cli.py b/cli.py index a8428e0..e6f6b1c 100644 --- a/cli.py +++ b/cli.py @@ -303,6 +303,12 @@ def build_parser() -> argparse.ArgumentParser: reset_p.add_argument("--path", help="Specific settings file to remove") reset_p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation") + parser.add_argument( + "--skip-preflight", + action="store_true", + help="Skip Ollama preflight connectivity and model validation checks" + ) + return parser @@ -338,9 +344,10 @@ def main(argv: Optional[List[str]] = None) -> int: if found: console.print(f"[dim]Using settings: {found}[/dim]") - # Run the Ollama preflight check. If it fails, exit cleanly without starting the wizard. - if not check_ollama_preflight(settings): - return 1 + # Run the Ollama preflight check unless --skip-preflight is passed. + if not args.skip_preflight: + if not check_ollama_preflight(settings): + return 1 main_menu(settings) return 0 diff --git a/rooms/ollama_preflight.py b/rooms/ollama_preflight.py new file mode 100644 index 0000000..c5594ef --- /dev/null +++ b/rooms/ollama_preflight.py @@ -0,0 +1,69 @@ +import urllib.request +import urllib.error +import json +import sys +from rich.console import Console +from rich.panel import Panel + +def run_ollama_preflight(settings) -> bool: + """ + Verifies if the configured local Ollama instance is running + and contains the requested model tag. + """ + model_string = getattr(settings.defaults, "litellm_model", "") + if not model_string.startswith("ollama/"): + return True + + # Extract the tag name (e.g., 'ollama/gemma4:e2b' -> 'gemma4:e2b') + configured_tag = model_string.split("/", 1)[1] + base_url = getattr(settings.ollama, "base_url", "http://localhost:11434").rstrip("/") + tags_url = f"{base_url}/api/tags" + + console = Console() + + try: + req = urllib.request.Request(tags_url, method="GET") + with urllib.request.urlopen(req, timeout=3.0) as response: + if response.status != 200: + raise urllib.error.URLError(f"HTTP Status {response.status}") + + data = json.loads(response.read().decode("utf-8")) + models = data.get("models", []) + + available_tags = [] + for m in models: + if "name" in m: + available_tags.append(m["name"]) + if "model" in m: + available_tags.append(m["model"]) + + if configured_tag in available_tags or f"{configured_tag}:latest" in available_tags: + return True + + # Server is up, but model tag is missing + panel = Panel( + f"[bold yellow]Warning:[/bold yellow] Configured Ollama model [bold cyan]'{configured_tag}'[/bold cyan] was not found locally.\n\n" + f"[bold white]Actionable Fixes:[/bold white]\n" + f" • Run: [green]ollama pull {configured_tag}[/green]\n" + f" • Edit your configuration file to use an available tag.\n" + f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", + title="[bold red]Ollama Preflight Verification Failed[/bold red]", + expand=False + ) + console.print(panel) + return False + + except (urllib.error.URLError, TimeoutError, ConnectionError) as e: + # Ollama service is completely unreachable + panel = Panel( + f"[bold yellow]Warning:[/bold yellow] Could not connect to Ollama server at [cyan]{base_url}[/cyan]\n" + f"Error Details: {str(e)}\n\n" + f"[bold white]Actionable Fixes:[/bold white]\n" + f" • Ensure Ollama is running by executing: [green]ollama serve[/green]\n" + f" • Verify your [magenta]ollama.base_url[/magenta] settings match your active instance.\n" + f" • Run with [green]python cli.py --skip-preflight[/green] to bypass.", + title="[bold red]Ollama Server Unreachable[/bold red]", + expand=False + ) + console.print(panel) + return False \ No newline at end of file diff --git a/tests/test_ollama_preflight.py b/tests/test_ollama_preflight.py new file mode 100644 index 0000000..6f754d0 --- /dev/null +++ b/tests/test_ollama_preflight.py @@ -0,0 +1,48 @@ +import unittest +from unittest.mock import patch, MagicMock +import urllib.error +import io + +from rooms.ollama_preflight import run_ollama_preflight + +class TestOllamaPreflightLogic(unittest.TestCase): + def setUp(self): + # Setup clean configuration mock structure + self.mock_settings = MagicMock() + self.mock_settings.defaults.litellm_model = "ollama/gemma4:e2b" + self.mock_settings.ollama.base_url = "http://localhost:11434" + + def test_skips_non_ollama_models(self): + self.mock_settings.defaults.litellm_model = "openai/gpt-4" + result = run_ollama_preflight(self.mock_settings) + self.assertTrue(result) + + @patch("urllib.request.urlopen") + def test_preflight_success_exact_match(self, mock_urlopen): + # Simulate clean API json payload back from Ollama + mock_response = MagicMock() + mock_response.status = 200 + mock_response.read.return_value = b'{"models": [{"name": "gemma4:e2b"}, {"name": "llama3:latest"}]}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + result = run_ollama_preflight(self.mock_settings) + self.assertTrue(result) + + @patch("urllib.request.urlopen") + @patch("sys.stdout", new_callable=io.StringIO) + def test_preflight_missing_model_tag(self, mock_stdout, mock_urlopen): + mock_response = MagicMock() + mock_response.status = 200 + mock_response.read.return_value = b'{"models": [{"name": "llama3:latest"}]}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + result = run_ollama_preflight(self.mock_settings) + self.assertFalse(result) + + @patch("urllib.request.urlopen") + @patch("sys.stdout", new_callable=io.StringIO) + def test_preflight_server_unreachable(self, mock_stdout, mock_urlopen): + mock_urlopen.side_effect = urllib.error.URLError("Connection refused") + + result = run_ollama_preflight(self.mock_settings) + self.assertFalse(result) \ No newline at end of file