|
| 1 | +# src/tests/backend/test_utils_kernel.py |
| 2 | +import os |
| 3 | +import sys |
| 4 | +import json |
| 5 | +import asyncio |
| 6 | +import pytest |
| 7 | +import types |
| 8 | +import requests |
| 9 | + |
| 10 | +# Stub out app_config.config so utils_kernel can import it |
| 11 | +import types as _types |
| 12 | +import sys as _sys |
| 13 | + |
| 14 | +class _DummyConfigImport: |
| 15 | + def create_kernel(self): |
| 16 | + from backend.utils_kernel import DummyKernel |
| 17 | + return DummyKernel() |
| 18 | + |
| 19 | +app_cfg = _types.ModuleType("app_config") |
| 20 | +app_cfg.config = _DummyConfigImport() |
| 21 | +_sys.modules["app_config"] = app_cfg |
| 22 | + |
| 23 | +# Ensure src is on path |
| 24 | +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) |
| 25 | +SRC = os.path.join(ROOT, 'src') |
| 26 | +if SRC not in sys.path: |
| 27 | + sys.path.insert(0, SRC) |
| 28 | + |
| 29 | +# Stub semantic_kernel and its submodules |
| 30 | +sk_pkg = types.ModuleType('semantic_kernel') |
| 31 | +sk_pkg.__path__ = [] |
| 32 | +sk_funcs = types.ModuleType('semantic_kernel.functions') |
| 33 | +def kernel_function(name=None, description=None): |
| 34 | + def decorator(func): return func |
| 35 | + return decorator |
| 36 | +sk_funcs.kernel_function = kernel_function |
| 37 | +sk_funcs.KernelFunction = lambda *args, **kwargs: (lambda f: f) |
| 38 | +sk_pkg.Kernel = type('Kernel', (), {}) |
| 39 | + |
| 40 | +sys.modules['semantic_kernel'] = sk_pkg |
| 41 | +sys.modules['semantic_kernel.functions'] = sk_funcs |
| 42 | + |
| 43 | +# Stub semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgent |
| 44 | +agents_pkg = types.ModuleType('semantic_kernel.agents') |
| 45 | +agents_pkg.__path__ = [] |
| 46 | +az_pkg = types.ModuleType('semantic_kernel.agents.azure_ai') |
| 47 | +az_pkg.__path__ = [] |
| 48 | +aazure_pkg = types.ModuleType('semantic_kernel.agents.azure_ai.azure_ai_agent') |
| 49 | +class AzureAIAgent: |
| 50 | + def __init__(self): pass |
| 51 | +aazure_pkg.AzureAIAgent = AzureAIAgent |
| 52 | + |
| 53 | +sys.modules['semantic_kernel.agents'] = agents_pkg |
| 54 | +sys.modules['semantic_kernel.agents.azure_ai'] = az_pkg |
| 55 | +sys.modules['semantic_kernel.agents.azure_ai.azure_ai_agent'] = aazure_pkg |
| 56 | + |
| 57 | +# Stub azure.identity.DefaultAzureCredential |
| 58 | +azure_pkg = types.ModuleType('azure') |
| 59 | +identity_pkg = types.ModuleType('azure.identity') |
| 60 | +def dummy_credential(): |
| 61 | + class C: |
| 62 | + def get_token(self, scope): return types.SimpleNamespace(token='token') |
| 63 | + return C() |
| 64 | +identity_pkg.DefaultAzureCredential = dummy_credential |
| 65 | +azure_pkg.identity = identity_pkg |
| 66 | +sys.modules['azure'] = azure_pkg |
| 67 | +sys.modules['azure.identity'] = identity_pkg |
| 68 | + |
| 69 | +# Stub models.messages_kernel.AgentType |
| 70 | +models_pkg = types.ModuleType('models') |
| 71 | +msgs_mod = types.ModuleType('models.messages_kernel') |
| 72 | +from enum import Enum |
| 73 | +class AgentType(Enum): |
| 74 | + HR = 'hr_agent' |
| 75 | + PROCUREMENT = 'procurement_agent' |
| 76 | + GENERIC = 'generic' |
| 77 | + PRODUCT = 'product_agent' |
| 78 | + MARKETING = 'marketing_agent' |
| 79 | + TECH_SUPPORT = 'tech_support_agent' |
| 80 | + HUMAN = 'human_agent' |
| 81 | + PLANNER = 'planner_agent' |
| 82 | + GROUP_CHAT_MANAGER = 'group_chat_manager' |
| 83 | +msgs_mod.AgentType = AgentType |
| 84 | +models_pkg.messages_kernel = msgs_mod |
| 85 | +sys.modules['models'] = models_pkg |
| 86 | +sys.modules['models.messages_kernel'] = msgs_mod |
| 87 | + |
| 88 | +# Stub context.cosmos_memory_kernel.CosmosMemoryContext |
| 89 | +context_pkg = types.ModuleType('context') |
| 90 | +cos_pkg = types.ModuleType('context.cosmos_memory_kernel') |
| 91 | +class _TempCosmos: |
| 92 | + def __init__(self, session_id, user_id): |
| 93 | + self.session_id = session_id |
| 94 | + self.user_id = user_id |
| 95 | +cos_pkg.CosmosMemoryContext = _TempCosmos |
| 96 | +context_pkg.cosmos_memory_kernel = cos_pkg |
| 97 | +sys.modules['context'] = context_pkg |
| 98 | +sys.modules['context.cosmos_memory_kernel'] = cos_pkg |
| 99 | + |
| 100 | +# Stub kernel_agents and agent classes |
| 101 | +ka_pkg = types.ModuleType('kernel_agents') |
| 102 | +ka_pkg.__path__ = [] |
| 103 | +submods = [ |
| 104 | + 'agent_factory','generic_agent','group_chat_manager','hr_agent', |
| 105 | + 'human_agent','marketing_agent','planner_agent','procurement_agent', |
| 106 | + 'product_agent','tech_support_agent' |
| 107 | +] |
| 108 | +for sub in submods: |
| 109 | + m = types.ModuleType(f'kernel_agents.{sub}') |
| 110 | + sys.modules[f'kernel_agents.{sub}'] = m |
| 111 | + setattr(ka_pkg, sub, m) |
| 112 | +# Stub AgentFactory |
| 113 | +class AgentFactory: |
| 114 | + @staticmethod |
| 115 | + async def create_all_agents(session_id, user_id, temperature): |
| 116 | + return {} |
| 117 | +sys.modules['kernel_agents.agent_factory'].AgentFactory = AgentFactory |
| 118 | +# Stub other agent classes |
| 119 | +for sub in submods: |
| 120 | + mod = sys.modules[f'kernel_agents.{sub}'] |
| 121 | + cls_name = ''.join(part.title() for part in sub.split('_')) |
| 122 | + setattr(mod, cls_name, type(cls_name, (), {})) |
| 123 | +sys.modules['kernel_agents'] = ka_pkg |
| 124 | + |
| 125 | +# Import module under test |
| 126 | +from backend.utils_kernel import ( |
| 127 | + initialize_runtime_and_context, |
| 128 | + get_agents, |
| 129 | + load_tools_from_json_files, |
| 130 | + rai_success, |
| 131 | + agent_instances, |
| 132 | + config, |
| 133 | + CosmosMemoryContext |
| 134 | +) |
| 135 | + |
| 136 | +# Dummy Kernel for testing |
| 137 | +class DummyKernel: |
| 138 | + pass |
| 139 | + |
| 140 | +class DummyConfig: |
| 141 | + def create_kernel(self): return DummyKernel() |
| 142 | + |
| 143 | +# Setup overrides |
| 144 | +def setup_module(module): |
| 145 | + import backend.utils_kernel as uk |
| 146 | + uk.config = DummyConfig() |
| 147 | + uk.CosmosMemoryContext = _TempCosmos |
| 148 | + |
| 149 | +@pytest.mark.asyncio |
| 150 | +async def test_initialize_runtime_and_context_valid(): |
| 151 | + kernel, mem = await initialize_runtime_and_context(user_id='u1') |
| 152 | + assert isinstance(kernel, DummyKernel) |
| 153 | + assert mem.user_id == 'u1' |
| 154 | + |
| 155 | +@pytest.mark.asyncio |
| 156 | +async def test_initialize_runtime_and_context_invalid(): |
| 157 | + with pytest.raises(ValueError): |
| 158 | + await initialize_runtime_and_context() |
| 159 | + |
| 160 | +@pytest.mark.asyncio |
| 161 | +async def test_get_agents_caching(monkeypatch): |
| 162 | + class DummyAgent: |
| 163 | + def __init__(self, name): self.name = name |
| 164 | + async def fake_create_all_agents(session_id, user_id, temperature): |
| 165 | + return {AgentType.HR: DummyAgent('hr'), AgentType.PRODUCT: DummyAgent('prod')} |
| 166 | + import backend.utils_kernel as uk |
| 167 | + # Override the AgentFactory class in utils_kernel module completely |
| 168 | + FakeFactory = type('AgentFactory', (), {'create_all_agents': staticmethod(fake_create_all_agents)}) |
| 169 | + monkeypatch.setattr(uk, 'AgentFactory', FakeFactory) |
| 170 | + |
| 171 | + agent_instances.clear() |
| 172 | + agents = await get_agents('s', 'u') |
| 173 | + assert isinstance(agents, dict) |
| 174 | + agents2 = await get_agents('s', 'u') |
| 175 | + assert agents2 is agents |
| 176 | + |
| 177 | +def test_load_tools_from_json_files(tmp_path, monkeypatch, caplog): |
| 178 | + tools_dir = tmp_path / 'tools' |
| 179 | + tools_dir.mkdir() |
| 180 | + data = {'tools':[{'name':'foo','description':'desc','parameters':{'a':1}}]} |
| 181 | + (tools_dir / 'hr_tools.json').write_text(json.dumps(data)) |
| 182 | + (tools_dir / 'bad.json').write_text('{bad') |
| 183 | + import backend.utils_kernel as uk |
| 184 | + monkeypatch.setattr(uk.os.path, 'dirname', lambda _: str(tmp_path)) |
| 185 | + caplog.set_level('WARNING') |
| 186 | + funcs = load_tools_from_json_files() |
| 187 | + assert any(f['function']=='foo' for f in funcs) |
| 188 | + assert 'Error loading tool file bad.json' in caplog.text |
| 189 | + |
| 190 | +@pytest.mark.asyncio |
| 191 | +async def test_rai_success_missing_env(monkeypatch): |
| 192 | + monkeypatch.delenv('AZURE_OPENAI_ENDPOINT', raising=False) |
| 193 | + monkeypatch.delenv('AZURE_OPENAI_API_VERSION', raising=False) |
| 194 | + monkeypatch.delenv('AZURE_OPENAI_MODEL_NAME', raising=False) |
| 195 | + class Cred: |
| 196 | + def get_token(self, _): return types.SimpleNamespace(token='t') |
| 197 | + monkeypatch.setattr('backend.utils_kernel.DefaultAzureCredential', lambda: Cred()) |
| 198 | + res = await rai_success('x') |
| 199 | + assert res is True |
| 200 | + |
| 201 | +@pytest.mark.asyncio |
| 202 | +async def test_rai_success_api(monkeypatch): |
| 203 | + monkeypatch.setenv('AZURE_OPENAI_ENDPOINT','http://e') |
| 204 | + monkeypatch.setenv('AZURE_OPENAI_API_VERSION','v') |
| 205 | + monkeypatch.setenv('AZURE_OPENAI_MODEL_NAME','n') |
| 206 | + class Cred: |
| 207 | + def get_token(self, _): return types.SimpleNamespace(token='t') |
| 208 | + monkeypatch.setattr('backend.utils_kernel.DefaultAzureCredential', lambda: Cred()) |
| 209 | + class Resp: |
| 210 | + status_code=200 |
| 211 | + def json(self): return {'choices':[{'message':{'content':'FALSE'}}]} |
| 212 | + def raise_for_status(self): pass |
| 213 | + monkeypatch.setattr(requests, 'post', lambda *a, **k: Resp()) |
| 214 | + res = await rai_success('y') |
| 215 | + assert res is True |
| 216 | + |
| 217 | +# New test to cover no-tools-dir path |
| 218 | + |
| 219 | +def test_load_tools_from_json_files_no_dir(tmp_path, monkeypatch): |
| 220 | + # No 'tools' subdirectory exists |
| 221 | + import backend.utils_kernel as uk |
| 222 | + # Make dirname() point to a path without tools folder |
| 223 | + monkeypatch.setattr(uk.os.path, 'dirname', lambda _: str(tmp_path)) |
| 224 | + funcs = load_tools_from_json_files() |
| 225 | + assert funcs == [] |
0 commit comments