@@ -307,3 +307,133 @@ def complete(self, *, prompt, model=None, api_key=None):
307307 assert rc == 0
308308 assert cap .out == "ok\n "
309309 assert calls == [{"model" : "claude-opus-4-7" , "api_key" : "override-secret" }]
310+
311+
312+ # ── Ollama local-mode fallback ─────────────────────────────────────────────────
313+
314+ def test_generate_falls_back_to_ollama_when_no_key_and_ollama_running (
315+ tmp_path , capsys , monkeypatch
316+ ):
317+ """No explicit provider, no API key, Ollama reachable → auto-switch to Ollama."""
318+ _seed_repo_with_config (tmp_path )
319+ (tmp_path / "forerunner.config.yaml" ).unlink ()
320+ calls : list [dict ] = []
321+
322+ class FakeOllamaProvider :
323+ default_env_var = "OLLAMA_HOST"
324+ default_model = "llama3"
325+
326+ def complete (self , * , prompt , model = None , api_key = None ):
327+ calls .append ({"model" : model , "api_key" : api_key })
328+ return CompletionResult (text = "ollama output" , model = model or "llama3" )
329+
330+ from codeforerunner import providers
331+ from unittest .mock import patch
332+
333+ monkeypatch .setitem (providers .REGISTRY , "ollama" , FakeOllamaProvider )
334+ monkeypatch .delenv ("ANTHROPIC_API_KEY" , raising = False )
335+
336+ with patch ("codeforerunner.providers.ollama_available" , return_value = True ):
337+ rc = main (["--repo" , str (tmp_path ), "generate" , "readme" ])
338+
339+ cap = capsys .readouterr ()
340+ assert rc == 0
341+ assert "local mode" in cap .err
342+ assert cap .out == "ollama output\n "
343+ assert calls [0 ]["model" ] == "llama3"
344+
345+
346+ def test_generate_no_fallback_when_provider_explicit_and_key_missing (
347+ tmp_path , capsys , monkeypatch
348+ ):
349+ """Explicit --provider means no auto-fallback even if Ollama is running."""
350+ _seed_repo_with_config (tmp_path )
351+ (tmp_path / "forerunner.config.yaml" ).unlink ()
352+
353+ class FakeProvider :
354+ default_env_var = "FAKE_API_KEY"
355+ default_model = "fake-default"
356+
357+ def complete (self , * , prompt , model = None , api_key = None ): # pragma: no cover
358+ raise AssertionError ("should not be called" )
359+
360+ from codeforerunner import providers
361+ from unittest .mock import patch
362+
363+ monkeypatch .setitem (providers .REGISTRY , "fake" , FakeProvider )
364+ monkeypatch .delenv ("FAKE_API_KEY" , raising = False )
365+
366+ with patch ("codeforerunner.providers.ollama_available" , return_value = True ):
367+ rc = main (["--repo" , str (tmp_path ), "generate" , "readme" , "--provider" , "fake" ])
368+
369+ cap = capsys .readouterr ()
370+ assert rc == 3
371+ assert "missing API key" in cap .err
372+
373+
374+ def test_generate_no_fallback_when_config_provider_set_and_key_missing (
375+ tmp_path , capsys , monkeypatch
376+ ):
377+ """Provider in config file counts as explicit — no auto-fallback."""
378+ _seed_repo_with_config (tmp_path )
379+ (tmp_path / "forerunner.config.yaml" ).write_text (
380+ "provider: anthropic\n " , encoding = "utf-8"
381+ )
382+
383+ from unittest .mock import patch
384+
385+ monkeypatch .delenv ("ANTHROPIC_API_KEY" , raising = False )
386+
387+ with patch ("codeforerunner.providers.ollama_available" , return_value = True ):
388+ rc = main (["--repo" , str (tmp_path ), "generate" , "readme" ])
389+
390+ cap = capsys .readouterr ()
391+ assert rc == 3
392+ assert "missing API key" in cap .err
393+
394+
395+ def test_generate_missing_key_includes_ollama_hint_when_ollama_absent (
396+ tmp_path , capsys , monkeypatch
397+ ):
398+ """No explicit provider, no API key, Ollama not running → error + Ollama hint."""
399+ _seed_repo_with_config (tmp_path )
400+ (tmp_path / "forerunner.config.yaml" ).unlink ()
401+
402+ from unittest .mock import patch
403+
404+ monkeypatch .delenv ("ANTHROPIC_API_KEY" , raising = False )
405+
406+ with patch ("codeforerunner.providers.ollama_available" , return_value = False ):
407+ rc = main (["--repo" , str (tmp_path ), "generate" , "readme" ])
408+
409+ cap = capsys .readouterr ()
410+ assert rc == 3
411+ assert "missing API key" in cap .err
412+ assert "Ollama" in cap .err
413+
414+
415+ def test_generate_ollama_fallback_uses_explicit_model (tmp_path , capsys , monkeypatch ):
416+ """--model flag is preserved when falling back to Ollama."""
417+ _seed_repo_with_config (tmp_path )
418+ (tmp_path / "forerunner.config.yaml" ).unlink ()
419+ calls : list [dict ] = []
420+
421+ class FakeOllamaProvider :
422+ default_env_var = "OLLAMA_HOST"
423+ default_model = "llama3"
424+
425+ def complete (self , * , prompt , model = None , api_key = None ):
426+ calls .append ({"model" : model })
427+ return CompletionResult (text = "ok" , model = model or "llama3" )
428+
429+ from codeforerunner import providers
430+ from unittest .mock import patch
431+
432+ monkeypatch .setitem (providers .REGISTRY , "ollama" , FakeOllamaProvider )
433+ monkeypatch .delenv ("ANTHROPIC_API_KEY" , raising = False )
434+
435+ with patch ("codeforerunner.providers.ollama_available" , return_value = True ):
436+ rc = main (["--repo" , str (tmp_path ), "generate" , "readme" , "--model" , "llama3.2" ])
437+
438+ assert rc == 0
439+ assert calls [0 ]["model" ] == "llama3.2"
0 commit comments