import os import sys import pytest sys.path.insert( 0, os.path.abspath("../../..") ) # Adds the parent directory to the system path from unittest.mock import MagicMock, patch from litellm.integrations.gitlab.gitlab_prompt_manager import GitLabPromptManager # ----------------------------- # Basic init & template loading # ----------------------------- @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_initialization_with_root_folder(mock_client_class): """Loads a prompt from the repo root when no prompts_path is specified.""" mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 temperature: 0.7 max_tokens: 150 --- System: You are a helpful assistant. User: {{user_message}}""" mock_client_class.return_value = mock_client config = { "project": "group/sub/repo", "access_token": "glpat_xxx", # no prompts_path -> root } manager = GitLabPromptManager(config, prompt_id="test_prompt") # Should have loaded the prompt assert "test_prompt" in manager.prompt_manager.prompts template = manager.prompt_manager.prompts["test_prompt"] assert template.model == "gpt-4" assert template.temperature == 0.7 assert template.max_tokens == 150 # Ensures correct file path was requested at repo root (test_prompt.prompt) mock_client.get_file_content.assert_called_with("test_prompt.prompt", ref=None) # Rendering rendered = manager.prompt_manager.render_template( "test_prompt", {"user_message": "What is AI?"} ) assert "You are a helpful assistant." in rendered assert "What is AI?" in rendered @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_with_prompts_path(mock_client_class): """Loads a prompt from a configured prompts folder; ID maps to folder + .prompt.""" mock_client = MagicMock() mock_client.get_file_content.return_value = "Hello {{name}}!" mock_client_class.return_value = mock_client config = { "project": "group/repo", "access_token": "token", "prompts_path": "prompts/chat", # folder setting } manager = GitLabPromptManager(config, prompt_id="greet/hi") # Expected path: prompts/chat/greet/hi.prompt mock_client.get_file_content.assert_called_with( "prompts/chat/greet/hi.prompt", ref=None ) rendered = manager.prompt_manager.render_template("greet/hi", {"name": "World"}) assert rendered == "Hello World!" # ----------------------------- # Error handling / validation # ----------------------------- @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_error_handling_load(mock_client_class): """Errors from GitLabClient surface with helpful context.""" mock_client = MagicMock() mock_client.get_file_content.side_effect = Exception("GitLab API error") mock_client_class.return_value = mock_client config = {"project": "g/s/r", "access_token": "tkn"} with pytest.raises( Exception, match="Failed to load prompt 'gitlab::oops' from GitLab" ): GitLabPromptManager(config, prompt_id="oops").prompt_manager def test_gitlab_prompt_manager_config_validation_via_client_ctor(): """ If GitLabClient validates config in __init__, simulate that with a side_effect. Ensures manager surfaces the ValueError while building prompt_manager. """ with patch( "litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient", side_effect=ValueError("project and access_token are required"), ): with pytest.raises(ValueError, match="project and access_token are required"): GitLabPromptManager({}).prompt_manager # ----------------------------- # Message parsing # ----------------------------- @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_message_parsing(mock_client_class): mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 --- System: You are a helpful assistant. User: {{user_message}} Assistant: I'll help you with that.""" mock_client_class.return_value = mock_client config = {"project": "g/s/r", "access_token": "t"} manager = GitLabPromptManager(config, prompt_id="conversation_prompt") messages = manager._parse_prompt_to_messages( "System: You are a helpful assistant.\n\nUser: Hello!\n\nAssistant: Hi there!" ) assert len(messages) == 3 assert messages[0]["role"] == "system" assert messages[0]["content"] == "You are a helpful assistant." assert messages[1]["role"] == "user" assert messages[1]["content"] == "Hello!" assert messages[2]["role"] == "assistant" assert messages[2]["content"] == "Hi there!" # ----------------------------- # pre_call_hook behavior & ref precedence # ----------------------------- @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_pre_call_hook_updates_params(mock_client_class): mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4o temperature: 0.8 max_tokens: 256 --- System: You are a helpful assistant. User: {{user_message}}""" mock_client_class.return_value = mock_client config = {"project": "g/s/r", "access_token": "tkn"} manager = GitLabPromptManager(config, prompt_id="test_prompt") original_messages = [{"role": "user", "content": "This will be ignored"}] litellm_params = {"api_key": "keep-me"} result_messages, result_params = manager.pre_call_hook( user_id="u", messages=original_messages, litellm_params=litellm_params, prompt_id="test_prompt", prompt_variables={"user_message": "What is AI?"}, ) # Prompt parsed into messages assert len(result_messages) == 2 assert result_messages[0]["role"] == "system" assert result_messages[1]["role"] == "user" assert result_messages[1]["content"] == "What is AI?" # Params merged + preserved assert result_params["model"] == "gpt-4o" assert result_params["temperature"] == 0.8 assert result_params["max_tokens"] == 256 assert result_params["api_key"] == "keep-me" @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_pre_call_hook_ref_precedence(mock_client_class): """ Precedence for selecting git ref: prompt_version (arg) > git_ref kwarg > manager's _ref_override > client's default Validate that the chosen ref gets passed down to client.get_file_content. """ mock_client = MagicMock() # Return any minimal valid prompt; we just need the call path to succeed. mock_client.get_file_content.return_value = """--- model: gpt-4 --- User: {{q}}""" mock_client_class.return_value = mock_client config = {"project": "g/s/r", "access_token": "tkn"} # Set a manager-level default ref override manager = GitLabPromptManager(config, prompt_id=None, ref="manager-default") # 1) No prior load; call with prompt_version -> should win _msgs, _params = manager.pre_call_hook( user_id="u", messages=[], litellm_params={}, prompt_id="p1", prompt_variables={"q": "hello"}, prompt_version="explicit-sha", ) # get_file_content called with ref="explicit-sha" mock_client.get_file_content.assert_any_call("p1.prompt", ref="explicit-sha") # 2) Use git_ref kwarg (when no prompt_version) _msgs, _params = manager.pre_call_hook( user_id="u", messages=[], litellm_params={}, prompt_id="p2", prompt_variables={"q": "hello"}, git_ref="per-call-branch", ) mock_client.get_file_content.assert_any_call("p2.prompt", ref="per-call-branch") # 3) Neither prompt_version nor git_ref -> falls back to manager _ref_override _msgs, _params = manager.pre_call_hook( user_id="u", messages=[], litellm_params={}, prompt_id="p3", prompt_variables={"q": "hello"}, ) mock_client.get_file_content.assert_any_call("p3.prompt", ref="manager-default") # ----------------------------- # Listing & availability # ----------------------------- @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_list_templates_with_prompts_path(mock_client_class): mock_client = MagicMock() mock_client.list_files.return_value = [ "prompts/chat/a.prompt", "prompts/chat/sub/b.prompt", "prompts/chat/ignore.txt", ] mock_client.get_file_content.return_value = "Hello" mock_client_class.return_value = mock_client config = { "project": "g/s/r", "access_token": "tkn", "prompts_path": "prompts/chat", } manager = GitLabPromptManager(config, prompt_id="a") # list_templates strips folder prefix + extension ids = manager.get_available_prompts() assert "a" in ids assert "gitlab::sub::b" in ids assert all(not x.endswith(".prompt") for x in ids) assert all("/prompts/chat/" not in x for x in ids) @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_template_manager_load_all_prompts(mock_client_class): """load_all_prompts should fetch all .prompt files and populate the internal cache.""" mock_client = MagicMock() mock_client.list_files.return_value = [ "prompts/a.prompt", "prompts/sub/b.prompt", ] mock_client.get_file_content.side_effect = [ "Hello {{x}}", # for a.prompt "---\nmodel: gpt-4\n---\nUser: {{y}}", # for b.prompt with frontmatter ] mock_client_class.return_value = mock_client config = { "project": "g/s/r", "access_token": "tkn", "prompts_path": "prompts", } pm = GitLabPromptManager(config).prompt_manager loaded = pm.load_all_prompts() assert set(loaded) == {"gitlab::a", "gitlab::sub::b"} assert "gitlab::a" in pm.prompts and "gitlab::sub::b" in pm.prompts # ----------------------------- # post_call & integration name # ----------------------------- def test_gitlab_prompt_manager_integration_name(): config = {"project": "g/s/r", "access_token": "tkn"} manager = GitLabPromptManager(config) assert manager.integration_name == "gitlab" @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_manager_post_call_hook_passthrough(mock_client_class): mock_client = MagicMock() mock_client.get_file_content.return_value = "User: {{m}}" mock_client_class.return_value = mock_client config = {"project": "g/s/r", "access_token": "tkn"} manager = GitLabPromptManager(config, prompt_id="p") dummy_response = MagicMock() out = manager.post_call_hook( user_id="u", response=dummy_response, input_messages=[{"role": "user", "content": "x"}], litellm_params={}, prompt_id="p", ) assert out is dummy_response @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_version_precedence_prompt_version_wins(mock_client_class): """ prompt_version > git_ref kwarg > manager _ref_override. Ensure prompt_version wins and is passed down to GitLabClient.get_file_content. """ mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 --- User: {{q}}""" mock_client_class.return_value = mock_client cfg = {"project": "g/s/r", "access_token": "tkn"} # Manager with a default override ref mgr = GitLabPromptManager(cfg, ref="manager-default") # Provide both git_ref kwarg and prompt_version, the latter should win msgs, params = mgr.pre_call_hook( user_id="u", messages=[], litellm_params={}, prompt_id="promptA", prompt_variables={"q": "hello"}, prompt_version="sha-111", # highest precedence git_ref="feature/branch-xyz", # should be ignored because prompt_version provided ) mock_client.get_file_content.assert_any_call("promptA.prompt", ref="sha-111") # sanity — prompt parsed and params returned assert any(m["role"] == "user" for m in msgs) assert params.get("model") == "gpt-4" @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_version_ref_kwarg_used_when_no_prompt_version(mock_client_class): """ If prompt_version is omitted, git_ref kwarg should be used. """ mock_client = MagicMock() mock_client.get_file_content.return_value = "User: {{q}}" mock_client_class.return_value = mock_client cfg = {"project": "g/s/r", "access_token": "tkn"} mgr = GitLabPromptManager(cfg, ref="fallback-manager-ref") _msgs, _params = mgr.pre_call_hook( user_id="u", messages=[], litellm_params={}, prompt_id="promptB", prompt_variables={"q": "hi"}, git_ref="hotfix/ref-2", # used since prompt_version not provided ) mock_client.get_file_content.assert_any_call("promptB.prompt", ref="hotfix/ref-2") @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_version_manager_override_used_when_no_prompt_version_or_kwarg( mock_client_class, ): """ If neither prompt_version nor git_ref is supplied, fall back to manager-level ref override. """ mock_client = MagicMock() mock_client.get_file_content.return_value = "User: {{q}}" mock_client_class.return_value = mock_client cfg = {"project": "g/s/r", "access_token": "tkn"} mgr = GitLabPromptManager(cfg, ref="manager-override-ref") _msgs, _params = mgr.pre_call_hook( user_id="u", messages=[], litellm_params={}, prompt_id="promptC", prompt_variables={"q": "hey"}, ) mock_client.get_file_content.assert_any_call( "promptC.prompt", ref="manager-override-ref" ) @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_get_prompt_template_explicit_ref_param(mock_client_class): """ Directly calling get_prompt_template(ref=...) should pass that ref to GitLabClient. """ mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4o --- User: {{x}}""" mock_client_class.return_value = mock_client cfg = {"project": "g/s/r", "access_token": "tkn"} mgr = GitLabPromptManager(cfg) rendered, metadata = mgr.get_prompt_template( prompt_id="promptD", prompt_variables={"x": "value"}, ref="v1.2.3", # explicit tag ) mock_client.get_file_content.assert_any_call("promptD.prompt", ref="v1.2.3") assert "value" in rendered assert metadata.get("model") == "gpt-4o" @patch("litellm.integrations.gitlab.gitlab_prompt_manager.GitLabClient") def test_gitlab_prompt_version_with_prompts_path(mock_client_class): """ Ensure prompts_path + prompt_version work together (path resolution + ref). """ mock_client = MagicMock() mock_client.get_file_content.return_value = "User: {{q}}" mock_client_class.return_value = mock_client cfg = { "project": "g/s/r", "access_token": "tkn", "prompts_path": "prompts/chat", } mgr = GitLabPromptManager(cfg) _msgs, _params = mgr.pre_call_hook( user_id="u", messages=[], litellm_params={}, prompt_id="folder/sub/my_prompt", prompt_variables={"q": "ok"}, prompt_version="commit-sha-999", ) # Path should include prompts_path and end with .prompt mock_client.get_file_content.assert_any_call( "prompts/chat/folder/sub/my_prompt.prompt", ref="commit-sha-999" )