项目文件夹

文件
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

168 行
6.9 KiB
Python

"""Tests for tui_gateway background-review summary delivery.
When the self-improvement background review fires and saves a skill or
memory entry, it calls ``agent.background_review_callback(message)``. In
the CLI that routes through a prompt_toolkit-safe ``_cprint``; in the TUI
there is no print surface, so without a callback wired up the review
writes the change silently. ``_init_session`` attaches a callback that
emits a ``review.summary`` event which Ink renders as a persistent
transcript line.
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture()
def server():
with patch.dict(
"sys.modules",
{
"hermes_constants": MagicMock(
get_hermes_home=MagicMock(return_value="/tmp/hermes_test_review_summary")
),
"hermes_cli.env_loader": MagicMock(),
"hermes_cli.banner": MagicMock(),
"hermes_state": MagicMock(),
},
):
import importlib
mod = importlib.import_module("tui_gateway.server")
yield mod
# Reset module-level session state without re-importing. importlib.reload
# would re-register the module's atexit hooks (ThreadPoolExecutor
# shutdown, _shutdown_sessions); the duplicates race the stderr
# buffer at interpreter shutdown and surface as Fatal Python error:
# _enter_buffered_busy. Clearing the per-session dicts gives the
# next test a clean slate; _methods is NOT cleared because it's
# populated at module import time and re-registration only happens
# via reload (which we don't do).
mod._sessions.clear()
mod._pending.clear()
mod._answers.clear()
def test_init_session_attaches_background_review_callback(server, monkeypatch):
"""After _init_session, agent.background_review_callback is set to a
function that emits 'review.summary' for the session's sid."""
# Neutralize side-effect calls inside _init_session so we're testing
# just the callback wiring.
monkeypatch.setattr(server, "_SlashWorker", lambda *a, **kw: object())
monkeypatch.setattr(server, "_wire_callbacks", lambda sid: None)
monkeypatch.setattr(server, "_notify_session_boundary", lambda *a, **kw: None)
monkeypatch.setattr(server, "_session_info", lambda agent, session=None: {"model": "m"})
monkeypatch.setattr(server, "_load_show_reasoning", lambda: False)
monkeypatch.setattr(server, "_load_tool_progress_mode", lambda: "all")
captured_emits: list = []
monkeypatch.setattr(
server,
"_emit",
lambda event, sid, payload=None: captured_emits.append(
(event, sid, payload)
),
)
class FakeAgent:
model = "fake/model"
# Presence of the attribute is all the Python side needs; the real
# AIAgent has it defaulted to None in __init__.
background_review_callback = None
agent = FakeAgent()
server._init_session("sid-abc", "session-key", agent, [], cols=80)
cb = getattr(agent, "background_review_callback", None)
assert callable(cb), (
"_init_session must attach a background_review_callback to the "
"agent so the self-improvement review is visible in the TUI."
)
# Clear the session.info emit captured during _init_session.
captured_emits.clear()
# Invoke the callback the way AIAgent._spawn_background_review would.
cb("💾 Self-improvement review: Skill 'hermes-release' patched")
# Exactly one review.summary event should have been emitted, bound to
# the session id we passed in, carrying the full message text.
matched = [e for e in captured_emits if e[0] == "review.summary"]
assert len(matched) == 1, captured_emits
event, sid, payload = matched[0]
assert sid == "sid-abc"
assert payload == {
"text": "💾 Self-improvement review: Skill 'hermes-release' patched"
}
def test_review_summary_callback_survives_agent_without_attribute(server, monkeypatch):
"""If the agent is a bare object that doesn't allow attribute
assignment (e.g. some stubbed test double), _init_session must not
raise — session startup stays robust."""
monkeypatch.setattr(server, "_SlashWorker", lambda *a, **kw: object())
monkeypatch.setattr(server, "_wire_callbacks", lambda sid: None)
monkeypatch.setattr(server, "_notify_session_boundary", lambda *a, **kw: None)
monkeypatch.setattr(server, "_session_info", lambda agent, session=None: {"model": "m"})
monkeypatch.setattr(server, "_load_show_reasoning", lambda: False)
monkeypatch.setattr(server, "_load_tool_progress_mode", lambda: "all")
monkeypatch.setattr(server, "_emit", lambda *a, **kw: None)
class LockedAgent:
__slots__ = ("model",)
def __init__(self):
self.model = "fake/model"
# LockedAgent's __slots__ blocks background_review_callback assignment.
server._init_session("sid-x", "key-x", LockedAgent(), [], cols=80)
# If we got here, _init_session swallowed the AttributeError gracefully.
def test_init_session_sets_memory_notifications_from_config(server, monkeypatch):
"""_init_session must apply display.memory_notifications to the agent so
the TUI/desktop honors the same off/on/verbose toggle as the messaging
gateway and CLI. Without this the review always behaved as 'on'."""
monkeypatch.setattr(server, "_SlashWorker", lambda *a, **kw: object())
monkeypatch.setattr(server, "_wire_callbacks", lambda sid: None)
monkeypatch.setattr(server, "_notify_session_boundary", lambda *a, **kw: None)
monkeypatch.setattr(server, "_session_info", lambda agent, session=None: {"model": "m"})
monkeypatch.setattr(server, "_load_show_reasoning", lambda: False)
monkeypatch.setattr(server, "_load_tool_progress_mode", lambda: "all")
monkeypatch.setattr(server, "_emit", lambda *a, **kw: None)
monkeypatch.setattr(server, "_load_memory_notifications", lambda: "verbose")
class FakeAgent:
model = "fake/model"
background_review_callback = None
memory_notifications = "on"
agent = FakeAgent()
server._init_session("sid-mn", "key-mn", agent, [], cols=80)
assert agent.memory_notifications == "verbose"
@pytest.mark.parametrize(
"raw,expected",
[
(None, "on"), # unset → default on
("on", "on"),
("off", "off"),
("verbose", "verbose"),
("VERBOSE", "verbose"), # case-normalized
(True, "on"), # bool back-compat
(False, "off"),
],
)
def test_load_memory_notifications_normalization(server, monkeypatch, raw, expected):
"""_load_memory_notifications mirrors the gateway's bool→str normalization
and defaults to 'on' when the key is absent."""
display = {} if raw is None else {"memory_notifications": raw}
monkeypatch.setattr(server, "_load_cfg", lambda: {"display": display})
assert server._load_memory_notifications() == expected