refactor(agents): delete deliverable dead twins in shared/tools; fix live image api_base bug

The deliverables subagent runs its own generate_image/podcast/report/resume/
video_presentation (via tools/index.py); the shared/tools copies had zero
production importers — classic dead twins. Removed them so deliverable tools
live only in their vertical slice.

While repointing the 2 stranded unit tests at the LIVE deliverables modules,
found the OpenRouter empty-api_base defense (resolve_api_base) existed ONLY in
the dead shared generate_image, never propagated to the live multi-agent copy.
Ported the fix into deliverables/tools/generate_image.py (both the global-config
and user-DB-config branches) so an empty api_base no longer falls through to
LiteLLM's global api_base (Azure) and 404s.

Tests now exercise the live Command/receipt-returning tools (invoke the raw
coroutine with a hand-built ToolRuntime; resume progress events neutralized).
This commit is contained in:
CREDO23 2026-06-04 20:30:30 +02:00
parent 64512c604d
commit 8d0090c6a1
10 changed files with 104 additions and 2519 deletions

View file

@ -1,17 +1,58 @@
"""Unit tests for resume page-limit helpers and enforcement flow."""
"""Unit tests for resume page-limit helpers and enforcement flow.
Targets the live deliverables resume tool. The tool returns a
``Command`` (payload JSON-encoded in ``update["messages"][0].content``
plus a receipt), so flow tests invoke it via a ToolCall dict and unwrap
the payload.
"""
import io
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pypdf
import pytest
from langchain.tools import ToolRuntime
from app.agents.shared.tools import resume as resume_tool
from app.agents.multi_agent_chat.subagents.builtins.deliverables.tools import (
resume as resume_tool,
)
pytestmark = pytest.mark.unit
@pytest.fixture(autouse=True)
def _silence_progress_events(monkeypatch):
"""The live tool emits ``dispatch_custom_event`` progress updates that
require a langgraph run context; neutralize them for direct unit calls."""
monkeypatch.setattr(resume_tool, "dispatch_custom_event", lambda *a, **k: None)
def _runtime(tool_call_id: str = "call-1") -> ToolRuntime:
"""Minimal ToolRuntime; the resume tool only reads ``tool_call_id``."""
return ToolRuntime(
state={},
context=None,
config={},
stream_writer=None,
tool_call_id=tool_call_id,
store=None,
)
async def _invoke(tool, args: dict) -> dict:
"""Drive a Command-returning tool and return its decoded payload.
These tools take an injected ``ToolRuntime`` and return a
``Command``; invoke the raw coroutine with a hand-built runtime
(the repo's pattern for unit-testing such tools) and decode the
ToolMessage payload.
"""
command = await tool.coroutine(runtime=_runtime(), **args)
return json.loads(command.update["messages"][0].content)
class _FakeReport:
_next_id = 1000
@ -108,7 +149,7 @@ async def test_generate_resume_defaults_to_one_page_target(monkeypatch) -> None:
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: 1)
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
result = await tool.ainvoke({"user_info": "Jane Doe experience"})
result = await _invoke(tool, {"user_info": "Jane Doe experience"})
assert result["status"] == "ready"
assert prompts
@ -138,7 +179,7 @@ async def test_generate_resume_compresses_when_over_limit(monkeypatch) -> None:
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: next(page_counts))
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
result = await tool.ainvoke({"user_info": "Jane Doe experience", "max_pages": 1})
result = await _invoke(tool, {"user_info": "Jane Doe experience", "max_pages": 1})
assert result["status"] == "ready"
assert write_session.added, "Expected successful report write"
@ -173,7 +214,7 @@ async def test_generate_resume_returns_ready_when_target_not_met(monkeypatch) ->
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: next(page_counts))
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
result = await tool.ainvoke({"user_info": "Jane Doe experience", "max_pages": 1})
result = await _invoke(tool, {"user_info": "Jane Doe experience", "max_pages": 1})
assert result["status"] == "ready"
assert "could not fit the target" in (result["message"] or "").lower()
@ -206,7 +247,7 @@ async def test_generate_resume_fails_when_hard_limit_exceeded(monkeypatch) -> No
monkeypatch.setattr(resume_tool, "_count_pdf_pages", lambda _pdf: next(page_counts))
tool = resume_tool.create_generate_resume_tool(search_space_id=1, thread_id=1)
result = await tool.ainvoke({"user_info": "Jane Doe experience", "max_pages": 1})
result = await _invoke(tool, {"user_info": "Jane Doe experience", "max_pages": 1})
assert result["status"] == "failed"
assert "hard page limit" in (result["error"] or "").lower()

View file

@ -20,6 +20,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from langchain.tools import ToolRuntime
pytestmark = pytest.mark.unit
@ -90,7 +91,9 @@ async def test_global_openrouter_image_gen_sets_api_base_when_config_empty():
async def test_generate_image_tool_global_sets_api_base_when_config_empty():
"""Same defense at the agent tool entry point — both surfaces share
the same OpenRouter config payloads."""
from app.agents.shared.tools import generate_image as gi_module
from app.agents.multi_agent_chat.subagents.builtins.deliverables.tools import (
generate_image as gi_module,
)
cfg = {
"id": -20_001,
@ -150,7 +153,19 @@ async def test_generate_image_tool_global_sets_api_base_when_config_empty():
tool = gi_module.create_generate_image_tool(
search_space_id=1, db_session=MagicMock()
)
await tool.ainvoke({"prompt": "a cat", "n": 1})
# The live tool takes an injected ToolRuntime and returns a Command;
# drive the raw coroutine with a minimal runtime (the tool only reads
# ``tool_call_id``). We assert on what was forwarded to litellm, not
# on the return value.
runtime = ToolRuntime(
state={},
context=None,
config={},
stream_writer=None,
tool_call_id="call-1",
store=None,
)
await tool.coroutine(prompt="a cat", n=1, runtime=runtime)
assert captured.get("api_base") == "https://openrouter.ai/api/v1"
assert captured["model"] == "openrouter/openai/gpt-image-1"