diff --git a/.github/workflows/inneros-runtime-gates.yml b/.github/workflows/inneros-runtime-gates.yml index 827c0fa5..47d3f3b0 100644 --- a/.github/workflows/inneros-runtime-gates.yml +++ b/.github/workflows/inneros-runtime-gates.yml @@ -3,6 +3,9 @@ name: InnerOS P0 Runtime Safety Gates on: pull_request: paths: + - "platform/inneros_core_runtime/local_model_router.py" + - "platform/raphiia_openai/__init__.py" + - "platform/tests/test_local_model_router_fallback.py" - "platform/inneros_core_runtime/execution_binding.py" - "platform/inneros_core_runtime/capability_gateway.py" - "platform/inneros_core_runtime/capability_gateway_peer.py" @@ -28,6 +31,9 @@ on: push: branches: [main] paths: + - "platform/inneros_core_runtime/local_model_router.py" + - "platform/raphiia_openai/__init__.py" + - "platform/tests/test_local_model_router_fallback.py" - "platform/inneros_core_runtime/execution_binding.py" - "platform/inneros_core_runtime/capability_gateway.py" - "platform/inneros_core_runtime/capability_gateway_peer.py" @@ -70,6 +76,9 @@ jobs: - name: Compile critical runtime contracts run: | python -m compileall -q \ + platform/inneros_core_runtime/local_model_router.py \ + platform/raphiia_openai/__init__.py \ + platform/tests/test_local_model_router_fallback.py \ platform/inneros_core_runtime/execution_binding.py \ platform/inneros_core_runtime/capability_gateway.py \ platform/inneros_core_runtime/capability_gateway_peer.py \ @@ -90,6 +99,7 @@ jobs: python -m pytest -q \ platform/tests/test_global_coordination_guards.py \ platform/tests/test_execution_binding.py \ + platform/tests/test_local_model_router_fallback.py \ platform/tests/test_execution_receipts.py \ platform/tests/test_capability_gateway.py \ platform/tests/test_capability_gateway_regressions_p0.py \ diff --git a/platform/tests/test_local_model_router_fallback.py b/platform/tests/test_local_model_router_fallback.py index 2a4cc17e..cd5c29e0 100644 --- a/platform/tests/test_local_model_router_fallback.py +++ b/platform/tests/test_local_model_router_fallback.py @@ -2,12 +2,19 @@ from __future__ import annotations import unittest +from pathlib import Path from unittest.mock import patch from raphiia_openai import local_model_router as lmr class LocalModelRouterFallbackTests(unittest.TestCase): + def test_scheduler_uses_active_canonical_router_from_shim(self): + from inneros_core_runtime import dev_swarm_scheduler as scheduler + self.assertIs(scheduler.local_model_router, lmr) + self.assertEqual(Path(lmr.__file__).parent.name, "inneros_core_runtime") + self.assertTrue(callable(getattr(lmr, "_intel_ollama_fallback", None))) + @patch.object(lmr, "_intel_ollama_fallback") @patch.object(lmr, "_http_ok") @patch.object(lmr, "local_model_health") @@ -35,5 +42,46 @@ def test_amd_vllm_unreachable_uses_ollama_fallback(self, mock_health, mock_ok, m mock_fallback.assert_called_once() + @patch.object(lmr, "_intel_ollama_fallback") + @patch.object(lmr, "_http_ok") + @patch.object(lmr, "local_model_health") + def test_vllm_generation_failure_uses_intel_ollama(self, mock_health, mock_ok, mock_fallback): + mock_health.return_value = {"ok": True} + mock_ok.return_value = {"ok": True} + mock_fallback.return_value = { + "ok": True, "runtime": "local_model", "selected_node": "intel", + "response": "fallback generated", "fallback_reason": "amd_vllm_generation_failed", + } + with patch.object(lmr, "classify_task_runtime") as classifier, patch.object(lmr, "_vllm_chat") as vllm: + classifier.return_value = { + "task_type": "coding", "recommended_model": "amd-qwen", + "recommended_provider": "local-amd-5", "recommended_backend": "vllm", + "reason": "test", + } + vllm.return_value = {"ok": False, "error": "vllm_unavailable"} + result = lmr.run_local_model(task_type="coding", prompt="test") + self.assertTrue(result["ok"]) + self.assertEqual(result["selected_node"], "intel") + self.assertEqual(mock_fallback.call_args.kwargs["fallback_reason"], "amd_vllm_generation_failed") + + @patch.object(lmr, "_intel_ollama_fallback") + @patch.object(lmr, "_http_ok") + @patch.object(lmr, "local_model_health") + def test_failed_fallback_remains_fail_closed(self, mock_health, mock_ok, mock_fallback): + mock_health.return_value = {"ok": True} + mock_ok.side_effect = lambda url: {"ok": "/v1/models" not in url} + mock_fallback.return_value = {"ok": False, "error": "intel_ollama_no_models"} + with patch.object(lmr, "classify_task_runtime") as classifier: + classifier.return_value = { + "task_type": "coding", "recommended_model": "amd-qwen", + "recommended_provider": "local-amd-5", "recommended_backend": "vllm", + "reason": "test", + } + result = lmr.run_local_model(task_type="coding", prompt="test") + self.assertFalse(result["ok"]) + self.assertEqual(result["error"], "intel_ollama_no_models") + self.assertFalse(result["external_needed"]) + mock_fallback.assert_called_once() + if __name__ == "__main__": unittest.main()