from __future__ import annotations from types import SimpleNamespace import pytest from investibook.core import agent_runtime as runtime_module from investibook.core import acp as legacy_acp_module from investibook.core.agent_runtime import ( AgentRuntimeDefinition, AgentEventBridge, AgentSession, get_agent_runtime_definition, list_agent_runtime_types, normalize_agent_type, register_agent_runtime, _resolve_agent_binary, _pi_model_parts, _pi_prompt_payload_from_blocks, ) def test_pi_model_parts_parse_provider_model_and_thinking_level(): assert _pi_model_parts("openai/gpt-5.2:high") == ( "openai", "gpt-5.2", "high", ) assert _pi_model_parts("anthropic/claude-sonnet-4-20250514") == ( "anthropic", "claude-sonnet-4-20250514", None, ) assert _pi_model_parts("chat/standard") == ("chat", "standard", None) assert _pi_model_parts("gpt-5.2") is None def test_normalize_agent_type_accepts_dynamic_runtimes(): assert normalize_agent_type("opencode") == "opencode" assert normalize_agent_type("pi") == "pi" assert normalize_agent_type("claude") == "claude" with pytest.raises(ValueError): normalize_agent_type("unknown") def test_pi_runtime_uses_thin_core_host_instead_of_full_cli(): definition = get_agent_runtime_definition("pi") assert definition.binary_names == ("pi-core",) assert definition.command_args == () assert "pi-coding-agent" not in " ".join(definition.command_args) def test_pi_runtime_falls_back_to_source_for_local_development(monkeypatch): monkeypatch.delenv("ACP_AGENT_BIN", raising=False) monkeypatch.setattr( runtime_module.shutil, "which", lambda name: "/usr/local/bin/bun" if name == "bun" else None, ) command, label = _resolve_agent_binary("pi") assert command[0] == "/usr/local/bin/bun" assert command[1].endswith("agentkit/runtime/pi-core/index.mjs") assert label == "Pi Core" def test_pi_runtime_prefers_standalone_binary_over_source_runtime(monkeypatch): monkeypatch.delenv("ACP_AGENT_BIN", raising=False) binaries = { "pi-core": "/usr/local/bin/pi-core", "bun": "/usr/local/bin/bun", } monkeypatch.setattr(runtime_module.shutil, "which", binaries.get) command, label = _resolve_agent_binary("pi") assert command == ["/usr/local/bin/pi-core"] assert label == "Pi Core" def test_runtime_registry_supports_future_adapters(): definition = AgentRuntimeDefinition( name="codex-test", label="Codex Test", transport="acp", binary_names=("codex-test",), standard_model_setting="CODEX_MODEL_STANDARD", pro_model_setting="CODEX_MODEL_PRO", ) register_agent_runtime(definition) try: assert normalize_agent_type("CODEX-TEST") == "codex-test" assert "codex-test" in list_agent_runtime_types() assert get_agent_runtime_definition("codex-test") == definition finally: runtime_module._runtime_definitions.pop("codex-test", None) def test_legacy_acp_module_exports_runtime_aliases(): assert legacy_acp_module.ACPSession is AgentSession assert legacy_acp_module.WebACPClient is AgentEventBridge assert legacy_acp_module.get_acp_session is runtime_module.get_agent_session def test_pi_prompt_payload_from_blocks_converts_text_and_images(): message, images = _pi_prompt_payload_from_blocks( [ {"text": "hello"}, SimpleNamespace(data="base64", mime_type="image/png"), ] ) assert message == "hello" assert images == [{"type": "image", "data": "base64", "mimeType": "image/png"}] def test_shared_model_tier_is_adapted_for_pi(monkeypatch, tmp_path): provider_config = tmp_path / "opencode.jsonc" provider_config.write_text( """{ "provider": { "chat": { "npm": "@ai-sdk/amazon-bedrock", "models": { "standard": {"id": "bedrock-profile", "variants": {"high": {}}} } } } }""" ) monkeypatch.setenv("AGENT_PROVIDER_CONFIG", str(provider_config)) monkeypatch.setattr(runtime_module.cfg, "AGENT_MODEL_STANDARD", "chat/standard/high") monkeypatch.setattr(runtime_module.cfg, "PI_MODEL_STANDARD", "") assert runtime_module._resolve_model_for_tier("standard", "pi") == ( "amazon-bedrock/bedrock-profile:high" ) @pytest.mark.asyncio async def test_web_client_pi_emitters_use_existing_sse_shapes(): client = AgentEventBridge(workspace_id="ws-pi") await client.emit_text_delta("hi", event_source="pi_text_delta") await client.emit_thought_delta("thinking", event_source="pi_thinking_delta") await client.emit_tool_start( tool_id="call-1", title="bash", raw_input={"command": "pwd"}, session_id="pi-session", event_source="pi_tool_start", ) await client.emit_tool_progress( tool_id="call-1", title="bash", raw_input={"command": "pwd"}, raw_output={"content": "/tmp\n"}, status="completed", session_id="pi-session", event_source="pi_tool_end", ) assert await client.queue.get() == {"type": "text", "text": "hi"} assert await client.queue.get() == {"type": "thought", "text": "thinking"} start = await client.queue.get() assert start["type"] == "tool" assert start["id"] == "call-1" assert start["status"] == "running" done = await client.queue.get() assert done["type"] == "tool" assert done["id"] == "call-1" assert done["status"] == "completed" assert done["output"] == {"content": "/tmp\n"}