""" Tests for ``_build_cursor_spawn_env`` in ``omnigent/runtime/workflow.py``. The spawn-env builder maps ``spec`` fields to the ``HARNESS_CURSOR_*`` env vars the cursor harness wrap reads at first-turn time. Unlike the gateway-backed builders, cursor has NO Databricks-gateway path: only an explicit ``api_key`` auth maps to ``HARNESS_CURSOR_API_KEY``, and a ``DatabricksAuth`` profile is deliberately ignored (cursor-agent talks only to Cursor's own backend). Mirrors ``test_openai_agents_sdk_spawn_env.py``. This is a unit test — no subprocess spawn. End-to-end verification of the spawn-env → wrap → executor path lives in the harness e2e tests. """ from __future__ import annotations from pathlib import Path import pytest import yaml from omnigent.runtime.workflow import _build_cursor_spawn_env 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 so the developer's real ``~/.omnigent/config.yaml`` can't leak in, and clear any ambient ``CURSOR_API_KEY`` so the no-auth / DatabricksAuth cases are deterministic (the builder falls back to an ambient key — see the ambient-fallback test).""" monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path)) monkeypatch.delenv("CURSOR_API_KEY", raising=False) def _make_spec( *, model: str | None = "gpt-5", name: str = "test-cursor", auth: ApiKeyAuth | DatabricksAuth | None = None, ) -> AgentSpec: """Build a minimal cursor :class:`AgentSpec` for the spawn-env tests.""" config: dict[str, object] = {"harness": "cursor"} if model is not None: config["model"] = model return AgentSpec( spec_version=1, name=name, 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.model`` is encoded into ``HARNESS_CURSOR_MODEL``.""" env = _build_cursor_spawn_env(_make_spec(model="gpt-5")) assert env["HARNESS_CURSOR_MODEL"] == "gpt-5" def test_no_model_produces_no_model_env_var() -> None: """A spec with no model omits ``HARNESS_CURSOR_MODEL`` (cursor's default applies).""" env = _build_cursor_spawn_env(_make_spec(model=None)) assert "HARNESS_CURSOR_MODEL" not in env def test_api_key_auth_sets_api_key_env_var() -> None: """``executor.auth: {type: api_key, ...}`` sets ``HARNESS_CURSOR_API_KEY``.""" env = _build_cursor_spawn_env(_make_spec(auth=ApiKeyAuth(api_key="cur_test_123"))) assert env["HARNESS_CURSOR_API_KEY"] == "cur_test_123" def test_databricks_auth_does_not_set_api_key() -> None: """A ``DatabricksAuth`` profile has no cursor equivalent and is ignored. Failure means a Databricks profile is mis-forwarded as a Cursor API key — cursor-agent has no gateway path, so the only correct behaviour is to leave auth to an inherited ``CURSOR_API_KEY`` / ``cursor-agent login``. """ env = _build_cursor_spawn_env(_make_spec(auth=DatabricksAuth(profile="oss"))) assert "HARNESS_CURSOR_API_KEY" not in env def test_no_auth_omits_api_key_env_var() -> None: """With no spec auth and no ambient key, no ``HARNESS_CURSOR_API_KEY`` is written.""" env = _build_cursor_spawn_env(_make_spec(auth=None)) assert "HARNESS_CURSOR_API_KEY" not in env def test_ambient_cursor_api_key_used_when_no_spec_auth(monkeypatch: pytest.MonkeyPatch) -> None: """With no spec api-key auth, an ambient ``CURSOR_API_KEY`` is threaded as ``HARNESS_CURSOR_API_KEY`` so a user who exported the key can run cursor without declaring auth in the spec (the SDK needs it in the harness env).""" monkeypatch.setenv("CURSOR_API_KEY", "crsr_ambient") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert env["HARNESS_CURSOR_API_KEY"] == "crsr_ambient" def test_ambient_cursor_api_key_stripped_before_forwarding( monkeypatch: pytest.MonkeyPatch, ) -> None: """A padded ambient ``CURSOR_API_KEY`` is cleaned before reaching the SDK.""" monkeypatch.setenv("CURSOR_API_KEY", "\ncrsr_ambient\n") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert env["HARNESS_CURSOR_API_KEY"] == "crsr_ambient" def test_spec_api_key_wins_over_ambient(monkeypatch: pytest.MonkeyPatch) -> None: """An explicit spec api-key auth takes precedence over an ambient key.""" monkeypatch.setenv("CURSOR_API_KEY", "crsr_ambient") env = _build_cursor_spawn_env(_make_spec(auth=ApiKeyAuth(api_key="crsr_spec"))) assert env["HARNESS_CURSOR_API_KEY"] == "crsr_spec" def test_skills_filter_always_set() -> None: """``HARNESS_CURSOR_SKILLS_FILTER`` is always written so the wrap never falls back to ``"all"`` and overrides an explicit ``skills: none``.""" env = _build_cursor_spawn_env(_make_spec()) assert "HARNESS_CURSOR_SKILLS_FILTER" in env def test_name_threads_into_agent_name_env_var() -> None: """``spec.name`` is forwarded as ``HARNESS_CURSOR_AGENT_NAME``.""" env = _build_cursor_spawn_env(_make_spec(name="polly")) assert env["HARNESS_CURSOR_AGENT_NAME"] == "polly" def test_workdir_threads_into_bundle_dir_env_var(tmp_path: Path) -> None: """A bundle ``workdir`` is forwarded as ``HARNESS_CURSOR_BUNDLE_DIR``.""" env = _build_cursor_spawn_env(_make_spec(), workdir=tmp_path) assert env["HARNESS_CURSOR_BUNDLE_DIR"] == str(tmp_path) def test_no_workdir_omits_bundle_dir_env_var() -> None: """No ``workdir`` omits ``HARNESS_CURSOR_BUNDLE_DIR``.""" env = _build_cursor_spawn_env(_make_spec()) assert "HARNESS_CURSOR_BUNDLE_DIR" not in env def _write_cursor_config(tmp_path: Path, ref: str) -> None: """Write a ``cursor:`` block referencing *ref* into the isolated config. :param tmp_path: The isolated ``OMNIGENT_CONFIG_HOME`` (see the autouse fixture). :param ref: The secret reference to record, e.g. ``"env:CURSOR_KEY_SRC"``. """ (tmp_path / "config.yaml").write_text(yaml.safe_dump({"cursor": {"api_key_ref": ref}})) def test_stored_cursor_key_used_when_spec_has_no_auth( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """A CURSOR_API_KEY registered via ``omnigent setup`` flows when the spec declares no auth — so a user need not export it in every shell.""" monkeypatch.setenv("CURSOR_KEY_SRC", "crsr_stored_123") _write_cursor_config(tmp_path, "env:CURSOR_KEY_SRC") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert env["HARNESS_CURSOR_API_KEY"] == "crsr_stored_123" def test_stored_env_cursor_key_stripped_before_forwarding( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """A padded ``env:`` cursor key resolves cleanly before SDK forwarding.""" monkeypatch.setenv("CURSOR_KEY_SRC", "\ncrsr_stored_123\n") _write_cursor_config(tmp_path, "env:CURSOR_KEY_SRC") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert env["HARNESS_CURSOR_API_KEY"] == "crsr_stored_123" def test_stored_cursor_key_wins_over_ambient_when_both_set( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """With no spec auth, the stored ``cursor:`` key (registered via ``omnigent setup``) wins over an ambient ``CURSOR_API_KEY`` when BOTH are present. This pins the middle rung of the precedence chain (spec auth > stored > ambient): a refactor that swapped the two branches would silently let an ambient key override the user's configured one, with no other test failing. """ monkeypatch.setenv("CURSOR_KEY_SRC", "crsr_stored_123") _write_cursor_config(tmp_path, "env:CURSOR_KEY_SRC") monkeypatch.setenv("CURSOR_API_KEY", "crsr_ambient_456") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert env["HARNESS_CURSOR_API_KEY"] == "crsr_stored_123" def test_spec_api_key_auth_wins_over_stored_key( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """An explicit api-key auth on the spec takes precedence over the stored key. Failure means a per-agent ``executor.auth`` is silently overridden by the machine-wide default — the spec must always win. """ monkeypatch.setenv("CURSOR_KEY_SRC", "crsr_stored_123") _write_cursor_config(tmp_path, "env:CURSOR_KEY_SRC") env = _build_cursor_spawn_env(_make_spec(auth=ApiKeyAuth(api_key="crsr_explicit_999"))) assert env["HARNESS_CURSOR_API_KEY"] == "crsr_explicit_999" def test_databricks_auth_does_not_adopt_stored_cursor_key( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """An explicit ``DatabricksAuth`` never adopts the stored cursor key. The stored-key fallback applies ONLY to a spec with no auth at all; a databricks-routed spec has explicitly chosen a non-cursor credential, so pulling the cursor key would mis-authenticate the run. """ monkeypatch.setenv("CURSOR_KEY_SRC", "crsr_stored_123") _write_cursor_config(tmp_path, "env:CURSOR_KEY_SRC") env = _build_cursor_spawn_env(_make_spec(auth=DatabricksAuth(profile="oss"))) assert "HARNESS_CURSOR_API_KEY" not in env def test_empty_stored_env_key_is_omitted(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """A configured ``env:CURSOR_API_KEY`` ref pointing at an EMPTY var is omitted. ``resolve_secret``'s ``env:`` branch only raises on an *unset* variable, so an exported-but-empty ``CURSOR_API_KEY=""`` resolves to ``""`` — which the cursor layer folds to ``None``. The builder must therefore write no ``HARNESS_CURSOR_API_KEY`` (the ambient fallback reads the same empty var and is also skipped), agreeing with ``cursor_api_key_configured() is False`` rather than forwarding a blank credential. """ monkeypatch.setenv("CURSOR_API_KEY", "") _write_cursor_config(tmp_path, "env:CURSOR_API_KEY") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert "HARNESS_CURSOR_API_KEY" not in env def test_whitespace_only_stored_env_key_is_omitted( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """A configured ``env:`` ref pointing at an all-whitespace var is omitted.""" monkeypatch.setenv("CURSOR_API_KEY", " \n\t ") _write_cursor_config(tmp_path, "env:CURSOR_API_KEY") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert "HARNESS_CURSOR_API_KEY" not in env def test_unresolvable_stored_key_is_omitted(tmp_path: Path) -> None: """A dangling stored reference resolves softly to no env var. The ``cursor:`` block names ``env:CURSOR_KEY_SRC`` but the var is unset, so the builder must omit ``HARNESS_CURSOR_API_KEY`` (leaving cursor's own login / inherited key to satisfy auth) rather than crash the spawn. """ _write_cursor_config(tmp_path, "env:CURSOR_KEY_SRC") env = _build_cursor_spawn_env(_make_spec(auth=None)) assert "HARNESS_CURSOR_API_KEY" not in env