Files
skill-seekers-reference/tests/test_adaptors/test_minimax_adaptor.py
yusyus 4f87de6b56 fix: improve MiniMax adaptor from PR #318 review (#319)
* feat: add MiniMax AI as LLM platform adaptor

Original implementation by octo-patch in PR #318.
This commit includes comprehensive improvements and documentation.

Code Improvements:
- Fix API key validation to properly check JWT format (eyJ prefix)
- Add specific exception handling for timeout and connection errors
- Remove unused variable in upload method

Dependencies:
- Add MiniMax to [all-llms] extra group in pyproject.toml

Tests:
- Remove duplicate setUp method in integration test class
- Add 4 new test methods:
  * test_package_excludes_backup_files
  * test_upload_success_mocked (with OpenAI mocking)
  * test_upload_network_error
  * test_upload_connection_error
  * test_validate_api_key_jwt_format
- Update test_validate_api_key_valid to use JWT format keys
- Fix test assertions for error message matching

Documentation:
- Create comprehensive MINIMAX_INTEGRATION.md guide (380+ lines)
- Update MULTI_LLM_SUPPORT.md with MiniMax platform entry
- Update 01-installation.md extras table
- Update INTEGRATIONS.md AI platforms table
- Update AGENTS.md adaptor import pattern example
- Fix README.md platform count from 4 to 5

All tests pass (33 passed, 3 skipped)
Lint checks pass

Co-authored-by: octo-patch <octo-patch@users.noreply.github.com>

* fix: improve MiniMax adaptor — typed exceptions, key validation, tests, docs

- Remove invalid "minimax" self-reference from all-llms dependency group
- Use typed OpenAI exceptions (APITimeoutError, APIConnectionError)
  instead of string-matching on generic Exception
- Replace incorrect JWT assumption in validate_api_key with length check
- Use DEFAULT_API_ENDPOINT constant instead of hardcoded URLs (3 sites)
- Add Path() cast for output_path before .is_dir() call
- Add sys.modules mock to test_enhance_missing_library
- Add mocked test_enhance_success with backup/content verification
- Update test assertions for new exception types and key validation
- Add MiniMax to __init__.py docstrings (module, get_adaptor, list_platforms)
- Add MiniMax sections to MULTI_LLM_SUPPORT.md (install, format, API key,
  workflow example, export-to-all)

Follows up on PR #318 by @octo-patch (feat: add MiniMax AI as LLM platform adaptor).

Co-Authored-By: Octopus <octo-patch@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: octo-patch <octo-patch@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:12:23 +03:00

518 lines
21 KiB
Python

