Files
omnigent-ai--omnigent/tests/cli/test_cli.py
T
2026-07-13 13:12:00 +08:00

5063 lines
181 KiB
Python

"""Tests for omnigent.cli — bundle env var resolution."""
from __future__ import annotations
import io
import logging
import os
import re
import subprocess
import sys
import tarfile
from collections.abc import Callable, Iterator
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import Mock
import pytest
import yaml
from click import ClickException
from click.testing import CliRunner, Result
from omnigent.cli import (
_CLICK_SUBCOMMANDS,
_GLOBAL_CONFIG_KEYS,
_adopt_ambient_credentials,
_announce_auto_configured_credentials,
_bundle,
_bundled_example_path,
_dispatch_native_terminal_harness,
_dispatch_run,
_ensure_sqlite_parent_dir,
_expand_config_env_vars,
_HostHttpResult,
_is_removed_ad_hoc_invocation,
_is_run_shorthand,
_load_global_config,
_manage_goose_harness,
_manage_kimi_harness,
_manage_qwen_harness,
_materialize_harness_launcher_file,
_node_dependency_problem,
_node_version,
_pick_first_run_harness,
_preregister_agent,
_qwen_auth_configured,
_resolve_auto_open_conversation_from_config,
_resolve_auto_open_conversation_setting,
_resolve_bundle_env_vars,
_resolve_default_agent_target,
_resolve_first_run_plan,
_save_global_config,
_start_cli_runner_process,
_warn_missing_harness_dependencies,
cli,
)
from omnigent.errors import OmnigentError
from omnigent.onboarding.ambient import DetectedProvider
from omnigent.runner.identity import (
RUNNER_ID_ENV_VAR,
RUNNER_PARENT_PID_ENV_VAR,
RUNNER_TUNNEL_BINDING_TOKEN_ENV_VAR,
RUNNER_WORKSPACE_ENV_VAR,
token_bound_runner_id,
)
from omnigent.runner.transports.ws_tunnel.limits import (
RUNNER_TUNNEL_MAX_MESSAGE_BYTES,
TUNNEL_KEEPALIVE_PING_INTERVAL_S,
TUNNEL_KEEPALIVE_PING_TIMEOUT_S,
)
@pytest.fixture(autouse=True)
def _restore_logging_state() -> Iterator[None]:
"""
Restore process-global logging mutations after each CLI test.
Several CLI tests exercise the real entrypoint, which installs
diagnostics handlers and sets package loggers to ``propagate=False``.
Leaving that state behind makes later ``caplog`` assertions in
other modules silently miss package warnings.
:returns: A pytest finalizer implemented by yielding.
"""
names = ("omnigent", "omnigent_ui_sdk", "httpx", "httpcore", "asyncio", "urllib3")
snapshots = {}
for name in names:
logger = logging.getLogger(name)
snapshots[name] = (logger.level, logger.propagate, tuple(logger.handlers))
yield
for name, (level, propagate, handlers) in snapshots.items():
logger = logging.getLogger(name)
original_handler_ids = {id(handler) for handler in handlers}
for handler in list(logger.handlers):
if id(handler) not in original_handler_ids:
logger.removeHandler(handler)
handler.close()
logger.handlers = list(handlers)
logger.setLevel(level)
logger.propagate = propagate
def test_python_module_entrypoint_uses_unified_click_cli() -> None:
"""
``python -m omnigent`` must dispatch through the same click CLI
as the installed ``omnigent`` console script.
This catches ``omnigent/__main__.py`` pointing at the legacy
argparse CLI, which bypasses the Omnigent REPL path. In that broken
state ``python -m omnigent run ...`` opens the old ``>``
prompt and loses AP-only input features such as slash-command
autocomplete and bracketed-paste abstraction.
"""
result = subprocess.run(
[sys.executable, "-m", "omnigent", "--help"],
check=True,
capture_output=True,
text=True,
timeout=20,
)
assert "Usage: python -m omnigent [OPTIONS] COMMAND [ARGS]..." in result.stdout
assert "Commands:" in result.stdout
assert "run" in result.stdout and "Attach the REPL to a LIVE session" in result.stdout
assert "Omnigent quick chat" not in result.stdout
@pytest.mark.parametrize(
("argv", "expected"),
[
(["run", "tests/resources/examples/hello_world.yaml"], False),
(["attach", "tests/resources/examples/hello_world.yaml"], False),
(["--help"], False),
(["what does this repo do?"], True),
(["--system-prompt", "You are terse"], True),
# A single command-shaped word is an unknown subcommand, not
# ad-hoc chat — it must fall through to click's "No such
# command" handling rather than the ad-hoc removal notice.
(["blah"], False),
],
)
def test_removed_ad_hoc_detection(argv: list[str], expected: bool) -> None:
"""
Top-level prompt-shaped invocations no longer reach ``inner.cli``.
:param argv: CLI arguments without program name.
:param expected: Whether the arguments target the removed ad-hoc
prompt shape.
"""
assert _is_removed_ad_hoc_invocation(argv) is expected
def _fake_run_claude_native_capture(
captured: dict[str, object],
) -> Callable[..., None]:
"""
Build a ``run_claude_native`` stub that records its kwargs.
Shared by the ``omnigent claude`` CLI parsing tests below so a
signature change to ``run_claude_native`` (new kwarg, renamed
kwarg) updates one place instead of every test.
:param captured: Dict the stub writes recorded kwargs into.
:returns: Stub callable that accepts arbitrary kwargs.
"""
def _stub(**kwargs: object) -> None:
"""
Capture parsed CLI arguments without launching Claude.
:param kwargs: Whatever ``omnigent.claude_native.run_claude_native``
is called with — accepted permissively so new kwargs
(``resume_picker``, future flags) flow through to assertions
without breaking the signature here.
"""
captured.update(kwargs)
return _stub
def _fake_run_codex_native_capture(
captured: dict[str, object],
) -> Callable[..., None]:
"""
Build a ``run_codex_native`` stub that records its kwargs.
Shared by ``omnigent codex`` parsing tests so wrapper signature
changes only update one helper.
:param captured: Dict the stub writes recorded kwargs into.
:returns: Stub callable that accepts arbitrary kwargs.
"""
def _stub(**kwargs: object) -> None:
"""
Capture parsed CLI arguments without launching Codex.
:param kwargs: Whatever ``omnigent.codex_native.run_codex_native``
is called with.
"""
captured.update(kwargs)
return _stub
def _fake_run_kiro_native_capture(
captured: dict[str, object],
) -> Callable[..., None]:
"""Build a ``run_kiro_native`` stub that records its kwargs."""
def _stub(**kwargs: object) -> None:
captured.update(kwargs)
return _stub
def test_claude_command_resume_binds_session_and_passes_unknown_args(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``omnigent claude --resume <conv_id>`` binds the Omnigent
session; unknown args after ``--`` reach ``run_claude_native``
as raw passthrough.
The wrapper's defensive strip (``_strip_resume_from_claude_args``)
runs INSIDE ``run_claude_native`` and is tested separately at
``tests/test_claude_native.py::test_strip_resume_from_claude_args_*``.
This test mocks ``run_claude_native`` so it covers the Click
parsing seam: ``--resume`` is consumed by Click, the post-``--``
tokens land in ``claude_args`` raw, and the wrapper takes it from
there.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr(
"omnigent.claude_native.run_claude_native",
_fake_run_claude_native_capture(captured),
)
result = CliRunner().invoke(
cli,
[
"claude",
"--server",
"https://example.com",
"--resume",
"conv_abc",
"--",
"--resume",
"claude-session",
"-p",
"say hi",
],
)
assert result.exit_code == 0, result.output
# ``--resume conv_abc`` binds the Omnigent conv id; everything
# post-``--`` reaches ``run_claude_native`` raw (the strip runs
# there).
assert captured["server"] == "https://example.com"
assert captured["session_id"] == "conv_abc"
assert captured["claude_args"] == ("--resume", "claude-session", "-p", "say hi")
# No picker requested when ``--resume`` carries a value.
assert captured["resume_picker"] is False
# Default: Databricks auth is active (``--use-native-config`` not set) —
# a True here means the configured provider would be silently skipped.
assert captured["use_claude_config"] is False
def test_claude_command_short_r_binds_omnigent_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``omnigent claude -r <conv_id>`` is the Omnigent resume shortcut.
With the unified ``--resume`` UX, ``-r`` is the Omnigent alias
(not Claude's own short flag). Users who need Claude's own
resume can rely on the wrapper to translate the Omnigent conv
id internally — see ``omnigent.claude_native._resolve_cold_resume_args``
for the cold-resume injection.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda *_: "http://localhost:0")
monkeypatch.setattr(
"omnigent.claude_native.run_claude_native",
_fake_run_claude_native_capture(captured),
)
result = CliRunner().invoke(cli, ["claude", "-r", "conv_abc"])
assert result.exit_code == 0, result.output
assert captured["session_id"] == "conv_abc"
# ``-r <conv_id>`` consumes both tokens; no leftover claude args.
assert captured["claude_args"] == ()
assert captured["resume_picker"] is False
def test_claude_command_bare_resume_requests_picker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``omnigent claude --resume`` (no value) requests the picker.
Bare ``--resume`` sets the picker sentinel, which the CLI
translates into ``resume_picker=True`` for ``run_claude_native``.
Critical: the Omnigent conv id MUST stay ``None`` so the
wrapper actually runs the picker instead of binding to a
bogus literal sentinel string.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda *_: "http://localhost:0")
monkeypatch.setattr(
"omnigent.claude_native.run_claude_native",
_fake_run_claude_native_capture(captured),
)
result = CliRunner().invoke(cli, ["claude", "--resume"])
assert result.exit_code == 0, result.output
assert captured["session_id"] is None
assert captured["resume_picker"] is True
def test_claude_command_session_legacy_alias_routes_to_session_id(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``--session <id>`` is the legacy spelling kept around for one
release. It must route into ``session_id`` exactly like
``--resume <id>``; mixing it with ``--resume`` is a usage
error (mutually exclusive).
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda *_: "http://localhost:0")
monkeypatch.setattr(
"omnigent.claude_native.run_claude_native",
_fake_run_claude_native_capture(captured),
)
result = CliRunner().invoke(cli, ["claude", "--session", "conv_legacy"])
assert result.exit_code == 0, result.output
assert captured["session_id"] == "conv_legacy"
assert captured["resume_picker"] is False
def test_claude_command_session_and_resume_mutually_exclusive(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Passing ``--session`` and ``--resume`` together fails fast.
Both spellings target the same kwarg; accepting both would
silently let one win (and which one would depend on the
implementation), which is exactly the kind of ambiguity the
unified resume UX is trying to fix.
"""
monkeypatch.setattr(
"omnigent.cli._ensure_backend",
lambda *_: pytest.fail("invalid args must not start the backend"),
)
result = CliRunner().invoke(
cli,
["claude", "--session", "conv_a", "--resume", "conv_b"],
)
# ``UsageError`` translates to a non-zero exit at the Click layer.
assert result.exit_code != 0
assert "mutually exclusive" in result.output
def test_claude_command_profile_startup_threads_profiler(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``--profile-startup`` starts timing before backend setup.
This covers the slow-start diagnostic path users need for
``omnigent claude``: the profiler must be created in the Click
command, emit early marks, and be passed to ``run_claude_native``
so native launch marks share the same timer.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: None.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr(
"omnigent.cli._ensure_backend",
lambda server: "https://example.com",
)
monkeypatch.setattr(
"omnigent.claude_native.run_claude_native",
_fake_run_claude_native_capture(captured),
)
result = CliRunner().invoke(
cli,
["claude", "--server", "https://example.com", "--profile-startup"],
)
assert result.exit_code == 0, result.output
assert "cli entered" in result.stderr
assert "ensuring backend" in result.stderr
startup_profiler = captured["startup_profiler"]
assert startup_profiler.enabled is True
def test_claude_command_use_native_config_bypasses_databricks_auth(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``--use-native-config`` sets ``use_claude_config=True`` in ``run_claude_native``.
Regression target: if the flag is dropped at the Click parsing
seam, ``use_claude_config`` stays ``False`` and Databricks/ucode
auth is injected even when the user explicitly opted out.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda *_: "http://localhost:0")
monkeypatch.setattr(
"omnigent.claude_native.run_claude_native",
_fake_run_claude_native_capture(captured),
)
result = CliRunner().invoke(cli, ["claude", "--use-native-config"])
assert result.exit_code == 0, result.output
# Flag must arrive as True — a False here means Click dropped it.
assert captured["use_claude_config"] is True
def test_codex_command_resume_binds_session_and_passes_unknown_args(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``omnigent codex --resume <conv_id>`` binds the Omnigent
session and preserves Codex CLI passthrough args after ``--``.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr(
"omnigent.codex_native.run_codex_native",
_fake_run_codex_native_capture(captured),
)
result = CliRunner().invoke(
cli,
[
"codex",
"--server",
"https://example.com",
"--resume",
"conv_abc",
"--model",
"gpt-test",
"-p",
"say hi",
"--",
"-c",
"approval_policy=on-request",
],
)
assert result.exit_code == 0, result.output
assert captured["server"] == "https://example.com"
assert captured["session_id"] == "conv_abc"
assert captured["codex_args"] == ("-c", "approval_policy=on-request")
assert captured["model"] == "gpt-test"
assert captured["prompt"] == "say hi"
assert captured["resume_picker"] is False
def test_codex_command_bare_resume_requests_picker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``omnigent codex --resume`` requests the codex-native picker.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda *_: "http://localhost:0")
monkeypatch.setattr(
"omnigent.codex_native.run_codex_native",
_fake_run_codex_native_capture(captured),
)
result = CliRunner().invoke(cli, ["codex", "--resume"])
assert result.exit_code == 0, result.output
assert captured["session_id"] is None
assert captured["resume_picker"] is True
def test_codex_command_session_legacy_alias_routes_to_session_id(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``omnigent codex --session <id>`` routes into ``session_id``.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda *_: "http://localhost:0")
monkeypatch.setattr(
"omnigent.codex_native.run_codex_native",
_fake_run_codex_native_capture(captured),
)
result = CliRunner().invoke(cli, ["codex", "--session", "conv_legacy"])
assert result.exit_code == 0, result.output
assert captured["session_id"] == "conv_legacy"
assert captured["resume_picker"] is False
def test_codex_command_session_and_resume_mutually_exclusive(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Passing ``--session`` and ``--resume`` together fails fast.
"""
monkeypatch.setattr(
"omnigent.cli._ensure_backend",
lambda *_: pytest.fail("invalid args must not start the backend"),
)
result = CliRunner().invoke(
cli,
["codex", "--session", "conv_a", "--resume", "conv_b"],
)
assert result.exit_code != 0
assert "mutually exclusive" in result.output
def test_kiro_command_is_registered_in_click_help() -> None:
"""``omnigent kiro`` is a true top-level Click command."""
result = CliRunner().invoke(cli, ["--help"])
assert result.exit_code == 0, result.output
assert "kiro" in _CLICK_SUBCOMMANDS
assert "kiro" in result.output
def test_kiro_command_parses_native_options_and_prompt(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``omnigent kiro`` routes mapped options to the native Kiro runner."""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", lambda: {"server": "https://cfg"})
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda server: server)
monkeypatch.setattr(
"omnigent.kiro_native.run_kiro_native",
_fake_run_kiro_native_capture(captured),
)
result = CliRunner().invoke(
cli,
[
"kiro",
"--model",
"auto",
"--effort",
"high",
"--agent",
"dev",
"--trust-tools",
"Read",
"--trust-all-tools",
"-p",
"hi",
],
)
assert result.exit_code == 0, result.output
assert captured["server"] == "https://cfg"
assert captured["session_id"] is None
assert captured["resume_picker"] is False
assert captured["model"] == "auto"
assert captured["prompt"] == "hi"
assert captured["kiro_args"] == (
"--effort",
"high",
"--agent",
"dev",
"--trust-tools",
"Read",
"--trust-all-tools",
)
def test_kiro_command_bare_resume_requests_picker(monkeypatch: pytest.MonkeyPatch) -> None:
"""``omnigent kiro --resume`` requests the Kiro-native picker."""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda *_: "http://localhost:0")
monkeypatch.setattr(
"omnigent.kiro_native.run_kiro_native",
_fake_run_kiro_native_capture(captured),
)
result = CliRunner().invoke(cli, ["kiro", "--resume"])
assert result.exit_code == 0, result.output
assert captured["session_id"] is None
assert captured["resume_picker"] is True
def test_kiro_command_session_and_resume_mutually_exclusive(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Invalid Kiro resume inputs fail before backend side effects."""
monkeypatch.setattr(
"omnigent.cli._ensure_backend",
lambda *_: pytest.fail("invalid args must not start the backend"),
)
result = CliRunner().invoke(cli, ["kiro", "--session", "conv_a", "--resume", "conv_b"])
assert result.exit_code != 0
assert "mutually exclusive" in result.output
def test_kiro_command_rejects_kiro_resume_passthrough_flags(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Kiro-owned resume flags are reserved for internal cold-resume mapping."""
monkeypatch.setattr(
"omnigent.cli._ensure_backend",
lambda *_: pytest.fail("invalid args must not start the backend"),
)
result = CliRunner().invoke(cli, ["kiro", "--", "--resume-id", "kiro-session"])
assert result.exit_code != 0
assert "Kiro resume flags are reserved" in result.output
# ── bundled-agent shorthands (omnigent polly / omnigent debby) ──────────
def _invoke_bundled_agent_command(
monkeypatch: pytest.MonkeyPatch, args: list[str]
) -> tuple[Result, Mock]:
"""Invoke a bundled-agent shorthand with ``run``'s dispatcher mocked.
Stubs ``_load_effective_config`` (no developer-machine config leakage)
and ``_dispatch_run`` (no server/daemon side effects), so the test
observes exactly what the forwarded ``run`` invocation would launch.
:param monkeypatch: Pytest monkeypatch fixture.
:param args: Full CLI argv, e.g. ``["polly", "-p", "hi"]``.
:returns: The Click invocation result and the ``_dispatch_run`` mock.
"""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
dispatch = Mock()
monkeypatch.setattr("omnigent.cli._dispatch_run", dispatch)
result = CliRunner().invoke(cli, args)
return result, dispatch
def test_polly_command_runs_bundled_polly_and_forwards_run_flags(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``omnigent polly`` dispatches ``run`` on the packaged polly agent.
The shorthand must target the same bundled polly directory the bare
``omnigent`` first-run plan uses, and pass-through ``run`` flags
(``-p``, ``--model``) must survive the forwarding unchanged.
"""
result, dispatch = _invoke_bundled_agent_command(
monkeypatch, ["polly", "-p", "review the last commit", "--model", "m1"]
)
assert result.exit_code == 0, result.output
dispatch.assert_called_once()
kwargs = dispatch.call_args.kwargs
assert kwargs["target"] == _bundled_example_path("polly")
assert kwargs["prompt"] == "review the last commit"
assert kwargs["model"] == "m1"
# The resume replay prefix must be the canonical (re-runnable) run form,
# not "omnigent polly <path>" which would parse the path as a 2nd target.
assert kwargs["resume_parts"][:3] == ["omnigent", "run", _bundled_example_path("polly")]
def test_debby_command_runs_bundled_debby(monkeypatch: pytest.MonkeyPatch) -> None:
"""``omnigent debby`` dispatches ``run`` on the packaged debby agent."""
result, dispatch = _invoke_bundled_agent_command(monkeypatch, ["debby"])
assert result.exit_code == 0, result.output
dispatch.assert_called_once()
assert dispatch.call_args.kwargs["target"] == _bundled_example_path("debby")
def test_bundled_agent_command_rejects_extra_positional_target(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A stray positional after the shorthand is a usage error, not a launch.
``run`` takes a single AGENT positional which the shorthand already
supplies (the bundled path); a second one must fail loudly rather than
silently launching the wrong agent.
"""
result, dispatch = _invoke_bundled_agent_command(monkeypatch, ["polly", "other_agent.yaml"])
assert result.exit_code != 0
dispatch.assert_not_called()
def test_first_run_plan_and_polly_command_agree_on_bundled_path(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Bare ``omnigent`` (Claude creds) and ``omnigent polly`` launch the SAME agent.
Pins the "same thing as bare ``omni``" contract: the first-run plan's
default agent and the polly shorthand resolve to one bundled directory.
"""
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for("claude-sdk"),
)
plan = _pick_first_run_harness()
assert plan is not None
assert plan.agent == _bundled_example_path("polly")
def _write_isolated_provider_config(
config_home: Path,
providers: dict[str, object],
) -> Path:
"""Write an isolated ``~/.omnigent/config.yaml`` with *providers*.
:param config_home: Directory to use as ``$OMNIGENT_CONFIG_HOME``.
:param providers: The raw ``providers:`` mapping to write.
:returns: The written config file path.
"""
config_home.mkdir(parents=True, exist_ok=True)
config_path = config_home / "config.yaml"
config_path.write_text(yaml.safe_dump({"providers": providers}))
return config_path
@pytest.mark.parametrize("shorthand", ["polly", "debby"])
def test_bundled_agent_launches_with_first_available_credential(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
shorthand: str,
) -> None:
"""Polly/Debby launch with the first available credential (#334).
With a Claude (``anthropic``) credential configured but NOT marked
``default: true``, the shorthand must mark it the default for the
brain's family and proceed to launch — rather than requiring the user
to pick/configure a specific default up front. Both shorthands share
the ``_run_bundled_agent`` path and the ``claude-sdk`` brain, so the
fallback fires identically for each.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
# No ambient credentials — the explicit provider below is the only one.
monkeypatch.setattr("omnigent.onboarding.detected.detect_providers", list)
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
_write_isolated_provider_config(
tmp_path,
{
"anthropic_key": {
"kind": "key",
"anthropic": {
"base_url": "https://api.anthropic.invalid/v1",
"api_key_ref": "env:ANTHROPIC_KEY",
},
}
},
)
dispatch = Mock()
monkeypatch.setattr("omnigent.cli._dispatch_run", dispatch)
result = CliRunner().invoke(cli, [shorthand])
assert result.exit_code == 0, result.output
# The first available credential was promoted to the anthropic default.
# A single-family (anthropic-only) provider renders its default as the
# compact `True` form — the whole served set, per _default_raw_value.
saved = yaml.safe_load((tmp_path / "config.yaml").read_text())
provider = saved["providers"]["anthropic_key"]
assert provider.get("default") is True
# The silent config mutation is announced (mirrors setup / /model).
assert "saving it as the default" in result.output
# And the launch proceeded (the brain credential resolved).
dispatch.assert_called_once()
assert dispatch.call_args.kwargs["target"] == _bundled_example_path(shorthand)
def test_bundled_agent_leaves_existing_default_credential_alone(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""An existing explicit default is not re-written on bundled launch (#334).
When a ``default: true`` credential is already configured for the
brain's family, the shorthand must NOT touch the config — the fallback
only fires when no default exists.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.onboarding.detected.detect_providers", list)
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
config_path = _write_isolated_provider_config(
tmp_path,
{
"anthropic_key": {
"kind": "key",
"default": True,
"anthropic": {
"base_url": "https://api.anthropic.invalid/v1",
"api_key_ref": "env:ANTHROPIC_KEY",
},
}
},
)
before = config_path.read_text()
dispatch = Mock()
monkeypatch.setattr("omnigent.cli._dispatch_run", dispatch)
result = CliRunner().invoke(cli, ["polly"])
assert result.exit_code == 0, result.output
assert config_path.read_text() == before # unchanged — default already set
dispatch.assert_called_once()
def test_bundled_agent_no_credential_does_not_write_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""No available credential → no config write; the launch still dispatches (#334).
When nothing serves the brain's family, the shorthand must not fabricate
a default (the harness raises its own launch error downstream). The
config is left untouched and ``run`` is still forwarded so that error
surfaces in the normal launch path rather than as a silent no-op.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.onboarding.detected.detect_providers", list)
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
# An OpenAI-only credential — the claude-sdk brain needs anthropic.
config_path = _write_isolated_provider_config(
tmp_path,
{
"openai_key": {
"kind": "key",
"openai": {
"base_url": "https://api.openai.invalid/v1",
"api_key_ref": "env:OPENAI_KEY",
},
}
},
)
before = config_path.read_text()
dispatch = Mock()
monkeypatch.setattr("omnigent.cli._dispatch_run", dispatch)
result = CliRunner().invoke(cli, ["polly"])
assert result.exit_code == 0, result.output
assert config_path.read_text() == before # no default fabricated
dispatch.assert_called_once()
def test_bundled_agent_unreadable_global_config_degrades_to_launch(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""A corrupt on-disk config degrades to launch, never crashes (#334).
The fallback reads the on-disk providers via the non-forgiving
``_load_global_config`` to decide what to persist. If that read blows up
on a malformed/corrupt config, the bundled launch must degrade to a no-op
(letting the harness raise its own credential error) rather than crashing
before the harness ever runs — the exact failure mode this path exists to
avoid. An explicit anthropic key keeps the fallback loop reaching that read.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.onboarding.detected.detect_providers", list)
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
_write_isolated_provider_config(
tmp_path,
{
"anthropic_key": {
"kind": "key",
"anthropic": {
"base_url": "https://api.anthropic.invalid/v1",
"api_key_ref": "env:ANTHROPIC_KEY",
},
}
},
)
def _corrupt() -> dict[str, object]:
raise yaml.YAMLError("corrupt global config")
monkeypatch.setattr("omnigent.cli._load_global_config", _corrupt)
dispatch = Mock()
monkeypatch.setattr("omnigent.cli._dispatch_run", dispatch)
result = CliRunner().invoke(cli, ["polly"])
# Degrades cleanly: no traceback, and the launch still dispatches so the
# harness surfaces any credential error through the normal path.
assert result.exit_code == 0, result.output
assert result.exception is None, result.exception
dispatch.assert_called_once()
def test_bundled_agent_ambiguous_default_config_degrades_to_launch(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""An ambiguous config (two defaults for one family) degrades, never crashes.
The fallback touches the config through several readers
(``effective_config_with_detected``, ``default_provider_for_harness``,
``set_default_provider``), any of which raise ``OmnigentError`` on a
malformed/ambiguous config. Here two anthropic providers are both marked
``default: true`` — which ``get_default_provider`` rejects — so the read
raises before the loop. The bundled launch must swallow it and dispatch,
letting the harness raise its own credential error, rather than crashing
with a traceback. Regression for the narrow guard that caught only the
``_load_global_config`` read.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.onboarding.detected.detect_providers", list)
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
_write_isolated_provider_config(
tmp_path,
{
"anthropic_a": {
"kind": "key",
"default": True,
"anthropic": {
"base_url": "https://api.anthropic.invalid/v1",
"api_key_ref": "env:ANTHROPIC_A",
},
},
"anthropic_b": {
"kind": "key",
"default": True,
"anthropic": {
"base_url": "https://api.anthropic.invalid/v1",
"api_key_ref": "env:ANTHROPIC_B",
},
},
},
)
dispatch = Mock()
monkeypatch.setattr("omnigent.cli._dispatch_run", dispatch)
result = CliRunner().invoke(cli, ["polly"])
assert result.exit_code == 0, result.output
assert result.exception is None, result.exception
dispatch.assert_called_once()
def test_start_cli_runner_process_uses_token_bound_runner_id(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Authenticated remote runners advertise the tunnel-token-bound id.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory fixture used as the runner
workspace root.
:returns: None.
"""
captured: dict[str, object] = {}
monkeypatch.setattr("omnigent.cli.secrets.token_urlsafe", lambda _size: "bind-token")
class _Proc:
"""Subprocess stub returned by ``subprocess.Popen``.
:param args: Command line passed to ``Popen``.
:param env: Environment passed to ``Popen``.
:param _kwargs: Remaining Popen kwargs (e.g. ``stdout``/
``stderr`` when ``capture_logs=True``); absorbed.
"""
returncode: int | None = None
def __init__(self, args: list[str], *, env: dict[str, str], **_kwargs: object) -> None:
captured["args"] = args
captured["env"] = env
def poll(self) -> None:
"""Report the runner process as still alive.
:returns: ``None``.
"""
return
monkeypatch.setattr("omnigent.cli.subprocess.Popen", _Proc)
workspace = tmp_path / "workspace"
workspace.mkdir()
runner = _start_cli_runner_process(
server_url="https://example.databricksapps.com",
workspace_cwd=workspace,
)
expected_runner_id = token_bound_runner_id("bind-token")
assert runner.runner_id == expected_runner_id
assert runner.tunnel_token == "bind-token"
env = captured["env"]
assert isinstance(env, dict)
assert env[RUNNER_ID_ENV_VAR] == expected_runner_id
assert "OMNIGENT_RUNNER_TUNNEL_TOKEN" not in env
assert env[RUNNER_TUNNEL_BINDING_TOKEN_ENV_VAR] == "bind-token"
assert env[RUNNER_PARENT_PID_ENV_VAR] == str(os.getpid())
assert env[RUNNER_WORKSPACE_ENV_VAR] == str(workspace.resolve())
def test_start_cli_runner_process_binds_stable_local_runner_to_generated_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Local server runners keep stable identity and use token auth.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: None.
"""
captured: dict[str, object] = {}
monkeypatch.delenv("OMNIGENT_RUNNER_TUNNEL_TOKEN", raising=False)
monkeypatch.setattr(
"omnigent.cli.secrets.token_urlsafe",
lambda _size: "local-bind-token",
)
class _Proc:
"""Subprocess stub returned by ``subprocess.Popen``.
:param args: Command line passed to ``Popen``.
:param env: Environment passed to ``Popen``.
:param _kwargs: Remaining Popen kwargs (e.g. ``stdout``/
``stderr`` when ``capture_logs=True``); absorbed.
"""
returncode: int | None = None
def __init__(self, args: list[str], *, env: dict[str, str], **_kwargs: object) -> None:
captured["args"] = args
captured["env"] = env
def poll(self) -> None:
"""Report the runner process as still alive.
:returns: ``None``.
"""
return
monkeypatch.setattr("omnigent.cli.subprocess.Popen", _Proc)
runner = _start_cli_runner_process(
server_url="http://127.0.0.1:8000",
runner_id="runner_local_stable",
)
assert runner.runner_id == "runner_local_stable"
assert runner.tunnel_token == "local-bind-token"
env = captured["env"]
assert isinstance(env, dict)
assert env["RUNNER_SERVER_URL"] == "http://127.0.0.1:8000"
assert env[RUNNER_ID_ENV_VAR] == "runner_local_stable"
assert env[RUNNER_TUNNEL_BINDING_TOKEN_ENV_VAR] == "local-bind-token"
assert env[RUNNER_PARENT_PID_ENV_VAR] == str(os.getpid())
assert "OMNIGENT_RUNNER_TUNNEL_TOKEN" not in env
def test_start_cli_runner_process_reports_captured_log_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Runner startup failure points users at the captured log file.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: None.
"""
class _ExitedProc:
"""Subprocess stub that exits before startup completes.
:param args: Command line passed to ``Popen``.
:param env: Environment passed to ``Popen``.
:param kwargs: Remaining ``Popen`` keyword args.
"""
returncode = 17
def __init__(
self,
args: list[str],
*,
env: dict[str, str],
**kwargs: object,
) -> None:
del args, env, kwargs
def poll(self) -> int:
"""Report the runner process as already exited.
:returns: Exit code, e.g. ``17``.
"""
return 17
monkeypatch.setattr("omnigent.cli.subprocess.Popen", _ExitedProc)
log_dir = tmp_path / "logs"
with pytest.raises(ClickException) as excinfo:
_start_cli_runner_process(
server_url="http://127.0.0.1:8000",
runner_id="runner_exited",
capture_logs=True,
log_dir=log_dir,
)
assert "Runner process exited early with code 17" in excinfo.value.message
assert "Runner log:" in excinfo.value.message
assert str(tmp_path / "logs" / "runner") in excinfo.value.message
assert "runner-" in excinfo.value.message
assert ".log" in excinfo.value.message
def test_server_command_reads_tunnel_token_and_does_not_spawn_runner(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""``omnigent server`` is a pure state server — no embedded runner.
The server reads ``OMNIGENT_RUNNER_TUNNEL_TOKEN`` from the
environment and passes it as ``runner_tunnel_tokens`` to
``create_app`` so the caller (``_start_local_server``) can spawn
a sibling runner whose tunnel the server accepts. Without the env
var, ``runner_tunnel_tokens`` is ``None`` (accept any token —
the deployed-server posture).
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Per-test data directory for the SQLite store.
:returns: None.
"""
import uvicorn
import uvicorn.server
captured: dict[str, Any] = {}
_original_create_app = None
def _spy_create_app(**kwargs: Any) -> Any:
"""Capture create_app kwargs and delegate to the real function.
:param kwargs: Forwarded keyword arguments from ``server``.
:returns: The real FastAPI app.
"""
captured["create_app_kwargs"] = kwargs
return _original_create_app(**kwargs)
def _fake_server_run(self: Any) -> None:
"""Skip the blocking server loop; capture config as flat kwargs dict.
:param self: The uvicorn Server instance whose config holds all options.
:returns: None.
"""
captured["uvicorn_kwargs"] = {
"ws_max_size": self.config.ws_max_size,
"ws_ping_interval": self.config.ws_ping_interval,
"ws_ping_timeout": self.config.ws_ping_timeout,
"log_config": self.config.log_config,
"port": self.config.port,
"host": self.config.host,
}
captured["uvicorn_called"] = True
from omnigent.server import app as app_module
_original_create_app = app_module.create_app
monkeypatch.setattr(app_module, "create_app", _spy_create_app)
monkeypatch.setattr(uvicorn.server.Server, "run", _fake_server_run)
monkeypatch.setenv("OMNIGENT_RUNNER_TUNNEL_TOKEN", "test-tunnel-token-abc")
# On a loopback bind the `server` command reuses an already-running
# local server (and registers itself in ~/.omnigent/local_server.pid).
# Pin the unified-server helpers so the test exercises the spawn path
# deterministically: no pre-existing server to reuse, the requested port
# is taken as-is, and we don't touch the developer's real pidfile.
from omnigent.host import local_server as _local_server_mod
monkeypatch.setattr(_local_server_mod, "local_server_url_if_healthy", lambda: None)
monkeypatch.setattr(_local_server_mod, "pick_local_port", lambda preferred: preferred)
monkeypatch.setattr(_local_server_mod, "register_local_server", lambda port: None)
monkeypatch.setattr(_local_server_mod, "clear_local_server_record", lambda: None)
db_path = tmp_path / "chat.db"
artifact_dir = tmp_path / "artifacts"
result = CliRunner().invoke(
cli,
[
"server",
"--host",
"127.0.0.1",
"--port",
"9999",
"--database-uri",
f"sqlite:///{db_path}",
"--artifact-location",
str(artifact_dir),
],
)
assert result.exit_code == 0, result.output
assert captured.get("uvicorn_called") is True
assert captured["uvicorn_kwargs"]["ws_max_size"] == RUNNER_TUNNEL_MAX_MESSAGE_BYTES
# Tunnel protocol keepalive aligned to the 90s app-level budget, not uvicorn's
# 20s default that drops a busy-but-healthy tunnel with 1011 (issue #1116).
assert captured["uvicorn_kwargs"]["ws_ping_interval"] == TUNNEL_KEEPALIVE_PING_INTERVAL_S
assert captured["uvicorn_kwargs"]["ws_ping_timeout"] == TUNNEL_KEEPALIVE_PING_TIMEOUT_S
assert (
captured["uvicorn_kwargs"]["log_config"]["formatters"]["access"]["()"]
== "omnigent.server.performance_metrics.RequestDurationAccessFormatter"
)
assert captured["create_app_kwargs"]["runner_tunnel_tokens"] == frozenset(
{"test-tunnel-token-abc"}
)
def test_server_with_explicit_db_does_not_reuse_canonical_server(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""A `server` with explicit --database-uri binds its own port, never reuses.
Regression: the unified machine-global lifecycle (reuse a running
canonical server via ~/.omnigent/local_server.pid) must apply ONLY
to a *bare* loopback ``omnigent server``. The daemon and the e2e
harness spawn DEDICATED servers with explicit --database-uri /
--artifact-location / --port; if the reuse-check fired for them, a
healthy canonical server on a *different* DB would make the dedicated
spawn print "already running — reusing it" and exit WITHOUT binding
its port, so the caller's "server failed to start". Here we assert
that with a healthy canonical server present (stubbed), an explicit-DB
invocation still starts uvicorn on its own port and never touches the
shared pidfile.
"""
import uvicorn
import uvicorn.server
captured: dict[str, Any] = {}
_original_create_app = None
def _spy_create_app(**kwargs: Any) -> Any:
"""Capture kwargs and delegate to the real create_app.
:param kwargs: Forwarded keyword arguments from ``server``.
:returns: The real FastAPI app.
"""
captured["create_app_kwargs"] = kwargs
return _original_create_app(**kwargs)
def _fake_server_run(self: Any) -> None:
"""Skip the blocking server loop, record that it was called.
:param self: The uvicorn Server instance.
:returns: None.
"""
captured["uvicorn_kwargs"] = {"port": self.config.port}
captured["uvicorn_called"] = True
from omnigent.server import app as app_module
_original_create_app = app_module.create_app
monkeypatch.setattr(app_module, "create_app", _spy_create_app)
monkeypatch.setattr(uvicorn.server.Server, "run", _fake_server_run)
# A healthy canonical server EXISTS. A bare `omnigent server` would
# reuse it; an explicit-DB server must ignore it. register/clear must
# never fire for the dedicated server (it doesn't own the pidfile).
from omnigent.host import local_server as _local_server_mod
monkeypatch.setattr(
_local_server_mod, "local_server_url_if_healthy", lambda: "http://127.0.0.1:39811"
)
def _must_not_register(_port: int) -> None:
raise AssertionError("explicit-DB server must not register in the shared pidfile")
monkeypatch.setattr(_local_server_mod, "register_local_server", _must_not_register)
db_path = tmp_path / "chat.db"
result = CliRunner().invoke(
cli,
[
"server",
"--host",
"127.0.0.1",
"--port",
"44769",
"--database-uri",
f"sqlite:///{db_path}",
"--artifact-location",
str(tmp_path / "artifacts"),
"--no-open",
],
)
assert result.exit_code == 0, result.output
# Did NOT bail with "already running"; bound its own explicit port.
assert "already running" not in result.output
assert captured.get("uvicorn_called") is True
assert captured["uvicorn_kwargs"]["port"] == 44769
def test_server_with_explicit_port_does_not_check_canonical_server(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
An explicit ``--port`` starts a dedicated local server.
A healthy canonical local server on another port must not make
``omnigent server --port <new>`` reuse and exit. Explicit port
selection is the user asking for another listener, while the
canonical local-server reuse path is only for bare
``omnigent server``.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Per-test data directory for default server state.
:returns: None.
"""
import uvicorn
import uvicorn.server
captured: dict[str, Any] = {}
def _fake_server_run(self: Any) -> None:
"""
Skip the blocking server loop.
:param self: The uvicorn Server instance.
:returns: None.
"""
captured["uvicorn_kwargs"] = {"port": self.config.port}
def _must_not_check_existing() -> str | None:
"""
Fail if the explicit-port path consults the canonical server record.
:returns: Never returns in this test.
"""
raise AssertionError("explicit --port must not check the canonical local server")
def _must_not_touch_pidfile(_port: int | None = None) -> None:
"""
Fail if the explicit-port path mutates the canonical server record.
:param _port: Optional port argument accepted for register calls.
:returns: Never returns in this test.
"""
raise AssertionError("explicit --port must not touch the shared pidfile")
from omnigent.host import local_server as _local_server_mod
monkeypatch.setattr(uvicorn.server.Server, "run", _fake_server_run)
monkeypatch.setattr(_local_server_mod, "local_server_url_if_healthy", _must_not_check_existing)
monkeypatch.setattr(_local_server_mod, "register_local_server", _must_not_touch_pidfile)
monkeypatch.setattr(_local_server_mod, "clear_local_server_record", _must_not_touch_pidfile)
monkeypatch.setenv("OMNIGENT_AUTH_ENABLED", "0")
monkeypatch.setenv("OMNIGENT_DATA_DIR", str(tmp_path / "data"))
result = CliRunner().invoke(
cli,
[
"server",
"--host",
"127.0.0.1",
"--port",
"44770",
"--no-open",
],
)
assert result.exit_code == 0, result.output
assert "already running" not in result.output
assert captured["uvicorn_kwargs"]["port"] == 44770
assert "Starting omnigent server on 127.0.0.1:44770" in result.output
def test_server_command_explicit_occupied_port_fails() -> None:
"""
An explicit ``--port`` must fail instead of choosing a replacement.
The test owns a real listening socket on a kernel-assigned port,
then asks ``omnigent server`` for that exact port. The command
must fail during preflight, before uvicorn or fallback-port logic
can start the server elsewhere.
:returns: None.
"""
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
listener.bind(("127.0.0.1", 0))
listener.listen(1)
port = int(listener.getsockname()[1])
result = CliRunner().invoke(
cli,
[
"server",
"--host",
"127.0.0.1",
"--port",
str(port),
],
)
assert result.exit_code != 0
assert f"Cannot start server on 127.0.0.1:{port}" in result.output
assert "port is unavailable" in result.output
assert "using" not in result.output
assert "Starting omnigent server" not in result.output
def test_server_command_explicit_port_uses_bind_probe_not_connect_probe(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
A refused client connection does not make an explicit port unavailable.
A connect-based probe would classify this free port as unusable
because the pre-test client connection is refused. The CLI should
use a bind probe instead, continue startup, and pass the requested
port to uvicorn unchanged.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Per-test data directory for the SQLite store.
:returns: None.
"""
import socket
import uvicorn
import uvicorn.server
captured: dict[str, Any] = {}
def _fake_server_run(self: Any) -> None:
"""
Skip the blocking server loop.
:param self: The uvicorn Server instance.
:returns: None.
"""
captured["uvicorn_kwargs"] = {"port": self.config.port}
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.bind(("127.0.0.1", 0))
port = int(probe.getsockname()[1])
with pytest.raises(OSError):
socket.create_connection(("127.0.0.1", port), timeout=0.01)
monkeypatch.setattr(uvicorn.server.Server, "run", _fake_server_run)
monkeypatch.setenv("OMNIGENT_AUTH_ENABLED", "0")
db_path = tmp_path / "chat.db"
artifact_dir = tmp_path / "artifacts"
result = CliRunner().invoke(
cli,
[
"server",
"--host",
"127.0.0.1",
"--port",
str(port),
"--database-uri",
f"sqlite:///{db_path}",
"--artifact-location",
str(artifact_dir),
],
)
assert result.exit_code == 0, result.output
assert captured["uvicorn_kwargs"]["port"] == port
assert f"Starting omnigent server on 127.0.0.1:{port}" in result.output
assert "port is unavailable" not in result.output
def _write_config(
agent_dir: Path,
config: dict[str, Any],
) -> None:
"""
Write a config.yaml to the agent directory.
:param agent_dir: The agent image directory.
:param config: The config dict to serialize.
"""
(agent_dir / "config.yaml").write_text(
yaml.dump(config, default_flow_style=False),
)
def _write_mcp_config(
agent_dir: Path,
name: str,
config: dict[str, Any],
) -> None:
"""
Write an MCP server YAML file under tools/mcp/.
:param agent_dir: The agent image directory.
:param name: The MCP config filename (without .yaml).
:param config: The MCP config dict to serialize.
"""
mcp_dir = agent_dir / "tools" / "mcp"
mcp_dir.mkdir(parents=True, exist_ok=True)
(mcp_dir / f"{name}.yaml").write_text(
yaml.dump(config, default_flow_style=False),
)
# ── _expand_config_env_vars ──────────────────────────
def test_expand_config_expands_llm_connection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_expand_config_env_vars`` resolves ``${VAR}`` in
``llm.connection`` values.
"""
monkeypatch.setenv("TEST_API_KEY", "sk-resolved-123")
from omnigent.spec import expand_env_vars
raw: dict[str, Any] = {
"spec_version": 1,
"llm": {
"model": "gpt-5.4",
"connection": {"api_key": "${TEST_API_KEY}"},
},
}
changed = _expand_config_env_vars(raw, expand_env_vars)
assert changed is True
# The resolved value should replace the ${VAR} reference.
assert raw["llm"]["connection"]["api_key"] == "sk-resolved-123", (
"llm.connection.api_key should be expanded from env var"
)
def test_expand_config_expands_builtin_tool_config(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_expand_config_env_vars`` resolves ``${VAR}`` in
``tools.builtins`` dict-entry config fields.
"""
monkeypatch.setenv("PPLX_KEY", "pplx-resolved")
from omnigent.spec import expand_env_vars
raw: dict[str, Any] = {
"spec_version": 1,
"tools": {
"builtins": [
"web_search",
{"name": "web_search_pplx", "api_key": "${PPLX_KEY}"},
],
},
}
changed = _expand_config_env_vars(raw, expand_env_vars)
assert changed is True
# String entries are untouched.
assert raw["tools"]["builtins"][0] == "web_search"
# Dict entry api_key should be expanded.
entry = raw["tools"]["builtins"][1]
assert entry["api_key"] == "pplx-resolved", (
"builtin tool api_key should be expanded from env var"
)
# 'name' is preserved.
assert entry["name"] == "web_search_pplx"
def test_expand_config_expands_executor_connection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_expand_config_env_vars`` resolves ``${VAR}`` in
``executor.connection`` values.
The server no longer expands uploaded
bundles, so the client must resolve ``executor.connection`` (not
just ``llm.connection``) or local ``omnigent run`` specs using the
consolidated executor block would ship unresolved ``${VAR}``.
"""
monkeypatch.setenv("EXEC_API_KEY", "sk-exec-999")
from omnigent.spec import expand_env_vars
raw: dict[str, Any] = {
"spec_version": 1,
"executor": {
"type": "omnigent",
"model": "gpt-5.4-mini",
"connection": {"api_key": "${EXEC_API_KEY}"},
"config": {"harness": "openai-agents"},
},
}
changed = _expand_config_env_vars(raw, expand_env_vars)
assert changed is True
# If this is still "${EXEC_API_KEY}", the executor.connection branch
# was not wired and the secret would ship unresolved to the server.
assert raw["executor"]["connection"]["api_key"] == "sk-exec-999"
def test_expand_config_expands_executor_auth_api_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_expand_config_env_vars`` resolves ``${VAR}`` in
``executor.auth.api_key`` / ``base_url`` when ``type: api_key``.
Mirrors ``_parse_executor_auth``'s server-side expansion sites.
"""
monkeypatch.setenv("AUTH_KEY", "sk-auth-7")
monkeypatch.setenv("AUTH_URL", "https://llm.example.invalid/v1")
from omnigent.spec import expand_env_vars
raw: dict[str, Any] = {
"spec_version": 1,
"executor": {
"type": "omnigent",
"auth": {
"type": "api_key",
"api_key": "${AUTH_KEY}",
"base_url": "${AUTH_URL}",
},
},
}
changed = _expand_config_env_vars(raw, expand_env_vars)
assert changed is True
assert raw["executor"]["auth"]["api_key"] == "sk-auth-7"
assert raw["executor"]["auth"]["base_url"] == "https://llm.example.invalid/v1"
# 'type' is preserved (not a secret-bearing field).
assert raw["executor"]["auth"]["type"] == "api_key"
def test_expand_config_leaves_databricks_auth_untouched(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``executor.auth`` with a non-``api_key`` type is not expanded.
A ``type: databricks`` auth block carries a profile name, not a
secret value — expanding it would be wrong (and there is no
``api_key`` to resolve). ``changed`` stays ``False`` so the file
is bundled as-is.
"""
from omnigent.spec import expand_env_vars
raw: dict[str, Any] = {
"spec_version": 1,
"executor": {
"type": "omnigent",
"auth": {"type": "databricks", "profile": "my-profile"},
},
}
changed = _expand_config_env_vars(raw, expand_env_vars)
# No secret-bearing field present → nothing expanded.
assert changed is False
assert raw["executor"]["auth"] == {"type": "databricks", "profile": "my-profile"}
def test_expand_config_no_env_vars_returns_false() -> None:
"""
``_expand_config_env_vars`` returns ``False`` when the
config has no fields that need expansion.
"""
from omnigent.spec import expand_env_vars
raw: dict[str, Any] = {
"spec_version": 1,
"name": "simple-agent",
}
changed = _expand_config_env_vars(raw, expand_env_vars)
assert changed is False
def test_expand_config_unresolved_var_raises(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_expand_config_env_vars`` raises ``OmnigentError``
when a ``${VAR}`` reference cannot be resolved.
"""
monkeypatch.delenv("MISSING_KEY_12345", raising=False)
from omnigent.spec import expand_env_vars
raw: dict[str, Any] = {
"llm": {
"model": "gpt-5.4",
"connection": {"api_key": "${MISSING_KEY_12345}"},
},
}
with pytest.raises(OmnigentError, match="MISSING_KEY_12345"):
_expand_config_env_vars(raw, expand_env_vars)
# ── _resolve_bundle_env_vars ─────────────────────────
def test_resolve_bundle_expands_config_yaml(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_resolve_bundle_env_vars`` returns resolved
``config.yaml`` content with expanded env vars.
"""
monkeypatch.setenv("BUNDLE_TEST_KEY", "resolved-value")
_write_config(
tmp_path,
{
"spec_version": 1,
"llm": {
"model": "gpt-5.4",
"connection": {"api_key": "${BUNDLE_TEST_KEY}"},
},
},
)
resolved = _resolve_bundle_env_vars(tmp_path)
assert "config.yaml" in resolved
# Parse the resolved YAML and verify the value.
parsed = yaml.safe_load(resolved["config.yaml"])
assert parsed["llm"]["connection"]["api_key"] == "resolved-value"
def test_resolve_bundle_expands_mcp_headers(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_resolve_bundle_env_vars`` returns resolved MCP config
YAML with expanded header env vars.
"""
monkeypatch.setenv("MCP_TOKEN", "tok-abc")
_write_mcp_config(
tmp_path,
"github",
{
"name": "github",
"transport": "http",
"url": "http://localhost:9000/mcp",
"headers": {"Authorization": "Bearer ${MCP_TOKEN}"},
},
)
# config.yaml must exist (even if empty) for a valid agent dir.
_write_config(tmp_path, {"spec_version": 1})
resolved = _resolve_bundle_env_vars(tmp_path)
arcname = "tools/mcp/github.yaml"
assert arcname in resolved
parsed = yaml.safe_load(resolved[arcname])
assert parsed["headers"]["Authorization"] == "Bearer tok-abc"
def test_resolve_bundle_expands_mcp_env(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_resolve_bundle_env_vars`` resolves ``${VAR}`` in a stdio MCP
server's ``env`` block, not just HTTP ``headers``.
Stdio ``env`` is a server-side expansion site,
so the client must resolve it too or a local stdio MCP server using
``${VAR}`` would receive the literal reference after the server
stopped expanding uploaded bundles.
"""
monkeypatch.setenv("STDIO_SECRET", "stdio-tok-xyz")
_write_mcp_config(
tmp_path,
"local-tool",
{
"name": "local-tool",
"transport": "stdio",
"command": "my-mcp-server",
"env": {"API_TOKEN": "${STDIO_SECRET}"},
},
)
_write_config(tmp_path, {"spec_version": 1})
resolved = _resolve_bundle_env_vars(tmp_path)
arcname = "tools/mcp/local-tool.yaml"
assert arcname in resolved
parsed = yaml.safe_load(resolved[arcname])
# If this is still "${STDIO_SECRET}", the stdio env branch was not
# wired and the local MCP subprocess would get a literal reference.
assert parsed["env"]["API_TOKEN"] == "stdio-tok-xyz"
def test_resolve_bundle_no_env_vars_returns_empty(
tmp_path: Path,
) -> None:
"""
``_resolve_bundle_env_vars`` returns an empty dict when
the config has no env var references.
"""
_write_config(
tmp_path,
{
"spec_version": 1,
"name": "plain-agent",
},
)
resolved = _resolve_bundle_env_vars(tmp_path)
assert resolved == {}
def test_resolve_bundle_missing_env_var_raises(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_resolve_bundle_env_vars`` raises ``OmnigentError``
when a config.yaml env var cannot be resolved.
"""
monkeypatch.delenv("NONEXISTENT_DEPLOY_KEY", raising=False)
_write_config(
tmp_path,
{
"spec_version": 1,
"tools": {
"builtins": [
{
"name": "web_search_goog",
"api_key": "${NONEXISTENT_DEPLOY_KEY}",
},
],
},
},
)
with pytest.raises(OmnigentError, match="NONEXISTENT_DEPLOY_KEY"):
_resolve_bundle_env_vars(tmp_path)
# ── _bundle integration tests ──────────────────────────
def _extract_yaml_from_bundle(
bundle_bytes: bytes,
arcname: str,
) -> dict[str, Any]:
"""
Extract and parse a YAML file from a tar.gz bundle.
:param bundle_bytes: The gzipped tarball bytes.
:param arcname: The archive member name, e.g.
``"config.yaml"`` or ``"tools/mcp/github.yaml"``.
:returns: The parsed YAML content as a dict.
"""
with tarfile.open(fileobj=io.BytesIO(bundle_bytes), mode="r:gz") as tf:
member = tf.getmember(arcname)
extracted = tf.extractfile(member)
assert extracted is not None, f"Expected {arcname!r} to be a regular file in the bundle"
# ``yaml.safe_load`` returns ``Any``; the caller declares
# ``dict[str, Any]``. Every call site feeds this real YAML
# config bundles whose top-level is a mapping, so the cast
# is sound and matches the annotated return type.
parsed = yaml.safe_load(extracted.read())
assert isinstance(parsed, dict), (
f"Expected {arcname!r} to parse to a dict, got {type(parsed).__name__}"
)
return cast(dict[str, Any], parsed)
def test_bundle_resolves_config_env_vars(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_bundle`` produces a tarball where ``config.yaml`` has
``${VAR}`` references replaced with resolved values.
Verifies the end-to-end path: write agent dir with env var
refs → call ``_bundle`` → extract tarball → assert resolved.
"""
monkeypatch.setenv("BUNDLE_LLM_KEY", "sk-live-abc123")
monkeypatch.setenv("BUNDLE_PPLX_KEY", "pplx-live-xyz")
_write_config(
tmp_path,
{
"spec_version": 1,
"name": "env-test-agent",
"llm": {
"model": "openai/gpt-4o",
"connection": {"api_key": "${BUNDLE_LLM_KEY}"},
},
"tools": {
"builtins": [
"web_search",
{
"name": "web_search_pplx",
"api_key": "${BUNDLE_PPLX_KEY}",
},
],
},
},
)
bundle_bytes = _bundle(tmp_path)
parsed = _extract_yaml_from_bundle(bundle_bytes, "config.yaml")
# LLM connection key must be resolved — if still "${BUNDLE_LLM_KEY}",
# the server would receive an unresolved reference it can't expand.
assert parsed["llm"]["connection"]["api_key"] == "sk-live-abc123", (
"LLM api_key should be resolved in the bundle tarball"
)
# Builtin tool config key must be resolved.
perplexity_entry = parsed["tools"]["builtins"][1]
assert perplexity_entry["api_key"] == "pplx-live-xyz", (
"Builtin tool api_key should be resolved in the bundle tarball"
)
assert perplexity_entry["name"] == "web_search_pplx", (
"Builtin tool name must be preserved after expansion"
)
# String entries pass through unchanged.
assert parsed["tools"]["builtins"][0] == "web_search", (
"String builtin entries should be unchanged in the bundle"
)
# Non-secret fields survive bundling.
assert parsed["name"] == "env-test-agent"
assert parsed["llm"]["model"] == "openai/gpt-4o"
def test_bundle_resolves_mcp_header_env_vars(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_bundle`` produces a tarball where MCP server YAML files
have ``${VAR}`` references in headers replaced with resolved
values.
"""
monkeypatch.setenv("BUNDLE_GH_TOKEN", "ghp-secret-tok")
_write_config(tmp_path, {"spec_version": 1, "name": "mcp-agent"})
_write_mcp_config(
tmp_path,
"github",
{
"name": "github",
"transport": "http",
"url": "http://localhost:9000/mcp",
"headers": {"Authorization": "Bearer ${BUNDLE_GH_TOKEN}"},
},
)
bundle_bytes = _bundle(tmp_path)
parsed = _extract_yaml_from_bundle(bundle_bytes, "tools/mcp/github.yaml")
# Header must be resolved — an unresolved "${BUNDLE_GH_TOKEN}"
# would cause MCP auth failures on the server.
assert parsed["headers"]["Authorization"] == "Bearer ghp-secret-tok", (
"MCP header env var should be resolved in the bundle tarball"
)
# Non-header fields survive bundling.
assert parsed["name"] == "github"
assert parsed["url"] == "http://localhost:9000/mcp"
def test_bundle_no_env_vars_preserves_files(
tmp_path: Path,
) -> None:
"""
``_bundle`` produces a valid tarball even when no env vars
need expansion — files are included as-is.
"""
_write_config(
tmp_path,
{
"spec_version": 1,
"name": "plain-agent",
"llm": {"model": "openai/gpt-4o"},
},
)
bundle_bytes = _bundle(tmp_path)
parsed = _extract_yaml_from_bundle(bundle_bytes, "config.yaml")
# Config content should be preserved exactly.
assert parsed["name"] == "plain-agent"
assert parsed["llm"]["model"] == "openai/gpt-4o"
def test_bundle_materializes_standalone_omnigent_yaml(tmp_path: Path) -> None:
"""
``_bundle`` wraps a standalone omnigent YAML file in a tarball.
``omnigent run <yaml> --server`` uploads the returned bytes
directly to ``POST /api/agents``. If the YAML bytes are passed
through unchanged, the remote server rejects them as an invalid
tarball before the runner tunnel can start.
"""
yaml_path = tmp_path / "hello.yaml"
yaml_path.write_text(
yaml.safe_dump(
{
"name": "hello",
"prompt": "Say hi briefly.",
"executor": {"harness": "claude-sdk", "model": "test-model"},
},
sort_keys=False,
)
)
bundle_bytes = _bundle(yaml_path)
parsed = _extract_yaml_from_bundle(bundle_bytes, "hello.yaml")
assert parsed["name"] == "hello"
assert parsed["prompt"] == "Say hi briefly."
assert parsed["executor"]["harness"] == "claude-sdk"
def test_bundle_passthrough_existing_tarball(
tmp_path: Path,
) -> None:
"""
``_bundle`` returns the raw bytes of an existing ``.tar.gz``
file without modification (env var expansion only applies to
directories).
"""
# Build a tarball with an unresolved env var reference.
config_bytes = yaml.dump(
{
"spec_version": 1,
"llm": {"connection": {"api_key": "${SHOULD_NOT_EXPAND}"}},
}
).encode()
tarball_path = tmp_path / "agent.tar.gz"
with tarfile.open(tarball_path, mode="w:gz") as tf:
info = tarfile.TarInfo(name="config.yaml")
info.size = len(config_bytes)
tf.addfile(info, io.BytesIO(config_bytes))
bundle_bytes = _bundle(tarball_path)
# Passthrough: bytes must match the original file exactly.
assert bundle_bytes == tarball_path.read_bytes(), (
"Existing tarball should be returned as-is without expansion"
)
def test_bundle_missing_env_var_raises(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``_bundle`` raises ``OmnigentError`` when the agent
directory contains an unresolvable ``${VAR}`` reference.
"""
monkeypatch.delenv("NONEXISTENT_BUNDLE_KEY", raising=False)
_write_config(
tmp_path,
{
"spec_version": 1,
"llm": {
"model": "openai/gpt-4o",
"connection": {"api_key": "${NONEXISTENT_BUNDLE_KEY}"},
},
},
)
with pytest.raises(OmnigentError, match="NONEXISTENT_BUNDLE_KEY"):
_bundle(tmp_path)
# ── _preregister_agent ──────────────────────────────────────
class _RecordingAgentStore:
"""
In-memory agent store stub capturing the exact shape
:func:`_preregister_agent` writes. Avoids MagicMock so an
accidental attribute access on a missing method surfaces as
``AttributeError`` instead of silently returning a MagicMock.
"""
def __init__(self) -> None:
self.created: list[dict[str, Any]] = []
def get_by_name(self, name: str) -> None:
""":returns: Always ``None`` — fresh store, no collisions."""
del name
return
def delete(self, agent_id: str) -> None:
"""Stubbed — replace-path not exercised by these tests."""
raise AssertionError(
f"delete() called unexpectedly with {agent_id!r} — "
f"tests were not supposed to hit the replace path."
)
def create(
self,
*,
agent_id: str,
name: str,
bundle_location: str,
description: str | None,
) -> None:
"""Record the create-call for assertions."""
self.created.append(
{
"agent_id": agent_id,
"name": name,
"bundle_location": bundle_location,
"description": description,
},
)
class _RecordingArtifactStore:
"""
In-memory artifact store stub.
"""
def __init__(self) -> None:
self.puts: list[tuple[str, bytes]] = []
def put(self, location: str, data: bytes) -> None:
"""Record the put-call for assertions."""
self.puts.append((location, data))
def delete(self, location: str) -> None:
"""Stubbed — replace-path not exercised."""
raise AssertionError(f"delete() called unexpectedly with {location!r}")
class _RecordingAgentCache:
"""
In-memory AgentCache stub. Captures the disk-cache swap path
so tests can assert ``replace()`` is wired up correctly without
hitting a real on-disk cache directory. Replace-path isn't
exercised by the create-only tests (no existing agent), so
``replace`` is stubbed to assert if unexpectedly called.
"""
def __init__(self) -> None:
self.replaces: list[tuple[str, str, bytes]] = []
def replace(
self,
agent_id: str,
location: str,
data: bytes,
*,
expand_env: bool = False,
) -> None:
"""Record the replace-call for assertions.
:param agent_id: Agent id being replaced.
:param location: New bundle location.
:param data: New bundle bytes.
:param expand_env: Ignored — accepted to match the real
``AgentCache.replace`` signature (
``_preregister_agent`` passes ``expand_env=True`` for the
operator template). Omitting it would raise ``TypeError``.
"""
del expand_env
self.replaces.append((agent_id, location, data))
def test_preregister_agent_accepts_directory(tmp_path: Path) -> None:
"""
A directory source (``config.yaml`` + assets) registers as the
canonical agent-image bundle. The stored bytes round-trip
through ``spec.load`` to the same name the YAML declared.
What breaks if this fails: ``omnigent server --agent my-agent/``
regresses — the standard case every omnigent user exercises.
"""
agent_dir = tmp_path / "native-agent"
agent_dir.mkdir()
_write_config(
agent_dir,
{
"spec_version": 1,
"name": "native-agent",
"executor": {"config": {"harness": "openai-agents"}},
},
)
agent_store = _RecordingAgentStore()
artifact_store = _RecordingArtifactStore()
agent_cache = _RecordingAgentCache()
agent_id = _preregister_agent(agent_dir, agent_store, artifact_store, agent_cache)
# Exactly one create + one put.
assert len(agent_store.created) == 1
assert len(artifact_store.puts) == 1
created = agent_store.created[0]
assert agent_id == created["agent_id"]
assert created["name"] == "native-agent"
# bundle_location is the deterministic ``<agent_id>/<sha256>``
# shape; checking it starts with the recorded agent_id is
# sufficient — the sha256 is an implementation detail.
assert created["bundle_location"].startswith(created["agent_id"] + "/")
# Artifact store received the same location and non-empty
# bytes.
put_location, put_bytes = artifact_store.puts[0]
assert put_location == created["bundle_location"]
assert len(put_bytes) > 0
def test_preregister_agent_accepts_omnigent_yaml_file(tmp_path: Path) -> None:
"""
A standalone omnigent YAML file registers identically — the
spec's ``name`` field becomes the agent name, and the tarball
stored in the artifact store round-trips through
``_find_omnigent_yaml_in_dir`` to the same spec.
What breaks if this fails: ``omnigent server --agent coding_supervisor.yaml``
either crashes on the bundle step or stores a broken tarball
the server later fails to rehydrate.
"""
yaml_path = tmp_path / "hello.yaml"
yaml_path.write_text(
yaml.dump(
{
"name": "hello-world",
"prompt": "hi",
"executor": {
"model": "databricks-claude-sonnet-4",
"harness": "claude-sdk",
},
},
),
)
agent_store = _RecordingAgentStore()
artifact_store = _RecordingArtifactStore()
agent_cache = _RecordingAgentCache()
agent_id = _preregister_agent(yaml_path, agent_store, artifact_store, agent_cache)
# The YAML's ``name`` field flows through the omnigent
# adapter into the AgentSpec and lands on the stored row.
# Any regression in that translation would surface here.
assert len(agent_store.created) == 1
assert agent_id == agent_store.created[0]["agent_id"]
assert agent_store.created[0]["name"] == "hello-world"
# A put happened with the tarball bytes.
assert len(artifact_store.puts) == 1
assert len(artifact_store.puts[0][1]) > 0
def test_preregister_agent_stored_tarball_rehydrates(tmp_path: Path) -> None:
"""
The bytes written to the artifact store must be a valid tarball
that, when extracted and loaded, produces the same spec name.
Catches regressions where the tarball shape drifts (wrong
``arcname``, nested bundle dirs, etc.) — the rehydrate-and-load
round-trip is the server's runtime contract.
"""
import tempfile
from omnigent.spec import load
yaml_path = tmp_path / "supervisor.yaml"
yaml_path.write_text(
yaml.dump(
{
"name": "supervisor-probe",
"prompt": "probe",
"executor": {
"model": "databricks-claude-sonnet-4",
"harness": "claude-sdk",
},
},
),
)
agent_store = _RecordingAgentStore()
artifact_store = _RecordingArtifactStore()
agent_cache = _RecordingAgentCache()
_preregister_agent(yaml_path, agent_store, artifact_store, agent_cache)
# Extract the stored bytes and re-load through the standard
# entrypoint. A successful load with the right name proves the
# bundle shape is correct end-to-end.
_, stored_bytes = artifact_store.puts[0]
with tempfile.TemporaryDirectory() as extracted_dir:
spec = load(stored_bytes, dest=Path(extracted_dir))
assert spec.name == "supervisor-probe"
# ── no-AGENT harness launch ───────────────────────────
def test_materialize_harness_launcher_file_writes_omnigent_yaml() -> None:
"""No-AGENT run materialization writes a standalone Omnigent YAML file."""
generated = _materialize_harness_launcher_file(
harness="claude",
model="databricks-claude-sonnet-4-6",
system_prompt="Custom instructions.",
)
assert generated.name == "claude-sdk.yaml"
assert generated.is_file()
raw = yaml.safe_load(generated.read_text())
assert raw == {
"name": "claude",
"prompt": "Custom instructions.",
"executor": {
"harness": "claude-sdk",
"model": "databricks-claude-sonnet-4-6",
},
"os_env": {"type": "caller_process", "sandbox": {"type": "none"}},
}
# The launcher must NEVER bake a Databricks profile into the ad-hoc
# spec (the --profile flag was removed): a baked profile would make
# _resolve_provider_for_build skip a configured provider and route the
# turn through the Databricks gateway. A "profile" key here means the
# removed baking behavior came back.
assert "profile" not in raw["executor"], raw["executor"]
def test_materialize_harness_launcher_file_acp_slug() -> None:
"""``run --harness acp:<slug>`` produces a valid spec that keeps the slug.
acp:<slug> canonicalizes to the base ``acp`` harness, but the slug selects a
configured ACP agent resolved at spawn — so executor.harness must keep the
full ``acp:<slug>``. The agent name has no colon (the validator requires
[a-zA-Z0-9_-]+), so it is sanitized to ``acp-<slug>``.
"""
import re
generated = _materialize_harness_launcher_file(
harness="acp:qwenacp", model=None, system_prompt=None
)
raw = yaml.safe_load(generated.read_text())
assert raw["executor"]["harness"] == "acp:qwenacp" # slug preserved for the runner
assert raw["name"] == "acp-qwenacp"
assert re.fullmatch(r"[a-zA-Z0-9_-]+", raw["name"]) # passes the agent-name validator
def test_materialize_harness_launcher_file_kimi_gets_os_env() -> None:
"""``run --harness kimi`` bakes a caller-process ``os_env`` so the SDK kimi
operates in the user's current directory, matching claude-sdk.
Regression: kimi was missing from ``_OS_ENV_HARNESSES``, so its launcher
spec had no ``os_env`` block and the headless harness ran detached from the
cwd (the user saw it operate out of the /tmp spec dir, not their folder)."""
for alias in ("kimi", "kimi-code"):
generated = _materialize_harness_launcher_file(
harness=alias, model=None, system_prompt=None
)
raw = yaml.safe_load(generated.read_text())
assert raw["executor"]["harness"] == "kimi"
assert raw["os_env"] == {"type": "caller_process", "sandbox": {"type": "none"}}
def test_run_without_agent_drops_into_configure_when_unconfigured(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Bare ``run`` (no AGENT, no --harness) with nothing configured drops into
``configure harnesses`` and exits cleanly — it no longer hard-errors.
The old "requires --harness" guidance was removed by the first-run
smart-defaults path (``cli._resolve_first_run_plan``): a bare first ``run``
now derives a harness from configured creds, or — when nothing is set up —
offers ``configure harnesses`` and exits cleanly rather than erroring. The
unconfigured decision itself is unit-tested in
``test_resolve_first_run_plan_drops_into_configure_when_empty``; this test
pins the ``run``-command wiring at the CLI layer.
Fully isolated so it neither depends on the developer's ambient creds nor
mutates the real ``~/.omnigent`` config, and never launches a daemon.
"""
# Empty config + no detectable provider before/after configure, so the
# first-run plan resolves to "nothing configured".
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
monkeypatch.setattr("omnigent.cli._promote_global_auth_to_provider", Mock())
monkeypatch.setattr("omnigent.cli._adopt_detected_providers", Mock(return_value=[]))
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for(), # nothing configured
)
# The kimi fallback in ``_pick_first_run_harness`` gates on the ``kimi``
# binary being on PATH. Stub it to False so the test stays deterministic
# on machines where the developer has kimi installed.
monkeypatch.setattr(
"omnigent.onboarding.harness_install.harness_cli_installed",
lambda _key: False,
)
# The configure picker would block on a real terminal; stub it.
configure = Mock()
monkeypatch.setattr("omnigent.cli._run_configure_harnesses_interactive", configure)
result = CliRunner().invoke(cli, ["run"])
# Exits cleanly (no error, no daemon launch) having dropped into configure.
assert result.exit_code == 0, result.output
configure.assert_called_once_with()
assert "Found no harnesses configured." in result.output
# The removed hard-error guidance must not reappear (regression guard).
assert "Provide an AGENT path" not in result.output
def test_run_without_agent_claude_alias_dispatches_generated_yaml_headlessly(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``run --harness -p`` dispatches headlessly with the generated YAML.
Under the daemon-backed model, headless ``-p`` (without ``--no-session``)
runs against the daemon-backed server via ``run_chat`` rather than the
legacy in-process ``run_prompt``.
"""
# Isolate from any real ~/.omnigent/config.yaml on the developer's machine
# (config defaults and ambient creds must not leak into the generated YAML
# or the dispatch kwargs asserted below).
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setenv("OMNIGENT_DISABLE_KEYRING", "1")
monkeypatch.setenv("HOME", str(tmp_path))
for _var in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"):
monkeypatch.delenv(_var, raising=False)
run_chat = Mock()
run_prompt = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
monkeypatch.setattr("omnigent.chat.run_prompt", run_prompt)
result = CliRunner().invoke(
cli,
[
"run",
"--harness",
"claude",
"--model",
"databricks-claude-sonnet-4-6",
"--system-prompt",
"Custom instructions.",
"--tools",
"coding",
"-p",
"hello",
],
)
assert result.exit_code == 0, result.output
run_prompt.assert_not_called()
run_chat.assert_called_once()
kwargs = run_chat.call_args.kwargs
generated = Path(kwargs["target"])
assert generated.suffix == ".yaml"
raw = yaml.safe_load(generated.read_text())
assert raw == {
"name": "claude",
"prompt": "Custom instructions.",
"executor": {
"harness": "claude-sdk",
"model": "databricks-claude-sonnet-4-6",
},
"os_env": {"type": "caller_process", "sandbox": {"type": "none"}},
}
# Daemon-backed one-shot: server_url=None (local daemon backend) and
# ephemeral=False. harness/model are already baked into the generated
# YAML, so they pass through as None.
assert kwargs["client_tools"] == "coding"
assert kwargs["server_url"] is None
assert kwargs["ephemeral"] is False
assert kwargs["prompt"] == "hello"
def _write_default_agent(tmp_path: Path, harness: str) -> str:
"""Write a minimal default-agent YAML declaring *harness*; return its path.
:param tmp_path: pytest temp dir.
:param harness: The ``executor.harness`` value, e.g. ``"openai-agents"``.
:returns: Absolute path to the written YAML.
"""
agent = tmp_path / "default_agent.yaml"
agent.write_text(f"name: a\nexecutor:\n harness: {harness}\n model: databricks-gpt-5-5\n")
return str(agent)
def test_resolve_default_agent_target_no_default_agent() -> None:
"""With no default_agent, the target is None (no-AGENT launcher / error path)."""
assert _resolve_default_agent_target(None, "codex") is None
assert _resolve_default_agent_target(None, None) is None
def test_resolve_default_agent_target_no_harness_uses_default(tmp_path: Path) -> None:
"""No --harness → the configured default_agent is used (unchanged behavior)."""
agent = _write_default_agent(tmp_path, "openai-agents")
assert _resolve_default_agent_target(agent, None) == agent
def test_resolve_default_agent_target_matching_harness_uses_default(tmp_path: Path) -> None:
"""--harness matching the default agent's harness → use the configured agent."""
agent = _write_default_agent(tmp_path, "openai-agents")
# canonicalize_harness("openai-agents") == the YAML's canonicalized harness.
assert _resolve_default_agent_target(agent, "openai-agents") == agent
def test_resolve_default_agent_target_mismatched_harness_warns_and_falls_back(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""
A --harness that differs from the default agent's harness warns and returns
None, so a minimal built-in agent launches instead of forcing the wrong
harness onto the configured (e.g. gpt) spec.
"""
agent = _write_default_agent(tmp_path, "openai-agents")
result = _resolve_default_agent_target(agent, "claude-sdk")
# Falls back to the minimal launcher (None), NOT the openai-agents default agent.
assert result is None
err = capsys.readouterr().err
# Warning names both the default agent's harness and the requested one.
assert "openai-agents" in err
assert "claude-sdk" in err
def test_run_without_agent_unsupported_harness_fails_before_dispatch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Unsupported no-AGENT harness values fail before run_chat dispatch."""
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(cli, ["run", "--harness", "unknown"])
assert result.exit_code != 0
assert "Unsupported harness 'unknown'" in result.output
run_chat.assert_not_called()
def test_run_with_agent_unsupported_harness_fails_before_dispatch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Unsupported harness values are validated for existing AGENT mode too."""
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(
cli,
["run", "tests/resources/examples/hello_world.yaml", "--harness", "unknown"],
)
assert result.exit_code != 0
assert "Unsupported harness 'unknown'" in result.output
run_chat.assert_not_called()
def test_run_with_agent_accepts_openai_agents_sdk_alias(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``--harness openai-agents-sdk`` passes validation and dispatches.
This is the spelling the project docs use in run examples; before
the alias existed, ``_validate_harness`` rejected it as unsupported.
"""
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(
cli,
["run", "tests/resources/examples/hello_world.yaml", "--harness", "openai-agents-sdk"],
)
assert result.exit_code == 0, result.output
# Dispatch happened — the alias cleared validation. The canonical
# rewrite happens later, at override materialization.
run_chat.assert_called_once()
@pytest.mark.parametrize("flag", ["--omnigent", "--no-sessions-api"])
def test_removed_runner_flow_flags_are_rejected(flag: str) -> None:
"""Removed runner-flow escape hatches are no longer accepted by click."""
result = CliRunner().invoke(cli, ["run", "tests/resources/examples/hello_world.yaml", flag])
assert result.exit_code != 0
# click 8.2 reworded this from "No such option: --x" to "No such option
# '--x'." (and may append a "Did you mean" suggestion), so match loosely.
assert "No such option" in result.output and flag in result.output
def test_attach_without_server_errors_loud(monkeypatch: pytest.MonkeyPatch) -> None:
"""``attach`` fails loud when there is no server to join — it never spawns one."""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
# No --server, no configured server, and no running local server.
monkeypatch.setattr("omnigent.cli.local_server_url_if_healthy", lambda: None)
result = CliRunner().invoke(cli, ["attach", "conv_abc"])
assert result.exit_code != 0
assert "No server to attach to" in result.output
def test_attach_without_conversation_errors_loud(monkeypatch: pytest.MonkeyPatch) -> None:
"""``attach`` with a server but no conversation id fails loud (no picker, no spawn)."""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
result = CliRunner().invoke(cli, ["attach", "--server", "http://localhost:8000"])
assert result.exit_code != 0
assert "Nothing to attach to" in result.output
def test_run_with_agent_still_dispatches_existing_path(monkeypatch: pytest.MonkeyPatch) -> None:
"""Existing ``run AGENT --harness`` behavior still passes through."""
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(
cli,
["run", "tests/resources/examples/hello_world.yaml", "--harness", "codex", "--model", "m"],
)
assert result.exit_code == 0, result.output
run_chat.assert_called_once_with(
target="tests/resources/examples/hello_world.yaml",
client_tools=None,
server_url=None,
harness="codex",
model="m",
prompt=None,
system_prompt=None,
ephemeral=False,
resume_conversation_id=None,
resume_latest=False,
resume_picker=False,
fork_session_id=None,
log=False,
debug_events=False,
resume_parts=[
"cli",
"run",
"tests/resources/examples/hello_world.yaml",
"--harness",
"codex",
"--model",
"m",
],
# Interactive ``run`` (no -p) defaults the browser-open ON.
auto_open_conversation=True,
)
def test_run_resume_picker_forwards_to_run_chat(monkeypatch: pytest.MonkeyPatch) -> None:
"""Bare ``--resume`` forwards as ``resume_picker=True``."""
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(
cli,
[
"run",
"tests/resources/examples/hello_world.yaml",
"--resume",
"--continue",
"--no-session",
"--log",
],
)
assert result.exit_code == 0, result.output
run_chat.assert_called_once_with(
target="tests/resources/examples/hello_world.yaml",
client_tools=None,
server_url=None,
harness=None,
model=None,
prompt=None,
system_prompt=None,
ephemeral=True,
resume_conversation_id=None,
resume_latest=True,
resume_picker=True,
fork_session_id=None,
log=True,
debug_events=False,
resume_parts=[
"cli",
"run",
"tests/resources/examples/hello_world.yaml",
"--log",
],
# Interactive ``run`` (no -p) defaults the browser-open ON.
auto_open_conversation=True,
)
def test_run_resume_with_conversation_id_forwards_to_run_chat(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``--resume <id>`` forwards as ``resume_conversation_id`` (not picker)."""
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(
cli,
[
"run",
"examples/hello_world.yaml",
"--resume",
"conv_123",
"--no-session",
"--log",
],
)
assert result.exit_code == 0, result.output
run_chat.assert_called_once_with(
target="examples/hello_world.yaml",
client_tools=None,
server_url=None,
harness=None,
model=None,
prompt=None,
system_prompt=None,
ephemeral=True,
resume_conversation_id="conv_123",
resume_latest=False,
resume_picker=False,
fork_session_id=None,
log=True,
debug_events=False,
resume_parts=["cli", "run", "examples/hello_world.yaml", "--log"],
# Interactive ``run`` (no -p) defaults the browser-open ON.
auto_open_conversation=True,
)
def test_attach_forwards_live_conversation_to_run_attach(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``attach <id> --server`` joins the live conversation via ``run_attach``
(the co-drive client that dispatches to the host's existing runner)."""
# Isolate from the developer's real ~/.omnigent config (a configured
# server/auto-open default would otherwise leak into the asserted
# run_attach kwargs).
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
# The session-exists probe is exercised separately; here we assert the forward.
monkeypatch.setattr("omnigent.cli._require_live_conversation", lambda **_kw: None)
run_attach = Mock()
monkeypatch.setattr("omnigent.chat.run_attach", run_attach)
result = CliRunner().invoke(cli, ["attach", "conv_456", "--server", "http://localhost:8000"])
assert result.exit_code == 0, result.output
# The server URL + live conversation id are forwarded; no harness/model/
# ephemeral knobs exist on attach (the host owns the agent + persistence).
run_attach.assert_called_once_with(
base_url="http://localhost:8000",
conversation_id="conv_456",
client_tools=None,
debug_events=False,
auto_open_conversation=False,
resume_parts=["cli", "attach", "conv_456", "--server", "http://localhost:8000"],
)
def test_attach_nonlive_conversation_errors_loud_without_connecting(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``attach`` fails loud when the session is not live, and never calls run_attach."""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
# Server reports the conversation does not exist (404).
monkeypatch.setattr(
"omnigent.cli._host_http_json",
lambda **_kw: _HostHttpResult(status_code=404, body={"detail": "not found"}),
)
run_attach = Mock()
monkeypatch.setattr("omnigent.chat.run_attach", run_attach)
result = CliRunner().invoke(cli, ["attach", "conv_x", "--server", "http://localhost:8000"])
assert result.exit_code != 0
assert "No live session" in result.output
run_attach.assert_not_called()
def test_resume_flags_with_prompt_dispatch_to_session_path(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Headless ``-p`` can resume by routing through the session-backed chat path."""
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
run_chat = Mock()
run_prompt = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
monkeypatch.setattr("omnigent.chat.run_prompt", run_prompt)
result = CliRunner().invoke(
cli,
["run", "tests/resources/examples/hello_world.yaml", "-p", "hi", "--resume"],
)
assert result.exit_code == 0, result.output
run_prompt.assert_not_called()
run_chat.assert_called_once_with(
target="tests/resources/examples/hello_world.yaml",
client_tools=None,
server_url=None,
harness=None,
model=None,
prompt="hi",
system_prompt=None,
ephemeral=False,
resume_conversation_id=None,
resume_latest=False,
resume_picker=True,
debug_events=False,
auto_open_conversation=False,
)
def test_run_with_agent_prompt_dispatches_headlessly(monkeypatch: pytest.MonkeyPatch) -> None:
"""``run AGENT -p`` runs one-shot against the daemon-backed server.
Without ``--no-session``, headless ``-p`` routes through ``run_chat``
(daemon-backed), not the legacy in-process ``run_prompt``.
"""
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
run_chat = Mock()
run_prompt = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
monkeypatch.setattr("omnigent.chat.run_prompt", run_prompt)
result = CliRunner().invoke(
cli,
[
"run",
"tests/resources/examples/hello_world.yaml",
"--harness",
"codex",
"--model",
"m",
"-p",
"hi",
],
)
assert result.exit_code == 0, result.output
run_prompt.assert_not_called()
run_chat.assert_called_once_with(
target="tests/resources/examples/hello_world.yaml",
client_tools=None,
server_url=None,
harness="codex",
model="m",
prompt="hi",
system_prompt=None,
ephemeral=False,
debug_events=False,
auto_open_conversation=False,
)
def test_dispatch_rejects_positional_server_url(monkeypatch: pytest.MonkeyPatch) -> None:
"""Server addresses must be passed with ``--server``, not as AGENT."""
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
with pytest.raises(ClickException, match="Server URLs are no longer accepted"):
_dispatch_run(
target="http://localhost:8000",
tools=None,
harness=None,
model=None,
prompt=None,
system_prompt=None,
)
run_chat.assert_not_called()
def test_run_server_without_agent_dispatches_direct_server(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``run --server URL`` connects directly to that server."""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(cli, ["run", "--server", "http://localhost:8000"])
assert result.exit_code == 0, result.output
run_chat.assert_called_once_with(
target="http://localhost:8000",
client_tools=None,
server_url=None,
harness=None,
model=None,
prompt=None,
system_prompt=None,
ephemeral=False,
resume_conversation_id=None,
resume_latest=False,
resume_picker=False,
fork_session_id=None,
log=False,
debug_events=False,
resume_parts=["cli", "run", "--server", "http://localhost:8000"],
# Interactive ``run --server`` (no -p) defaults the browser-open ON,
# including for remote servers.
auto_open_conversation=True,
)
def test_run_server_resume_by_id_forwards_to_run_attach(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``run --server URL --resume <id>`` (no AGENT) resumes via ``run_attach``.
Direct ``--server`` with no AGENT is a non-spawning client: it has no local
agent and therefore never launches a runner. Resuming a specific
conversation is an ATTACH (co-drive the session's existing host-bound
runner), so it must route to ``run_attach`` — not the picker+create
``run_chat`` path, which entered a non-attach REPL and crashed at
runner-bind with "Sessions API dispatch requires a registered runner id"
the moment it tried to bind a runner it never started.
"""
# Isolate from the developer's real ~/.omnigent config so a configured
# server default can't leak into the asserted run_attach kwargs.
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
# The live-session probe (precise not-found error) is exercised by the
# attach tests; here we assert the forward, so stub it out.
monkeypatch.setattr("omnigent.cli._require_live_conversation", lambda **_kw: None)
monkeypatch.setattr("omnigent.chat._redirect_native_resume_if_needed", lambda **_kw: False)
run_attach = Mock()
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_attach", run_attach)
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(
cli, ["run", "--server", "http://localhost:8000", "--resume", "conv_456"]
)
assert result.exit_code == 0, result.output
# The picker+create ``run_chat`` path is what crashed at bind — resuming a
# direct-``--server`` session must never reach it. If this fails (run_chat
# called), the fix regressed and the cryptic runner-id error is back.
run_chat.assert_not_called()
# The resume id is handed to the attach co-drive client, which reads the
# agent from the session snapshot (no bogus agent picker) and pre-flights an
# online runner. ``--resume conv_456`` is stripped from the on-exit resume
# hint (the caller re-supplies it); browser-open defaults ON for an
# interactive (no -p) invocation.
run_attach.assert_called_once_with(
base_url="http://localhost:8000",
conversation_id="conv_456",
client_tools=None,
debug_events=False,
auto_open_conversation=True,
resume_parts=["cli", "run", "--server", "http://localhost:8000"],
)
def test_run_server_resume_native_redirects_before_attach_preflight(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Terminal-native ``run --server --resume`` redirects before attach checks.
The pre-attach liveness check is for Omnigent REPL co-drive. Native-wrapper
sessions need to hand off to ``omnigent claude`` / ``omnigent codex``
even when their old runner is gone, otherwise a cold native resume fails
before the wrapper can relaunch its terminal.
"""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
redirected: list[dict[str, object]] = []
def _redirect(**kwargs: object) -> bool:
"""Record the native redirect probe and claim the resume."""
redirected.append(kwargs)
return True
def _must_not_preflight(**_kwargs: object) -> None:
"""Fail if direct-server resume reaches attach-only preflight."""
raise AssertionError("native resume should redirect before live-session preflight")
def _must_not_attach(**_kwargs: object) -> None:
"""Fail if direct-server resume reaches Omnigent attach after native redirect."""
raise AssertionError("native resume should not call run_attach after redirect")
monkeypatch.setattr("omnigent.chat._redirect_native_resume_if_needed", _redirect)
monkeypatch.setattr("omnigent.cli._require_live_conversation", _must_not_preflight)
monkeypatch.setattr("omnigent.chat.run_attach", _must_not_attach)
result = CliRunner().invoke(
cli, ["run", "--server", "http://localhost:8000", "--resume", "conv_native"]
)
assert result.exit_code == 0, result.output
assert redirected == [
{
"base_url": "http://localhost:8000",
"conversation_id": "conv_native",
"auto_open_conversation": True,
}
]
def test_run_server_resume_with_prompt_does_not_silently_attach(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A one-shot ``-p`` with ``--server --resume`` must NOT reroute to attach.
The attach client has no prompt channel, so rerouting a ``-p`` invocation
there would silently drop the turn and leave the user in interactive
attach. The reroute is gated to the pure-interactive shape; a prompt falls
through to the existing remote-URL ``run_chat`` path (which one-shots /
fails loud), carrying the prompt forward rather than discarding it.
"""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
# If the reroute were taken this would fire; it must not be.
monkeypatch.setattr("omnigent.cli._require_live_conversation", lambda **_kw: None)
run_attach = Mock()
run_chat = Mock()
monkeypatch.setattr("omnigent.chat.run_attach", run_attach)
monkeypatch.setattr("omnigent.chat.run_chat", run_chat)
result = CliRunner().invoke(
cli,
["run", "--server", "http://localhost:8000", "--resume", "conv_456", "-p", "hello"],
)
assert result.exit_code == 0, result.output
# Prompt-bearing resume must stay off the attach path (no prompt channel).
run_attach.assert_not_called()
# ...and the prompt must reach run_chat, not be dropped. If run_chat is not
# called or prompt is None, the -p turn was silently lost (the P1 regression).
run_chat.assert_called_once()
assert run_chat.call_args.kwargs["prompt"] == "hello"
assert run_chat.call_args.kwargs["resume_conversation_id"] == "conv_456"
@pytest.mark.parametrize(
"extra_flags",
[
["--log"],
["--no-session"],
["--model", "gpt-x"],
["--system-prompt", "be terse"],
],
)
def test_run_server_resume_with_local_only_flag_fails_loud_not_attach(
monkeypatch: pytest.MonkeyPatch,
extra_flags: list[str],
) -> None:
"""Local-agent-only flags with ``--server --resume`` fail loud, not no-op.
``--log`` / ``--no-session`` / ``--model`` / ``--system-prompt`` have no
meaning against a remote server, so they must keep their existing
remote-URL fail-loud handling (in ``run_chat``) rather than being silently
swallowed by the attach reroute. The real ``run_chat`` is left unmocked so
its early validation raises before any network call.
"""
monkeypatch.setattr("omnigent.cli._load_effective_config", dict)
run_attach = Mock()
monkeypatch.setattr("omnigent.chat.run_attach", run_attach)
result = CliRunner().invoke(
cli,
["run", "--server", "http://localhost:8000", "--resume", "conv_456", *extra_flags],
)
# Fail loud (non-zero) with the existing "only apply to local agent paths"
# rejection — not a silent attach that ignores the flag.
assert result.exit_code != 0
assert "local agent" in result.output
run_attach.assert_not_called()
# ---------------------------------------------------------------------------
# Global config helpers
# ---------------------------------------------------------------------------
def test_load_global_config_uses_env_override(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``OMNIGENT_CONFIG_HOME`` redirects the user config path.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory used as a fake config home.
"""
config_home = tmp_path / "isolated"
config_home.mkdir()
(config_home / "config.yaml").write_text("server: https://isolated.example.com\n")
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(config_home))
result = _load_global_config()
assert result == {"server": "https://isolated.example.com"}
def test_load_global_config_returns_empty_when_missing(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``_load_global_config`` returns ``{}`` when the config file does not exist.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory used as a fake HOME.
"""
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", tmp_path / "config.yaml")
result = _load_global_config()
# No file → empty dict; a missing config is not an error
assert result == {}
def test_save_and_load_global_config_round_trips(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``_save_global_config`` persists values that ``_load_global_config``
reads back unchanged.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory used as a fake config location.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"default_agent": "examples/hello.yaml", "profile": "oss"})
result = _load_global_config()
# Both keys must survive the YAML round-trip intact
assert result == {"default_agent": "examples/hello.yaml", "profile": "oss"}
def test_save_global_config_merges_with_existing(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
A second ``_save_global_config`` call merges new keys without
overwriting existing ones that were not passed.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory used as a fake config location.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"default_agent": "examples/hello.yaml"})
_save_global_config({"profile": "oss"})
result = _load_global_config()
# Both keys must be present: second call must not clobber first
assert result == {"default_agent": "examples/hello.yaml", "profile": "oss"}
def test_save_global_config_unset_removes_key(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``_save_global_config`` with ``unset_keys`` removes specified keys.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory used as a fake config location.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"default_agent": "examples/hello.yaml", "server": "https://example.com"})
_save_global_config({}, unset_keys=("server",))
result = _load_global_config()
# server must be removed; default_agent must remain untouched
assert result == {"default_agent": "examples/hello.yaml"}
# ---------------------------------------------------------------------------
# _is_run_shorthand
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("argv", "expected"),
[
# YAML file paths — should be treated as shorthand for `run`
(["myagent.yaml"], True),
(["examples/hello_world.yaml"], True),
(["./relative.yaml"], True),
(["../sibling.yaml"], True),
# HTTP URLs must be passed through --server, not as shorthand targets.
(["http://localhost:8000"], False),
(["https://example.databricksapps.com"], False),
# Known subcommands — must NOT be redirected
(["run", "myagent.yaml"], False),
(["attach", "myagent.yaml"], False),
(["config", "list"], False),
(["version"], False),
# Flag-only argv — not a shorthand (ad-hoc check handles these)
(["--harness", "codex"], False),
([], False),
# Plain text that looks like a prompt — must NOT redirect
(["what does this repo do?"], False),
],
)
def test_is_run_shorthand(argv: list[str], expected: bool) -> None:
"""
``_is_run_shorthand`` returns True only for file-path targets.
:param argv: CLI arguments without program name.
:param expected: Whether the arguments should be redirected to ``run``.
"""
assert _is_run_shorthand(argv) is expected
# ---------------------------------------------------------------------------
# `omnigent config` command
# ---------------------------------------------------------------------------
def test_config_list_empty(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent config list`` prints a no-defaults message when neither
global nor project-level config files exist.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent and cwd.
"""
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", tmp_path / "config.yaml")
monkeypatch.setattr("omnigent.cli._load_local_config", dict)
# Isolate the defaults section — the credentials section reads ambient
# machine state (env keys / CLI logins), which is not under test here.
monkeypatch.setattr("omnigent.cli._print_credentials_by_harness", lambda: None)
result = CliRunner().invoke(cli, ["config", "list"])
assert result.exit_code == 0, result.output
# Must mention how to set defaults, not crash or print nothing
assert "none set" in result.output
@pytest.mark.parametrize(
("argv", "hint"),
[
# Pre-split flat forms → point at the new noun-verb subcommand.
(["config", "default_agent=foo.yaml"], "config set"),
(["config", "--list"], "config list"),
(["config", "--unset", "server"], "config unset"),
(["config", "--global", "server=https://example.com"], "--global"),
],
)
def test_config_legacy_form_hints_at_new_subcommand(argv: list[str], hint: str) -> None:
"""The removed flat ``config`` forms error with a hint at the new subcommand.
Without the ``_ConfigGroup`` nudge, click would emit an opaque
``No such command`` / ``No such option``; this proves a migrating user is
pointed at ``config set`` / ``config list`` / ``config unset`` instead.
:param argv: A legacy ``config`` invocation, e.g. ``["config", "--list"]``.
:param hint: A substring the error must contain, e.g. ``"config list"``.
"""
result = CliRunner().invoke(cli, argv)
assert result.exit_code != 0
assert hint in result.output
def test_config_set_global_writes_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent config set --global key=value`` persists the value so that
``_load_global_config`` returns it.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
result = CliRunner().invoke(
cli,
[
"config",
"set",
"--global",
"default_agent=examples/hello_world.yaml",
"model=databricks-claude-sonnet-4-6",
],
)
assert result.exit_code == 0, result.output
cfg = _load_global_config()
# default_agent is resolved to an absolute path for --global writes
assert Path(cfg["default_agent"]).is_absolute()
assert cfg["default_agent"].endswith("examples/hello_world.yaml")
# The second key from the same invocation must land too (multi-key set).
assert cfg["model"] == "databricks-claude-sonnet-4-6"
def test_config_set_global_writes_auto_open_conversation_bool(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``auto_open_conversation=true`` persists as a real YAML boolean.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
result = CliRunner().invoke(
cli,
["config", "set", "--global", "auto_open_conversation=true"],
)
assert result.exit_code == 0, result.output
cfg = yaml.safe_load(config_path.read_text(encoding="utf-8"))
assert cfg["auto_open_conversation"] is True
assert _resolve_auto_open_conversation_from_config(cfg) is True
def test_config_set_global_reports_effective_config_home(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``OMNIGENT_CONFIG_HOME`` redirects both the write and the reported path.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
result = CliRunner().invoke(
cli,
["config", "set", "--global", "auto_open_conversation=true"],
)
assert result.exit_code == 0, result.output
assert f"Set 1 key(s) in {tmp_path / 'config.yaml'}" in result.output
cfg = yaml.safe_load((tmp_path / "config.yaml").read_text(encoding="utf-8"))
assert cfg["auto_open_conversation"] is True
def test_config_set_rejects_invalid_auto_open_conversation(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``auto_open_conversation`` accepts only explicit boolean values.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", tmp_path / "config.yaml")
result = CliRunner().invoke(
cli,
["config", "set", "--global", "auto_open_conversation=maybe"],
)
assert result.exit_code != 0
assert "auto_open_conversation" in result.output
assert "must be a boolean" in result.output
def test_config_list_shows_saved_values(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent config list`` prints all defaults that were previously
written with ``config set --global``.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
monkeypatch.setattr("omnigent.cli._load_local_config", dict)
_save_global_config({"default_agent": "examples/hello_world.yaml", "model": "my-model"})
monkeypatch.setattr("omnigent.cli._print_credentials_by_harness", lambda: None)
result = CliRunner().invoke(cli, ["config", "list"])
assert result.exit_code == 0, result.output
# Both saved keys must appear in the defaults section
assert "default_agent=examples/hello_world.yaml" in result.output
assert "model=my-model" in result.output
def test_config_list_dedups_when_cwd_is_config_home(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``config list`` shows a shared config file once when cwd is its home.
When the command runs from the user's home directory, the project-level
path (``cwd/.omnigent/config.yaml``) resolves to the SAME file as the
user-level path (``~/.omnigent/config.yaml``). The defaults section
must dedup on the resolved absolute path and print that one file once,
not twice under two different spellings.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for the home dir.
"""
# Global config lives at ``<home>/.omnigent/config.yaml``; chdir to
# that same home dir so the local loader reads the identical file.
config_dir = tmp_path / ".omnigent"
config_dir.mkdir()
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_dir / "config.yaml")
monkeypatch.chdir(tmp_path) # cwd == home → local path resolves to global
monkeypatch.setattr("omnigent.cli._print_credentials_by_harness", lambda: None)
_save_global_config({"default_agent": "examples/hello_world.yaml"})
result = CliRunner().invoke(cli, ["config", "list"])
assert result.exit_code == 0, result.output
# Exactly one value line and one source-comment line. Before the
# absolute-path dedup fix this appeared twice — once under the hardcoded
# ``# ~/.omnigent/config.yaml`` literal and once under the resolved
# local path — because the two sources were compared by raw spelling,
# not resolved path. A count of 2 means the dedup regressed.
assert result.output.count("default_agent=examples/hello_world.yaml") == 1
source_comments = [ln for ln in result.output.splitlines() if ln.lstrip().startswith("# ")]
assert len(source_comments) == 1, f"expected one config source, got {source_comments}"
def test_config_unset_removes_key(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent config unset --global server`` removes the key from
the config file without touching other keys.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config(
{"default_agent": "examples/hello_world.yaml", "server": "https://example.com"}
)
result = CliRunner().invoke(cli, ["config", "unset", "--global", "server"])
assert result.exit_code == 0, result.output
cfg = _load_global_config()
# server must be gone; default_agent must remain
assert "server" not in cfg
assert cfg["default_agent"] == "examples/hello_world.yaml"
def test_config_unknown_key_raises_error(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent config set --global unknown=value`` rejects keys that are
not in ``_GLOBAL_CONFIG_KEYS``.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", tmp_path / "config.yaml")
result = CliRunner().invoke(cli, ["config", "set", "--global", "unknown=value"])
assert result.exit_code != 0
# Must name the bad key and the supported ones
assert "unknown" in result.output
assert any(k in result.output for k in _GLOBAL_CONFIG_KEYS)
def test_config_set_profile_rejected_as_unknown_key(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent config set profile=...`` fails with the unknown-key error.
Pins the removal of the ``profile`` config key alongside the
``--profile`` CLI flag: a user migrating from an older release must
get the fail-loud unknown-key message (listing the supported keys),
not a silently persisted-but-ignored value.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", tmp_path / "config.yaml")
result = CliRunner().invoke(cli, ["config", "set", "--global", "profile=oss"])
assert result.exit_code != 0
# Exit alone isn't enough — the error must name the rejected key so the
# user knows profile-based server auth config is gone.
assert "profile" in result.output
assert "Unknown config key" in result.output
# Nothing may be written: a rejected set must not leave a partial file.
assert not (tmp_path / "config.yaml").exists()
def test_config_set_local_writes_project_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent config set key=value`` without ``--global`` writes to
``.omnigent/config.yaml`` in the current directory.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory used as a stand-in project root.
"""
monkeypatch.chdir(tmp_path)
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", tmp_path / "global.yaml")
result = CliRunner().invoke(cli, ["config", "set", "model=my-model"])
assert result.exit_code == 0, result.output
local_path = tmp_path / ".omnigent" / "config.yaml"
assert local_path.exists(), "local config file should have been created"
cfg = yaml.safe_load(local_path.read_text())
assert cfg["model"] == "my-model"
# Global config must not be touched
assert not (tmp_path / "global.yaml").exists()
# ---------------------------------------------------------------------------
# `omnigent run` picks up global config defaults
# ---------------------------------------------------------------------------
def test_run_applies_global_config_agent_default(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent run`` (no AGENT arg) uses the ``default_agent`` key from
global config as the target when no explicit target is given.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"default_agent": "examples/hello_world.yaml"})
dispatched: dict[str, object] = {}
def fake_dispatch(**kwargs: object) -> None:
"""Capture dispatch kwargs without launching the REPL."""
dispatched.update(kwargs)
monkeypatch.setattr("omnigent.cli._dispatch_run", fake_dispatch)
monkeypatch.setattr("omnigent.cli._build_resume_parts", lambda: None)
monkeypatch.setattr(
"omnigent.cli._split_resume_value",
lambda _: SimpleNamespace(picker=False, conversation_id=None),
)
result = CliRunner().invoke(cli, ["run"])
assert result.exit_code == 0, result.output
# Global agent default must be forwarded as the target
assert dispatched["target"] == "examples/hello_world.yaml"
def test_run_cli_arg_overrides_global_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
An explicit CLI arg on ``omnigent run`` takes precedence over the
corresponding key in global config.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"model": "global-model", "server": "https://global.example.com"})
dispatched: dict[str, object] = {}
def fake_dispatch(**kwargs: object) -> None:
"""Capture dispatch kwargs without launching the REPL."""
dispatched.update(kwargs)
monkeypatch.setattr("omnigent.cli._dispatch_run", fake_dispatch)
monkeypatch.setattr("omnigent.cli._build_resume_parts", lambda: None)
monkeypatch.setattr(
"omnigent.cli._split_resume_value",
lambda _: SimpleNamespace(picker=False, conversation_id=None),
)
result = CliRunner().invoke(cli, ["run", "myagent.yaml", "--model", "explicit-model"])
assert result.exit_code == 0, result.output
# Explicit --model must win over global config value
assert dispatched["model"] == "explicit-model"
# server had no CLI override — global config value must be used
assert dispatched["server"] == "https://global.example.com"
def test_run_applies_auto_open_conversation_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent run`` forwards the persisted browser-open setting.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"auto_open_conversation": True})
dispatched: dict[str, object] = {}
def fake_dispatch(**kwargs: object) -> None:
"""Capture dispatch kwargs without launching the REPL."""
dispatched.update(kwargs)
monkeypatch.setattr("omnigent.cli._dispatch_run", fake_dispatch)
monkeypatch.setattr("omnigent.cli._build_resume_parts", lambda: None)
monkeypatch.setattr(
"omnigent.cli._split_resume_value",
lambda _: SimpleNamespace(picker=False, conversation_id=None),
)
result = CliRunner().invoke(cli, ["run", "myagent.yaml"])
assert result.exit_code == 0, result.output
assert dispatched["auto_open_conversation"] is True
def _capture_run_dispatch(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> dict[str, object]:
"""
Wire ``omnigent run`` to capture dispatch kwargs without launching.
Points the global config at an empty *tmp_path* file (so the test is
isolated from the developer's real ``~/.omnigent/config.yaml``) and
stubs the dispatch / resume helpers. Callers that want a non-empty
config call ``_save_global_config`` any time before invoking the CLI
(the ``_GLOBAL_CONFIG_PATH`` monkeypatch this helper installs persists
for the rest of the test).
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
:returns: A dict that ``_dispatch_run`` populates with its
kwargs once ``run`` is invoked, e.g.
``{"auto_open_conversation": True, "target": "myagent.yaml"}``.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
dispatched: dict[str, object] = {}
def fake_dispatch(**kwargs: object) -> None:
"""Capture dispatch kwargs without launching the REPL."""
dispatched.update(kwargs)
monkeypatch.setattr("omnigent.cli._dispatch_run", fake_dispatch)
monkeypatch.setattr("omnigent.cli._build_resume_parts", lambda: None)
monkeypatch.setattr(
"omnigent.cli._split_resume_value",
lambda _: SimpleNamespace(picker=False, conversation_id=None),
)
return dispatched
def test_run_interactive_defaults_browser_open_on(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
Interactive ``omnigent run`` opens the browser by default.
With no ``auto_open_conversation`` configured, a bare interactive
``run`` (no ``-p``) defaults the browser-open ON so users discover
the web UI once the server is up.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
dispatched = _capture_run_dispatch(monkeypatch, tmp_path)
result = CliRunner().invoke(cli, ["run", "myagent.yaml"])
assert result.exit_code == 0, result.output
assert dispatched["auto_open_conversation"] is True
def test_run_headless_prompt_defaults_browser_open_off(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
Headless ``omnigent run -p`` stays quiet by default.
A one-shot ``-p`` invocation with no configured preference must NOT
open the browser — the user is scripting, not exploring the UI.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
dispatched = _capture_run_dispatch(monkeypatch, tmp_path)
result = CliRunner().invoke(cli, ["run", "myagent.yaml", "-p", "hi"])
assert result.exit_code == 0, result.output
assert dispatched["auto_open_conversation"] is False
def test_run_interactive_respects_explicit_opt_out(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
An explicit ``auto_open_conversation: false`` suppresses the open.
Users who opted out keep the browser closed even on interactive
``run`` — the new interactive default never overrides an explicit
config value.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
dispatched = _capture_run_dispatch(monkeypatch, tmp_path)
_save_global_config({"auto_open_conversation": False})
result = CliRunner().invoke(cli, ["run", "myagent.yaml"])
assert result.exit_code == 0, result.output
assert dispatched["auto_open_conversation"] is False
def test_run_headless_honors_explicit_opt_in(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
Headless ``run -p`` still opens when the user explicitly opted in.
The headless default is OFF, but an explicit
``auto_open_conversation: true`` wins for ``-p`` too.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
dispatched = _capture_run_dispatch(monkeypatch, tmp_path)
_save_global_config({"auto_open_conversation": True})
result = CliRunner().invoke(cli, ["run", "myagent.yaml", "-p", "hi"])
assert result.exit_code == 0, result.output
assert dispatched["auto_open_conversation"] is True
def test_resolve_auto_open_conversation_setting_is_tristate() -> None:
"""
``_resolve_auto_open_conversation_setting`` distinguishes unset from set.
Returns ``None`` when the key is absent (so ``run`` can apply its
interactive default) and the parsed boolean when present.
"""
assert _resolve_auto_open_conversation_setting({}) is None
assert _resolve_auto_open_conversation_setting({"auto_open_conversation": True}) is True
assert _resolve_auto_open_conversation_setting({"auto_open_conversation": "false"}) is False
def test_attach_applies_auto_open_conversation_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent attach`` reads browser-open from config and forwards it to run_attach.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"auto_open_conversation": True})
monkeypatch.setattr("omnigent.cli._require_live_conversation", lambda **_kw: None)
run_attach = Mock()
monkeypatch.setattr("omnigent.chat.run_attach", run_attach)
result = CliRunner().invoke(cli, ["attach", "conv_1", "--server", "http://localhost:8000"])
assert result.exit_code == 0, result.output
# The config-derived browser-open setting reaches the client unchanged.
assert run_attach.call_args.kwargs["auto_open_conversation"] is True
def test_claude_applies_auto_open_conversation_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent claude`` forwards the persisted browser-open setting.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"auto_open_conversation": True})
captured: dict[str, object] = {}
monkeypatch.setattr(
"omnigent.claude_native.run_claude_native",
_fake_run_claude_native_capture(captured),
)
result = CliRunner().invoke(cli, ["claude"])
assert result.exit_code == 0, result.output
assert captured["auto_open_conversation"] is True
def test_codex_applies_auto_open_conversation_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
``omnigent codex`` forwards the persisted browser-open setting.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"auto_open_conversation": True})
captured: dict[str, object] = {}
monkeypatch.setattr(
"omnigent.codex_native.run_codex_native",
_fake_run_codex_native_capture(captured),
)
result = CliRunner().invoke(cli, ["codex"])
assert result.exit_code == 0, result.output
assert captured["auto_open_conversation"] is True
def test_run_bare_omnigent_with_harness_only_config(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""
Bare ``omnigent`` with only ``harness`` in global config dispatches
to ``run`` (not ``--help``), so the harness default is applied.
Regression target: the bare-invocation check previously only looked
at ``agent`` and ``server``, missing the ``harness``-only case.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary directory standing in for ~/.omnigent.
"""
config_path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", config_path)
_save_global_config({"harness": "claude-sdk"})
dispatched: dict[str, object] = {}
def fake_dispatch(**kwargs: object) -> None:
"""Capture dispatch kwargs without launching the REPL."""
dispatched.update(kwargs)
monkeypatch.setattr("omnigent.cli._dispatch_run", fake_dispatch)
monkeypatch.setattr("omnigent.cli._build_resume_parts", lambda: None)
monkeypatch.setattr(
"omnigent.cli._split_resume_value",
lambda _: SimpleNamespace(picker=False, conversation_id=None),
)
result = CliRunner().invoke(cli, ["run"])
assert result.exit_code == 0, result.output
# harness default must be forwarded; no target was set
assert dispatched["harness"] == "claude-sdk"
assert dispatched["target"] is None
def test_bare_omnigent_harness_flag_dispatches_to_run(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``omnigent --harness ...`` is shorthand for ``omnigent run --harness ...``."""
from omnigent.cli import main
dispatched: dict[str, object] = {}
def fake_dispatch(**kwargs: object) -> None:
dispatched.update(kwargs)
monkeypatch.setattr("omnigent.cli._load_global_config", dict)
monkeypatch.setattr("omnigent.cli._dispatch_run", fake_dispatch)
monkeypatch.setattr("omnigent.cli._build_resume_parts", lambda: None)
monkeypatch.setattr(
"omnigent.cli._split_resume_value",
lambda _: SimpleNamespace(picker=False, conversation_id=None),
)
monkeypatch.setattr(sys, "argv", ["omnigent", "--harness", "claude"])
main()
assert dispatched["harness"] == "claude"
assert dispatched["target"] is None
def test_bare_omnigent_non_tty_shows_help(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Bare ``omnigent`` in a non-interactive shell (no TTY) shows help.
On a pipe / CI there is no terminal to drive a REPL, so the bare command
falls back to ``--help`` rather than launching ``run`` (which would hang
waiting on stdin).
"""
from omnigent.cli import main
monkeypatch.setattr(sys, "argv", ["omnigent"])
monkeypatch.setattr(sys.stdin, "isatty", lambda: False)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
stdout = capsys.readouterr().out
assert "Usage:" in stdout
assert "Commands:" in stdout
def test_bare_omnigent_tty_dispatches_to_run(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Bare ``omnigent`` on an interactive terminal behaves like ``omnigent run``.
``run`` then resolves the configured default / first-run plan. We assert
only that the bare invocation is rewritten to ``run`` before dispatch.
"""
from omnigent import cli as cli_module
monkeypatch.setattr(sys, "argv", ["omnigent"])
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
dispatched: dict[str, list[str]] = {}
def fake_cli(*, args: list[str], standalone_mode: bool = True) -> None:
dispatched["args"] = args
monkeypatch.setattr(cli_module, "cli", fake_cli)
cli_module.main()
assert dispatched["args"] == ["run"]
def test_bare_omnigent_rejects_positional_server_url(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Top-level server URLs must use ``run --server`` explicitly."""
from omnigent.cli import main
monkeypatch.setattr(sys, "argv", ["omnigent", "http://localhost:8000"])
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 2
terminal = capsys.readouterr()
assert "server URLs must be passed with --server" in terminal.err
assert "omnigent run --server http://localhost:8000" in terminal.err
def test_unknown_command_reports_no_such_command(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""
An unknown subcommand falls through to click's standard error.
A typo'd command (``omnigent blah``) must produce click's
"No such command" usage error, not the removed-ad-hoc-chat notice
that previously swallowed every non-subcommand invocation.
"""
from omnigent.cli import main
monkeypatch.setattr(sys, "argv", ["omnigent", "blah"])
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 2
terminal = capsys.readouterr()
combined = terminal.out + terminal.err
assert "No such command 'blah'" in combined
assert "ad-hoc chat was removed" not in combined
def test_setup_command_replaces_wizard(monkeypatch: pytest.MonkeyPatch) -> None:
"""``omnigent setup`` is the visible standard setup flow command."""
configure_flow = Mock()
configure_databricks = Mock()
run_onboarding = Mock(return_value=True)
monkeypatch.setattr(
"omnigent.cli._run_configure_harnesses_interactive",
configure_flow,
)
monkeypatch.setattr("omnigent.cli._run_configure_databricks", configure_databricks)
monkeypatch.setattr("omnigent.onboarding.setup.run_onboarding", run_onboarding)
result = CliRunner().invoke(cli, ["setup"])
assert result.exit_code == 0, result.output
configure_flow.assert_called_once_with()
configure_databricks.assert_not_called()
run_onboarding.assert_not_called()
help_result = CliRunner().invoke(cli, ["--help"])
assert help_result.exit_code == 0
assert "setup" in help_result.output
assert "wizard" not in help_result.output
removed_result = CliRunner().invoke(cli, ["wizard"])
assert removed_result.exit_code != 0
assert "No such command 'wizard'" in removed_result.output
onboard_result = CliRunner().invoke(cli, ["onboard"])
assert onboard_result.exit_code != 0
assert "No such command 'onboard'" in onboard_result.output
def test_setup_no_internal_beta_runs_configure_flow(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``--no-internal-beta`` runs the model/credential picker, not the Databricks bootstrap.
The generic onboarding wizard was removed; ``setup --no-internal-beta``
now runs the same interactive flow as ``configure harnesses``.
"""
configure_databricks = Mock()
run_onboarding = Mock()
configure_flow = Mock()
monkeypatch.setattr("omnigent.cli._run_configure_databricks", configure_databricks)
monkeypatch.setattr(
"omnigent.cli._run_configure_harnesses_interactive",
configure_flow,
)
monkeypatch.setattr("omnigent.onboarding.setup.run_onboarding", run_onboarding)
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"])
assert result.exit_code == 0, result.output
configure_flow.assert_called_once_with()
configure_databricks.assert_not_called()
run_onboarding.assert_not_called()
def test_setup_uses_compact_branding_on_short_terminals(monkeypatch: pytest.MonkeyPatch) -> None:
"""A short first-run terminal should show the setup picker, not scroll past it."""
configure_flow = Mock()
print_landing = Mock()
print_brandmark = Mock()
monkeypatch.setattr("omnigent.cli._run_configure_harnesses_interactive", configure_flow)
monkeypatch.setattr("omnigent.inner.ui.print_landing", print_landing)
monkeypatch.setattr("omnigent.inner.ui.print_brandmark", print_brandmark)
monkeypatch.setattr(
"omnigent.cli.shutil.get_terminal_size",
lambda fallback: os.terminal_size((80, 24)),
)
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"])
assert result.exit_code == 0, result.output
print_brandmark.assert_called_once_with("setup")
print_landing.assert_not_called()
configure_flow.assert_called_once_with()
def test_setup_keeps_full_landing_on_tall_terminals(monkeypatch: pytest.MonkeyPatch) -> None:
"""Roomy terminals keep the full first-run lockup."""
configure_flow = Mock()
print_landing = Mock()
print_brandmark = Mock()
monkeypatch.setattr("omnigent.cli._run_configure_harnesses_interactive", configure_flow)
monkeypatch.setattr("omnigent.inner.ui.print_landing", print_landing)
monkeypatch.setattr("omnigent.inner.ui.print_brandmark", print_brandmark)
monkeypatch.setattr(
"omnigent.cli.shutil.get_terminal_size",
lambda fallback: os.terminal_size((120, 40)),
)
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"])
assert result.exit_code == 0, result.output
print_landing.assert_called_once_with(tagline="all your agents, one cli")
print_brandmark.assert_not_called()
configure_flow.assert_called_once_with()
# ─── setup dependency preflight (Node / tmux) ─────────────────────────
def _fake_node_run(
version: str,
probe_returncode: int,
) -> Callable[..., subprocess.CompletedProcess[str]]:
"""
Build a fake ``subprocess.run`` for the Node preflight helpers.
Dispatches on the command so a single fake serves both calls the
helpers make: ``node --version`` yields *version* on stdout, while the
``node -e`` capability probe yields *probe_returncode* (0 = the
``markAsUncloneable`` symbol is present, 1 = too old).
:param version: Version string to report for ``node --version``,
without the trailing newline that the real CLI emits.
:param probe_returncode: Exit code the capability probe should return.
:returns: A callable suitable for ``monkeypatch.setattr`` on
``omnigent.cli.subprocess.run``.
"""
def _run(cmd: list[str], *args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
if "--version" in cmd:
return subprocess.CompletedProcess(cmd, 0, stdout=f"{version}\n", stderr="")
return subprocess.CompletedProcess(cmd, probe_returncode, stdout="", stderr="")
return _run
def test_node_dependency_problem_missing(monkeypatch: pytest.MonkeyPatch) -> None:
"""
A machine without ``node`` on PATH reports the missing-binary problem.
The harnesses that need Node (Claude, Codex, Pi) should be named so the
user knows why it matters, rather than a bare "not found".
"""
monkeypatch.setattr("omnigent.cli.shutil.which", lambda _: None)
problem = _node_dependency_problem()
assert problem is not None
assert "node not found" in problem
assert "Pi" in problem # the harnesses that need it are named
def test_node_dependency_problem_ok(monkeypatch: pytest.MonkeyPatch) -> None:
"""A Node new enough for the probe (exit 0) reports no problem."""
monkeypatch.setattr("omnigent.cli.shutil.which", lambda _: "/usr/bin/node")
monkeypatch.setattr(
"omnigent.cli.subprocess.run",
_fake_node_run("v22.14.0", probe_returncode=0),
)
assert _node_dependency_problem() is None
def test_node_dependency_problem_too_old(monkeypatch: pytest.MonkeyPatch) -> None:
"""
A Node failing the capability probe surfaces the detected version and
the exact runtime symptom so the warning is actionable.
"""
monkeypatch.setattr("omnigent.cli.shutil.which", lambda _: "/usr/bin/node")
monkeypatch.setattr(
"omnigent.cli.subprocess.run",
_fake_node_run("v20.12.2", probe_returncode=1),
)
problem = _node_dependency_problem()
assert problem is not None
assert "too old" in problem
# The concrete detected version and the required floor must both appear —
# that's what makes the compact warning recognizable and actionable.
assert "v20.12.2" in problem
assert "Node.js 22 LTS" in problem
def test_node_dependency_problem_probe_inconclusive(monkeypatch: pytest.MonkeyPatch) -> None:
"""
A flaky/timed-out probe yields no problem — setup must not block on a
transient ``subprocess`` failure.
"""
monkeypatch.setattr("omnigent.cli.shutil.which", lambda _: "/usr/bin/node")
def _boom(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
raise subprocess.TimeoutExpired(cmd="node", timeout=10)
monkeypatch.setattr("omnigent.cli.subprocess.run", _boom)
assert _node_dependency_problem() is None
def test_node_version_trims_and_handles_failure(monkeypatch: pytest.MonkeyPatch) -> None:
"""``_node_version`` strips the trailing newline and is non-fatal."""
monkeypatch.setattr(
"omnigent.cli.subprocess.run",
lambda *a, **k: subprocess.CompletedProcess(["node"], 0, stdout="v22.14.0\n", stderr=""),
)
assert _node_version("/usr/bin/node") == "v22.14.0"
def _boom(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
raise OSError("node vanished")
monkeypatch.setattr("omnigent.cli.subprocess.run", _boom)
assert _node_version("/usr/bin/node") is None
def test_warn_missing_harness_dependencies_silent_when_present(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""With a recent Node and tmux on PATH, the preflight prints nothing."""
monkeypatch.setattr("omnigent.cli.shutil.which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(
"omnigent.cli.subprocess.run",
_fake_node_run("v22.14.0", probe_returncode=0),
)
_warn_missing_harness_dependencies()
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
def test_warn_missing_harness_dependencies_lists_all_gaps(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""
When both Node (too old) and tmux (missing) are problems, a single
warning block lists both — the point of the up-front preflight is that
a fresh machine sees every gap at once.
"""
def _which(name: str) -> str | None:
return None if name == "tmux" else "/usr/bin/node"
monkeypatch.setattr("omnigent.cli.shutil.which", _which)
monkeypatch.setattr(
"omnigent.cli.subprocess.run",
_fake_node_run("v20.12.2", probe_returncode=1),
)
_warn_missing_harness_dependencies()
err = capsys.readouterr().err
assert "Node.js is too old" in err
assert "tmux not found" in err
def test_click_subcommands_allowlist_covers_registered_commands() -> None:
"""Every command registered on the ``cli`` group is in ``_CLICK_SUBCOMMANDS``.
``main()`` consults this allowlist *before* handing argv to click: a
first token not in the set is rejected as removed top-level ad-hoc chat
(see ``_is_removed_ad_hoc_invocation``). So a command registered on the
group but absent from the allowlist is unreachable from the real
entrypoint — exactly the bug where ``omnigent configure`` errored with
"ad-hoc chat was removed" despite being registered. A failure here means
a newly added top-level command must be added to ``_CLICK_SUBCOMMANDS``.
"""
from omnigent.cli import _CLICK_SUBCOMMANDS, cli
# Direction matters: the allowlist must be a superset of the registered
# commands. Extra allowlist entries (not registered) are harmless; a
# registered command missing from the allowlist is the unreachable bug.
missing = set(cli.commands) - set(_CLICK_SUBCOMMANDS)
assert missing == set(), (
"commands registered on the cli group but missing from "
f"_CLICK_SUBCOMMANDS (unreachable from main()): {sorted(missing)}"
)
# ── first-run smart defaults (omnigent run) ──────────
def _fake_provider_for(*configured: str):
"""Return a default_provider_for_harness stub truthy only for *configured*.
:param configured: Harness ids treated as having a usable credential, e.g.
``"claude-sdk"``.
:returns: A ``(config, harness) -> object|None`` callable.
"""
def _fn(config: object, harness: str) -> object | None:
return object() if harness in configured else None
return _fn
def test_pick_first_run_prefers_claude_with_polly(monkeypatch: pytest.MonkeyPatch) -> None:
"""Claude configured → claude-sdk + the bundled polly agent.
Claude wins the priority order and is the only family that gets a default
*example* agent (polly). A regression that dropped polly or picked the
wrong harness fails here.
"""
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for("claude-sdk", "codex"), # both configured → Claude wins
)
plan = _pick_first_run_harness()
assert plan is not None
assert plan.harness == "claude-sdk"
assert plan.agent is not None and plan.agent.endswith("polly")
def test_pick_first_run_harness_codex_then_pi_no_agent(monkeypatch: pytest.MonkeyPatch) -> None:
"""No Claude → Codex (then Pi) with NO default example agent (bare REPL)."""
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for("codex"),
)
plan = _pick_first_run_harness()
assert plan is not None and plan.harness == "codex" and plan.agent is None
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for("pi"),
)
plan = _pick_first_run_harness()
assert plan is not None and plan.harness == "pi" and plan.agent is None
def test_pick_first_run_harness_none_when_unconfigured(monkeypatch: pytest.MonkeyPatch) -> None:
"""Nothing configured → None (caller drops into configure)."""
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for(), # nothing configured
)
# The kimi fallback in _pick_first_run_harness gates on the ``kimi``
# binary being on PATH. Stub it to False so the test stays deterministic
# on machines where the developer has kimi installed.
monkeypatch.setattr(
"omnigent.onboarding.harness_install.harness_cli_installed",
lambda _key: False,
)
assert _pick_first_run_harness() is None
def test_resolve_first_run_plan_does_not_persist_derived_default(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""The derived first-run pick is returned but NOT persisted as a default.
Persisting it would pin a Codex-only user to Codex even after they add
Claude. Keeping it ephemeral lets the next bare ``run`` re-derive from the
current creds (and promote them to polly). Asserts the resolved plan is
Claude→polly yet no global ``harness`` / ``default_agent`` was written.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.cli._promote_global_auth_to_provider", Mock())
monkeypatch.setattr("omnigent.cli._adopt_detected_providers", Mock(return_value=[]))
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for("claude-sdk"),
)
plan = _resolve_first_run_plan()
assert plan is not None and plan.harness == "claude-sdk"
assert plan.agent is not None and plan.agent.endswith("polly")
# No global harness / default_agent was persisted — the pick is ephemeral.
config_path = tmp_path / "config.yaml"
saved = yaml.safe_load(config_path.read_text()) if config_path.exists() else {}
assert "harness" not in saved, f"derived harness must not be persisted; got {saved!r}"
assert "default_agent" not in saved, (
f"derived default_agent must not be persisted; got {saved!r}"
)
def test_resolve_first_run_plan_re_derives_when_creds_change(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Adding Claude promotes a Codex-only user to polly on the next bare run.
Because the pick is never persisted, the second resolution reflects the
*current* creds: Codex-only → a bare codex REPL; after Claude is added →
claude-sdk + polly (our primary). A regression that re-persisted the first
pick would pin the user to codex and fail the second half.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.cli._promote_global_auth_to_provider", Mock())
monkeypatch.setattr("omnigent.cli._adopt_detected_providers", Mock(return_value=[]))
# 1) Only Codex configured → codex REPL, no example agent.
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for("codex"),
)
first = _resolve_first_run_plan()
assert first is not None and first.harness == "codex" and first.agent is None
# 2) Claude added (now both configured) → promoted to claude-sdk + polly,
# NOT pinned to the earlier codex pick.
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for("claude-sdk", "codex"),
)
second = _resolve_first_run_plan()
assert second is not None and second.harness == "claude-sdk"
assert second.agent is not None and second.agent.endswith("polly")
def test_resolve_first_run_plan_drops_into_configure_when_empty(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""No creds → drop into `configure harnesses`; still none after → None.
The configure picker is stubbed (it would block on a real terminal). A
return of None signals the caller to exit cleanly rather than error.
"""
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
monkeypatch.setattr("omnigent.cli._promote_global_auth_to_provider", Mock())
monkeypatch.setattr("omnigent.cli._adopt_detected_providers", Mock(return_value=[]))
monkeypatch.setattr(
"omnigent.onboarding.provider_config.default_provider_for_harness",
_fake_provider_for(), # nothing configured, before and after configure
)
# The kimi fallback in ``_pick_first_run_harness`` gates on the ``kimi``
# binary being on PATH. Stub it to False so the test stays deterministic
# regardless of the developer's local install.
monkeypatch.setattr(
"omnigent.onboarding.harness_install.harness_cli_installed",
lambda _key: False,
)
configure = Mock()
monkeypatch.setattr("omnigent.cli._run_configure_harnesses_interactive", configure)
plan = _resolve_first_run_plan()
assert plan is None
configure.assert_called_once_with() # the user was dropped into configure
def test_announce_auto_configured_credentials_names_creds_compactly(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The callout names each adopted credential inline with a brand-qualified label.
A user who never ran setup must see exactly which credentials were
auto-configured. Asserts every credential's compact human label reaches the
one-line output — an env key as ``Anthropic API Key``, and the two CLI
logins as brand-qualified ``Claude Subscription`` / ``ChatGPT Subscription``
(NOT a bare ``Subscription``, which would be ambiguous in an inline list).
Detections are real :class:`DetectedProvider` objects so a field-handling
regression surfaces.
"""
detected = [
DetectedProvider(
name="anthropic", kind="key", family="anthropic", source="$ANTHROPIC_API_KEY"
),
DetectedProvider(
name="claude", kind="subscription", family="anthropic", source="claude CLI login"
),
DetectedProvider(
name="codex", kind="subscription", family="openai", source="codex CLI login"
),
]
monkeypatch.setattr("omnigent.onboarding.ambient.detect_providers", lambda: detected)
_announce_auto_configured_credentials(["anthropic", "claude", "codex"])
# Normalize whitespace: Rich wraps the line at the console width, so collapse
# any inserted newlines/runs of spaces before matching the inline sequence.
normalized = " ".join(capsys.readouterr().out.split())
assert "Found existing credentials on your machine" in normalized
# The three credentials render inline, comma-joined, in adoption order —
# the env key by vendor, the CLI logins brand-qualified (not a bare,
# ambiguous "Subscription").
assert "Anthropic API Key, Claude Subscription, ChatGPT Subscription" in normalized
def test_announce_auto_configured_credentials_empty_is_silent(
capsys: pytest.CaptureFixture[str],
) -> None:
"""Nothing adopted → no callout at all (no stray header on a quiet run).
The wrapper only announces when something was newly adopted; this guards
the announce helper itself printing a bare header for an empty list.
"""
_announce_auto_configured_credentials([])
assert capsys.readouterr().out == ""
def test_adopt_ambient_credentials_announces_only_what_was_adopted(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The shared adopt step self-heals, adopts, and announces the adopted creds.
Exercises the real announce path through the wrapper: with one credential
newly adopted the callout names it; the databricks backfill stays silent.
A regression that stopped calling the callout (or announced credentials
that were not actually adopted) fails here.
"""
monkeypatch.setattr("omnigent.cli._promote_global_auth_to_provider", Mock())
monkeypatch.setattr("omnigent.cli._adopt_detected_providers", Mock(return_value=["anthropic"]))
monkeypatch.setattr(
"omnigent.onboarding.ambient.detect_providers",
lambda: [
DetectedProvider(
name="anthropic", kind="key", family="anthropic", source="$ANTHROPIC_API_KEY"
)
],
)
adopted = _adopt_ambient_credentials()
assert adopted == ["anthropic"]
assert "Anthropic API Key" in capsys.readouterr().out
def test_ensure_sqlite_parent_dir_creates_missing_dir(tmp_path: Path) -> None:
"""A file-backed SQLite URI gets its parent directory created.
Reproduces the first-run failure: the machine-global default DB lives
at ``<data_dir>/chat.db`` and SQLite refuses to open it
("unable to open database file") when ``<data_dir>`` doesn't exist
yet. The helper must create the parent so the stores can connect; we
assert the dir exists afterward (the file itself is created later, on
first connect, so we don't assert on it here).
"""
db_path = tmp_path / "fresh" / "nested" / "chat.db"
assert not db_path.parent.exists()
_ensure_sqlite_parent_dir(f"sqlite:///{db_path}")
assert db_path.parent.is_dir()
def test_ensure_sqlite_parent_dir_idempotent_when_dir_exists(tmp_path: Path) -> None:
"""Calling twice (dir already present) is a no-op, not an error.
``exist_ok=True`` semantics — a second boot against an existing data
dir must not raise ``FileExistsError``.
"""
db_path = tmp_path / "chat.db"
_ensure_sqlite_parent_dir(f"sqlite:///{db_path}")
_ensure_sqlite_parent_dir(f"sqlite:///{db_path}") # must not raise
assert db_path.parent.is_dir()
def test_ensure_sqlite_parent_dir_noop_for_memory_and_non_sqlite(tmp_path: Path) -> None:
"""In-memory SQLite and non-SQLite URIs create nothing and don't raise.
``:memory:`` has no filesystem path, and Postgres/MySQL manage their
own storage — the helper must skip both rather than trying to mkdir a
bogus path (which would crash a Postgres-backed deployment at boot).
"""
# Neither call should raise, and neither should create a stray dir.
_ensure_sqlite_parent_dir("sqlite:///:memory:")
_ensure_sqlite_parent_dir("postgresql://user:pw@db.example.com:5432/omnigent")
assert list(tmp_path.iterdir()) == []
def _native_dispatch_kwargs(**overrides: object) -> dict[str, object]:
"""Build ``_dispatch_native_terminal_harness`` kwargs with safe defaults.
Keeps the call sites focused on the one or two fields each test exercises,
and means a new parameter only updates this helper rather than every test.
"""
base: dict[str, object] = {
"harness": "cursor-native",
"server": None,
"model": None,
"prompt": None,
"system_prompt": None,
"tools": None,
"log": False,
"debug_events": False,
"resume_conversation_id": None,
"resume_picker": False,
"resume_latest": False,
"fork_session_id": None,
"ephemeral": False,
"auto_open_conversation": False,
}
base.update(overrides)
return base
def test_dispatch_native_terminal_harness_cursor_launches_wrapper(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``run --harness cursor-native`` dispatches to the cursor TUI wrapper.
Regression for duplicate user messages: the materialized-launcher REPL
drove an Omnigent turn (which persists its own user item) on top of the
cursor forwarder mirroring the same message back, recording each message
twice. Native terminal harnesses must launch their wrapper directly so the
TUI is the single source of turns. A top-level ``--model`` is forwarded as a
passthrough ``--model`` flag.
"""
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda _s: "http://localhost:0")
captured: dict[str, object] = {}
monkeypatch.setattr(
"omnigent.cursor_native.run_cursor_native",
lambda **kwargs: captured.update(kwargs),
)
handled = _dispatch_native_terminal_harness(
**_native_dispatch_kwargs(
model="composer-2.5",
resume_conversation_id="conv_abc123",
auto_open_conversation=True,
)
)
assert handled is True
assert captured == {
"server": "http://localhost:0",
"session_id": "conv_abc123",
"resume_picker": False,
"auto_open_conversation": True,
"cursor_args": ("--model", "composer-2.5"),
}
def test_dispatch_native_terminal_harness_ignores_non_native(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A non-native harness returns False and never touches the backend.
The SDK harnesses (``cursor``, ``codex``, ``claude-sdk``, ...) must keep
falling through to the materialized-launcher REPL.
"""
def _must_not_run(_server: str | None) -> str:
raise AssertionError("_ensure_backend called for a non-native harness")
monkeypatch.setattr("omnigent.cli._ensure_backend", _must_not_run)
handled = _dispatch_native_terminal_harness(**_native_dispatch_kwargs(harness="openai-agents"))
assert handled is False
@pytest.mark.parametrize(
("override", "expected_flag"),
[
({"prompt": "hi"}, "-p/--prompt"),
({"system_prompt": "be terse"}, "--system-prompt"),
({"tools": "fs"}, "--tools"),
({"log": True}, "--log"),
({"debug_events": True}, "--debug-events"),
({"fork_session_id": "conv_src"}, "--fork"),
({"ephemeral": True}, "--no-session"),
],
)
def test_dispatch_native_terminal_harness_rejects_unsupported_flags(
monkeypatch: pytest.MonkeyPatch, override: dict[str, object], expected_flag: str
) -> None:
"""REPL-only flags have no analog in the TUI wrapper — fail loud, don't drop.
Covers Copilot's finding that ``--tools`` / ``--log`` / ``--debug-events``
were silently ignored on the native-harness path.
"""
def _must_not_run(_server: str | None) -> str:
raise AssertionError("_ensure_backend called despite unsupported flags")
monkeypatch.setattr("omnigent.cli._ensure_backend", _must_not_run)
# The rejected flag is named in the error, and it's framed as "remove them"
# (not "use the subcommand" — the subcommand doesn't accept these either).
with pytest.raises(ClickException, match=re.escape(expected_flag)) as excinfo:
_dispatch_native_terminal_harness(**_native_dispatch_kwargs(**override))
assert "remove them" in str(excinfo.value)
def test_dispatch_native_terminal_harness_continue_resumes_latest(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``--continue`` resolves the harness's latest conversation, not an error.
Regression guard for the resume path: before this, ``--continue`` was listed
as unsupported and raised. It must instead resolve the most-recent
``cursor-native-ui`` conversation and hand that to the wrapper as the
session id.
"""
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda _s: "http://localhost:0")
seen: dict[str, object] = {}
def _fake_latest(*, base_url: str, agent_name: str, headers: object) -> str:
seen["base_url"] = base_url
seen["agent_name"] = agent_name
return "conv_latest"
monkeypatch.setattr("omnigent.chat._resolve_latest_conversation_id", _fake_latest)
monkeypatch.setattr("omnigent.chat._remote_headers", lambda **_kw: {})
captured: dict[str, object] = {}
monkeypatch.setattr(
"omnigent.cursor_native.run_cursor_native",
lambda **kwargs: captured.update(kwargs),
)
handled = _dispatch_native_terminal_harness(**_native_dispatch_kwargs(resume_latest=True))
assert handled is True
# Resolved against the cursor-native agent identity, then passed as session_id.
assert seen == {"base_url": "http://localhost:0", "agent_name": "cursor-native-ui"}
assert captured["session_id"] == "conv_latest"
def test_dispatch_native_terminal_harness_continue_with_no_prior_fails_loud(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``--continue`` with nothing to continue errors, not a silent fresh start.
The user explicitly asked to resume; passing the unresolved ``None`` through
as ``session_id`` would silently open a new session. Matches the REPL's
``_resolve_resume_target`` "No prior conversation" behavior.
"""
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda _s: "http://localhost:0")
monkeypatch.setattr("omnigent.chat._resolve_latest_conversation_id", lambda **_kw: None)
monkeypatch.setattr("omnigent.chat._remote_headers", lambda **_kw: {})
def _must_not_launch(**_kw: object) -> None:
raise AssertionError("wrapper launched despite no conversation to continue")
monkeypatch.setattr("omnigent.cursor_native.run_cursor_native", _must_not_launch)
with pytest.raises(ClickException, match="No prior conversation"):
_dispatch_native_terminal_harness(**_native_dispatch_kwargs(resume_latest=True))
def test_dispatch_native_terminal_harness_explicit_id_skips_latest_lookup(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An explicit ``--resume <id>`` wins over ``--continue`` (no latest lookup)."""
monkeypatch.setattr("omnigent.cli._ensure_backend", lambda _s: "http://localhost:0")
def _must_not_lookup(**_kw: object) -> str:
raise AssertionError("latest-conversation lookup ran despite an explicit id")
monkeypatch.setattr("omnigent.chat._resolve_latest_conversation_id", _must_not_lookup)
captured: dict[str, object] = {}
monkeypatch.setattr(
"omnigent.cursor_native.run_cursor_native",
lambda **kwargs: captured.update(kwargs),
)
_dispatch_native_terminal_harness(
**_native_dispatch_kwargs(resume_conversation_id="conv_explicit", resume_latest=True)
)
assert captured["session_id"] == "conv_explicit"
def test_run_agent_with_native_terminal_harness_is_rejected() -> None:
"""``run AGENT --harness cursor-native`` is rejected (the TUI is the agent).
The native TUI ignores the AGENT spec, and routing it through the REPL would
double-record every message — so the combination has no coherent meaning.
"""
with pytest.raises(ClickException, match="ignores an AGENT spec"):
_dispatch_run(
target="examples/hello_world.yaml",
tools=None,
harness="cursor-native",
model=None,
prompt=None,
system_prompt=None,
)
# ── omnigent setup: Qwen Code drill-in (_manage_qwen_harness) ────────────
def test_qwen_auth_configured_detects_env_var(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Env-var auth is detected; a fresh install with no auth is NOT.
Guards the reported bug: a just-installed qwen (no env vars, no ~/.qwen
creds) must report unauthenticated rather than a false "signed in".
"""
# Point HOME at an empty dir so on-disk creds/settings are absent
# (Path.home() honors $HOME on POSIX).
monkeypatch.setenv("HOME", str(tmp_path))
for var in ("OPENAI_API_KEY", "BAILIAN_CODING_PLAN_API_KEY", "OPENROUTER_API_KEY"):
monkeypatch.delenv(var, raising=False)
assert _qwen_auth_configured() is False
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
assert _qwen_auth_configured() is True
def test_qwen_auth_configured_reads_settings_selected_type(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""An auth type selected via /auth (persisted to settings.json) is detected."""
monkeypatch.setenv("HOME", str(tmp_path))
for var in ("OPENAI_API_KEY", "BAILIAN_CODING_PLAN_API_KEY", "OPENROUTER_API_KEY"):
monkeypatch.delenv(var, raising=False)
qwen_dir = tmp_path / ".qwen"
qwen_dir.mkdir()
(qwen_dir / "settings.json").write_text(
'{"security": {"auth": {"selectedType": "openai"}}}', encoding="utf-8"
)
assert _qwen_auth_configured() is True
def test_manage_qwen_harness_declines_install_returns(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When the qwen CLI is missing and the user declines, the drill-in returns.
Declining install (choice 1) must bail without installing or launching qwen.
"""
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: False)
monkeypatch.setattr(it, "console", Mock())
install = Mock()
monkeypatch.setattr(hi, "install_harness_cli", install)
launch = Mock()
monkeypatch.setattr("omnigent.cli._launch_qwen_auth", launch)
# The install prompt offers [install, no, show-command]; pick "No".
monkeypatch.setattr(it, "select", lambda *a, **k: 1)
_manage_qwen_harness()
install.assert_not_called()
launch.assert_not_called()
def test_manage_qwen_harness_back_does_not_launch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With the CLI installed, choosing "← Back" exits without launching qwen.
There is no ``qwen login`` to drive — the drill-in only offers /auth launch
and help, so a plain Back must be a clean no-op.
"""
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: True)
monkeypatch.setattr("omnigent.cli._qwen_auth_configured", lambda: False)
monkeypatch.setattr(it, "console", Mock())
launch = Mock(return_value="x")
monkeypatch.setattr("omnigent.cli._launch_qwen_auth", launch)
# rows = [Open Qwen to run /auth, Show auth options, ← Back]; pick Back (2).
monkeypatch.setattr(it, "select", lambda *a, **k: 2)
_manage_qwen_harness()
launch.assert_not_called()
# ── omnigent setup: Goose drill-in (_manage_goose_harness) ───────────────
def test_manage_goose_harness_missing_cli_shows_hint_returns(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When the goose CLI is missing, the drill-in shows the install hint and
returns without launching ``goose configure`` (Goose has no npm auto-install).
"""
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: False)
monkeypatch.setattr(it, "console", Mock())
launch = Mock()
monkeypatch.setattr("omnigent.cli._launch_goose_configure", launch)
# Should never reach the select() menu when the CLI is absent.
monkeypatch.setattr(it, "select", Mock(side_effect=AssertionError("select called")))
_manage_goose_harness()
launch.assert_not_called()
def test_manage_goose_harness_back_does_not_launch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With the CLI installed, choosing "← Back" exits without launching configure."""
import omnigent.onboarding.goose_auth as ga
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: True)
monkeypatch.setattr(
ga,
"goose_config_summary",
lambda: ga.GooseConfigSummary(installed=True, provider=None, model=None),
)
monkeypatch.setattr(it, "console", Mock())
launch = Mock(return_value="x")
monkeypatch.setattr("omnigent.cli._launch_goose_configure", launch)
# rows = [Run goose configure, Show configuration options, ← Back]; pick Back (2).
monkeypatch.setattr(it, "select", lambda *a, **k: 2)
_manage_goose_harness()
launch.assert_not_called()
def test_manage_goose_harness_configure_launches(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Choosing "Run goose configure" launches the configure flow, then exits."""
import omnigent.onboarding.goose_auth as ga
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: True)
monkeypatch.setattr(
ga,
"goose_config_summary",
lambda: ga.GooseConfigSummary(installed=True, provider="anthropic", model="claude-x"),
)
monkeypatch.setattr(it, "console", Mock())
launch = Mock(return_value="✓ provider configured: anthropic")
monkeypatch.setattr("omnigent.cli._launch_goose_configure", launch)
# First iteration: pick "Run goose configure" (0); second: "← Back" (2).
choices = iter([0, 2])
monkeypatch.setattr(it, "select", lambda *a, **k: next(choices))
_manage_goose_harness()
launch.assert_called_once()
# ── omnigent setup: Kimi Code drill-in (_manage_kimi_harness) ────────────
def test_manage_kimi_harness_not_installed_shows_hint_returns(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A missing kimi CLI shows the curl install_hint and returns.
Kimi is curl-installed (no npm ``package``), so the drill-in can't
auto-install it — it must surface the install_hint and bail without
touching login / logout.
"""
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: False)
console = Mock()
monkeypatch.setattr(it, "console", console)
login = Mock()
logout = Mock()
monkeypatch.setattr(hi, "harness_login", login)
monkeypatch.setattr(hi, "harness_logout", logout)
# If the drill-in wrongly reached the menu loop, this select would drive it.
monkeypatch.setattr(it, "select", lambda *a, **k: 0)
_manage_kimi_harness()
login.assert_not_called()
logout.assert_not_called()
# The curl install command was surfaced to the user.
printed = " ".join(str(c.args[0]) for c in console.print.call_args_list if c.args)
assert "code.kimi.com/kimi-code/install.sh" in printed
def test_manage_kimi_harness_back_does_not_login(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With the CLI installed, choosing "← Back" exits without signing in."""
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: True)
monkeypatch.setattr(it, "console", Mock())
login = Mock()
logout = Mock()
monkeypatch.setattr(hi, "harness_login", login)
monkeypatch.setattr(hi, "harness_logout", logout)
# rows = [Sign in, Sign out, Show auth options, ← Back]; pick Back (3).
monkeypatch.setattr(it, "select", lambda *a, **k: 3)
_manage_kimi_harness()
login.assert_not_called()
logout.assert_not_called()
def test_manage_kimi_harness_login_runs_kimi_login(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Selecting "Sign in" drives ``harness_login(KIMI_KEY)`` then loops; Back exits."""
import omnigent.onboarding.harness_install as hi
import omnigent.onboarding.interactive as it
monkeypatch.setattr(hi, "harness_cli_installed", lambda key: True)
monkeypatch.setattr(it, "console", Mock())
login = Mock(return_value=False) # kimi has no status probe; return is ignored
logout = Mock()
monkeypatch.setattr(hi, "harness_login", login)
monkeypatch.setattr(hi, "harness_logout", logout)
# First iteration: Sign in (0); second: ← Back (3) to exit the loop.
choices = iter([0, 3])
monkeypatch.setattr(it, "select", lambda *a, **k: next(choices))
_manage_kimi_harness()
login.assert_called_once_with(hi.KIMI_KEY)
logout.assert_not_called()