Skip to content

symfonic.core.testing

testing

symfonic.core.testing — Test doubles for symfonic.core.

Provides MockModelProvider and MockChatModel for testing without live API keys. Install via: pip install symfonic-core[testing]

MockChatModel

Bases: BaseChatModel

Minimal chat model that returns scripted responses. For testing only.

bind_tools

bind_tools(
    tools: list[Any], **kwargs: Any
) -> MockChatModel

v7.12.0: testing pass-through.

Pre-v7.12.0 the framework never registered tools unless the adopter did, so MockModel-based tests rarely exercised bind_tools. v7.12.0 auto-registers recall_tool_result whenever tool_result_compaction_enabled=True (the new default), which means the react node now binds at least one tool even on bare SymfonicAgent(model_provider=MockModelProvider()) instances. Without this override BaseChatModel.bind_tools raises NotImplementedError and the entire suite of FastAPI / engine / scope-injection / tenant-auth tests regress.

Behaviour: returns self so the bound runnable is still the mock. tool_choice and other kwargs are accepted and ignored -- the mock does not honour forcing semantics.

Source code in src/symfonic/core/testing/__init__.py
def bind_tools(self, tools: list[Any], **kwargs: Any) -> MockChatModel:
    """v7.12.0: testing pass-through.

    Pre-v7.12.0 the framework never registered tools unless the
    adopter did, so MockModel-based tests rarely exercised
    ``bind_tools``.  v7.12.0 auto-registers ``recall_tool_result``
    whenever ``tool_result_compaction_enabled=True`` (the new
    default), which means the react node now binds at least one
    tool even on bare ``SymfonicAgent(model_provider=MockModelProvider())``
    instances.  Without this override ``BaseChatModel.bind_tools``
    raises ``NotImplementedError`` and the entire suite of
    FastAPI / engine / scope-injection / tenant-auth tests
    regress.

    Behaviour: returns ``self`` so the bound runnable is still
    the mock.  ``tool_choice`` and other kwargs are accepted and
    ignored -- the mock does not honour forcing semantics.
    """
    return self

with_structured_output

with_structured_output(schema: Any, **kwargs: Any) -> Any

Return a runnable that yields a validated schema instance.

Mirrors BaseChatModel.with_structured_output so structured-output tests run without a live tool-calling model. The instance is built from structured_response (validated against schema); if that is unset, a best-effort default instance is constructed so the happy path still returns a typed object.

Source code in src/symfonic/core/testing/__init__.py
def with_structured_output(
    self, schema: Any, **kwargs: Any
) -> Any:
    """Return a runnable that yields a validated ``schema`` instance.

    Mirrors ``BaseChatModel.with_structured_output`` so structured-output
    tests run without a live tool-calling model. The instance is built
    from ``structured_response`` (validated against ``schema``); if that
    is unset, a best-effort default instance is constructed so the happy
    path still returns a typed object.
    """
    from langchain_core.runnables import RunnableLambda

    payload = self.structured_response

    async def _invoke(_messages: Any) -> Any:
        if payload is not None:
            return schema.model_validate(payload)
        # No scripted payload: build a schema instance from field
        # defaults, filling required fields with zero-values by type.
        data: dict[str, Any] = {}
        for name, field in schema.model_fields.items():
            if field.is_required():
                ann = field.annotation
                data[name] = "" if ann is str else (0 if ann is int else None)
        return schema.model_validate(data)

    return RunnableLambda(_invoke)

MockModelProvider

MockModelProvider(
    response: str = "Mock response",
    tool_calls: list[dict[str, Any]] | None = None,
    structured_response: dict[str, Any] | None = None,
)

Test double for ModelProvider. Returns configurable responses without a live API key.

Install via: pip install symfonic-core[testing]

Source code in src/symfonic/core/testing/__init__.py
def __init__(
    self,
    response: str = "Mock response",
    tool_calls: list[dict[str, Any]] | None = None,
    structured_response: dict[str, Any] | None = None,
) -> None:
    self._response = response
    self._tool_calls = tool_calls
    self._structured_response = structured_response