#!/usr/bin/env python3
"""
Tests for MiniMax AI adaptor
"""
import json
import os
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest.mock import patch, MagicMock
try:
from openai import APITimeoutError, APIConnectionError
except ImportError:
APITimeoutError = None
APIConnectionError = None
from skill_seekers.cli.adaptors import get_adaptor, is_platform_available
from skill_seekers.cli.adaptors.base import SkillMetadata
class TestMiniMaxAdaptor(unittest.TestCase):
"""Test MiniMax AI adaptor functionality"""
def setUp(self):
"""Set up test adaptor"""
self.adaptor = get_adaptor("minimax")
def test_platform_info(self):
"""Test platform identifiers"""
self.assertEqual(self.adaptor.PLATFORM, "minimax")
self.assertEqual(self.adaptor.PLATFORM_NAME, "MiniMax AI")
self.assertIsNotNone(self.adaptor.DEFAULT_API_ENDPOINT)
self.assertIn("minimax", self.adaptor.DEFAULT_API_ENDPOINT)
def test_platform_available(self):
"""Test that minimax platform is registered"""
self.assertTrue(is_platform_available("minimax"))
def test_validate_api_key_valid(self):
"""Test valid MiniMax API keys (any string >10 chars)"""
self.assertTrue(
self.adaptor.validate_api_key("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test.key")
)
self.assertTrue(self.adaptor.validate_api_key("sk-some-long-api-key-string-here"))
self.assertTrue(self.adaptor.validate_api_key(" a-valid-key-with-spaces "))
def test_validate_api_key_invalid(self):
"""Test invalid API keys"""
self.assertFalse(self.adaptor.validate_api_key(""))
self.assertFalse(self.adaptor.validate_api_key(" "))
self.assertFalse(self.adaptor.validate_api_key("short"))
def test_get_env_var_name(self):
"""Test environment variable name"""
self.assertEqual(self.adaptor.get_env_var_name(), "MINIMAX_API_KEY")
def test_supports_enhancement(self):
"""Test enhancement support"""
self.assertTrue(self.adaptor.supports_enhancement())
def test_format_skill_md_no_frontmatter(self):
"""Test that MiniMax format has no YAML frontmatter"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir)
(skill_dir / "references").mkdir()
(skill_dir / "references" / "test.md").write_text("# Test content")
metadata = SkillMetadata(name="test-skill", description="Test skill description")
formatted = self.adaptor.format_skill_md(skill_dir, metadata)
self.assertFalse(formatted.startswith("---"))
self.assertIn("You are an expert assistant", formatted)
self.assertIn("test-skill", formatted)
self.assertIn("Test skill description", formatted)
def test_format_skill_md_with_existing_content(self):
"""Test formatting when SKILL.md already has substantial content"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir)
(skill_dir / "references").mkdir()
existing_content = "# Existing Content\n\n" + "x" * 200
(skill_dir / "SKILL.md").write_text(existing_content)
metadata = SkillMetadata(name="test-skill", description="Test description")
formatted = self.adaptor.format_skill_md(skill_dir, metadata)
self.assertIn("You are an expert assistant", formatted)
self.assertIn("test-skill", formatted)
def test_format_skill_md_without_references(self):
"""Test formatting without references directory"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir)
metadata = SkillMetadata(name="test-skill", description="Test description")
formatted = self.adaptor.format_skill_md(skill_dir, metadata)
self.assertIn("You are an expert assistant", formatted)
self.assertIn("test-skill", formatted)
def test_package_creates_zip(self):
"""Test that package creates ZIP file with correct structure"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("You are an expert assistant")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "test.md").write_text("# Reference")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
self.assertTrue(package_path.exists())
self.assertTrue(str(package_path).endswith(".zip"))
self.assertIn("minimax", package_path.name)
with zipfile.ZipFile(package_path, "r") as zf:
names = zf.namelist()
self.assertIn("system_instructions.txt", names)
self.assertIn("minimax_metadata.json", names)
self.assertTrue(any("knowledge_files" in name for name in names))
def test_package_metadata_content(self):
"""Test that packaged ZIP contains correct metadata"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("Test instructions")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "guide.md").write_text("# User Guide")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
with zipfile.ZipFile(package_path, "r") as zf:
instructions = zf.read("system_instructions.txt").decode("utf-8")
self.assertEqual(instructions, "Test instructions")
self.assertIn("knowledge_files/guide.md", zf.namelist())
metadata_content = zf.read("minimax_metadata.json").decode("utf-8")
metadata = json.loads(metadata_content)
self.assertEqual(metadata["platform"], "minimax")
self.assertEqual(metadata["name"], "test-skill")
self.assertEqual(metadata["model"], "MiniMax-M2.7")
self.assertIn("minimax", metadata["api_base"])
def test_package_output_path_as_file(self):
"""Test packaging when output_path is a file path"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("Test")
output_file = Path(temp_dir) / "output" / "custom-name-minimax.zip"
output_file.parent.mkdir(parents=True, exist_ok=True)
package_path = self.adaptor.package(skill_dir, output_file)
self.assertTrue(package_path.exists())
self.assertTrue(str(package_path).endswith(".zip"))
def test_package_without_references(self):
"""Test packaging without reference files"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("Test instructions")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
self.assertTrue(package_path.exists())
with zipfile.ZipFile(package_path, "r") as zf:
names = zf.namelist()
self.assertIn("system_instructions.txt", names)
self.assertIn("minimax_metadata.json", names)
self.assertFalse(any("knowledge_files" in name for name in names))
def test_upload_missing_library(self):
"""Test upload when openai library is not installed"""
with tempfile.NamedTemporaryFile(suffix=".zip") as tmp:
with patch.dict(sys.modules, {"openai": None}):
result = self.adaptor.upload(Path(tmp.name), "test-api-key")
self.assertFalse(result["success"])
self.assertIn("openai", result["message"])
self.assertIn("not installed", result["message"])
def test_upload_invalid_file(self):
"""Test upload with invalid file"""
result = self.adaptor.upload(Path("/nonexistent/file.zip"), "test-api-key")
self.assertFalse(result["success"])
self.assertIn("not found", result["message"].lower())
def test_upload_wrong_format(self):
"""Test upload with wrong file format"""
with tempfile.NamedTemporaryFile(suffix=".tar.gz") as tmp:
result = self.adaptor.upload(Path(tmp.name), "test-api-key")
self.assertFalse(result["success"])
self.assertIn("not a zip", result["message"].lower())
@unittest.skip("covered by test_upload_success_mocked")
def test_upload_success(self):
"""Test successful upload - skipped (needs real API for integration test)"""
pass
def test_enhance_missing_references(self):
"""Test enhance when no reference files exist"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir)
success = self.adaptor.enhance(skill_dir, "test-api-key")
self.assertFalse(success)
@patch("openai.OpenAI")
def test_enhance_success_mocked(self, mock_openai_class):
"""Test successful enhancement with mocked OpenAI client"""
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Enhanced SKILL.md content"
mock_client.chat.completions.create.return_value = mock_response
mock_openai_class.return_value = mock_client
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir)
refs_dir = skill_dir / "references"
refs_dir.mkdir()
(refs_dir / "test.md").write_text("# Test\nContent")
(skill_dir / "SKILL.md").write_text("Original content")
success = self.adaptor.enhance(skill_dir, "test-api-key")
self.assertTrue(success)
new_content = (skill_dir / "SKILL.md").read_text()
self.assertEqual(new_content, "Enhanced SKILL.md content")
backup = skill_dir / "SKILL.md.backup"
self.assertTrue(backup.exists())
self.assertEqual(backup.read_text(), "Original content")
mock_client.chat.completions.create.assert_called_once()
def test_enhance_missing_library(self):
"""Test enhance when openai library is not installed"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir)
refs_dir = skill_dir / "references"
refs_dir.mkdir()
(refs_dir / "test.md").write_text("Test content")
with patch.dict(sys.modules, {"openai": None}):
success = self.adaptor.enhance(skill_dir, "test-api-key")
self.assertFalse(success)
def test_read_reference_files(self):
"""Test reading reference files"""
with tempfile.TemporaryDirectory() as temp_dir:
refs_dir = Path(temp_dir)
(refs_dir / "guide.md").write_text("# Guide\nContent here")
(refs_dir / "api.md").write_text("# API\nAPI docs")
references = self.adaptor._read_reference_files(refs_dir)
self.assertEqual(len(references), 2)
self.assertIn("guide.md", references)
self.assertIn("api.md", references)
def test_read_reference_files_empty_dir(self):
"""Test reading from empty references directory"""
with tempfile.TemporaryDirectory() as temp_dir:
references = self.adaptor._read_reference_files(Path(temp_dir))
self.assertEqual(len(references), 0)
def test_read_reference_files_nonexistent(self):
"""Test reading from nonexistent directory"""
references = self.adaptor._read_reference_files(Path("/nonexistent/path"))
self.assertEqual(len(references), 0)
def test_read_reference_files_truncation(self):
"""Test that large reference files are truncated"""
with tempfile.TemporaryDirectory() as temp_dir:
(Path(temp_dir) / "large.md").write_text("x" * 50000)
references = self.adaptor._read_reference_files(Path(temp_dir))
self.assertIn("large.md", references)
self.assertIn("truncated", references["large.md"])
self.assertLessEqual(len(references["large.md"]), 31000)
def test_build_enhancement_prompt(self):
"""Test enhancement prompt generation"""
references = {
"guide.md": "# User Guide\nContent here",
"api.md": "# API Reference\nAPI docs",
}
prompt = self.adaptor._build_enhancement_prompt(
"test-skill", references, "Existing SKILL.md content"
)
self.assertIn("test-skill", prompt)
self.assertIn("guide.md", prompt)
self.assertIn("api.md", prompt)
self.assertIn("Existing SKILL.md content", prompt)
self.assertIn("MiniMax", prompt)
def test_build_enhancement_prompt_no_existing(self):
"""Test enhancement prompt when no existing SKILL.md"""
references = {"test.md": "# Test\nContent"}
prompt = self.adaptor._build_enhancement_prompt("test-skill", references, None)
self.assertIn("test-skill", prompt)
self.assertIn("create from scratch", prompt)
def test_config_initialization(self):
"""Test adaptor initializes with config"""
config = {"custom_model": "MiniMax-M2.5"}
adaptor = get_adaptor("minimax", config)
self.assertEqual(adaptor.config, config)
def test_default_config(self):
"""Test adaptor initializes with empty config by default"""
self.assertEqual(self.adaptor.config, {})
def test_package_excludes_backup_files(self):
"""Test that backup files are excluded from package"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("Test instructions")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "guide.md").write_text("# Guide")
(skill_dir / "references" / "guide.md.backup").write_text("# Old backup")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
with zipfile.ZipFile(package_path, "r") as zf:
names = zf.namelist()
self.assertIn("knowledge_files/guide.md", names)
self.assertNotIn("knowledge_files/guide.md.backup", names)
@patch("openai.OpenAI")
def test_upload_success_mocked(self, mock_openai_class):
"""Test successful upload with mocked OpenAI client"""
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Ready to assist with Python testing"
mock_client.chat.completions.create.return_value = mock_response
mock_openai_class.return_value = mock_client
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("You are an expert assistant")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "test.md").write_text("# Test")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
result = self.adaptor.upload(package_path, "test-long-api-key-string")
self.assertTrue(result["success"])
self.assertIn("validated", result["message"])
self.assertEqual(result["url"], "https://platform.minimaxi.com/")
mock_client.chat.completions.create.assert_called_once()
@unittest.skipUnless(APITimeoutError, "openai library not installed")
@patch("openai.OpenAI")
def test_upload_network_error(self, mock_openai_class):
"""Test upload with network timeout error"""
mock_client = MagicMock()
mock_client.chat.completions.create.side_effect = APITimeoutError(request=MagicMock())
mock_openai_class.return_value = mock_client
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("Test")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "test.md").write_text("Content")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
result = self.adaptor.upload(package_path, "test-long-api-key-string")
self.assertFalse(result["success"])
self.assertIn("timed out", result["message"].lower())
@unittest.skipUnless(APIConnectionError, "openai library not installed")
@patch("openai.OpenAI")
def test_upload_connection_error(self, mock_openai_class):
"""Test upload with connection error"""
mock_client = MagicMock()
mock_client.chat.completions.create.side_effect = APIConnectionError(request=MagicMock())
mock_openai_class.return_value = mock_client
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("Test")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "test.md").write_text("Content")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
result = self.adaptor.upload(package_path, "test-long-api-key-string")
self.assertFalse(result["success"])
self.assertIn("connection", result["message"].lower())
def test_validate_api_key_format(self):
"""Test that API key validation uses length-based check"""
# Valid - long enough strings
self.assertTrue(self.adaptor.validate_api_key("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test"))
self.assertTrue(self.adaptor.validate_api_key("sk-api-abc123-long-enough"))
# Invalid - too short
self.assertFalse(self.adaptor.validate_api_key("eyJshort"))
self.assertFalse(self.adaptor.validate_api_key("short"))
class TestMiniMaxAdaptorIntegration(unittest.TestCase):
"""Integration tests for MiniMax AI adaptor (require MINIMAX_API_KEY)"""
def setUp(self):
"""Set up test adaptor"""
self.adaptor = get_adaptor("minimax")
@unittest.skipUnless(
os.getenv("MINIMAX_API_KEY"), "MINIMAX_API_KEY not set - skipping integration test"
)
def test_enhance_with_real_api(self):
"""Test enhancement with real MiniMax API"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir)
refs_dir = skill_dir / "references"
refs_dir.mkdir()
(refs_dir / "test.md").write_text(
"# Python Testing\n\n"
"Use pytest for testing:\n"
"```python\n"
"def test_example():\n"
" assert 1 + 1 == 2\n"
"```\n"
)
api_key = os.getenv("MINIMAX_API_KEY")
success = self.adaptor.enhance(skill_dir, api_key)
self.assertTrue(success)
skill_md = (skill_dir / "SKILL.md").read_text()
self.assertTrue(len(skill_md) > 100)
@unittest.skipUnless(
os.getenv("MINIMAX_API_KEY"), "MINIMAX_API_KEY not set - skipping integration test"
)
def test_upload_with_real_api(self):
"""Test upload validation with real MiniMax API"""
with tempfile.TemporaryDirectory() as temp_dir:
skill_dir = Path(temp_dir) / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("You are an expert assistant for Python testing.")
(skill_dir / "references").mkdir()
(skill_dir / "references" / "test.md").write_text("# Test\nContent")
output_dir = Path(temp_dir) / "output"
output_dir.mkdir()
package_path = self.adaptor.package(skill_dir, output_dir)
api_key = os.getenv("MINIMAX_API_KEY")
result = self.adaptor.upload(package_path, api_key)
self.assertTrue(result["success"])
self.assertIn("validated", result["message"])
@unittest.skipUnless(
os.getenv("MINIMAX_API_KEY"), "MINIMAX_API_KEY not set - skipping integration test"
)
def test_validate_api_key_real(self):
"""Test validating a real API key"""
api_key = os.getenv("MINIMAX_API_KEY")
self.assertTrue(self.adaptor.validate_api_key(api_key))
if __name__ == "__main__":
unittest.main()