molecule-core/scripts/bridge/test_bridge.py
Hongming Wang 24fec62d7f initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1)
with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo.

Brand: Starfire → Molecule AI.
Slug: starfire / agent-molecule → molecule.
Env vars: STARFIRE_* → MOLECULE_*.
Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform.
Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent.
DB: agentmolecule → molecule.

History truncated; see public repo for prior commits and contributor
attribution. Verified green: go test -race ./... (platform), pytest
(workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:55:37 -07:00

216 lines
6.7 KiB
Python

"""Unit tests for the external workspace bridge."""
import json
import io
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# Add scripts/ to path so bridge package is importable
sys.path.insert(0, str(Path(__file__).parent.parent))
from bridge.processor import (
MessageProcessor,
ClaudeCodeProcessor,
OpenAIProcessor,
AnthropicProcessor,
HTTPForwardProcessor,
EchoProcessor,
PROCESSORS,
create_processor,
)
# ─── Processor registry ───
class TestProcessorRegistry:
def test_all_processors_registered(self):
expected = {"claude-code", "openai", "anthropic", "http", "echo"}
assert set(PROCESSORS.keys()) == expected
def test_create_processor_valid(self):
p = create_processor("echo")
assert isinstance(p, EchoProcessor)
def test_create_processor_invalid(self):
with pytest.raises(ValueError, match="Unknown processor"):
create_processor("nonexistent")
def test_create_processor_with_kwargs(self):
p = create_processor("openai", model="gpt-4o", api_key="test-key")
assert p.model == "gpt-4o"
assert p.api_key == "test-key"
# ─── EchoProcessor ───
class TestEchoProcessor:
def test_echoes_message(self):
p = EchoProcessor()
result = p.process("hello", "PM", {})
assert result == "Echo from bridge: hello"
def test_name(self):
assert EchoProcessor.name == "echo"
def test_empty_message(self):
p = EchoProcessor()
result = p.process("", "PM", {})
assert result == "Echo from bridge: "
# ─── OpenAIProcessor ───
class TestOpenAIProcessor:
def test_missing_api_key_returns_error(self):
p = OpenAIProcessor(api_key="")
result = p.process("hello", "PM", {})
assert "not configured" in result
def test_init_reads_env(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-123")
p = OpenAIProcessor()
assert p.api_key == "sk-test-123"
def test_init_explicit_key_overrides_env(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "from-env")
p = OpenAIProcessor(api_key="explicit")
assert p.api_key == "explicit"
@patch("httpx.post")
def test_successful_response(self, mock_post):
mock_resp = MagicMock()
mock_resp.json.return_value = {
"choices": [{"message": {"content": "Hello from GPT"}}]
}
mock_post.return_value = mock_resp
p = OpenAIProcessor(api_key="test-key")
result = p.process("hi", "PM", {})
assert result == "Hello from GPT"
@patch("httpx.post", side_effect=Exception("connection refused"))
def test_api_error(self, mock_post):
p = OpenAIProcessor(api_key="test-key")
result = p.process("hi", "PM", {})
assert "OpenAI API error" in result
# ─── AnthropicProcessor ───
class TestAnthropicProcessor:
def test_missing_api_key_returns_error(self):
p = AnthropicProcessor(api_key="")
result = p.process("hello", "PM", {})
assert "not configured" in result
def test_init_reads_env(self, monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")
p = AnthropicProcessor()
assert p.api_key == "sk-ant-test"
@patch("httpx.post")
def test_successful_response(self, mock_post):
mock_resp = MagicMock()
mock_resp.json.return_value = {
"content": [{"text": "Hello from Claude"}]
}
mock_post.return_value = mock_resp
p = AnthropicProcessor(api_key="test-key")
result = p.process("hi", "PM", {})
assert result == "Hello from Claude"
# ─── HTTPForwardProcessor ───
class TestHTTPForwardProcessor:
def test_no_url_returns_error(self):
p = HTTPForwardProcessor(url="")
result = p.process("hi", "PM", {})
assert "not configured" in result
@patch("httpx.post")
def test_forwards_message(self, mock_post):
mock_resp = MagicMock()
mock_resp.text = "forwarded response"
mock_post.return_value = mock_resp
p = HTTPForwardProcessor(url="http://my-agent:8000")
result = p.process("hello", "PM", {"sender_id": "ws-1"})
assert result == "forwarded response"
mock_post.assert_called_once()
call_kwargs = mock_post.call_args
assert call_kwargs[1]["json"]["message"] == "hello"
assert call_kwargs[1]["json"]["sender"] == "PM"
# ─── ClaudeCodeProcessor ───
class TestClaudeCodeProcessor:
def test_name(self):
assert ClaudeCodeProcessor.name == "claude-code"
@patch("subprocess.run")
def test_successful_response(self, mock_run):
mock_run.return_value = MagicMock(
returncode=0, stdout="The answer is 42", stderr=""
)
p = ClaudeCodeProcessor(cwd="/tmp")
result = p.process("what is the answer?", "PM", {})
assert result == "The answer is 42"
@patch("subprocess.run")
def test_json_output_parsed(self, mock_run):
mock_run.return_value = MagicMock(
returncode=0,
stdout=json.dumps({"result": "parsed answer", "session_id": "abc"}),
stderr="",
)
p = ClaudeCodeProcessor(cwd="/tmp")
result = p.process("test", "PM", {})
assert result == "parsed answer"
@patch("subprocess.run")
def test_error_exit_code(self, mock_run):
mock_run.return_value = MagicMock(
returncode=1, stdout="", stderr="auth error"
)
p = ClaudeCodeProcessor(cwd="/tmp")
result = p.process("test", "PM", {})
assert "auth error" in result
@patch("subprocess.run", side_effect=FileNotFoundError)
def test_claude_not_installed(self, mock_run):
p = ClaudeCodeProcessor(cwd="/tmp")
result = p.process("test", "PM", {})
assert "not found" in result.lower()
def test_model_flag(self):
p = ClaudeCodeProcessor(model="opus")
assert p.model == "opus"
# ─── MessageProcessor interface ───
class TestMessageProcessorInterface:
def test_cannot_instantiate_abstract(self):
with pytest.raises(TypeError):
MessageProcessor()
def test_subclass_must_implement_process(self):
class Incomplete(MessageProcessor):
name = "incomplete"
with pytest.raises(TypeError):
Incomplete()
def test_valid_subclass(self):
class Valid(MessageProcessor):
name = "valid"
def process(self, message, sender, context):
return "ok"
v = Valid()
assert v.process("x", "y", {}) == "ok"