feat(cli): add omnigent host --background to run the host daemon detached (#4317)
## Related issue Closes OMNI-2516 — https://linear.app/omnigent/issue/OMNI-2516 ## Summary - `omnigent host` only ever ran in the foreground, so registering a machine as a host cost a dedicated terminal — even though the detached daemon it needs already exists and is what `run` / `claude` / `codex` spawn via `_ensure_host_daemon()`. `--background` exposes that path directly: spawn (or adopt) the daemon, report it, and return. - Sign-in stays interactive. A detached daemon has no terminal to run the browser login on, so `_ensure_databricks_server_auth()` runs in the foreground *before* the spawn; otherwise the daemon dies in the background with an opaque "redirected to a login page" error. `--non-interactive` still fails with the `omnigent login` hint instead of prompting. - In local mode the daemon also owns the local Omnigent server, so the command waits for that server and reports its URL — otherwise the Web UI is unreachable without a follow-up `omnigent server status`. That makes `omnigent host --background` the whole "start everything" step, which is now the README quickstart (it replaces the `server --background` + `host` pair). - A daemon that dies on startup (bad URL, missing credentials) leaves nothing on the terminal, so the command waits a 2s grace and surfaces the daemon log rather than falsely reporting success. Output is a colorized headline plus aligned detail rows, with the stop command on its own line so it can be copied: ``` Started the host daemon in the background (pid 74241). server: https://dbc-…/api/2.0/omnigent log: ~/.omnigent/logs/host/host-20260806-205308-765542.log Stop it with: omnigent host stop --server https://dbc-…/api/2.0/omnigent ``` That stop command mirrors the invocation: `host` and `host stop` resolve their target identically (the `--server` value, else config, else local), so the flag is echoed only when the user named a target — a bare `host --background` prints a bare `omnigent host stop`. Colorizing reuses the existing `NO_COLOR`-aware helper, renamed `_help_style` → `_cli_style` now that it is not help-only. ## Test Plan - `uv run --extra dev pytest tests/host/test_cli_host.py -q` → 21 passed. - Manually, local mode: `omnigent host --background` reported `server: http://127.0.0.1:6767` and a bare `omnigent host stop` (no `--server` typed, none echoed), which then stopped it. - Manually, remote mode: `omnigent host --background --server https://dbc-…` printed the block quoted above; `omnigent host status` showed `process=online host=online`; re-running reported `already running (pid …)` with no second spawn; and the echoed `host stop --server …` stopped it. ## Demo N/A — CLI-only change; the new output is quoted above. ## Type of change - [ ] Bug fix - [x] Feature - [ ] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [ ] Integration tests added / updated - [ ] E2E tests added / updated - [x] Manual verification completed - [ ] Existing tests cover this change - [ ] Not applicable ## Coverage notes Four new tests in `tests/host/test_cli_host.py` cover the spawn output (including the local server URL and a flagless stop hint), that the foreground daemon loop and in-process local-server bring-up are skipped, reuse of a healthy daemon via an explicit `--server ""` (whose stop hint keeps the flag), and that sign-in runs before the spawn. The daemon spawn and local-server discovery are stubbed, so no process or log file is created. Manual verification covered both modes end to end; the exits-immediately grace path is covered by tests only. ## Changelog `omnigent host --background` starts the local server and registers this machine as a host without tying up a terminal. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
This commit is contained in:
@@ -301,15 +301,16 @@ full pages through an MCP search server, and verifies each claim across
|
||||
independent sources. It's also the simplest example to copy from: one agent
|
||||
plus one `tools/mcp/*.yaml` server, no sub-agents.
|
||||
|
||||
**Prefer the browser?** Start a server and register your machine as a host:
|
||||
**Prefer the browser?** One command starts the local server and registers this
|
||||
machine as a host:
|
||||
|
||||
```bash
|
||||
omnigent server --background # start the local server and web UI in the background
|
||||
omnigent host # (separate terminal) register this machine as a host
|
||||
omnigent host --background # starts the local server too, then returns
|
||||
```
|
||||
|
||||
In the web UI, hit **New Chat**, pick your machine, and go. Check status with
|
||||
`omnigent server status`; stop everything with `omnigent stop`.
|
||||
Open the server URL it prints, hit **New Chat**, pick your machine, and go.
|
||||
Check status with `omnigent server status`; stop everything with
|
||||
`omnigent stop`.
|
||||
|
||||
### 3. Choose & switch models
|
||||
|
||||
|
||||
+168
-10
@@ -1604,8 +1604,8 @@ def _harness_extra_checks() -> dict[str, Callable[[], bool]]:
|
||||
}
|
||||
|
||||
|
||||
def _help_style(text: str, **style: object) -> str:
|
||||
"""Colorize *text* for help output, honoring ``NO_COLOR``.
|
||||
def _cli_style(text: str, **style: object) -> str:
|
||||
"""Colorize *text* for terminal output, honoring ``NO_COLOR``.
|
||||
|
||||
Click's ``echo`` already strips ANSI when the sink is not a TTY, so
|
||||
this only needs to guard the explicit ``NO_COLOR`` opt-out; on an
|
||||
@@ -1629,7 +1629,7 @@ class _OmnigentCLI(click.Group):
|
||||
def format_usage(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
||||
"""Render the usage line with an accent-colored ``Usage:`` prefix."""
|
||||
pieces = self.collect_usage_pieces(ctx)
|
||||
prefix = f"{_help_style('Usage:', fg=_ACCENT_RGB, bold=True)} "
|
||||
prefix = f"{_cli_style('Usage:', fg=_ACCENT_RGB, bold=True)} "
|
||||
formatter.write_usage(ctx.command_path, " ".join(pieces), prefix=prefix)
|
||||
|
||||
def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
||||
@@ -1638,9 +1638,9 @@ class _OmnigentCLI(click.Group):
|
||||
for param in self.get_params(ctx):
|
||||
rv = param.get_help_record(ctx)
|
||||
if rv is not None:
|
||||
opts.append((_help_style(rv[0], fg="green"), rv[1]))
|
||||
opts.append((_cli_style(rv[0], fg="green"), rv[1]))
|
||||
if opts:
|
||||
with formatter.section(_help_style("Options", fg=_ACCENT_RGB, bold=True)):
|
||||
with formatter.section(_cli_style("Options", fg=_ACCENT_RGB, bold=True)):
|
||||
formatter.write_dl(opts)
|
||||
self.format_commands(ctx, formatter)
|
||||
|
||||
@@ -1688,10 +1688,10 @@ class _OmnigentCLI(click.Group):
|
||||
def _emit(title: str, rows: list[tuple[str, click.Command]], name_fg: object) -> None:
|
||||
if not rows:
|
||||
return
|
||||
with formatter.section(_help_style(title, fg=_ACCENT_RGB, bold=True)):
|
||||
with formatter.section(_cli_style(title, fg=_ACCENT_RGB, bold=True)):
|
||||
formatter.write_dl(
|
||||
[
|
||||
(_help_style(name, fg=name_fg), cmd.get_short_help_str(limit))
|
||||
(_cli_style(name, fg=name_fg), cmd.get_short_help_str(limit))
|
||||
for name, cmd in rows
|
||||
]
|
||||
)
|
||||
@@ -1700,7 +1700,7 @@ class _OmnigentCLI(click.Group):
|
||||
if any_hidden:
|
||||
formatter.write_paragraph()
|
||||
formatter.write_text(
|
||||
_help_style(
|
||||
_cli_style(
|
||||
"Some harnesses need an optional extra — run `omnigent setup` to enable them.",
|
||||
dim=True,
|
||||
)
|
||||
@@ -7642,8 +7642,146 @@ def _prompt_stop_local_server() -> None:
|
||||
click.echo(f"Left the local server running at {url}.")
|
||||
|
||||
|
||||
# Grace period a freshly spawned background host daemon must survive before
|
||||
# `host --background` reports success. A daemon that dies on startup (bad
|
||||
# server URL, missing credentials) leaves nothing on the terminal, so we wait
|
||||
# this long and surface its log instead of falsely reporting success.
|
||||
_BACKGROUND_HOST_GRACE_S = 2.0
|
||||
|
||||
|
||||
def _confirm_background_host_alive(record: _HostDaemonRecord) -> None:
|
||||
"""Fail loud if a freshly spawned background host daemon dies at once.
|
||||
|
||||
:param record: Registry record of the spawned daemon.
|
||||
:raises click.ClickException: If the daemon exits within
|
||||
:data:`_BACKGROUND_HOST_GRACE_S`.
|
||||
"""
|
||||
deadline = time.time() + _BACKGROUND_HOST_GRACE_S
|
||||
while True:
|
||||
if not _pid_alive(record.pid):
|
||||
from omnigent._runner_startup import format_runner_log_tail
|
||||
|
||||
log_path = Path(record.log_path) if record.log_path else None
|
||||
raise click.ClickException(
|
||||
"The host daemon exited immediately after starting."
|
||||
f"{format_runner_log_tail(log_path)}"
|
||||
)
|
||||
if time.time() >= deadline:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def _run_background_host(
|
||||
server: str | None,
|
||||
*,
|
||||
explicit_server: str | None,
|
||||
non_interactive: bool,
|
||||
) -> None:
|
||||
"""Spawn (or reuse) the detached host daemon and report it.
|
||||
|
||||
The background counterpart of the foreground ``omnigent host`` body,
|
||||
selected by ``--background``: the same daemon loop runs detached (see
|
||||
:func:`_ensure_host_daemon`) so the command returns instead of blocking.
|
||||
|
||||
Sign-in stays in the foreground. The detached daemon has no terminal to
|
||||
prompt on, so a Databricks-fronted server is authenticated here, before the
|
||||
spawn — otherwise the daemon would die in the background with an opaque
|
||||
redirect error.
|
||||
|
||||
:param server: Resolved Omnigent server URL, e.g.
|
||||
``"https://example.databricksapps.com"``. ``None`` or ``""`` selects
|
||||
local mode (the daemon starts or reuses a local Omnigent server).
|
||||
:param explicit_server: The ``--server`` value as the user spelled it
|
||||
(``""`` for local mode), or ``None`` when the option was omitted.
|
||||
Only used to echo a matching ``host stop`` command.
|
||||
:param non_interactive: When ``True``, never launch the browser login —
|
||||
fail with the ``omnigent login`` hint instead.
|
||||
:raises click.ClickException: If the daemon cannot be spawned, exits
|
||||
immediately after starting, or (local mode) never serves its local
|
||||
Omnigent server.
|
||||
"""
|
||||
if server:
|
||||
_ensure_databricks_server_auth(server, non_interactive=non_interactive)
|
||||
target = _normalize_daemon_target(server)
|
||||
previous = _find_daemon_record(target)
|
||||
_ensure_host_daemon(server or None)
|
||||
record = _find_daemon_record(target)
|
||||
if record is None:
|
||||
# No record for this target: either the live local-mode daemon already
|
||||
# serves the requested URL, or the spawn itself failed.
|
||||
if _local_daemon_serves_target(target, server or None):
|
||||
click.echo(f"The local host daemon already serves {target}.")
|
||||
return
|
||||
raise click.ClickException(
|
||||
"Could not spawn the background host daemon. See ~/.omnigent/logs/host/ for details."
|
||||
)
|
||||
if previous is not None and previous.pid == record.pid:
|
||||
headline = _cli_style("Host daemon already running", fg="yellow", bold=True)
|
||||
else:
|
||||
_confirm_background_host_alive(record)
|
||||
headline = _cli_style("Started the host daemon in the background", fg="green", bold=True)
|
||||
click.echo(f"{headline} (pid {record.pid}).")
|
||||
if record.mode == "local":
|
||||
# A local-mode daemon owns the local Omnigent server, so this command is
|
||||
# the whole "start everything" step — wait for that server and report
|
||||
# its URL, otherwise the Web UI is unreachable without a follow-up
|
||||
# `omnigent server status`. Resolved after the headline above so a cold
|
||||
# start isn't a silent terminal.
|
||||
server_url = _discover_local_server_url()
|
||||
_update_daemon_resolved_server_url(target, server_url)
|
||||
else:
|
||||
server_url = target
|
||||
_echo_host_field("server", _cli_style(server_url, fg="cyan"))
|
||||
if record.log_path is not None:
|
||||
_echo_host_field("log", _display_path(Path(record.log_path)))
|
||||
click.echo()
|
||||
click.echo(_cli_style("Stop it with:", dim=True))
|
||||
click.echo(f" {_cli_style(_host_stop_command(explicit_server), bold=True)}")
|
||||
|
||||
|
||||
def _echo_host_field(label: str, value: str) -> None:
|
||||
"""Echo one aligned ``label: value`` detail row.
|
||||
|
||||
:param label: Row label without its colon, e.g. ``"server"``.
|
||||
:param value: Row value, possibly already ANSI-styled — padding is
|
||||
applied to the label so escape codes can't skew the alignment.
|
||||
"""
|
||||
click.echo(f" {label + ':':<8}{value}")
|
||||
|
||||
|
||||
def _host_stop_command(explicit_server: str | None) -> str:
|
||||
"""Build the ``host stop`` command that mirrors how ``host`` was invoked.
|
||||
|
||||
``host`` and ``host stop`` resolve their target the same way (the
|
||||
``--server`` value, else config, else local), so repeating the flag the
|
||||
user omitted would be noise — and repeating the one they passed keeps the
|
||||
command correct when config names a different target.
|
||||
|
||||
:param explicit_server: The ``--server`` value as the user spelled it,
|
||||
e.g. ``"https://example.databricksapps.com"`` or ``""`` for local
|
||||
mode. ``None`` when the option was omitted.
|
||||
:returns: A copy-pasteable command, e.g. ``"omnigent host stop"``.
|
||||
"""
|
||||
if explicit_server is None:
|
||||
return "omnigent host stop"
|
||||
stop_target = explicit_server if explicit_server else '""'
|
||||
return f"omnigent host stop --server {stop_target}"
|
||||
|
||||
|
||||
@cli.group("host", cls=_HostGroup, invoke_without_command=True)
|
||||
@click.option("--server", default=None, help="Remote omnigent server URL.")
|
||||
@click.option(
|
||||
"--background",
|
||||
"background",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help=(
|
||||
"Spawn the host daemon as a detached background process (returning "
|
||||
"immediately) instead of running it in the foreground. Reuses a "
|
||||
"healthy daemon if one is already up. Sign-in still happens in the "
|
||||
"foreground, before the spawn."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--non-interactive",
|
||||
"non_interactive",
|
||||
@@ -7656,7 +7794,12 @@ def _prompt_stop_local_server() -> None:
|
||||
),
|
||||
)
|
||||
@click.pass_context
|
||||
def host(ctx: click.Context, server: str | None, non_interactive: bool) -> None:
|
||||
def host(
|
||||
ctx: click.Context,
|
||||
server: str | None,
|
||||
background: bool,
|
||||
non_interactive: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Register this machine as a host with a server.
|
||||
|
||||
@@ -7665,6 +7808,7 @@ def host(ctx: click.Context, server: str | None, non_interactive: bool) -> None:
|
||||
omnigent host https://omnigent-app.databricksapps.com
|
||||
omnigent host --server https://omnigent-app.databricksapps.com
|
||||
omnigent host "" # spawn + connect to a local server
|
||||
omnigent host --background # spawn detached, return immediately
|
||||
|
||||
The server URL may be given positionally (``omnigent host
|
||||
<url>``) or via ``--server <url>``. A leading ``status``, ``stop``,
|
||||
@@ -7674,13 +7818,16 @@ def host(ctx: click.Context, server: str | None, non_interactive: bool) -> None:
|
||||
in, ``host`` runs the same flow ``omnigent login`` would before
|
||||
connecting (an interactive browser flow). Pass ``--non-interactive``
|
||||
to keep the old scripted behavior: fail with the login command to run
|
||||
instead of prompting.
|
||||
instead of prompting. This holds for ``--background`` too: the login
|
||||
flow runs here, in your terminal, before the daemon is detached.
|
||||
|
||||
:param ctx: Click invocation context. ``ctx.invoked_subcommand`` is
|
||||
set when a management subcommand such as ``"status"`` is running.
|
||||
:param server: Remote Omnigent server URL, e.g.
|
||||
``"https://example.databricksapps.com"``. ``None`` falls back
|
||||
to config; empty string selects local mode.
|
||||
:param background: When ``True``, spawn the daemon detached and return
|
||||
instead of running the daemon loop in the foreground.
|
||||
:param non_interactive: When ``True``, never launch the browser login
|
||||
for an un-authed remote server — fail with the ``omnigent login``
|
||||
hint instead.
|
||||
@@ -7689,6 +7836,9 @@ def host(ctx: click.Context, server: str | None, non_interactive: bool) -> None:
|
||||
ctx.obj["server"] = server
|
||||
if ctx.invoked_subcommand is not None:
|
||||
return
|
||||
# Kept before the config fallback below: `--background` echoes a `host
|
||||
# stop` command that mirrors how this command was invoked.
|
||||
explicit_server = server
|
||||
cfg = _load_effective_config()
|
||||
if server is None:
|
||||
server = cfg.get("server")
|
||||
@@ -7699,6 +7849,14 @@ def host(ctx: click.Context, server: str | None, non_interactive: bool) -> None:
|
||||
# the sign-in pre-flight.
|
||||
remote_mode = bool(server)
|
||||
|
||||
if background:
|
||||
_run_background_host(
|
||||
server,
|
||||
explicit_server=explicit_server,
|
||||
non_interactive=non_interactive,
|
||||
)
|
||||
return
|
||||
|
||||
from omnigent.host.connect import run_host_process
|
||||
|
||||
# ``host`` IS the daemon (foreground). With no server URL, start (or
|
||||
|
||||
@@ -635,3 +635,190 @@ def test_host_http_json_handles_remote_headers_oserror() -> None:
|
||||
assert "OSError" in result.body if isinstance(result.body, str) else True
|
||||
# Ensure we didn't cache the failed result.
|
||||
assert url not in _host_http_headers_cache
|
||||
|
||||
|
||||
# ── host --background ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def _patch_background_host_spawn(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
*,
|
||||
pid: int = 4242,
|
||||
) -> tuple[list[list[str]], Path]:
|
||||
"""
|
||||
Isolate ``host --background`` from real daemon spawns and log files.
|
||||
|
||||
:param monkeypatch: Pytest monkeypatch fixture.
|
||||
:param tmp_path: Test-scoped directory used as the config home.
|
||||
:param pid: Fake pid the stub spawn reports, e.g. ``4242``.
|
||||
:returns: The recorded spawn argv list and the fake daemon log path.
|
||||
"""
|
||||
from omnigent.cli import _SpawnedDaemonProcess
|
||||
|
||||
monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path))
|
||||
monkeypatch.setattr("omnigent.cli._HOST_PID_PATH", tmp_path / "host.pid")
|
||||
# No fixed grace: the stubbed pid is trivially "alive", so waiting for it
|
||||
# only slows the test down.
|
||||
monkeypatch.setattr("omnigent.cli._BACKGROUND_HOST_GRACE_S", 0.0)
|
||||
monkeypatch.setattr("omnigent.cli._pid_alive", lambda checked: checked == pid)
|
||||
# Local mode waits for the server the daemon owns; no real server here.
|
||||
monkeypatch.setattr("omnigent.cli._discover_local_server_url", lambda: "http://127.0.0.1:6767")
|
||||
log_path = tmp_path / "host-test.log"
|
||||
log_path.write_text("")
|
||||
spawned_args: list[list[str]] = []
|
||||
|
||||
def _fake_spawn(*, args: list[str], env: dict[str, str]) -> _SpawnedDaemonProcess:
|
||||
"""Record the daemon argv instead of spawning a process.
|
||||
|
||||
:param args: Daemon process argv.
|
||||
:param env: Daemon environment (ignored).
|
||||
:returns: Fake spawned-process metadata.
|
||||
"""
|
||||
del env
|
||||
spawned_args.append(args)
|
||||
return _SpawnedDaemonProcess(pid=pid, log_path=str(log_path))
|
||||
|
||||
monkeypatch.setattr("omnigent.cli._spawn_host_daemon_process", _fake_spawn)
|
||||
return spawned_args, log_path
|
||||
|
||||
|
||||
def test_host_background_spawns_detached_daemon(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
``host --background`` spawns the detached daemon and reports pid + log.
|
||||
|
||||
A detached daemon prints nothing to the terminal, so the pid and log
|
||||
path are the only handles the user gets on it.
|
||||
"""
|
||||
spawned_args, log_path = _patch_background_host_spawn(monkeypatch, tmp_path)
|
||||
|
||||
result = CliRunner().invoke(cli, ["host", "--background"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "pid 4242" in result.output
|
||||
assert log_path.name in result.output
|
||||
# `--server` was omitted, so the stop hint omits it too — a bare `host
|
||||
# stop` resolves its target exactly like the bare `host` that started it.
|
||||
assert "omnigent host stop" in result.output
|
||||
assert "--server" not in result.output
|
||||
# Bare `--background` is local mode: the daemon owns the local server, so
|
||||
# its URL is reported too (the Web UI is otherwise unreachable).
|
||||
assert spawned_args and "--local" in spawned_args[0]
|
||||
assert "server: http://127.0.0.1:6767" in result.output
|
||||
|
||||
|
||||
def test_host_background_does_not_block(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
``host --background`` must not run the foreground daemon loop.
|
||||
|
||||
The whole point of the flag is that the blocking ``run_host_process``
|
||||
loop (and the in-process local-server bring-up that precedes it) moves
|
||||
into the detached child.
|
||||
"""
|
||||
_patch_background_host_spawn(monkeypatch, tmp_path)
|
||||
foreground_runs: list[str] = []
|
||||
|
||||
def _fail_local_server() -> LocalServerStartup:
|
||||
raise AssertionError("--background must not start the local server in-process")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"omnigent.host.connect.run_host_process",
|
||||
lambda server_url, **kwargs: foreground_runs.append(server_url),
|
||||
),
|
||||
patch("omnigent.cli.ensure_local_omnigent_server", _fail_local_server),
|
||||
):
|
||||
result = CliRunner().invoke(cli, ["host", "--background"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert foreground_runs == []
|
||||
|
||||
|
||||
def test_host_background_reuses_running_daemon(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
``host --background`` adopts a healthy daemon instead of double-spawning.
|
||||
|
||||
Two daemons for one target would register the same machine twice; the
|
||||
command reports the existing pid and returns.
|
||||
"""
|
||||
from omnigent.cli import (
|
||||
_LOCAL_DAEMON_MARKER,
|
||||
_HostDaemonRecord,
|
||||
_write_daemon_record,
|
||||
server_config_signature,
|
||||
)
|
||||
|
||||
spawned_args, _ = _patch_background_host_spawn(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr("omnigent.cli._pid_alive", lambda checked: checked in {4242, 5150})
|
||||
_write_daemon_record(
|
||||
_HostDaemonRecord(
|
||||
pid=5150,
|
||||
target=_LOCAL_DAEMON_MARKER,
|
||||
mode="local",
|
||||
server_url=None,
|
||||
log_path=str(tmp_path / "existing.log"),
|
||||
# Fresh record: young daemons skip the tunnel-health probe.
|
||||
started_at=int(time.time()),
|
||||
# No host_id: the tmp config home has no identity file, and a
|
||||
# mismatch would tear the "running" daemon down as stale.
|
||||
host_id=None,
|
||||
config_sig=server_config_signature(),
|
||||
)
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(cli, ["host", "--background", "--server", ""])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "already running (pid 5150" in result.output
|
||||
assert "server: http://127.0.0.1:6767" in result.output
|
||||
# Local mode was requested explicitly, so the stop hint says so too.
|
||||
assert 'omnigent host stop --server ""' in result.output
|
||||
assert spawned_args == [], "a healthy daemon must not be respawned"
|
||||
|
||||
|
||||
def test_host_background_signs_in_before_spawning(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
Sign-in for a remote server happens in the foreground, before the spawn.
|
||||
|
||||
The detached daemon has no terminal to run the browser login on, so an
|
||||
un-authed Databricks-fronted target must be authenticated by this
|
||||
invocation first — and ``--non-interactive`` must still reach that check.
|
||||
"""
|
||||
spawned_args, _ = _patch_background_host_spawn(monkeypatch, tmp_path)
|
||||
auth_calls: list[tuple[str, bool]] = []
|
||||
|
||||
def _fake_auth(server: str, *, non_interactive: bool = False) -> None:
|
||||
"""Record the sign-in pre-flight.
|
||||
|
||||
:param server: Server URL being authenticated.
|
||||
:param non_interactive: Whether prompting is suppressed.
|
||||
"""
|
||||
assert spawned_args == [], "sign-in must precede the daemon spawn"
|
||||
auth_calls.append((server, non_interactive))
|
||||
|
||||
monkeypatch.setattr("omnigent.cli._ensure_databricks_server_auth", _fake_auth)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
cli, ["host", "--background", "--server", "https://example.databricksapps.com"]
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert auth_calls == [("https://example.databricksapps.com", False)]
|
||||
assert spawned_args and "--server" in spawned_args[0]
|
||||
# Remote target: reported as-is (a server we don't own isn't discovered),
|
||||
# and named in the stop hint because a bare stop would resolve the
|
||||
# configured target instead.
|
||||
assert "server: https://example.databricksapps.com" in result.output
|
||||
assert "omnigent host stop --server https://example.databricksapps.com" in result.output
|
||||
|
||||
Reference in New Issue
Block a user