552 lines
17 KiB
Python
552 lines
17 KiB
Python
"""
|
|
Tests for :mod:`omnigent.resume_dispatch` — the top-level
|
|
``omnigent resume`` dispatcher.
|
|
|
|
The dispatcher's job is to translate the user's "take me back to
|
|
where I was" intent into the right wrapper call. The two important
|
|
properties under test are (a) we always preserve the Omnigent
|
|
conversation id end-to-end (no new id minted on resume) and (b)
|
|
claude-native conversations route to ``run_claude_native``,
|
|
everything else surfaces a clear redirect hint.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import click
|
|
import httpx
|
|
import pytest
|
|
|
|
from omnigent import resume_dispatch
|
|
|
|
# ── run_resume — top-level entry ──────────────────────────
|
|
|
|
|
|
def test_run_resume_picker_form_requires_server() -> None:
|
|
"""
|
|
``omnigent resume`` (no conv id, no --server) must fail loud.
|
|
|
|
Without ``target`` we'd open the cross-agent picker; without
|
|
``--server`` we have no Omnigent endpoint to query. Starting an
|
|
empty local server just for the picker would race with any
|
|
other ``omnigent`` process the user has running, so we
|
|
redirect via UsageError instead of silently doing it.
|
|
"""
|
|
with pytest.raises(click.UsageError) as excinfo:
|
|
resume_dispatch.run_resume(target=None, server=None)
|
|
# Message names both ways out of the error: a conv id OR --server.
|
|
assert "conv_" in str(excinfo.value)
|
|
assert "--server" in str(excinfo.value)
|
|
|
|
|
|
def test_run_resume_picker_cancel_exits_cleanly(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
Picker returns ``None`` (user pressed q / Enter on empty list)
|
|
→ dispatcher MUST return cleanly without calling
|
|
``run_claude_native``. A misroute that called the wrapper with
|
|
``session_id=None`` would silently create a fresh session the
|
|
user explicitly chose not to create.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_pick_conversation_for_resume",
|
|
lambda *, server: None,
|
|
)
|
|
invoked: list[str] = []
|
|
|
|
def _fail_if_called(**kwargs: Any) -> None:
|
|
"""
|
|
Marker for ``run_claude_native`` — fails the test if reached.
|
|
|
|
:param kwargs: Wrapper kwargs (ignored).
|
|
"""
|
|
del kwargs
|
|
invoked.append("run_claude_native")
|
|
|
|
monkeypatch.setattr(
|
|
"omnigent.claude_native.run_claude_native",
|
|
_fail_if_called,
|
|
)
|
|
|
|
resume_dispatch.run_resume(
|
|
target=None,
|
|
server="https://example.com",
|
|
)
|
|
# If the wrapper was invoked we'd see "run_claude_native" here —
|
|
# which would be the silent-fresh-session bug.
|
|
assert invoked == []
|
|
|
|
|
|
# ── _dispatch_by_runtime — id-known dispatch ──────────────
|
|
|
|
|
|
def test_dispatch_by_runtime_claude_native_remote_routes_to_wrapper(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Remote claude-native conv ⇒ ``run_claude_native(server=..., session_id=conv_id)``.
|
|
|
|
The Omnigent conv id MUST be preserved as ``session_id`` (the
|
|
wrapper's resume kwarg). A bug that passed ``None`` would mint a
|
|
fresh session and the user would lose their prior context.
|
|
Also asserts ``server`` carries through so the wrapper hits the
|
|
right Omnigent server.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_remote",
|
|
lambda *, server, conv_id: "claude-code-native-ui",
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _capture(**kwargs: Any) -> None:
|
|
"""
|
|
Record the kwargs ``run_claude_native`` was called with.
|
|
|
|
:param kwargs: Wrapper kwargs.
|
|
"""
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("omnigent.claude_native.run_claude_native", _capture)
|
|
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_abc",
|
|
server="https://example.com/", # trailing slash — must be normalized
|
|
)
|
|
|
|
# session_id preserves the Omnigent conv id end-to-end.
|
|
assert captured["session_id"] == "conv_abc"
|
|
# Trailing slash stripped — the wrapper expects a bare base URL.
|
|
assert captured["server"] == "https://example.com"
|
|
# No leaking claude args; the wrapper builds its own.
|
|
assert captured["claude_args"] == ()
|
|
|
|
|
|
def test_dispatch_by_runtime_codex_native_remote_routes_to_wrapper(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Remote codex-native conv ⇒ ``run_codex_native(server=..., session_id=conv_id)``.
|
|
|
|
The Omnigent conv id must be preserved exactly like the
|
|
claude-native path, but the runtime-specific passthrough kwarg is
|
|
``codex_args``.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_remote",
|
|
lambda *, server, conv_id: "codex-native-ui",
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _capture(**kwargs: Any) -> None:
|
|
"""
|
|
Record the kwargs ``run_codex_native`` was called with.
|
|
|
|
:param kwargs: Wrapper kwargs.
|
|
"""
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("omnigent.codex_native.run_codex_native", _capture)
|
|
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_abc",
|
|
server="https://example.com/",
|
|
)
|
|
|
|
assert captured["session_id"] == "conv_abc"
|
|
assert captured["server"] == "https://example.com"
|
|
assert captured["codex_args"] == ()
|
|
|
|
|
|
def test_dispatch_by_runtime_codex_native_local_routes_to_wrapper(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Local codex-native conv routes to ``run_codex_native``.
|
|
|
|
:param monkeypatch: Pytest monkeypatch fixture.
|
|
:returns: None.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_local",
|
|
lambda *, conv_id: "codex-native-ui",
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _capture(**kwargs: Any) -> None:
|
|
"""
|
|
Record the kwargs ``run_codex_native`` was called with.
|
|
|
|
:param kwargs: Wrapper kwargs.
|
|
:returns: None.
|
|
"""
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("omnigent.codex_native.run_codex_native", _capture)
|
|
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_codex",
|
|
server=None,
|
|
)
|
|
|
|
assert captured["session_id"] == "conv_codex"
|
|
assert captured["server"] is None
|
|
assert captured["codex_args"] == ()
|
|
|
|
|
|
def test_dispatch_by_runtime_kiro_native_remote_routes_to_wrapper(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Remote kiro-native conv routes to ``run_kiro_native``."""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_remote",
|
|
lambda *, server, conv_id: "kiro-native-ui",
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _capture(**kwargs: Any) -> None:
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("omnigent.kiro_native.run_kiro_native", _capture)
|
|
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_kiro",
|
|
server="https://example.com/",
|
|
)
|
|
|
|
assert captured["session_id"] == "conv_kiro"
|
|
assert captured["server"] == "https://example.com"
|
|
assert captured["kiro_args"] == ()
|
|
|
|
|
|
def test_dispatch_by_runtime_antigravity_native_remote_routes_to_wrapper(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Remote antigravity-native conv ⇒ ``run_antigravity_native(server=..., session_id=...)``.
|
|
|
|
The Omnigent conv id must be preserved exactly like the codex/claude
|
|
paths, but the runtime-specific passthrough kwarg is
|
|
``antigravity_args``.
|
|
|
|
:param monkeypatch: Pytest monkeypatch fixture.
|
|
:returns: None.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_remote",
|
|
lambda *, server, conv_id: "antigravity-native-ui",
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _capture(**kwargs: Any) -> None:
|
|
"""
|
|
Record the kwargs ``run_antigravity_native`` was called with.
|
|
|
|
:param kwargs: Wrapper kwargs.
|
|
:returns: None.
|
|
"""
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("omnigent.antigravity_native.run_antigravity_native", _capture)
|
|
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_agy",
|
|
server="https://example.com/",
|
|
)
|
|
|
|
assert captured["session_id"] == "conv_agy"
|
|
assert captured["server"] == "https://example.com"
|
|
assert captured["antigravity_args"] == ()
|
|
|
|
|
|
def test_dispatch_by_runtime_antigravity_native_local_routes_to_wrapper(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Local antigravity-native conv routes to ``run_antigravity_native``.
|
|
|
|
:param monkeypatch: Pytest monkeypatch fixture.
|
|
:returns: None.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_local",
|
|
lambda *, conv_id: "antigravity-native-ui",
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _capture(**kwargs: Any) -> None:
|
|
"""
|
|
Record the kwargs ``run_antigravity_native`` was called with.
|
|
|
|
:param kwargs: Wrapper kwargs.
|
|
:returns: None.
|
|
"""
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("omnigent.antigravity_native.run_antigravity_native", _capture)
|
|
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_agy_local",
|
|
server=None,
|
|
)
|
|
|
|
assert captured["session_id"] == "conv_agy_local"
|
|
assert captured["server"] is None
|
|
assert captured["antigravity_args"] == ()
|
|
|
|
|
|
def test_dispatch_by_runtime_claude_native_local_still_routes_to_wrapper(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Local claude-native dispatch remains routed to ``run_claude_native``.
|
|
|
|
:param monkeypatch: Pytest monkeypatch fixture.
|
|
:returns: None.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_local",
|
|
lambda *, conv_id: "claude-code-native-ui",
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _capture(**kwargs: Any) -> None:
|
|
"""
|
|
Record the kwargs ``run_claude_native`` was called with.
|
|
|
|
:param kwargs: Wrapper kwargs.
|
|
:returns: None.
|
|
"""
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("omnigent.claude_native.run_claude_native", _capture)
|
|
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_claude",
|
|
server=None,
|
|
)
|
|
|
|
assert captured["session_id"] == "conv_claude"
|
|
assert captured["server"] is None
|
|
assert captured["claude_args"] == ()
|
|
|
|
|
|
def test_dispatch_by_runtime_non_wrapper_local_raises_with_hint(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Local non-wrapper conv surfaces the ``omnigent run --resume`` hint.
|
|
|
|
:param monkeypatch: Pytest monkeypatch fixture.
|
|
:returns: None.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_local",
|
|
lambda *, conv_id: None,
|
|
)
|
|
|
|
with pytest.raises(click.ClickException) as excinfo:
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_chat",
|
|
server=None,
|
|
)
|
|
|
|
msg = excinfo.value.message
|
|
assert "conv_chat" in msg
|
|
assert "omnigent run --resume" in msg
|
|
assert "<agent.yaml>" in msg
|
|
|
|
|
|
def test_read_wrapper_label_local_reads_persistent_store(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""
|
|
Local dispatch classifies sessions from ``~/.omnigent/chat.db``.
|
|
|
|
:param monkeypatch: Pytest monkeypatch fixture.
|
|
:param tmp_path: Temporary persistent Omnigent directory.
|
|
:returns: None.
|
|
"""
|
|
import omnigent.chat as chat_mod
|
|
from omnigent.stores.conversation_store.sqlalchemy_store import (
|
|
SqlAlchemyConversationStore,
|
|
)
|
|
|
|
db_path = tmp_path / "chat.db"
|
|
store = SqlAlchemyConversationStore(f"sqlite:///{db_path}")
|
|
created = store.create_session_with_agent(
|
|
agent_id="ag_codex",
|
|
agent_name="codex-native-ui",
|
|
agent_bundle_location="ag_codex/bundle",
|
|
agent_description=None,
|
|
labels={"omnigent.wrapper": "codex-native-ui"},
|
|
)
|
|
monkeypatch.setattr(chat_mod, "_omnigent_persistent_dir", lambda: tmp_path)
|
|
|
|
result = resume_dispatch._read_wrapper_label_local(conv_id=created.conversation.id)
|
|
|
|
assert result == "codex-native-ui"
|
|
|
|
|
|
def test_dispatch_by_runtime_non_claude_native_remote_raises_with_hint(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Remote non-claude-native conv ⇒ ``ClickException`` with a
|
|
copy-pasteable ``omnigent run --resume`` hint.
|
|
|
|
The hint MUST include both the conv id and the original
|
|
``--server`` URL so the user's next attempt works without
|
|
them having to remember additional flags. A regression that
|
|
surfaced a generic "wrong runtime" error would leave the
|
|
user stuck.
|
|
"""
|
|
monkeypatch.setattr(
|
|
resume_dispatch,
|
|
"_read_wrapper_label_remote",
|
|
lambda *, server, conv_id: None, # no wrapper label
|
|
)
|
|
|
|
def _fail_if_called(**kwargs: Any) -> None:
|
|
"""Marker — fails the test if ``run_claude_native`` is called."""
|
|
del kwargs
|
|
raise AssertionError("run_claude_native invoked on non-claude conv")
|
|
|
|
monkeypatch.setattr("omnigent.claude_native.run_claude_native", _fail_if_called)
|
|
|
|
with pytest.raises(click.ClickException) as excinfo:
|
|
resume_dispatch._dispatch_by_runtime(
|
|
target="conv_xyz",
|
|
server="https://example.com",
|
|
)
|
|
msg = excinfo.value.message
|
|
# All three load-bearing pieces of the hint must appear.
|
|
assert "conv_xyz" in msg
|
|
assert "omnigent run --resume" in msg
|
|
assert "https://example.com" in msg
|
|
|
|
|
|
# ── _read_wrapper_label_remote ────────────────────────────
|
|
|
|
|
|
def test_read_wrapper_label_remote_returns_label_when_present(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
Happy path: 200 response with the wrapper label set returns the
|
|
label value, which the caller compares against the claude-native
|
|
sentinel.
|
|
"""
|
|
|
|
def _fake_get(url: str, *, headers: dict[str, str], timeout: float) -> httpx.Response:
|
|
"""
|
|
Return a canned ``GET /v1/sessions/{id}`` response.
|
|
|
|
:param url: Request URL (used to validate path shape).
|
|
:param headers: Auth headers (ignored).
|
|
:param timeout: Request timeout (ignored).
|
|
:returns: A 200 response with a labelled body.
|
|
"""
|
|
del headers, timeout
|
|
assert url.endswith("/v1/sessions/conv_abc"), url
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "conv_abc",
|
|
"agent_id": "ag_1",
|
|
"status": "idle",
|
|
"created_at": 1,
|
|
"labels": {"omnigent.wrapper": "claude-code-native-ui"},
|
|
},
|
|
)
|
|
|
|
monkeypatch.setattr(httpx, "get", _fake_get)
|
|
monkeypatch.setattr(
|
|
"omnigent.chat._remote_headers",
|
|
lambda *, server_url: {},
|
|
)
|
|
|
|
result = resume_dispatch._read_wrapper_label_remote(
|
|
server="https://example.com",
|
|
conv_id="conv_abc",
|
|
)
|
|
assert result == "claude-code-native-ui"
|
|
|
|
|
|
def test_read_wrapper_label_remote_returns_none_when_label_missing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
A conv with no ``omnigent.wrapper`` label returns ``None``, which
|
|
the caller treats as "not claude-native" (the right call — wrappers
|
|
stamp their label on every session they own; absence means a
|
|
different runtime).
|
|
"""
|
|
|
|
def _fake_get(url: str, *, headers: dict[str, str], timeout: float) -> httpx.Response:
|
|
"""Return a 200 with no wrapper label."""
|
|
del url, headers, timeout
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "conv_abc",
|
|
"agent_id": "ag_1",
|
|
"status": "idle",
|
|
"created_at": 1,
|
|
"labels": {"some.other": "label"},
|
|
},
|
|
)
|
|
|
|
monkeypatch.setattr(httpx, "get", _fake_get)
|
|
monkeypatch.setattr(
|
|
"omnigent.chat._remote_headers",
|
|
lambda *, server_url: {},
|
|
)
|
|
|
|
result = resume_dispatch._read_wrapper_label_remote(
|
|
server="https://example.com",
|
|
conv_id="conv_abc",
|
|
)
|
|
assert result is None
|
|
|
|
|
|
def test_read_wrapper_label_remote_raises_on_404(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""
|
|
404 means the conv id doesn't exist — surface a clear error with
|
|
the conv id and server so the user can fix a typo or check the
|
|
server. Without this, the caller would proceed with a None label
|
|
and surface the generic "not claude-native" hint, which would
|
|
misdirect the user.
|
|
"""
|
|
|
|
def _fake_get(url: str, *, headers: dict[str, str], timeout: float) -> httpx.Response:
|
|
"""Return a 404."""
|
|
del url, headers, timeout
|
|
return httpx.Response(404, json={"error": {"code": "not_found"}})
|
|
|
|
monkeypatch.setattr(httpx, "get", _fake_get)
|
|
monkeypatch.setattr(
|
|
"omnigent.chat._remote_headers",
|
|
lambda *, server_url: {},
|
|
)
|
|
|
|
with pytest.raises(click.ClickException) as excinfo:
|
|
resume_dispatch._read_wrapper_label_remote(
|
|
server="https://example.com",
|
|
conv_id="conv_missing",
|
|
)
|
|
assert "conv_missing" in excinfo.value.message
|
|
assert "not found" in excinfo.value.message
|