chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:00 +08:00
commit 3de48288cb
2986 changed files with 1131193 additions and 0 deletions
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+415
View File
@@ -0,0 +1,415 @@
"""Unit tests for CLI OIDC token storage (omnigent/cli_auth.py).
Tests the store/load/clear lifecycle for session tokens persisted
by ``omnigent login``.
"""
from __future__ import annotations
import time
import pytest
@pytest.fixture()
def token_dir(tmp_path, monkeypatch):
"""Redirect the token file to a temp directory.
Patches ``state_dir`` to return ``tmp_path`` so tests don't
touch ``~/.omnigent``.
:param tmp_path: Pytest temp directory.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: The temp directory path.
"""
monkeypatch.setattr(
"omnigent.cli_auth._token_file_path",
lambda: tmp_path / "auth_tokens.json",
)
return tmp_path
def test_store_and_load_token(token_dir) -> None:
"""A stored token can be loaded back by server URL.
This is the happy path: ``omnigent login`` stores a token,
``omnigent run --server`` loads it.
"""
from omnigent.cli_auth import load_token, store_token
store_token(
server_url="http://localhost:8000",
token="jwt-abc",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
result = load_token("http://localhost:8000")
# Token must be the exact value stored.
assert result == "jwt-abc", f"Expected 'jwt-abc', got {result!r}."
def test_load_returns_none_when_no_file(token_dir) -> None:
"""load_token returns None when no token file exists.
The first time a user runs ``omnigent run --server`` without
having run ``omnigent login``, there should be no crash.
"""
from omnigent.cli_auth import load_token
assert load_token("http://localhost:8000") is None
def test_load_returns_none_for_unknown_server(token_dir) -> None:
"""load_token returns None for a server with no stored token.
A token stored for one server must not leak to another.
"""
from omnigent.cli_auth import load_token, store_token
store_token(
server_url="http://localhost:8000",
token="jwt-abc",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
assert load_token("http://other-server:9000") is None
def test_load_returns_none_for_expired_token(token_dir) -> None:
"""load_token returns None when the stored token has expired.
Expired tokens must not be used — the user needs to re-run
``omnigent login``.
"""
from omnigent.cli_auth import load_token, store_token
store_token(
server_url="http://localhost:8000",
token="jwt-expired",
user_id="alice@example.com",
expires_at=time.time() - 1, # Already expired.
)
assert load_token("http://localhost:8000") is None
def test_clear_token(token_dir) -> None:
"""clear_token removes a stored token for a server.
After clearing, load_token must return None.
"""
from omnigent.cli_auth import clear_token, load_token, store_token
store_token(
server_url="http://localhost:8000",
token="jwt-abc",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
clear_token("http://localhost:8000")
assert load_token("http://localhost:8000") is None
def test_trailing_slash_normalization(token_dir) -> None:
"""Server URLs are normalized (trailing slash stripped).
``http://localhost:8000/`` and ``http://localhost:8000`` must
resolve to the same stored token.
"""
from omnigent.cli_auth import load_token, store_token
store_token(
server_url="http://localhost:8000/",
token="jwt-slash",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
# Load without trailing slash.
assert load_token("http://localhost:8000") == "jwt-slash"
def test_file_permissions(token_dir) -> None:
"""Token file is created with 0o600 (user-only read/write).
Tokens are sensitive — they must not be world-readable.
"""
from omnigent.cli_auth import store_token
store_token(
server_url="http://localhost:8000",
token="jwt-abc",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
path = token_dir / "auth_tokens.json"
mode = path.stat().st_mode & 0o777
# 0o600 = user read + write only.
assert mode == 0o600, (
f"Token file should have 0o600 permissions, got {oct(mode)}. "
f"This means the token could be readable by other users."
)
def test_store_overwrites_existing(token_dir) -> None:
"""Storing a token for the same server overwrites the old one.
Re-running ``omnigent login`` should update the token, not
append.
"""
from omnigent.cli_auth import load_token, store_token
store_token(
server_url="http://localhost:8000",
token="old-token",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
store_token(
server_url="http://localhost:8000",
token="new-token",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
assert load_token("http://localhost:8000") == "new-token"
def test_multiple_servers(token_dir) -> None:
"""Tokens for different servers are stored independently.
A user may have accounts on multiple servers.
"""
from omnigent.cli_auth import load_token, store_token
store_token(
server_url="http://localhost:8000",
token="token-a",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
store_token(
server_url="https://prod.example.com",
token="token-b",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
assert load_token("http://localhost:8000") == "token-a"
assert load_token("https://prod.example.com") == "token-b"
# ── Databricks Apps pointer records ────────────────────────────────
def test_store_and_load_databricks_record(token_dir) -> None:
"""A stored Databricks pointer record resolves back to its workspace.
``omnigent login <apps-url>`` stores the record; the server-auth
chain looks up the workspace host to mint fresh tokens.
"""
from omnigent.cli_auth import load_databricks_workspace_host, store_databricks_auth
store_databricks_auth(
server_url="https://myapp-123.aws.databricksapps.com",
workspace_host="https://example.databricks.com",
user_id="alice@example.com",
)
host = load_databricks_workspace_host("https://myapp-123.aws.databricksapps.com")
assert host == "https://example.databricks.com", (
f"Expected the stored workspace host back, got {host!r}. A miss means "
"the auth chain would silently fall through to ambient credentials."
)
def test_databricks_request_headers_org_only(token_dir) -> None:
"""A recorded ?o= selector surfaces as the workspace-routing header.
When the bare host is the account, the request routes by this header
(equivalently to ``?o=``). A record with no org id (single-workspace
host) yields no header, so those callers are unaffected.
"""
from omnigent.cli_auth import databricks_request_headers, store_databricks_auth
store_databricks_auth(
server_url="https://acme.databricks.com/api/2.0/omnigent",
workspace_host="https://acme.databricks.com",
org_id="2850744067564480",
)
assert databricks_request_headers("https://acme.databricks.com/api/2.0/omnigent") == {
"X-Databricks-Org-Id": "2850744067564480"
}
store_databricks_auth(
server_url="https://single.databricks.com/api/2.0/omnigent",
workspace_host="https://single.databricks.com",
)
assert databricks_request_headers("https://single.databricks.com/api/2.0/omnigent") == {}
def test_databricks_request_headers_pairs_bearer_and_org(token_dir) -> None:
"""The paired minter always emits the bearer and the ?o= header together.
The static-dict seams (WS handshakes, hook-config replay) call this so a
workspace request can never carry ``Authorization`` without the routing
header. A missing token or selector is omitted, so single-workspace and
local-unauthenticated callers are unaffected.
"""
from omnigent.cli_auth import databricks_request_headers, store_databricks_auth
store_databricks_auth(
server_url="https://acme.databricks.com/api/2.0/omnigent",
workspace_host="https://acme.databricks.com",
org_id="2850744067564480",
)
recorded = "https://acme.databricks.com/api/2.0/omnigent"
# Bearer + org travel together.
assert databricks_request_headers(recorded, bearer_token="tok") == {
"Authorization": "Bearer tok",
"X-Databricks-Org-Id": "2850744067564480",
}
# Recorded selector but no token (local/unauth): org still rides, no bearer.
assert databricks_request_headers(recorded) == {"X-Databricks-Org-Id": "2850744067564480"}
# No record (unknown server): bearer only, no routing header.
assert databricks_request_headers("https://other.example.com", bearer_token="tok") == {
"Authorization": "Bearer tok"
}
def test_databricks_request_headers_folds_extra_headers(token_dir, monkeypatch) -> None:
"""OMNIGENT_DATABRICKS_EXTRA_HEADERS rides every request built via the helper.
Databricks deployments set it to opaque request-routing selector headers so
a request pins to a specific server instance. Because it is folded into this
one helper, any caller that routes through it gets the selectors for free; a
caller that hand-rolls a bare bearer misses them. Malformed / unset input is
a no-op (prod-safe).
"""
from omnigent.cli_auth import databricks_request_headers, store_databricks_auth
store_databricks_auth(
server_url="https://acme.databricks.com/api/2.0/omnigent",
workspace_host="https://acme.databricks.com",
org_id="2850744067564480",
)
recorded = "https://acme.databricks.com/api/2.0/omnigent"
monkeypatch.setenv(
"OMNIGENT_DATABRICKS_EXTRA_HEADERS",
'{"x-databricks-route-hint": "instance-abc"}',
)
# Extra header travels alongside the bearer + ?o= routing header.
assert databricks_request_headers(recorded, bearer_token="tok") == {
"Authorization": "Bearer tok",
"X-Databricks-Org-Id": "2850744067564480",
"x-databricks-route-hint": "instance-abc",
}
# It rides even for an unknown server with no token (routing-only request).
assert databricks_request_headers("https://other.example.com") == {
"x-databricks-route-hint": "instance-abc",
}
# Malformed JSON is ignored (no crash) so a bad value can't break requests.
monkeypatch.setenv("OMNIGENT_DATABRICKS_EXTRA_HEADERS", "not-json")
assert databricks_request_headers("https://other.example.com", bearer_token="tok") == {
"Authorization": "Bearer tok",
}
# Unset (prod default) is a no-op.
monkeypatch.delenv("OMNIGENT_DATABRICKS_EXTRA_HEADERS", raising=False)
assert databricks_request_headers("https://other.example.com", bearer_token="tok") == {
"Authorization": "Bearer tok",
}
def test_load_token_returns_none_for_databricks_record(token_dir) -> None:
"""A Databricks pointer record carries NO bearer — load_token must miss.
Databricks OAuth tokens expire after ~1h, so the record deliberately
stores only the workspace host. If load_token returned anything here,
the JWT path would send a garbage Authorization header.
"""
from omnigent.cli_auth import load_token, store_databricks_auth
store_databricks_auth(
server_url="https://myapp-123.aws.databricksapps.com",
workspace_host="https://example.databricks.com",
)
assert load_token("https://myapp-123.aws.databricksapps.com") is None
def test_load_databricks_host_returns_none_for_jwt_record(token_dir) -> None:
"""A session-JWT record is not a Databricks pointer record.
The Databricks resolution path must not fire for servers the user
logged into via accounts/OIDC — those send the stored JWT instead.
"""
import time
from omnigent.cli_auth import load_databricks_workspace_host, store_token
store_token(
server_url="http://localhost:8000",
token="jwt-abc",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
assert load_databricks_workspace_host("http://localhost:8000") is None
def test_databricks_record_normalizes_workspace_trailing_slash(token_dir) -> None:
"""The stored workspace host is normalized (trailing slash stripped).
``Config(host=...)`` treats ``https://ws`` and ``https://ws/`` as
distinct cache keys in some SDK paths — store one canonical form.
"""
from omnigent.cli_auth import load_databricks_workspace_host, store_databricks_auth
store_databricks_auth(
server_url="https://myapp-123.aws.databricksapps.com/",
workspace_host="https://example.databricks.com/",
)
# Lookup without the trailing slash hits the same record, and the
# stored host comes back canonical.
host = load_databricks_workspace_host("https://myapp-123.aws.databricksapps.com")
assert host == "https://example.databricks.com"
def test_databricks_record_overwrites_jwt_record(token_dir) -> None:
"""Re-logging into a server replaces its record wholesale.
A server that switched deployment shape (accounts → Databricks Apps)
must not keep serving the stale JWT.
"""
import time
from omnigent.cli_auth import (
load_databricks_workspace_host,
load_token,
store_databricks_auth,
store_token,
)
store_token(
server_url="https://server.example.com",
token="old-jwt",
user_id="alice@example.com",
expires_at=time.time() + 3600,
)
store_databricks_auth(
server_url="https://server.example.com",
workspace_host="https://example.databricks.com",
)
# The JWT is gone; the pointer record answers instead.
assert load_token("https://server.example.com") is None
assert (
load_databricks_workspace_host("https://server.example.com")
== "https://example.databricks.com"
)
+476
View File
@@ -0,0 +1,476 @@
"""Tests for the always-on CLI diagnostics log."""
from __future__ import annotations
import io
import logging
import sys
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
import pytest
from omnigent import cli_diagnostics
@dataclass(frozen=True)
class _LoggerSnapshot:
"""
Logger state captured before a test configures CLI diagnostics.
:param handlers: Handlers present before the test started.
:param level: Numeric logging level configured before the test started.
:param propagate: Whether the logger propagated before the test started.
"""
handlers: list[logging.Handler]
level: int
propagate: bool
class _FailingRedirectedStderr:
"""
Stderr stub whose close path fails after exposing the original stream.
:param original: Terminal stderr stream that ``restore_stderr`` should
restore before attempting to close the redirected stream.
"""
def __init__(self, original: io.TextIOBase) -> None:
"""
Create the failing redirected stderr stub.
:param original: Terminal stderr stream saved for restoration.
:returns: ``None``.
"""
self._original_stderr = original
def close(self) -> None:
"""
Raise the close failure that ``restore_stderr`` must log.
:returns: ``None``.
:raises OSError: Always, to exercise the diagnostics path.
"""
raise OSError("close failed")
def _capture_logger_snapshots() -> dict[str, _LoggerSnapshot]:
"""
Capture package logger state mutated by ``setup_cli_logging``.
:returns: Snapshot keyed by logger name.
"""
snapshots: dict[str, _LoggerSnapshot] = {}
for name in ("", "omnigent", "omnigent_ui_sdk", "databricks.sdk"):
logger = logging.getLogger(name)
snapshots[name] = _LoggerSnapshot(
handlers=list(logger.handlers),
level=logger.level,
propagate=logger.propagate,
)
return snapshots
def _restore_logger_snapshots(snapshots: dict[str, _LoggerSnapshot]) -> None:
"""
Restore package loggers after ``setup_cli_logging`` added file handlers.
:param snapshots: Logger state returned by
:func:`_capture_logger_snapshots`.
:returns: ``None``.
"""
for name, snapshot in snapshots.items():
logger = logging.getLogger(name)
for handler in list(logger.handlers):
logger.removeHandler(handler)
if handler not in snapshot.handlers:
handler.close()
for handler in snapshot.handlers:
logger.addHandler(handler)
logger.setLevel(snapshot.level)
logger.propagate = snapshot.propagate
@pytest.fixture
def isolated_cli_diagnostics(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> Iterator[None]:
"""
Isolate CLI diagnostics global logging state for a test.
:param monkeypatch: Pytest monkeypatch fixture.
:param tmp_path: Temporary home directory root for diagnostics logs.
:returns: Iterator yielding control to the test body.
"""
snapshots = _capture_logger_snapshots()
original_stderr = sys.stderr
monkeypatch.setenv("HOME", str(tmp_path))
yield
cli_diagnostics.restore_stderr()
sys.stderr = original_stderr
_restore_logger_snapshots(snapshots)
def test_redirect_stderr_to_log_redacts_direct_stderr_writes(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Redirected raw stderr writes must honor the diagnostics redaction contract.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: ``None``.
"""
del isolated_cli_diagnostics
terminal_stderr = io.StringIO()
monkeypatch.setattr(sys, "stderr", terminal_stderr)
ctx = cli_diagnostics.setup_cli_logging(["run", "agent.yaml"])
cli_diagnostics.redirect_stderr_to_log()
print("MY_API_KEY=super-secret-token", file=sys.stderr)
sys.stderr.write("Authorization: Bearer sk-directsecret12345\n")
sys.stderr.flush()
cli_diagnostics.restore_stderr()
log_text = ctx.path.read_text(encoding="utf-8")
assert "super-secret-token" not in log_text, (
f"redirected stderr leaked an API key into the CLI diagnostics log: {log_text!r}"
)
assert "sk-directsecret12345" not in log_text, (
f"redirected stderr leaked an SDK token into the CLI diagnostics log: {log_text!r}"
)
assert "[REDACTED]" in log_text, (
f"redirected stderr should preserve context with redacted values, got: {log_text!r}"
)
assert terminal_stderr.getvalue() == "", (
f"redirected stderr should not paint into the terminal, got: "
f"{terminal_stderr.getvalue()!r}"
)
def test_restore_stderr_returns_writes_to_original_terminal(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``restore_stderr`` must return subsequent writes to the original stream.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: ``None``.
"""
del isolated_cli_diagnostics
terminal_stderr = io.StringIO()
monkeypatch.setattr(sys, "stderr", terminal_stderr)
ctx = cli_diagnostics.setup_cli_logging(["run", "agent.yaml"])
cli_diagnostics.redirect_stderr_to_log()
print("during-tui", file=sys.stderr)
cli_diagnostics.restore_stderr()
print("after-tui", file=sys.stderr)
log_text = ctx.path.read_text(encoding="utf-8")
assert "during-tui" in log_text, (
f"stderr written during the TUI lifetime should land in the log: {log_text!r}"
)
assert "after-tui" not in log_text, (
f"stderr written after restore should not keep landing in the log: {log_text!r}"
)
assert terminal_stderr.getvalue() == "after-tui\n", (
f"stderr was not restored to the original terminal stream: {terminal_stderr.getvalue()!r}"
)
def test_log_cli_error_hint_uses_original_stderr_when_redirected(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Fatal-error hints must stay visible even while TUI stderr is redirected.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: ``None``.
"""
del isolated_cli_diagnostics
terminal_stderr = io.StringIO()
monkeypatch.setattr(sys, "stderr", terminal_stderr)
ctx = cli_diagnostics.setup_cli_logging(["run", "agent.yaml"])
cli_diagnostics.redirect_stderr_to_log()
try:
raise RuntimeError("boom")
except RuntimeError as exc:
cli_diagnostics.log_cli_error_hint(exc)
cli_diagnostics.restore_stderr()
hint = terminal_stderr.getvalue()
assert hint == f"Details logged to {ctx.path}\n", (
f"fatal-error hint should print to the original stderr stream, got: {hint!r}"
)
log_text = ctx.path.read_text(encoding="utf-8")
assert "Fatal CLI error: boom" in log_text, (
f"fatal exception context was not written to the diagnostics log: {log_text!r}"
)
assert "Details logged to" not in log_text, (
f"user-facing fatal-error hint should not be redirected into the log: {log_text!r}"
)
def test_redirect_stderr_to_log_retargets_existing_logging_stderr_handlers(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Existing stderr-backed logging handlers must follow TUI stderr redirect.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: ``None``.
"""
del isolated_cli_diagnostics
terminal_stderr = io.StringIO()
monkeypatch.setattr(sys, "stderr", terminal_stderr)
root = logging.getLogger()
for handler in list(root.handlers):
root.removeHandler(handler)
handler = logging.StreamHandler(terminal_stderr)
handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s"))
root.addHandler(handler)
root.setLevel(logging.WARNING)
databricks_logger = logging.getLogger("databricks.sdk")
databricks_logger.handlers.clear()
databricks_logger.setLevel(logging.WARNING)
databricks_logger.propagate = True
ctx = cli_diagnostics.setup_cli_logging(["run", "agent.yaml"])
cli_diagnostics.redirect_stderr_to_log()
databricks_logger.warning(
"Databricks CLI v0.295.0 does not support --force-refresh "
"(requires >= v0.296.0). The CLI's token cache may provide stale tokens."
)
cli_diagnostics.restore_stderr()
databricks_logger.warning("after TUI")
log_text = ctx.path.read_text(encoding="utf-8")
warning_line = (
"WARNING:databricks.sdk:Databricks CLI v0.295.0 does not support --force-refresh"
)
assert warning_line in log_text, (
f"stderr-backed third-party logging should land in the CLI log: {log_text!r}"
)
assert "after TUI" not in log_text, (
f"logging handlers should be restored after the TUI exits: {log_text!r}"
)
assert terminal_stderr.getvalue() == "WARNING:databricks.sdk:after TUI\n", (
f"third-party logging painted into the terminal during TUI redirect: "
f"{terminal_stderr.getvalue()!r}"
)
def test_redirect_stderr_logs_open_failures(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``redirect_stderr_to_log`` must record failures instead of swallowing them.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: ``None``.
"""
del isolated_cli_diagnostics
terminal_stderr = io.StringIO()
monkeypatch.setattr(sys, "stderr", terminal_stderr)
ctx = cli_diagnostics.setup_cli_logging(["run", "agent.yaml"])
def _raise_open_failure(*_args: object, **_kwargs: object) -> io.TextIOWrapper:
"""
Stand in for ``open`` when the redirected stderr file cannot be opened.
:param _args: Positional arguments passed by ``redirect_stderr_to_log``.
:param _kwargs: Keyword arguments passed by ``redirect_stderr_to_log``.
:returns: Never returns.
:raises OSError: Always, to exercise the diagnostics path.
"""
raise OSError("open failed")
monkeypatch.setattr(cli_diagnostics, "open", _raise_open_failure, raising=False)
cli_diagnostics.redirect_stderr_to_log()
assert sys.stderr is terminal_stderr, (
"redirect_stderr_to_log should leave stderr alone when opening the diagnostics file fails."
)
log_text = ctx.path.read_text(encoding="utf-8")
assert "Failed to redirect stderr to CLI log: open failed" in log_text, (
f"stderr redirect setup failures must be captured in the diagnostics log: {log_text!r}"
)
assert "Traceback" in log_text, (
f"stderr redirect setup failures should include traceback context: {log_text!r}"
)
def test_restore_stderr_logs_close_failures(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
``restore_stderr`` must log close failures after restoring the terminal.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: ``None``.
"""
del isolated_cli_diagnostics
terminal_stderr = io.StringIO()
ctx = cli_diagnostics.setup_cli_logging(["run", "agent.yaml"])
monkeypatch.setattr(sys, "stderr", _FailingRedirectedStderr(terminal_stderr))
cli_diagnostics.restore_stderr()
assert sys.stderr is terminal_stderr, (
"restore_stderr should restore the original terminal stream before "
"closing the redirected stream."
)
log_text = ctx.path.read_text(encoding="utf-8")
assert "Failed to close redirected stderr: close failed" in log_text, (
f"redirected stderr close failures must be captured in the diagnostics log: {log_text!r}"
)
assert "Traceback" in log_text, (
f"redirected stderr close failures should include traceback context: {log_text!r}"
)
def test_main_logs_click_exceptions(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""
Click-handled command errors must still reach the diagnostics log.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:param capsys: Pytest capture fixture for terminal stderr.
:returns: ``None``.
"""
del isolated_cli_diagnostics
from omnigent import cli as cli_module
# An unsupported --harness is a deterministic ClickException trigger that
# raises before any daemon/network work. (A bare `omnigent run` no longer
# errors — it drops into first-run `configure harnesses` — so it can't be
# the trigger here.)
monkeypatch.setattr(sys, "argv", ["omnigent", "run", "--harness", "not-a-real-harness"])
# Isolate from any real ~/.omnigent/config.yaml on the developer's machine.
monkeypatch.setattr(cli_module, "_load_global_config", dict)
with pytest.raises(SystemExit) as exc_info:
cli_module.main()
assert exc_info.value.code == 1, (
f"ClickException should preserve Click's exit code, got {exc_info.value.code!r}"
)
terminal = capsys.readouterr()
assert "Error: Unsupported harness 'not-a-real-harness'" in terminal.err, (
f"Click's normal user-facing error output changed: {terminal.err!r}"
)
path = cli_diagnostics.current_cli_log_path()
assert path is not None, "main() should set up the active CLI diagnostics log."
log_text = path.read_text(encoding="utf-8")
assert "Click CLI error: Unsupported harness 'not-a-real-harness'" in log_text, (
f"ClickException was not captured in the diagnostics log: {log_text!r}"
)
assert "Traceback" in log_text, (
f"ClickException log entry should include traceback context: {log_text!r}"
)
@pytest.mark.asyncio
async def test_slash_command_exceptions_reach_cli_log(
isolated_cli_diagnostics: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
REPL slash-command exceptions must be recorded in the CLI diagnostics log.
:param isolated_cli_diagnostics: Fixture isolating logging globals.
:param monkeypatch: Pytest monkeypatch fixture.
:returns: ``None``.
"""
del isolated_cli_diagnostics
from omnigent_ui_sdk import RichBlockFormatter
from omnigent.repl._repl import handle_slash_command
from tests.repl.helpers import CapturingHost
class _SessionWithoutModelSetter:
"""
Session stub matching the broken adapter surface from the REPL.
It intentionally exposes ``model_override`` and ``is_streaming``
but not ``set_model_override`` so ``/model <name>`` raises the
production AttributeError this regression covers.
"""
model_override: str | None = None
is_streaming = False
terminal_stderr = io.StringIO()
monkeypatch.setattr(sys, "stderr", terminal_stderr)
ctx = cli_diagnostics.setup_cli_logging(["run", "agent.yaml"])
host = CapturingHost()
await handle_slash_command(
"/model openai/gpt-5.4-mini",
_SessionWithoutModelSetter(), # type: ignore[arg-type]
None, # type: ignore[arg-type]
host,
RichBlockFormatter(),
)
assert "Error: '_SessionWithoutModelSetter' object has no attribute" in host.text, (
f"slash-command failures should still render inline for the user: {host.text!r}"
)
log_text = ctx.path.read_text(encoding="utf-8")
assert "Slash command failed: /model" in log_text, (
f"slash-command failures must be captured in the diagnostics log: {log_text!r}"
)
assert "AttributeError" in log_text, (
f"slash-command diagnostics should include the exception type: {log_text!r}"
)
assert "openai/gpt-5.4-mini" not in log_text, (
f"slash-command diagnostics should not copy command arguments into the log: {log_text!r}"
)
def test_safe_mtime_returns_zero_for_vanished_file(tmp_path: Path) -> None:
"""A file present at glob time but gone before stat resolves to 0.0, not a raise."""
real = tmp_path / "cli-real.log"
real.write_text("x")
assert cli_diagnostics._safe_mtime(real) > 0.0
assert cli_diagnostics._safe_mtime(tmp_path / "cli-gone.log") == 0.0
def test_prune_old_logs_survives_file_vanishing_mid_sort(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Concurrent ``omnigent run`` launches race to prune the same logs; a file
globbed by one but deleted by the other must not crash the sort (previously a
FileNotFoundError in the stat sort key aborted CLI startup)."""
real = [tmp_path / f"cli-{i:03d}.log" for i in range(cli_diagnostics.MAX_LOG_FILES + 3)]
for p in real:
p.write_text("x")
vanished = tmp_path / "cli-vanished.log" # globbed, then deleted by a peer run
monkeypatch.setattr(Path, "glob", lambda self, pattern: [*real, vanished])
cli_diagnostics._prune_old_logs(tmp_path) # must not raise
surviving = [p for p in real if p.exists()]
assert len(surviving) == cli_diagnostics.MAX_LOG_FILES # newest kept, oldest pruned
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
"""Tests for the OpenCode ``omni setup`` default-model picker helpers."""
from __future__ import annotations
import subprocess
from pathlib import Path
from types import SimpleNamespace
import pytest
import omnigent.cli as cli
from omnigent.cli import _list_opencode_models, _load_global_config, _set_opencode_default_model
@pytest.fixture
def _isolated_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
"""Point the global config at a tmp file so saves don't touch ``~/.omnigent``."""
path = tmp_path / "config.yaml"
monkeypatch.setattr("omnigent.cli._GLOBAL_CONFIG_PATH", path)
return path
def _fake_spec() -> SimpleNamespace:
return SimpleNamespace(binary="opencode")
# ── _list_opencode_models ───────────────────────────────────────────────────
def test_list_models_parses_nonblank_lines(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"omnigent.onboarding.harness_install.harness_install_spec", lambda _key: _fake_spec()
)
monkeypatch.setattr(
cli.subprocess,
"run",
lambda *a, **k: subprocess.CompletedProcess(
a, 0, stdout="anthropic/claude-sonnet-4-5\nopenai/gpt-5.5\n\n \n", stderr=""
),
)
assert _list_opencode_models() == ["anthropic/claude-sonnet-4-5", "openai/gpt-5.5"]
def test_list_models_empty_when_cli_absent(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"omnigent.onboarding.harness_install.harness_install_spec", lambda _key: None
)
assert _list_opencode_models() == []
def test_list_models_empty_on_subprocess_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"omnigent.onboarding.harness_install.harness_install_spec", lambda _key: _fake_spec()
)
def _boom(*_a: object, **_k: object) -> object:
raise OSError("no binary")
monkeypatch.setattr(cli.subprocess, "run", _boom)
assert _list_opencode_models() == []
# ── _set_opencode_default_model ─────────────────────────────────────────────
def test_set_default_model_persists_choice(
monkeypatch: pytest.MonkeyPatch, _isolated_config: Path
) -> None:
monkeypatch.setattr(
cli, "_list_opencode_models", lambda: ["anthropic/claude-sonnet-4-5", "x/y"]
)
monkeypatch.setattr("omnigent.onboarding.interactive.select", lambda *a, **k: 0)
status = _set_opencode_default_model(current=None)
assert status == "✓ default model: anthropic/claude-sonnet-4-5"
assert _load_global_config()["opencode_model"] == "anthropic/claude-sonnet-4-5"
def test_set_default_model_clear_unsets(
monkeypatch: pytest.MonkeyPatch, _isolated_config: Path
) -> None:
cli._save_global_config({"opencode_model": "x/y"})
monkeypatch.setattr(cli, "_list_opencode_models", lambda: ["a/b"])
# options == ["a/b", "Clear default ..."]; index 1 is the clear row.
monkeypatch.setattr("omnigent.onboarding.interactive.select", lambda *a, **k: 1)
status = _set_opencode_default_model(current="x/y")
assert status == "✓ default model cleared"
assert "opencode_model" not in _load_global_config()
def test_set_default_model_cancel_is_noop(
monkeypatch: pytest.MonkeyPatch, _isolated_config: Path
) -> None:
monkeypatch.setattr(cli, "_list_opencode_models", lambda: ["a/b"])
monkeypatch.setattr("omnigent.onboarding.interactive.select", lambda *a, **k: -1)
assert _set_opencode_default_model(current=None) is None
assert _load_global_config() == {}
def test_set_default_model_no_models_short_circuits(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cli, "_list_opencode_models", list)
called = False
def _select(*_a: object, **_k: object) -> int:
nonlocal called
called = True
return 0
monkeypatch.setattr("omnigent.onboarding.interactive.select", _select)
status = _set_opencode_default_model(current=None)
assert status is not None and status.startswith("")
assert called is False # never prompts when there's nothing to pick
+171
View File
@@ -0,0 +1,171 @@
"""
Unit tests for ``omnigent pane-picker``'s argv normalization.
The picker is exec'd as the new tmux pane's initial command after a
``pane-split``. It reads the parent pane's launch context, strips
flags that don't make sense for a sibling pane (resume modes,
one-shot prompts), then ``os.execvp``\\s into a fresh REPL.
These tests pin the strip helpers — the real exec path is exercised
manually in the design's § 6 phase 5 verification.
"""
from __future__ import annotations
import pytest
from omnigent.cli import _strip_one_shot_flags, _strip_resume_flags
@pytest.mark.parametrize(
("argv", "expected"),
[
# Bare ``--resume`` (picker mode): drop the single token.
(
["omnigent", "run", "a.yaml", "--profile", "prf", "--resume"],
["omnigent", "run", "a.yaml", "--profile", "prf"],
),
# ``--resume`` with a conversation id: drop both tokens.
(
["omnigent", "run", "a.yaml", "--resume", "conv_abc"],
["omnigent", "run", "a.yaml"],
),
# ``--resume=conv_id`` long-form: drop the combined token.
(
["omnigent", "run", "a.yaml", "--resume=conv_abc"],
["omnigent", "run", "a.yaml"],
),
# ``-r`` short form, no value: drop the single token.
(
["omnigent", "run", "a.yaml", "-r"],
["omnigent", "run", "a.yaml"],
),
# ``-r conv_id`` short form with value: drop both tokens.
(
["omnigent", "run", "a.yaml", "-r", "conv_abc"],
["omnigent", "run", "a.yaml"],
),
# Continue forms (always boolean).
(
["omnigent", "run", "a.yaml", "-c"],
["omnigent", "run", "a.yaml"],
),
(
["omnigent", "run", "a.yaml", "--continue"],
["omnigent", "run", "a.yaml"],
),
# Legacy ``--session`` / ``-s`` shapes still strip cleanly so
# a parent argv saved before the resume/session consolidation
# sanitizes without errors.
(
["omnigent", "run", "a.yaml", "--session", "conv_abc"],
["omnigent", "run", "a.yaml"],
),
(
["omnigent", "run", "a.yaml", "-s", "conv_abc"],
["omnigent", "run", "a.yaml"],
),
(
["omnigent", "run", "a.yaml", "--session=conv_abc"],
["omnigent", "run", "a.yaml"],
),
# Multiple resume flags in one argv: all dropped.
(
[
"omnigent",
"run",
"a.yaml",
"--profile",
"prf",
"--resume",
"--continue",
"--resume",
"conv_x",
],
["omnigent", "run", "a.yaml", "--profile", "prf"],
),
# Non-resume flags survive intact even when sandwiched
# between resume flags. Bare ``--resume`` followed by
# another flag must NOT swallow that flag as its value.
(
[
"omnigent",
"run",
"a.yaml",
"--resume",
"--profile",
"prf",
"--resume",
"x",
"--model",
"m",
],
["omnigent", "run", "a.yaml", "--profile", "prf", "--model", "m"],
),
# Empty argv → empty.
([], []),
# Non-resume argv: identity.
(
["omnigent", "run", "a.yaml", "--model", "m", "--profile", "prf"],
["omnigent", "run", "a.yaml", "--model", "m", "--profile", "prf"],
),
],
)
def test_strip_resume_flags(argv: list[str], expected: list[str]) -> None:
"""
The strip helper must remove every shape of resume flag
(bare ``--resume`` for the picker, ``--resume <id>`` for an
explicit pin, the ``--resume=<id>`` long form, short ``-r``
variants, and ``--continue`` / ``-c``) and leave every other
flag untouched. Legacy ``--session`` / ``-s`` are still
handled for backwards compatibility with parent argvs saved
before the consolidation.
Claim: each input → its expected pruned argv. Live regression
that prompted this helper: the live pane's argv had
``--resume``, the click ``run`` subcommand at the time didn't
accept that option, so exec'ing the parent's verbatim argv
exited with a click ``Error: No such option: --resume``
immediately, closing the new pane within seconds.
"""
assert _strip_resume_flags(argv) == expected
@pytest.mark.parametrize(
("argv", "expected"),
[
# ``-p`` short form: drop the flag and its value.
(
["omnigent", "run", "a.yaml", "-p", "hello there"],
["omnigent", "run", "a.yaml"],
),
# ``--prompt`` long form.
(
["omnigent", "run", "a.yaml", "--prompt", "hello"],
["omnigent", "run", "a.yaml"],
),
# ``--prompt=value``.
(
["omnigent", "run", "a.yaml", "--prompt=hello"],
["omnigent", "run", "a.yaml"],
),
# ``--system-prompt`` (note: spans both an arg-bearing flag
# and a similarly named flag — make sure we don't strip
# ``--system`` or ``--prompt-foo`` accidentally).
(
["omnigent", "run", "a.yaml", "--system-prompt", "be terse"],
["omnigent", "run", "a.yaml"],
),
],
)
def test_strip_one_shot_flags(argv: list[str], expected: list[str]) -> None:
"""
One-shot flags (``-p``, ``--prompt``, ``--system-prompt``) tied
to the parent's first turn must be removed before exec'ing in
the new pane — otherwise the new pane silently auto-sends the
parent's prompt, surprising the user.
Claim: every variant of one-shot flag is removed; everything
else passes through.
"""
assert _strip_one_shot_flags(argv) == expected
+325
View File
@@ -0,0 +1,325 @@
"""Tests for ``omnigent._runner_startup`` (startup UX helpers).
These cover the two pieces of UX added when local-runner startup
fails or stalls:
1. ``format_runner_log_tail`` — the failure-message helper that
surfaces the captured runner log path and a tail of its
contents on the ``ClickException``.
2. ``runner_startup_progress`` — the context manager that wraps
the runner-spawn → registration-wait window with a rich
spinner on a TTY, falling back to plain ``click.echo`` lines
off-TTY.
The behavior assertions matter because a regression in either
helper degrades the diagnosis story for "my runner won't start"
back to the opaque pre-fix state ("Local runner did not register
within 60s.").
"""
from __future__ import annotations
import re
import sys
import pytest
from omnigent._runner_startup import (
_NO_SPINNER_ENV_VAR,
STARTUP_PHASE_LABELS,
_spinner_enabled,
format_runner_log_tail,
runner_startup_progress,
)
# ---------------------------------------------------------------------------
# format_runner_log_tail
# ---------------------------------------------------------------------------
def test_format_runner_log_tail_none_path_returns_empty_string() -> None:
"""``log_path=None`` means no log was captured; suppress the block.
Callers concatenate the return value onto a ``ClickException``
message, so an empty string is the right signal that nothing
should be appended.
:returns: None.
"""
assert format_runner_log_tail(None) == ""
def test_format_runner_log_tail_surfaces_path_only(tmp_path) -> None:
"""A real log path is surfaced as a single ``Runner log:`` line.
The helper deliberately does NOT include log contents inline:
a wall of log lines drowns the actual error summary, and the
log file is one ``cat`` away once the user has the path. This
test pins the policy so a future regression that re-introduces
inline log tailing fails loud.
:param tmp_path: Pytest tmp dir fixture.
:returns: None.
"""
log = tmp_path / "runner.log"
log.write_text("ERROR: tunnel rejected (HTTP 401)\n")
out = format_runner_log_tail(log)
# The path is named explicitly so the user can ``cat`` it.
assert out == f"\nRunner log: {log}"
# Content of the log MUST NOT leak into the error message —
# that was the previous, overwhelming behavior.
assert "ERROR: tunnel rejected" not in out
def test_format_runner_log_tail_does_not_require_existing_file(tmp_path) -> None:
"""The helper does not stat the path before surfacing it.
The runner may fail before its log file actually lands on
disk (fork error, bad cwd, immediate import crash). We still
want to surface the configured log location so the user can
see where we *expected* the log to live — "file is missing"
is itself useful diagnostic information.
:param tmp_path: Pytest tmp dir fixture.
:returns: None.
"""
missing = tmp_path / "never-existed.log"
assert format_runner_log_tail(missing) == f"\nRunner log: {missing}"
# ---------------------------------------------------------------------------
# _spinner_enabled (TTY + env-var policy)
# ---------------------------------------------------------------------------
def test_spinner_enabled_requires_a_tty() -> None:
"""Off a TTY (CI logs, piped stderr) we must not emit a spinner.
A spinner's cursor-rewrites would render as raw escape codes
in a captured log, defeating the point of capturing it.
:returns: None.
"""
assert _spinner_enabled(stream_isatty=False, env={}) is False
def test_spinner_enabled_on_tty_default() -> None:
"""On a TTY with no opt-out, the spinner renders.
:returns: None.
"""
assert _spinner_enabled(stream_isatty=True, env={}) is True
@pytest.mark.parametrize("value", ["1", "true", "TRUE", "yes", "on"])
def test_spinner_disabled_by_env_var(value: str) -> None:
"""Any truthy ``OMNIGENT_NO_SPINNER`` value disables the spinner.
Lets users with mis-detecting terminals (tmux quirks, ssh into
bare containers) force the plain-echo fallback without code
changes.
:param value: Truthy spelling under test.
:returns: None.
"""
assert _spinner_enabled(stream_isatty=True, env={_NO_SPINNER_ENV_VAR: value}) is False
def test_spinner_not_disabled_by_falsy_env_var() -> None:
"""``OMNIGENT_NO_SPINNER=0`` (or empty) leaves the spinner on.
Mirrors typical "0 is off, 1 is on" UX so users do not
accidentally suppress the spinner by exporting the variable
with the value they meant as a disable.
:returns: None.
"""
assert _spinner_enabled(stream_isatty=True, env={_NO_SPINNER_ENV_VAR: "0"}) is True
assert _spinner_enabled(stream_isatty=True, env={_NO_SPINNER_ENV_VAR: ""}) is True
# ---------------------------------------------------------------------------
# runner_startup_progress
# ---------------------------------------------------------------------------
def test_runner_startup_progress_plain_mode_prints_to_stderr(
capsys,
) -> None:
"""Off-TTY: initial + every ``.update`` call go to stderr.
Plain-mode lines stay in scrollback so a CI log capture
reflects the same phase transitions a user would see
interactively.
:param capsys: Pytest stdio capture fixture.
:returns: None.
"""
with runner_startup_progress(
initial_message="Starting local runner\u2026",
enabled=False,
) as p:
p.update("Waiting for runner to register with example.com\u2026")
captured = capsys.readouterr()
# Nothing leaks to stdout (one-shot ``-p`` callers pipe stdout
# to consumers that should see only the agent's reply).
assert captured.out == ""
# Both phases land on stderr with the standard prefix.
assert "omnigent: Starting local runner" in captured.err
assert "omnigent: Waiting for runner to register" in captured.err
def test_runner_startup_progress_plain_mode_does_not_swallow_exceptions(
capsys,
) -> None:
"""An exception inside the body propagates with no extra wrapping.
The context manager only owns the renderer — it must not turn
a real runner-startup failure into a generic "context exited
with error" message.
:param capsys: Pytest stdio capture fixture.
:returns: None.
"""
with pytest.raises(RuntimeError, match="boom"):
with runner_startup_progress(
initial_message="Starting\u2026",
enabled=False,
):
raise RuntimeError("boom")
# The initial message still printed; the exception did not
# suppress it.
assert "omnigent: Starting" in capsys.readouterr().err
def test_runner_startup_progress_rich_mode_writes_only_to_stderr(
capsys,
) -> None:
"""Spinner output goes to stderr; stdout stays clean.
Stdout cleanliness matters for ``run --server -p "\u2026"`` where the
one-shot prints the agent's reply to stdout; we cannot let a
spinner contaminate that stream.
:param capsys: Pytest stdio capture fixture.
:returns: None.
"""
with runner_startup_progress(
initial_message="Starting\u2026",
enabled=True,
) as p:
p.update("Waiting\u2026")
captured = capsys.readouterr()
assert captured.out == ""
# Rich's transient spinner emits ANSI escape sequences while
# running, but on successful exit the line is cleared. We do
# NOT assert the post-exit stream is byte-empty (a CR or
# cursor-restore sequence may remain). The contract that
# matters for the user is: stdout is clean, and the visible
# cursor row is empty when the context exits.
# ---------------------------------------------------------------------------
# Cold-start phase labels (run / chat startup UX)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("label", STARTUP_PHASE_LABELS)
def test_startup_phase_labels_avoid_internal_jargon(label: str) -> None:
"""Cold-start labels must stay plain-language, not architecture terms.
The whole point of the ``run`` / ``chat`` startup spinner is to make
the silent cold-start gap read as ordinary forward motion to a user
who does not (and should not need to) know the framework's internals.
A regression that surfaces an internal term — "Waiting for runner
tunnel registration…", "Spawning host daemon…" — defeats that, so
pin the intent: none of the labels may contain an internal term.
A failure here means a phase label was changed to leak an
architecture word; rename it to something a non-developer user would
understand.
:param label: One cold-start phase label under test, e.g.
``"Starting the local server…"``.
:returns: None.
"""
# Internal nouns that mean nothing to an end user staring at startup.
# "server" is intentionally NOT here: "local server" is user-facing
# vocabulary (it is the ``omnigent server`` they may run directly).
jargon = {
"daemon",
"host",
"runner",
"tunnel",
"websocket",
"socket",
"rpc",
"bundle",
"registration",
"executor",
"subprocess",
}
words = set(re.findall(r"[a-z]+", label.lower()))
leaked = words & jargon
assert not leaked, (
f"Startup label {label!r} leaks internal jargon {sorted(leaked)}. "
"Cold-start labels are shown to end users; keep them plain-language."
)
def test_startup_phase_labels_render_in_order_plain_mode(capsys) -> None:
"""Driving the spinner with the real labels emits them in sequence.
This is the contract the ``cli._ensure_backend`` /
``chat._prepare_chat_session_via_daemon`` wiring relies on: each
``update`` with a ``STARTUP_PHASE_*`` constant produces a distinct
stderr line, in call order, with stdout untouched (so one-shot
``-p`` stdout stays clean).
A failure means the progress helper stopped forwarding ``update``
calls to stderr in order, or started leaking onto stdout.
:param capsys: Pytest stdio capture fixture.
:returns: None.
"""
with runner_startup_progress(
initial_message=STARTUP_PHASE_LABELS[0],
enabled=False,
) as p:
for label in STARTUP_PHASE_LABELS[1:]:
p.update(label)
captured = capsys.readouterr()
# stdout stays clean — the spinner is a stderr-only affordance.
assert captured.out == ""
# Every label landed on stderr, and in the order it was emitted (the
# find-index of each label is strictly increasing). A wrong order
# would mean the helper buffered/reordered updates.
indices = [captured.err.find(label) for label in STARTUP_PHASE_LABELS]
assert all(i != -1 for i in indices), (
f"Not all labels reached stderr. Indices: {indices}. stderr was:\n{captured.err}"
)
assert indices == sorted(indices), (
f"Labels rendered out of order. Indices: {indices}. stderr was:\n{captured.err}"
)
def test_runner_startup_progress_auto_detects_tty(monkeypatch, capsys) -> None:
"""``enabled=None`` consults ``sys.stderr.isatty()``.
Production callers leave ``enabled`` unset; this test pins the
contract that a non-TTY stderr in pytest's captured environment
falls through to plain-echo mode.
:param monkeypatch: Pytest monkeypatch fixture.
:param capsys: Pytest stdio capture fixture.
:returns: None.
"""
# Under pytest capture stderr.isatty() is already False, but be
# explicit so the test does not depend on capture mode.
monkeypatch.setattr(sys.stderr, "isatty", lambda: False, raising=False)
with runner_startup_progress(initial_message="Starting\u2026"):
pass
assert "omnigent: Starting" in capsys.readouterr().err
+381
View File
@@ -0,0 +1,381 @@
"""Tests for the server-lifecycle CLI: ``server start/stop/status`` and top-level ``stop``."""
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import Mock
import pytest
from click import ClickException
from click.testing import CliRunner
from omnigent.cli import _HostDaemonRecord, _SessionPagesResult, cli
from omnigent.host.local_server import LocalServerInfo, LocalServerStartup
def _record(
target: str = "local", *, mode: str = "local", pid: int = 999_999
) -> _HostDaemonRecord:
"""Build a real daemon record for stubbing the registry.
:param target: Daemon target — ``"local"`` or a server URL.
:param mode: ``"local"`` or ``"server"``.
:param pid: Recorded daemon PID, e.g. ``999999``.
:returns: A populated :class:`_HostDaemonRecord`.
"""
return _HostDaemonRecord(
pid=pid,
target=target,
mode=mode,
server_url=None if mode == "local" else target,
log_path=None,
started_at=0,
)
# ── server status ──────────────────────────────────────────────────
def test_server_status_not_running(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server status`` reports not-running when no background server is recorded."""
monkeypatch.setattr(
"omnigent.cli.local_server_status",
lambda: LocalServerInfo(running=False, pid=None, port=None, url=None),
)
monkeypatch.setattr("omnigent.cli._find_daemon_record", lambda target: None)
result = CliRunner().invoke(cli, ["server", "status"])
assert result.exit_code == 0, result.output
assert "not running" in result.output
def test_server_status_running_reports_details(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server status`` prints url/pid/port, the live-session count, and daemon-attached state."""
monkeypatch.setattr(
"omnigent.cli.local_server_status",
lambda: LocalServerInfo(running=True, pid=4321, port=8123, url="http://127.0.0.1:8123"),
)
# A record exists → "host daemon attached: yes".
monkeypatch.setattr("omnigent.cli._find_daemon_record", lambda target: _record())
monkeypatch.setattr(
"omnigent.cli._fetch_session_pages",
lambda **kwargs: _SessionPagesResult(sessions=[], error=None),
)
result = CliRunner().invoke(cli, ["server", "status"])
assert result.exit_code == 0, result.output
assert "http://127.0.0.1:8123" in result.output
assert "pid 4321" in result.output
assert "live sessions: 0" in result.output # empty fetch → 0 live sessions
assert "host daemon attached: yes" in result.output
def test_server_status_json(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server status --json`` emits the structured fields incl. the log path."""
monkeypatch.setattr(
"omnigent.cli.local_server_status",
lambda: LocalServerInfo(
running=True,
pid=4321,
port=8123,
url="http://127.0.0.1:8123",
log_path=Path("/tmp/.omnigent/logs/server/local-server-ab12.log"),
),
)
monkeypatch.setattr("omnigent.cli._find_daemon_record", lambda target: None)
monkeypatch.setattr(
"omnigent.cli._fetch_session_pages",
lambda **kwargs: _SessionPagesResult(sessions=[], error=None),
)
result = CliRunner().invoke(cli, ["server", "status", "--json"])
assert result.exit_code == 0, result.output
assert json.loads(result.output) == {
"running": True,
"pid": 4321,
"port": 8123,
"url": "http://127.0.0.1:8123",
"log_path": "/tmp/.omnigent/logs/server/local-server-ab12.log",
"live_sessions": 0,
"daemon_attached": False,
}
def test_server_status_text_reports_log_path(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server status`` (text) names the running server's captured log file."""
monkeypatch.setattr(
"omnigent.cli.local_server_status",
lambda: LocalServerInfo(
running=True,
pid=4321,
port=8123,
url="http://127.0.0.1:8123",
log_path=Path.home() / ".omnigent" / "logs" / "server" / "local-server-ab12.log",
),
)
monkeypatch.setattr("omnigent.cli._find_daemon_record", lambda target: None)
monkeypatch.setattr(
"omnigent.cli._fetch_session_pages",
lambda **kwargs: _SessionPagesResult(sessions=[], error=None),
)
result = CliRunner().invoke(cli, ["server", "status"])
assert result.exit_code == 0, result.output
assert "running at http://127.0.0.1:8123" in result.output
assert "log: ~/.omnigent/logs/server/local-server-ab12.log" in result.output
def test_server_status_session_count_failure_is_graceful(monkeypatch: pytest.MonkeyPatch) -> None:
"""A failed session fetch leaves the count unknown rather than erroring the command."""
monkeypatch.setattr(
"omnigent.cli.local_server_status",
lambda: LocalServerInfo(running=True, pid=1, port=8123, url="http://127.0.0.1:8123"),
)
monkeypatch.setattr("omnigent.cli._find_daemon_record", lambda target: None)
def _boom(**kwargs: object) -> _SessionPagesResult:
raise ClickException("server unreachable")
monkeypatch.setattr("omnigent.cli._fetch_session_pages", _boom)
result = CliRunner().invoke(cli, ["server", "status"])
assert result.exit_code == 0, result.output
assert "running at http://127.0.0.1:8123" in result.output
assert "live sessions:" not in result.output # count omitted on fetch failure
# ── server start ───────────────────────────────────────────────────
def test_server_start_spawns(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server start`` reports the URL and exact log file of a spawned server."""
monkeypatch.setattr(
"omnigent.cli.ensure_local_omnigent_server",
lambda: LocalServerStartup(
url="http://127.0.0.1:8123",
spawned=True,
log_path=Path.home() / ".omnigent" / "logs" / "server" / "local-server-ab12.log",
),
)
result = CliRunner().invoke(cli, ["server", "start"])
assert result.exit_code == 0, result.output
assert "Started background server at http://127.0.0.1:8123" in result.output
# The exact captured-log file is surfaced so the detached server isn't a
# black box — collapsed to ``~`` for readability.
assert "log: ~/.omnigent/logs/server/local-server-ab12.log" in result.output
def test_server_start_reuses(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server start`` reports reuse and the reused server's log file."""
monkeypatch.setattr(
"omnigent.cli.ensure_local_omnigent_server",
lambda: LocalServerStartup(
url="http://127.0.0.1:8123",
spawned=False,
log_path=Path.home() / ".omnigent" / "logs" / "server" / "local-server-cd34.log",
),
)
result = CliRunner().invoke(cli, ["server", "start"])
assert result.exit_code == 0, result.output
assert "already running at http://127.0.0.1:8123" in result.output
# Even a reused server (one this invocation didn't spawn) names its log,
# read back from the sidecar.
assert "log: ~/.omnigent/logs/server/local-server-cd34.log" in result.output
def test_server_start_omits_log_when_unknown(monkeypatch: pytest.MonkeyPatch) -> None:
"""No log line when the running server has no captured-log file.
A foreground ``omnigent server`` streams logs to its terminal, so a
reuse of it carries ``log_path=None`` and ``server start`` must not print
a bogus or empty ``log:`` line.
"""
monkeypatch.setattr(
"omnigent.cli.ensure_local_omnigent_server",
lambda: LocalServerStartup(url="http://127.0.0.1:8123", spawned=False, log_path=None),
)
result = CliRunner().invoke(cli, ["server", "start"])
assert result.exit_code == 0, result.output
assert "log:" not in result.output
# ── server stop ────────────────────────────────────────────────────
def test_server_stop_stops_server_and_local_daemon(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server stop`` terminates the local daemon, then stops the background server."""
monkeypatch.setattr(
"omnigent.cli.local_server_url_if_healthy", lambda: "http://127.0.0.1:8123"
)
local_record = _record()
monkeypatch.setattr(
"omnigent.cli._find_daemon_record",
lambda target: local_record if target == "local" else None,
)
terminated: list[_HostDaemonRecord] = []
monkeypatch.setattr(
"omnigent.cli._terminate_daemon",
lambda record, *, force: terminated.append(record),
)
stop_server = Mock()
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", stop_server)
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: None)
result = CliRunner().invoke(cli, ["server", "stop"])
assert result.exit_code == 0, result.output
assert "Stopped the background server." in result.output
assert terminated == [local_record] # the local daemon was terminated
stop_server.assert_called_once_with() # and the server stopped
def test_server_stop_no_server_running(monkeypatch: pytest.MonkeyPatch) -> None:
"""``server stop`` reports nothing running when no background server exists."""
monkeypatch.setattr("omnigent.cli.local_server_url_if_healthy", lambda: None)
monkeypatch.setattr("omnigent.cli._find_daemon_record", lambda target: None)
stop_server = Mock()
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", stop_server)
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: None)
result = CliRunner().invoke(cli, ["server", "stop"])
assert result.exit_code == 0, result.output
assert "No background server is running." in result.output
stop_server.assert_called_once_with() # idempotent: still clears any stale pidfile
# ── top-level stop ─────────────────────────────────────────────────
def test_stop_terminates_all_daemons_and_server(monkeypatch: pytest.MonkeyPatch) -> None:
"""``stop`` terminates every daemon and stops the background server."""
records = [_record("local"), _record("https://example.com", mode="server")]
monkeypatch.setattr("omnigent.cli._list_daemon_records", lambda: records)
terminated: list[_HostDaemonRecord] = []
monkeypatch.setattr(
"omnigent.cli._terminate_daemon",
lambda record, *, force: terminated.append(record),
)
monkeypatch.setattr(
"omnigent.cli.local_server_url_if_healthy", lambda: "http://127.0.0.1:8123"
)
stop_server = Mock()
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", stop_server)
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: None)
result = CliRunner().invoke(cli, ["stop"])
assert result.exit_code == 0, result.output
assert terminated == records # both daemons terminated
stop_server.assert_called_once_with()
assert "Stopped 2 daemon(s) and the background server." in result.output
def test_stop_nothing_running(monkeypatch: pytest.MonkeyPatch) -> None:
"""``stop`` reports nothing to stop when no daemons or server exist."""
monkeypatch.setattr("omnigent.cli._list_daemon_records", list)
monkeypatch.setattr("omnigent.cli.local_server_url_if_healthy", lambda: None)
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", Mock())
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: None)
result = CliRunner().invoke(cli, ["stop"])
assert result.exit_code == 0, result.output
assert "Nothing to stop." in result.output
def test_stop_surfaces_failures_and_suggests_force(monkeypatch: pytest.MonkeyPatch) -> None:
"""A stubborn daemon makes ``stop`` exit nonzero and suggest ``--force``."""
records = [_record("local"), _record("https://example.com", mode="server")]
monkeypatch.setattr("omnigent.cli._list_daemon_records", lambda: records)
def _terminate(record: _HostDaemonRecord, *, force: bool) -> None:
if record.target == "local":
raise ClickException(f"daemon {record.pid} did not exit")
monkeypatch.setattr("omnigent.cli._terminate_daemon", _terminate)
monkeypatch.setattr("omnigent.cli.local_server_url_if_healthy", lambda: None)
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", Mock())
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: None)
result = CliRunner().invoke(cli, ["stop"])
assert result.exit_code != 0 # the stubborn daemon surfaces as a failure
assert "retry with --force" in result.output
def test_stop_clears_stale_legacy_host_pid(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""``stop`` clears a stale legacy ``host.pid`` (tracked only by the legacy pidfile).
Regression for the phantom where ``_delete_daemon_record`` removed only the
JSON record, leaving ``host.pid`` to reappear on every subsequent ``stop``.
Uses a dead PID so termination falls straight through to record deletion.
"""
host_pid = tmp_path / "host.pid"
# 2147483647 is not a real PID, so _pid_alive() is False → delete-only path.
host_pid.write_text("2147483647\nhttps://stale.example\n")
monkeypatch.setattr("omnigent.cli._HOST_PID_PATH", host_pid)
monkeypatch.setattr("omnigent.cli.local_server_url_if_healthy", lambda: None)
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", Mock())
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: None)
first = CliRunner().invoke(cli, ["stop"])
assert first.exit_code == 0, first.output
assert not host_pid.exists() # the legacy pidfile is cleared
second = CliRunner().invoke(cli, ["stop"])
assert "Nothing to stop." in second.output # phantom does not reappear
def test_stop_reports_untracked_orphan_server(monkeypatch: pytest.MonkeyPatch) -> None:
"""``stop`` sweeps and reports an orphaned server the pidfile lost track of.
Reproduces the reported symptom: no daemons, no pidfile-tracked server,
yet a live server lingers on :6767. The off-switch must stop it (via
:func:`stop_untracked_local_server`) and say so — not "Nothing to stop."
"""
monkeypatch.setattr("omnigent.cli._list_daemon_records", list)
monkeypatch.setattr("omnigent.cli.local_server_url_if_healthy", lambda: None)
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", Mock())
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: 93359)
result = CliRunner().invoke(cli, ["stop"])
assert result.exit_code == 0, result.output
assert "untracked server on :6767 (pid 93359)" in result.output
assert "Nothing to stop." not in result.output
def test_server_stop_finds_untracked_orphan_when_pidfile_lost(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``server stop`` reports success when only an untracked orphan is found.
The pidfile is gone (``local_server_url_if_healthy`` → ``None``), but a
live server is still on :6767. Previously this printed "No background
server is running" while the server kept running; now the orphan sweep
catches it.
"""
monkeypatch.setattr("omnigent.cli.local_server_url_if_healthy", lambda: None)
monkeypatch.setattr("omnigent.cli._find_daemon_record", lambda target: None)
monkeypatch.setattr("omnigent.cli.stop_local_omnigent_server", Mock())
monkeypatch.setattr("omnigent.cli.stop_untracked_local_server", lambda: 93359)
result = CliRunner().invoke(cli, ["server", "stop"])
assert result.exit_code == 0, result.output
assert "Stopped the background server." in result.output
File diff suppressed because it is too large Load Diff
+526
View File
@@ -0,0 +1,526 @@
"""Tests for the ``omni upgrade`` command (omnigent.cli.upgrade)."""
from __future__ import annotations
from pathlib import Path
import pytest
from click.testing import CliRunner
from omnigent.cli import cli
from omnigent.host.local_server import LocalServerInfo
from omnigent.update_check import _InstalledWheelInfo
def _uv_registry_info() -> _InstalledWheelInfo:
"""A registry uv-tool install → ``uv tool upgrade omnigent`` (runnable)."""
return _InstalledWheelInfo(
install_time_epoch=0.0,
installer="uv",
vcs_url=None,
commit_sha=None,
is_editable=False,
package_version="0.1.0",
detected_installer="uv",
)
def _git_install_info() -> _InstalledWheelInfo:
"""A git/VCS uv-tool install → ``uv tool install --reinstall git+…``."""
return _InstalledWheelInfo(
install_time_epoch=0.0,
installer="uv",
vcs_url="git+https://github.com/omnigent-ai/omnigent.git",
commit_sha="a" * 40,
is_editable=False,
package_version="0.1.0",
detected_installer="uv",
)
@pytest.fixture
def _wheel_install(monkeypatch: pytest.MonkeyPatch) -> None:
"""Make the running interpreter look like a registry uv-tool install.
Pins the resolved versions and stubs the install-shape detectors so
every upgrade test starts from "installed v0.1.0 via uv, not a clone".
The PyPI lookup and the server-stop side effects are stubbed per test.
"""
import importlib.metadata
monkeypatch.setattr(importlib.metadata, "version", lambda _name: "0.1.0")
monkeypatch.setattr("omnigent.update_check._find_repo_root", lambda: None)
monkeypatch.setattr("omnigent.update_check._read_installed_wheel_info", _uv_registry_info)
# Neutralize the process side effects unless a test opts in to assert them.
monkeypatch.setattr(
"omnigent.cli.local_server_status",
lambda: LocalServerInfo(running=False, pid=None, port=None, url=None),
)
monkeypatch.setattr("omnigent.cli._stop_local_server_and_daemon", lambda *, force: False)
def test_upgrade_up_to_date(monkeypatch: pytest.MonkeyPatch, _wheel_install: None) -> None:
"""Latest == installed → reports up to date, exit 0, nothing stopped/run."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.1.0")
def _must_not_run(*_a: object, **_k: object) -> int:
raise AssertionError("upgrade command ran while already up to date")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", _must_not_run)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code == 0, result.output
assert "up to date" in result.output
assert "0.1.0" in result.output
def test_upgrade_check_reports_newer_and_exits_nonzero(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""``--check`` with a newer release → prints the delta and exits non-zero, no upgrade."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.2.0")
def _must_not_run(*_a: object, **_k: object) -> int:
raise AssertionError("--check must not run the upgrade")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", _must_not_run)
result = CliRunner().invoke(cli, ["upgrade", "--check"])
assert result.exit_code == 1, result.output
assert "v0.1.0 → v0.2.0" in result.output
def test_upgrade_runs_installer_and_drains_first(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""A newer release → drain (no force), stop the server, run the uv command."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.2.0")
events: list[str] = []
monkeypatch.setattr(
"omnigent.cli._wait_for_local_sessions_to_drain", lambda: events.append("drained")
)
def _stop(*, force: bool) -> bool:
events.append(f"stop(force={force})")
return True
monkeypatch.setattr("omnigent.cli._stop_local_server_and_daemon", _stop)
ran: list[str] = []
def _run(command: str, _console: object) -> int:
ran.append(command)
return 0
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", _run)
# The post-upgrade verification re-reads the installed version in a fresh
# subprocess; stub it to report the version the installer "moved" to.
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: ("0.2.0", None)
)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code == 0, result.output
# Drain happened before the stop, before the install ran.
assert events == ["drained", "stop(force=False)"]
assert ran == ["uv tool upgrade omnigent"]
assert "Upgraded to v0.2.0" in result.output
def test_upgrade_force_skips_drain_and_force_stops(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""``--force`` skips the drain wait and force-stops the server."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.2.0")
def _no_drain() -> None:
raise AssertionError("--force must not wait for sessions to drain")
monkeypatch.setattr("omnigent.cli._wait_for_local_sessions_to_drain", _no_drain)
stop_calls: list[bool] = []
monkeypatch.setattr(
"omnigent.cli._stop_local_server_and_daemon",
lambda *, force: stop_calls.append(force) or False,
)
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", lambda *_a, **_k: 0)
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: ("0.2.0", None)
)
result = CliRunner().invoke(cli, ["upgrade", "--force"])
assert result.exit_code == 0, result.output
assert stop_calls == [True]
def test_upgrade_install_failure_surfaces(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""A non-zero installer exit → ClickException naming the status, exit non-zero."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.2.0")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", lambda *_a, **_k: 3)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code != 0
assert "exited with status 3" in result.output
def test_upgrade_index_unreachable(monkeypatch: pytest.MonkeyPatch, _wheel_install: None) -> None:
"""Index unreachable → clear error, no upgrade attempted."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: None)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code != 0
assert "package index" in result.output
def test_upgrade_rejects_dev_clone(monkeypatch: pytest.MonkeyPatch, _wheel_install: None) -> None:
"""A source checkout is redirected to ``git pull``, not upgraded."""
monkeypatch.setattr("omnigent.update_check._find_repo_root", lambda: Path("/repo"))
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code != 0
assert "git pull" in result.output
def test_upgrade_rejects_editable(monkeypatch: pytest.MonkeyPatch, _wheel_install: None) -> None:
"""An editable install is redirected to ``git pull``, not upgraded."""
editable = _InstalledWheelInfo(
install_time_epoch=0.0,
installer="uv",
vcs_url="file:///Users/me/omnigent",
commit_sha=None,
is_editable=True,
package_version="0.1.0",
detected_installer="uv",
)
monkeypatch.setattr("omnigent.update_check._read_installed_wheel_info", lambda: editable)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code != 0
assert "git pull" in result.output
def test_upgrade_pre_check_detects_release_candidate(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""``--pre --check`` includes pre-releases and reports the rc as available."""
captured: dict[str, object] = {}
def _fetch(include_prereleases: bool = False, **_kw: object) -> str:
captured["include_prereleases"] = include_prereleases
return "0.1.1rc1" if include_prereleases else "0.1.0"
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", _fetch)
result = CliRunner().invoke(cli, ["upgrade", "--pre", "--check"])
assert result.exit_code == 1, result.output
assert "v0.1.0 → v0.1.1rc1" in result.output
assert captured["include_prereleases"] is True
def test_upgrade_without_pre_ignores_release_candidate(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""Without ``--pre`` the rc is invisible → reports up to date."""
def _fetch(include_prereleases: bool = False, **_kw: object) -> str | None:
return "0.1.1rc1" if include_prereleases else "0.1.0"
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", _fetch)
result = CliRunner().invoke(cli, ["upgrade", "--check"])
assert result.exit_code == 0, result.output
assert "up to date" in result.output
def test_count_running_sessions_ignores_idle_connected(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Only ``status == "running"`` sessions count; idle-connected ones don't."""
from omnigent.cli import _count_running_sessions, _SessionPagesResult
sessions = [
{"id": "a", "status": "idle"},
{"id": "b", "status": "running"},
{"id": "c", "status": "idle"},
{"id": "d", "status": "running"},
{"id": "e"}, # missing status → not running
]
monkeypatch.setattr(
"omnigent.cli._fetch_session_pages",
lambda **_kw: _SessionPagesResult(sessions=sessions, error=None),
)
assert _count_running_sessions("http://127.0.0.1:6767") == 2
def test_drain_returns_immediately_when_only_idle_connected(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""Regression: 39 idle-but-connected sessions must not block the drain.
Previously the drain counted *connected* sessions, so a box with idle
sessions holding their connection open hung ``omni upgrade`` forever.
"""
from omnigent.cli import _SessionPagesResult, _wait_for_local_sessions_to_drain
monkeypatch.setattr(
"omnigent.cli.local_server_status",
lambda: LocalServerInfo(running=True, pid=1, port=6767, url="http://127.0.0.1:6767"),
)
monkeypatch.setattr(
"omnigent.cli._fetch_session_pages",
lambda **_kw: _SessionPagesResult(
sessions=[{"id": f"conv_{i}", "status": "idle"} for i in range(39)], error=None
),
)
_wait_for_local_sessions_to_drain() # must return, not hang
assert "Waiting for" not in capsys.readouterr().out
def test_upgrade_pre_passes_prerelease_flag_to_installer(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""``--pre`` upgrade runs the uv command with ``--prerelease allow``."""
monkeypatch.setattr(
"omnigent.update_check.fetch_latest_version",
lambda include_prereleases=False, **_kw: "0.1.1rc1",
)
monkeypatch.setattr("omnigent.cli._wait_for_local_sessions_to_drain", lambda: None)
monkeypatch.setattr("omnigent.cli._stop_local_server_and_daemon", lambda *, force: False)
ran: list[str] = []
monkeypatch.setattr(
"omnigent.update_check._run_upgrade_command",
lambda command, _console: ran.append(command) or 0,
)
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: ("0.1.1rc1", None)
)
result = CliRunner().invoke(cli, ["upgrade", "--pre"])
assert result.exit_code == 0, result.output
assert ran == ["uv tool upgrade omnigent --prerelease allow"]
# ── ``omni update`` alias ────────────────────────────────────────────
def test_update_is_alias_for_upgrade_same_callback() -> None:
"""``update`` and ``upgrade`` resolve to the exact same Click command.
The alias is registered by handing the *same* Command object to the
group under a second name, so its callback, options and help must be
identical — there is no second implementation to drift from ``upgrade``.
"""
update_cmd = cli.commands["update"]
upgrade_cmd = cli.commands["upgrade"]
assert update_cmd is upgrade_cmd
assert update_cmd.callback is upgrade_cmd.callback
# Same option surface (--check / --force / --pre).
assert [p.name for p in update_cmd.params] == [p.name for p in upgrade_cmd.params]
def test_update_up_to_date(monkeypatch: pytest.MonkeyPatch, _wheel_install: None) -> None:
"""``omni update`` runs the upgrade flow end-to-end (up-to-date path)."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.1.0")
def _must_not_run(*_a: object, **_k: object) -> int:
raise AssertionError("upgrade command ran while already up to date")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", _must_not_run)
result = CliRunner().invoke(cli, ["update"])
assert result.exit_code == 0, result.output
assert "up to date" in result.output
assert "0.1.0" in result.output
def test_update_check_matches_upgrade_check(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""``update --check`` behaves identically to ``upgrade --check``."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.2.0")
def _must_not_run(*_a: object, **_k: object) -> int:
raise AssertionError("--check must not run the upgrade")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", _must_not_run)
runner = CliRunner()
update_result = runner.invoke(cli, ["update", "--check"])
upgrade_result = runner.invoke(cli, ["upgrade", "--check"])
assert update_result.exit_code == upgrade_result.exit_code == 1
assert update_result.output == upgrade_result.output
assert "v0.1.0 → v0.2.0" in update_result.output
def test_update_suppresses_update_check_like_upgrade() -> None:
"""``update`` is special-cased alongside ``upgrade`` in the skip set."""
from omnigent.cli import _should_skip_update_check
assert _should_skip_update_check(["update"]) is True
assert _should_skip_update_check(["upgrade"]) is True
def test_upgrade_noop_install_reports_failure_not_success(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""Regression: a no-op upgrade must NOT claim success.
The installer exits 0 but the installed version doesn't move (a pinned
spec, a cooldown/exclude-newer excluding the release, or a stale index
cache). The old code printed "✓ Upgraded to v0.2.0" anyway, so the next
``--check`` kept reporting the same update — the bug this guards against.
"""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.2.0")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", lambda *_a, **_k: 0)
# The installer ran (exit 0) but the version is unchanged.
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: ("0.1.0", None)
)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code != 0, result.output
assert "still v0.1.0" in result.output
assert "✓ Upgraded" not in result.output
def test_upgrade_unconfirmed_version_is_honest(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""When the new version can't be read back, don't assert a version."""
monkeypatch.setattr("omnigent.update_check.fetch_latest_version", lambda *_a, **_k: "0.2.0")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", lambda *_a, **_k: 0)
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: (None, None)
)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code == 0, result.output
assert "couldn't confirm" in result.output
assert "✓ Upgraded" not in result.output
def test_upgrade_git_install_up_to_date(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""A git install whose ref hasn't moved reports up to date by commit."""
monkeypatch.setattr("omnigent.update_check._read_installed_wheel_info", _git_install_info)
# fetch_latest_version must never be consulted for a git install.
monkeypatch.setattr(
"omnigent.update_check.fetch_latest_version",
lambda *_a, **_k: pytest.fail("git install must not query PyPI"),
)
monkeypatch.setattr("omnigent.update_check._remote_git_head", lambda _url: "a" * 40)
def _must_not_run(*_a: object, **_k: object) -> int:
raise AssertionError("nothing to re-pull when already at the ref HEAD")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", _must_not_run)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code == 0, result.output
assert "up to date" in result.output
def test_upgrade_git_check_behind_exits_nonzero(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""``--check`` on a git install behind its ref reports the delta, exits non-zero."""
monkeypatch.setattr("omnigent.update_check._read_installed_wheel_info", _git_install_info)
monkeypatch.setattr("omnigent.update_check._remote_git_head", lambda _url: "b" * 40)
def _must_not_run(*_a: object, **_k: object) -> int:
raise AssertionError("--check must not re-pull")
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", _must_not_run)
result = CliRunner().invoke(cli, ["upgrade", "--check"])
assert result.exit_code == 1, result.output
assert "newer commit is available" in result.output
def test_upgrade_git_install_repulls_and_verifies_commit(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""A git install behind its ref re-pulls and reports the NEW commit."""
monkeypatch.setattr("omnigent.update_check._read_installed_wheel_info", _git_install_info)
monkeypatch.setattr("omnigent.update_check._remote_git_head", lambda _url: "b" * 40)
ran: list[str] = []
monkeypatch.setattr(
"omnigent.update_check._run_upgrade_command",
lambda command, _console: ran.append(command) or 0,
)
# After re-pull the install is at the new commit.
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: ("0.1.0", "b" * 40)
)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code == 0, result.output
assert ran == ["uv tool install --reinstall git+https://github.com/omnigent-ai/omnigent.git"]
assert "Updated to git bbbbbbbbb" in result.output
def test_upgrade_git_install_noop_does_not_claim_update(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""If the re-pull leaves the commit unchanged, say so — don't claim an update."""
monkeypatch.setattr("omnigent.update_check._read_installed_wheel_info", _git_install_info)
# Remote unknown (e.g. offline) → re-pull anyway, then verify by commit.
monkeypatch.setattr("omnigent.update_check._remote_git_head", lambda _url: None)
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", lambda *_a, **_k: 0)
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: ("0.1.0", "a" * 40)
)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code == 0, result.output
assert "nothing changed" in result.output
assert "✓ Updated" not in result.output
def test_upgrade_git_confirmed_behind_but_repull_noop_fails(
monkeypatch: pytest.MonkeyPatch, _wheel_install: None
) -> None:
"""Regression: known-behind git install whose re-pull doesn't move the commit
must FAIL, not silently exit 0 (else it recreates the loop on the git path)."""
monkeypatch.setattr("omnigent.update_check._read_installed_wheel_info", _git_install_info)
# Remote HEAD differs from the installed commit ("a"*40) → positively behind.
monkeypatch.setattr("omnigent.update_check._remote_git_head", lambda _url: "b" * 40)
monkeypatch.setattr("omnigent.update_check._run_upgrade_command", lambda *_a, **_k: 0)
# …but the re-pull left the install on the SAME commit (pinned ref / cached).
monkeypatch.setattr(
"omnigent.update_check._probe_installed_distribution", lambda: ("0.1.0", "a" * 40)
)
result = CliRunner().invoke(cli, ["upgrade"])
assert result.exit_code != 0, result.output
assert "still at aaaaaaaaa" in result.output
assert "✓ Updated" not in result.output