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:
@@ -360,95 +360,3 @@ class TestQueueConsumptionAfterCompletion:
|
||||
e.text for e in runner._queued_events[session_key]
|
||||
]
|
||||
assert collected == texts
|
||||
|
||||
|
||||
class TestBusyInputModeQueueFifo:
|
||||
"""Regression coverage for issue #28503.
|
||||
|
||||
``busy_input_mode: queue`` rapid follow-ups used to silently overwrite
|
||||
a single pending slot, losing every message except the last. The
|
||||
runner's busy/queue/steer-fallback entry point now routes through
|
||||
the same FIFO infrastructure as ``/queue``, so each follow-up gets
|
||||
its own turn in arrival order.
|
||||
"""
|
||||
|
||||
def _make_runner_and_adapter(self):
|
||||
from gateway.run import GatewayRunner
|
||||
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
runner._queued_events = {}
|
||||
adapter = _StubAdapter()
|
||||
runner.adapters = {Platform.TELEGRAM: adapter}
|
||||
return runner, adapter
|
||||
|
||||
def _text_event(self, text: str) -> MessageEvent:
|
||||
source = MagicMock(chat_id="c1", platform=Platform.TELEGRAM)
|
||||
return MessageEvent(
|
||||
text=text,
|
||||
message_type=MessageType.TEXT,
|
||||
source=source,
|
||||
message_id=f"m-{text}",
|
||||
)
|
||||
|
||||
def test_rapid_text_followups_are_queued_in_fifo_order(self):
|
||||
"""Five rapid texts in queue mode must all survive (none silently dropped)."""
|
||||
runner, adapter = self._make_runner_and_adapter()
|
||||
session_key = "telegram:user:fifo"
|
||||
|
||||
texts = ["one", "two", "three", "four", "five"]
|
||||
for text in texts:
|
||||
runner._queue_or_replace_pending_event(session_key, self._text_event(text))
|
||||
|
||||
# Head slot keeps the first; overflow keeps the rest in order.
|
||||
assert adapter._pending_messages[session_key].text == "one"
|
||||
assert [e.text for e in runner._queued_events[session_key]] == [
|
||||
"two",
|
||||
"three",
|
||||
"four",
|
||||
"five",
|
||||
]
|
||||
assert runner._queue_depth(session_key, adapter=adapter) == len(texts)
|
||||
|
||||
def test_queue_respects_bounded_cap(self):
|
||||
"""Beyond the per-session cap, follow-ups are dropped (with a warning)."""
|
||||
from gateway.run import GatewayRunner
|
||||
|
||||
runner, adapter = self._make_runner_and_adapter()
|
||||
session_key = "telegram:user:cap"
|
||||
|
||||
cap = GatewayRunner._BUSY_QUEUE_MAX_PENDING
|
||||
for i in range(cap + 5):
|
||||
runner._queue_or_replace_pending_event(
|
||||
session_key, self._text_event(f"msg-{i:03d}")
|
||||
)
|
||||
|
||||
# Exactly ``cap`` follow-ups retained (head + cap-1 in overflow).
|
||||
assert runner._queue_depth(session_key, adapter=adapter) == cap
|
||||
assert adapter._pending_messages[session_key].text == "msg-000"
|
||||
# The last accepted overflow item is msg-{cap-1}.
|
||||
assert runner._queued_events[session_key][-1].text == f"msg-{cap - 1:03d}"
|
||||
|
||||
def test_photo_burst_still_merges_in_head_slot(self):
|
||||
"""Photo bursts must keep album-merge semantics, not split into N turns."""
|
||||
runner, adapter = self._make_runner_and_adapter()
|
||||
session_key = "telegram:user:burst"
|
||||
|
||||
source = MagicMock(chat_id="c1", platform=Platform.TELEGRAM)
|
||||
for i in range(3):
|
||||
runner._queue_or_replace_pending_event(
|
||||
session_key,
|
||||
MessageEvent(
|
||||
text="",
|
||||
message_type=MessageType.PHOTO,
|
||||
source=source,
|
||||
message_id=f"p-{i}",
|
||||
media_urls=[f"http://example.com/{i}.jpg"],
|
||||
media_types=["image/jpeg"],
|
||||
),
|
||||
)
|
||||
|
||||
# Single merged head event with all three media URLs.
|
||||
assert session_key not in runner._queued_events or not runner._queued_events[session_key]
|
||||
head = adapter._pending_messages[session_key]
|
||||
assert head.message_type == MessageType.PHOTO
|
||||
assert len(head.media_urls) == 3
|
||||
|
||||
Reference in New Issue
Block a user