opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine
hermes --tui launches the native OpenTUI engine (Bun) when HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config); Ink stays the default and the shipping path is untouched. - _resolve_tui_engine() (env > config > ink); refuses opentui on Windows/Termux (no Bun) -> falls back to ink with a notice. - _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step). - _bun_bin() with HERMES_BUN override. - Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host must not bootstrap Node). - Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun is JSC; the V8 flag errors/ignores). Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI -> real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
@@ -91,6 +91,10 @@ class TestTelegramModelPicker:
|
||||
query.answer = AsyncMock()
|
||||
query.edit_message_text = AsyncMock()
|
||||
|
||||
update = MagicMock()
|
||||
update.callback_query = query
|
||||
context = MagicMock()
|
||||
|
||||
await adapter._handle_model_picker_callback(query, "mb", "12345")
|
||||
|
||||
edit_kwargs = query.edit_message_text.call_args[1]
|
||||
@@ -129,11 +133,17 @@ class TestTelegramModelPicker:
|
||||
|
||||
await adapter._handle_model_picker_callback(query, "mm:0", "12345")
|
||||
|
||||
# The callback was invoked with the selected model
|
||||
callback.assert_awaited_once()
|
||||
# edit_message_text MUST be called on the success path (this is the
|
||||
# regression we're guarding).
|
||||
query.edit_message_text.assert_awaited()
|
||||
edit_kwargs = query.edit_message_text.call_args[1]
|
||||
assert "MARKDOWN_V2" in repr(edit_kwargs["parse_mode"])
|
||||
# The dynamic result text was routed through format_message
|
||||
# (backtick code blocks survive escaping).
|
||||
assert "`gpt-5`" in edit_kwargs["text"]
|
||||
# State is cleaned up after a successful switch.
|
||||
assert "12345" not in adapter._model_picker_state
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -174,7 +184,7 @@ class TestTelegramModelPicker:
|
||||
providers = [
|
||||
{"slug": "minimax", "name": "MiniMax", "total_models": 2},
|
||||
{"slug": "minimax-cn", "name": "MiniMax (China)", "total_models": 3},
|
||||
{"slug": "xai", "name": "xAI", "total_models": 1},
|
||||
{"slug": "xai", "name": "xAI", "total_models": 1}, # lone group member
|
||||
]
|
||||
|
||||
await adapter.send_model_picker(
|
||||
@@ -187,11 +197,14 @@ class TestTelegramModelPicker:
|
||||
metadata=None,
|
||||
)
|
||||
|
||||
# Top-level keyboard: MiniMax family folded into one group button;
|
||||
# xai (lone member) degraded to a direct provider button.
|
||||
assert "mpg:minimax" in built
|
||||
assert "mp:xai" in built
|
||||
assert "mp:minimax" not in built
|
||||
assert "mp:minimax-cn" not in built
|
||||
|
||||
# Drill into the MiniMax group → members appear as mp: buttons + back.
|
||||
built.clear()
|
||||
query = AsyncMock()
|
||||
query.message = MagicMock()
|
||||
@@ -203,49 +216,7 @@ class TestTelegramModelPicker:
|
||||
|
||||
assert "mp:minimax" in built
|
||||
assert "mp:minimax-cn" in built
|
||||
assert "mb" in built
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expensive_model_requires_confirmation(self, monkeypatch):
|
||||
adapter = _make_adapter()
|
||||
callback = AsyncMock(return_value="Switched to `openai/gpt-5.5-pro`")
|
||||
adapter._model_picker_state["12345"] = {
|
||||
"providers": [
|
||||
{"slug": "openrouter", "name": "OpenRouter", "total_models": 1, "is_current": True}
|
||||
],
|
||||
"current_model": "model_1",
|
||||
"current_provider": "openrouter",
|
||||
"session_key": "s",
|
||||
"on_model_selected": callback,
|
||||
"selected_provider": "openrouter",
|
||||
"model_list": ["openai/gpt-5.5-pro"],
|
||||
"msg_id": 42,
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.model_cost_guard.expensive_model_warning",
|
||||
lambda *_args, **_kwargs: SimpleNamespace(
|
||||
message="!!! EXPENSIVE MODEL WARNING !!!\ndid you mean to select openai/gpt-5.5?"
|
||||
),
|
||||
)
|
||||
|
||||
query = AsyncMock()
|
||||
query.message = MagicMock()
|
||||
query.message.chat_id = 12345
|
||||
query.answer = AsyncMock()
|
||||
query.edit_message_text = AsyncMock()
|
||||
|
||||
await adapter._handle_model_picker_callback(query, "mm:0", "12345")
|
||||
|
||||
callback.assert_not_awaited()
|
||||
assert "12345" in adapter._model_picker_state
|
||||
first_edit = query.edit_message_text.call_args[1]
|
||||
assert "EXPENSIVE MODEL WARNING" in first_edit["text"]
|
||||
assert first_edit["reply_markup"] is not None
|
||||
|
||||
await adapter._handle_model_picker_callback(query, "mc:0", "12345")
|
||||
|
||||
callback.assert_awaited_once_with("12345", "openai/gpt-5.5-pro", "openrouter")
|
||||
assert "12345" not in adapter._model_picker_state
|
||||
assert "mb" in built # back-to-providers button present
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retries_without_thread_when_thread_not_found(self):
|
||||
|
||||
Reference in New Issue
Block a user