490 lines
18 KiB
Python
490 lines
18 KiB
Python
"""
|
|
Tests for ``_build_openai_agents_sdk_spawn_env`` in
|
|
``omnigent/runtime/workflow.py``.
|
|
|
|
The spawn-env builder maps ``spec.executor`` fields to
|
|
``HARNESS_OPENAI_AGENTS_*`` env vars that the openai-agents harness
|
|
wrap reads at first-turn time. Mirrors the
|
|
the spawn-env pattern used by the other SDK-style harness tests.
|
|
|
|
This is a unit test — no subprocess spawn, no real httpx. End-to-end
|
|
verification of the spawn-env → wrap → runtime executor → gateway
|
|
path lives in the harness e2e tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml as _yaml
|
|
|
|
from omnigent.runtime.workflow import _build_openai_agents_sdk_spawn_env, _load_global_auth
|
|
from omnigent.spec.types import (
|
|
AgentSpec,
|
|
ApiKeyAuth,
|
|
DatabricksAuth,
|
|
ExecutorSpec,
|
|
LLMConfig,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_global_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
"""
|
|
Point OMNIGENT_CONFIG_HOME at an empty temp dir for every test in
|
|
this file so tests that don't explicitly set up a global config are
|
|
not affected by the developer's real ``~/.omnigent/config.yaml``.
|
|
|
|
Tests that need a specific global config write their own config.yaml
|
|
into a separate temp dir and set OMNIGENT_CONFIG_HOME themselves —
|
|
that setenv call wins because monkeypatch applies in call order.
|
|
"""
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
|
|
|
|
|
|
def _make_spec(
|
|
*,
|
|
model: str | None = "databricks-gpt-5-4-mini",
|
|
profile: str | None = None,
|
|
use_responses: bool | None = None,
|
|
auth: ApiKeyAuth | DatabricksAuth | None = None,
|
|
) -> AgentSpec:
|
|
"""
|
|
Build a minimal openai-agents :class:`AgentSpec` for the
|
|
spawn-env tests.
|
|
|
|
:param model: ``spec.executor.config["model"]``; ``None`` omits
|
|
the model from the executor config.
|
|
:param profile: ``spec.executor.config["profile"]``; ``None``
|
|
omits it (no profile declared in YAML).
|
|
:param use_responses: ``spec.executor.config["use_responses"]``;
|
|
``None`` omits it (executor default applies).
|
|
:param auth: Typed auth object placed on ``spec.executor.auth``;
|
|
``None`` omits it (harness falls back to legacy/env-var paths).
|
|
:returns: A populated :class:`AgentSpec`.
|
|
"""
|
|
config: dict[str, object] = {"harness": "openai-agents"}
|
|
if model is not None:
|
|
config["model"] = model
|
|
if profile is not None:
|
|
config["profile"] = profile
|
|
if use_responses is not None:
|
|
config["use_responses"] = use_responses
|
|
return AgentSpec(
|
|
spec_version=1,
|
|
name="test-openai-agents",
|
|
instructions="You are a test agent.",
|
|
executor=ExecutorSpec(type="omnigent", config=config, model=model, auth=auth),
|
|
llm=LLMConfig(model=model) if model is not None else None,
|
|
)
|
|
|
|
|
|
def test_model_threads_into_env_var() -> None:
|
|
"""``executor.config["model"]`` is encoded into ``HARNESS_OPENAI_AGENTS_MODEL``."""
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(model="databricks-gpt-5-4-mini"))
|
|
assert env["HARNESS_OPENAI_AGENTS_MODEL"] == "databricks-gpt-5-4-mini"
|
|
|
|
|
|
def test_explicit_profile_threads_into_env_var() -> None:
|
|
"""An explicit ``executor.profile`` sets ``HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE``."""
|
|
env = _build_openai_agents_sdk_spawn_env(
|
|
_make_spec(model="databricks-gpt-5-4-mini", profile="my-profile")
|
|
)
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "my-profile"
|
|
|
|
|
|
def test_databricks_model_without_profile_gets_default_profile(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
A ``databricks-`` model with no explicit profile auto-sets
|
|
``HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE`` to ``"DEFAULT"``
|
|
when ``DATABRICKS_CONFIG_PROFILE`` is not set.
|
|
|
|
Without this, ``OPENAI_API_KEY`` in the caller's environment
|
|
(injected by Claude Code or a prior export) short-circuits the
|
|
executor's Databricks fallback and the request hits
|
|
``api.openai.com`` instead, producing a "model not found" error
|
|
for any ``databricks-`` model name.
|
|
"""
|
|
monkeypatch.delenv("DATABRICKS_CONFIG_PROFILE", raising=False)
|
|
env = _build_openai_agents_sdk_spawn_env(
|
|
_make_spec(model="databricks-kimi-k2-6", profile=None)
|
|
)
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "DEFAULT"
|
|
|
|
|
|
def test_databricks_slash_prefix_gets_default_profile(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
``databricks/`` provider-prefix form (LiteLLM convention) also triggers
|
|
auto-Databricks routing when no profile is explicitly set.
|
|
|
|
What breaks if this fails: users writing ``model: databricks/kimi-k2``
|
|
in a harness YAML get silent routing to ``api.openai.com`` instead of
|
|
the Databricks gateway, producing a confusing "model not found" error.
|
|
"""
|
|
monkeypatch.delenv("DATABRICKS_CONFIG_PROFILE", raising=False)
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(model="databricks/kimi-k2", profile=None))
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "DEFAULT"
|
|
|
|
|
|
def test_databricks_model_ignores_env_profile(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Ambient ``DATABRICKS_CONFIG_PROFILE`` does NOT steer the auto-Databricks
|
|
routing — credentials are controlled by the spec or by ``omnigent
|
|
setup`` provider config, never by shell environment. A databricks-*
|
|
model with no spec profile routes via the SDK ``DEFAULT`` profile.
|
|
"""
|
|
monkeypatch.setenv("DATABRICKS_CONFIG_PROFILE", "my-env-profile")
|
|
env = _build_openai_agents_sdk_spawn_env(
|
|
_make_spec(model="databricks-kimi-k2-6", profile=None)
|
|
)
|
|
# "DEFAULT" (not "my-env-profile") proves the env var no longer
|
|
# controls model provisioning; "my-env-profile" here would mean the
|
|
# removed ambient-env fallback was reintroduced.
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "DEFAULT"
|
|
|
|
|
|
def test_explicit_profile_wins_over_auto_default() -> None:
|
|
"""An explicit profile takes precedence over the auto-DEFAULT for ``databricks-`` models."""
|
|
env = _build_openai_agents_sdk_spawn_env(
|
|
_make_spec(model="databricks-kimi-k2-6", profile="staging")
|
|
)
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "staging"
|
|
|
|
|
|
def test_non_databricks_model_without_profile_omits_profile_env_var() -> None:
|
|
"""Non-``databricks-`` models without a profile omit the profile env var."""
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(model="gpt-5.4", profile=None))
|
|
assert "HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE" not in env
|
|
|
|
|
|
def test_use_responses_false_encodes_as_false_string() -> None:
|
|
"""``use_responses: false`` encodes as the string ``"false"``."""
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(use_responses=False))
|
|
assert env["HARNESS_OPENAI_AGENTS_USE_RESPONSES"] == "false"
|
|
|
|
|
|
def test_use_responses_true_encodes_as_true_string() -> None:
|
|
"""``use_responses: true`` encodes as the string ``"true"``."""
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(use_responses=True))
|
|
assert env["HARNESS_OPENAI_AGENTS_USE_RESPONSES"] == "true"
|
|
|
|
|
|
def test_use_responses_absent_omits_env_var() -> None:
|
|
"""When ``use_responses`` is unset, the env var is omitted (harness default applies)."""
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(use_responses=None))
|
|
assert "HARNESS_OPENAI_AGENTS_USE_RESPONSES" not in env
|
|
|
|
|
|
def test_no_model_produces_no_model_env_var() -> None:
|
|
"""A spec with no model produces no ``HARNESS_OPENAI_AGENTS_MODEL`` env var."""
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(model=None))
|
|
assert "HARNESS_OPENAI_AGENTS_MODEL" not in env
|
|
|
|
|
|
def test_profile_injects_ucode_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Profile-backed runs read OpenAI-compatible model and base URL from ucode.
|
|
|
|
:param monkeypatch: Pytest monkeypatch fixture.
|
|
"""
|
|
from omnigent.onboarding.ucode_state import UcodeAgentState, UcodeWorkspaceState
|
|
|
|
state = UcodeWorkspaceState(
|
|
workspace_url="https://example.databricks.com",
|
|
claude_models={},
|
|
codex_models=["databricks-gpt-test"],
|
|
base_urls={"codex": "https://example.databricks.com/ai-gateway/codex/v1"},
|
|
available_tools=["codex"],
|
|
agents={
|
|
"codex": UcodeAgentState(
|
|
model="databricks-gpt-test",
|
|
base_url="https://example.databricks.com/ai-gateway/codex/v1",
|
|
auth_command="printf token",
|
|
)
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
"omnigent.runtime.workflow.get_workspace_url_for_profile",
|
|
lambda profile: "https://example.databricks.com",
|
|
)
|
|
monkeypatch.setattr(
|
|
"omnigent.runtime.workflow.read_ucode_state",
|
|
lambda workspace_url: state,
|
|
)
|
|
|
|
env = _build_openai_agents_sdk_spawn_env(_make_spec(model=None, profile="test-profile"))
|
|
|
|
assert env["HARNESS_OPENAI_AGENTS_MODEL"] == "databricks-gpt-test"
|
|
assert (
|
|
env["HARNESS_OPENAI_AGENTS_GATEWAY_BASE_URL"]
|
|
== "https://example.databricks.com/ai-gateway/codex/v1"
|
|
)
|
|
assert env["HARNESS_OPENAI_AGENTS_GATEWAY_HOST"] == "https://example.databricks.com"
|
|
assert env["HARNESS_OPENAI_AGENTS_GATEWAY_AUTH_COMMAND"] == "printf token"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# executor.auth tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_databricks_auth_sets_profile_env_var() -> None:
|
|
"""
|
|
``executor.auth: {type: databricks, profile: oss}`` sets
|
|
``HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE`` and does NOT set the
|
|
API key env var.
|
|
|
|
Failure means DatabricksAuth on the spec is silently dropped and
|
|
the harness falls back to env-var auth instead of the explicit profile.
|
|
"""
|
|
spec = _make_spec(
|
|
model="databricks-gpt-5-4-mini",
|
|
auth=DatabricksAuth(profile="oss"),
|
|
)
|
|
env = _build_openai_agents_sdk_spawn_env(spec)
|
|
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "oss"
|
|
assert "HARNESS_OPENAI_AGENTS_API_KEY" not in env
|
|
|
|
|
|
def test_api_key_auth_sets_api_key_env_var() -> None:
|
|
"""
|
|
``executor.auth: {type: api_key, api_key: sk-test}`` sets
|
|
``HARNESS_OPENAI_AGENTS_API_KEY`` and does NOT set the Databricks
|
|
profile env var.
|
|
|
|
Failure means ApiKeyAuth on the spec is silently dropped and the
|
|
harness falls back to ambient OPENAI_API_KEY resolution instead.
|
|
"""
|
|
spec = _make_spec(
|
|
model="gpt-4o",
|
|
auth=ApiKeyAuth(api_key="sk-test-456"),
|
|
)
|
|
env = _build_openai_agents_sdk_spawn_env(spec)
|
|
|
|
assert env["HARNESS_OPENAI_AGENTS_API_KEY"] == "sk-test-456"
|
|
assert "HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE" not in env
|
|
|
|
|
|
def test_spec_auth_takes_precedence_over_global_config(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
When the spec declares ``executor.auth``, the global config auth
|
|
block is ignored.
|
|
|
|
Failure means per-spec auth is silently overridden by the global
|
|
config, breaking spec self-containment.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cfg_path = Path(td) / "config.yaml"
|
|
cfg_path.write_text(
|
|
_yaml.dump({"auth": {"type": "databricks", "profile": "global-profile"}})
|
|
)
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
|
|
spec = _make_spec(
|
|
model="databricks-gpt-5-4-mini",
|
|
auth=DatabricksAuth(profile="spec-profile"),
|
|
)
|
|
env = _build_openai_agents_sdk_spawn_env(spec)
|
|
|
|
# Spec-level profile must win over global config profile.
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "spec-profile"
|
|
|
|
|
|
def test_global_config_auth_used_when_spec_auth_absent(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
When the spec has no ``executor.auth``, the global config ``auth:``
|
|
block provides the fallback credentials.
|
|
|
|
Failure means users must declare auth in every agent YAML and
|
|
cannot rely on the once-configured global default from
|
|
``omnigent setup``.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cfg_path = Path(td) / "config.yaml"
|
|
cfg_path.write_text(
|
|
_yaml.dump({"auth": {"type": "databricks", "profile": "global-profile"}})
|
|
)
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
monkeypatch.delenv("DATABRICKS_CONFIG_PROFILE", raising=False)
|
|
|
|
spec = _make_spec(model="databricks-gpt-5-4-mini", auth=None, profile=None)
|
|
env = _build_openai_agents_sdk_spawn_env(spec)
|
|
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "global-profile"
|
|
|
|
|
|
def test_load_global_auth_databricks(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
``_load_global_auth()`` returns a :class:`DatabricksAuth` when the
|
|
config file has ``auth: {type: databricks, profile: …}``.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cfg_path = Path(td) / "config.yaml"
|
|
cfg_path.write_text(_yaml.dump({"auth": {"type": "databricks", "profile": "my-profile"}}))
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
result = _load_global_auth()
|
|
|
|
assert isinstance(result, DatabricksAuth)
|
|
assert result.profile == "my-profile"
|
|
|
|
|
|
def test_load_global_auth_api_key(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
``_load_global_auth()`` returns an :class:`ApiKeyAuth` when the
|
|
config file has ``auth: {type: api_key, api_key: …}`` and expands
|
|
env-var references.
|
|
"""
|
|
monkeypatch.setenv("MY_GLOBAL_KEY", "sk-global-999")
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cfg_path = Path(td) / "config.yaml"
|
|
cfg_path.write_text(_yaml.dump({"auth": {"type": "api_key", "api_key": "$MY_GLOBAL_KEY"}}))
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
result = _load_global_auth()
|
|
|
|
assert isinstance(result, ApiKeyAuth)
|
|
assert result.api_key == "sk-global-999"
|
|
|
|
|
|
def test_global_config_auth_not_applied_when_spec_has_legacy_profile(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
When the spec declares a profile via the legacy ``executor.config["profile"]``
|
|
path, the global config ``auth:`` block is ignored.
|
|
|
|
Failure means a YAML like ``executor.profile: oss`` silently has its
|
|
Databricks profile overridden by an api_key in the user's global
|
|
config — the agent then hits ``api.openai.com`` instead of the
|
|
Databricks gateway and gets an auth error.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cfg_path = Path(td) / "config.yaml"
|
|
# Global config has api_key auth — should NOT apply when spec has a profile.
|
|
cfg_path.write_text(_yaml.dump({"auth": {"type": "api_key", "api_key": "sk-global"}}))
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
monkeypatch.delenv("DATABRICKS_CONFIG_PROFILE", raising=False)
|
|
|
|
# Spec declares profile via the legacy config dict (omnigent compat path).
|
|
spec = _make_spec(model="databricks-gpt-5-4-mini", profile="oss", auth=None)
|
|
env = _build_openai_agents_sdk_spawn_env(spec)
|
|
|
|
# Legacy profile must be used; api_key from global config must be absent.
|
|
assert env["HARNESS_OPENAI_AGENTS_DATABRICKS_PROFILE"] == "oss"
|
|
assert "HARNESS_OPENAI_AGENTS_API_KEY" not in env
|
|
|
|
|
|
def test_load_global_auth_missing_file(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""``_load_global_auth()`` returns ``None`` when no config file exists."""
|
|
with tempfile.TemporaryDirectory() as td:
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
result = _load_global_auth()
|
|
|
|
assert result is None
|
|
|
|
|
|
def test_load_global_auth_api_key_with_base_url(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
``_load_global_auth()`` parses ``base_url`` from the global config
|
|
and expands env-var references in it.
|
|
|
|
Failure means a user who configures a custom endpoint in
|
|
``~/.omnigent/config.yaml`` via an env-var reference has the
|
|
literal ``$VAR`` string passed as the base URL.
|
|
"""
|
|
monkeypatch.setenv("MY_GLOBAL_KEY", "sk-global-abc")
|
|
monkeypatch.setenv("MY_BASE_URL", "https://my-gateway.example.com/v1")
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cfg_path = Path(td) / "config.yaml"
|
|
cfg_path.write_text(
|
|
_yaml.dump(
|
|
{
|
|
"auth": {
|
|
"type": "api_key",
|
|
"api_key": "$MY_GLOBAL_KEY",
|
|
"base_url": "$MY_BASE_URL",
|
|
}
|
|
}
|
|
)
|
|
)
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
result = _load_global_auth()
|
|
|
|
assert isinstance(result, ApiKeyAuth)
|
|
assert result.api_key == "sk-global-abc"
|
|
assert result.base_url == "https://my-gateway.example.com/v1"
|
|
|
|
|
|
def test_load_global_auth_unresolved_env_var_raises(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
``_load_global_auth()`` raises when ``api_key`` contains an unresolved
|
|
``$VAR`` reference (the env var is not set).
|
|
|
|
Failure means a config with ``api_key: $MISSING_KEY`` silently passes
|
|
the literal ``$MISSING_KEY`` string to the API, producing a confusing
|
|
401 "invalid API key" error rather than a clear configuration error.
|
|
"""
|
|
from omnigent.errors import OmnigentError
|
|
|
|
monkeypatch.delenv("MISSING_KEY", raising=False)
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cfg_path = Path(td) / "config.yaml"
|
|
cfg_path.write_text(_yaml.dump({"auth": {"type": "api_key", "api_key": "$MISSING_KEY"}}))
|
|
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", td)
|
|
|
|
with pytest.raises(OmnigentError):
|
|
_load_global_auth()
|
|
|
|
|
|
def test_api_key_auth_base_url_sets_base_url_env_var() -> None:
|
|
"""
|
|
``executor.auth: {type: api_key, base_url: …}`` writes
|
|
``HARNESS_OPENAI_AGENTS_GATEWAY_BASE_URL`` alongside the API key.
|
|
|
|
Failure means the custom endpoint declared in the spec is silently
|
|
dropped and the executor uses the default OpenAI endpoint instead.
|
|
"""
|
|
spec = _make_spec(
|
|
model="gpt-4o",
|
|
auth=ApiKeyAuth(api_key="sk-test-789", base_url="https://my-gw.example.com/v1"),
|
|
)
|
|
env = _build_openai_agents_sdk_spawn_env(spec)
|
|
|
|
assert env["HARNESS_OPENAI_AGENTS_API_KEY"] == "sk-test-789"
|
|
assert env["HARNESS_OPENAI_AGENTS_GATEWAY_BASE_URL"] == "https://my-gw.example.com/v1"
|
|
|
|
|
|
def test_api_key_auth_without_base_url_omits_base_url_env_var() -> None:
|
|
"""
|
|
When ``executor.auth.base_url`` is absent, the base-URL env var is
|
|
not written so the executor uses the default OpenAI endpoint.
|
|
"""
|
|
spec = _make_spec(model="gpt-4o", auth=ApiKeyAuth(api_key="sk-test-000"))
|
|
env = _build_openai_agents_sdk_spawn_env(spec)
|
|
|
|
assert env["HARNESS_OPENAI_AGENTS_API_KEY"] == "sk-test-000"
|
|
assert "HARNESS_OPENAI_AGENTS_GATEWAY_BASE_URL" not in env
|