refactor(trigger): share automation turn delivery
This commit is contained in:
@@ -134,6 +134,7 @@ def test_persist_local_trigger_turn_uses_hidden_automation_marker(tmp_path: Path
|
||||
"trigger_name": "PR review",
|
||||
"delivery_id": "tdel_456",
|
||||
"created_at_ms": 1_700_000_000_000,
|
||||
"persist_content": "Local trigger received: PR review\n\nReview PR #4502",
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -142,8 +143,7 @@ def test_persist_local_trigger_turn_uses_hidden_automation_marker(tmp_path: Path
|
||||
|
||||
assert persisted is True
|
||||
message = session.messages[-1]
|
||||
assert message["content"] == "Local trigger received: PR review"
|
||||
assert "Review PR #4502" not in message["content"]
|
||||
assert message["content"] == "Local trigger received: PR review\n\nReview PR #4502"
|
||||
assert message[AUTOMATION_HISTORY_META] == {
|
||||
"kind": "local_trigger",
|
||||
"trigger_id": "trg_123",
|
||||
@@ -156,6 +156,25 @@ def test_persist_local_trigger_turn_uses_hidden_automation_marker(tmp_path: Path
|
||||
assert message["trigger_delivery_id"] == "tdel_456"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_with_bot_suffix_does_not_persist_command(tmp_path: Path) -> None:
|
||||
loop = _make_full_loop(tmp_path)
|
||||
|
||||
response = await loop._process_message(
|
||||
InboundMessage(
|
||||
channel="websocket",
|
||||
sender_id="user",
|
||||
chat_id="chat-1",
|
||||
content="/new@nanobot_bot",
|
||||
)
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.content == "New session started."
|
||||
session = loop.sessions.get_or_create("websocket:chat-1")
|
||||
assert session.messages == []
|
||||
|
||||
|
||||
def test_clean_generated_title_strips_reasoning_tags() -> None:
|
||||
assert clean_generated_title("<think>reasoning</think> WebUI polish") == "WebUI polish"
|
||||
assert clean_generated_title("Title: <think> The user said hello") == ""
|
||||
|
||||
@@ -2044,6 +2044,7 @@ def test_gateway_local_trigger_queue_submits_agent_turns(
|
||||
class _FakeAgentLoop:
|
||||
@classmethod
|
||||
def from_config(cls, config, bus=None, **extra):
|
||||
seen["agent_from_config_kwargs"] = extra
|
||||
return cls(**extra)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
@@ -2093,8 +2094,11 @@ def test_gateway_local_trigger_queue_submits_agent_turns(
|
||||
cli_commands._run_gateway(config, health_server_enabled=False)
|
||||
|
||||
agent = seen["agent"]
|
||||
agent_kwargs = seen["agent_from_config_kwargs"]
|
||||
kwargs = seen["local_trigger_queue_kwargs"]
|
||||
assert kwargs["bus"] is bus
|
||||
assert "local_trigger_store" in agent_kwargs
|
||||
assert kwargs["store"] is agent_kwargs["local_trigger_store"]
|
||||
assert "bus" not in kwargs
|
||||
assert kwargs["submit_turn"] is agent.submit_local_trigger_turn
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
from nanobot.bus.events import InboundMessage
|
||||
from nanobot.command.builtin import build_help_text, register_builtin_commands
|
||||
from nanobot.command.router import CommandContext, CommandRouter
|
||||
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
|
||||
|
||||
@@ -45,6 +46,37 @@ async def test_trigger_command_creates_session_bound_local_trigger(tmp_path: Pat
|
||||
assert f"nanobot trigger {trigger.id} \"message\"" in response.content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_command_binds_inbound_session_when_unified_session_is_active(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
router = CommandRouter()
|
||||
register_builtin_commands(router)
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
loop = SimpleNamespace(workspace=tmp_path, local_trigger_store=store)
|
||||
msg = InboundMessage(
|
||||
channel="websocket",
|
||||
sender_id="user",
|
||||
chat_id="chat-1",
|
||||
content="/trigger PR review",
|
||||
session_key_override="websocket:chat-1:thread-a",
|
||||
)
|
||||
ctx = CommandContext(
|
||||
msg=msg,
|
||||
session=None,
|
||||
key=UNIFIED_SESSION_KEY,
|
||||
raw="/trigger PR review",
|
||||
loop=loop,
|
||||
)
|
||||
|
||||
response = await router.dispatch(ctx)
|
||||
|
||||
assert response is not None
|
||||
trigger = store.list_for_session("websocket:chat-1:thread-a")[0]
|
||||
assert trigger.session_key == "websocket:chat-1:thread-a"
|
||||
assert store.list_for_session(UNIFIED_SESSION_KEY) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_command_without_name_returns_usage_only(tmp_path: Path) -> None:
|
||||
router = CommandRouter()
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from nanobot.agent.automation_turns import AutomationTurnError
|
||||
from nanobot.bus.events import InboundMessage
|
||||
from nanobot.triggers.local_runner import run_local_trigger_queue
|
||||
from nanobot.triggers.local_store import LocalTriggerStore, TriggerDisabledError
|
||||
@@ -77,7 +78,7 @@ def test_recover_processing_deliveries_requeues_claimed_delivery(tmp_path: Path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_trigger_queue_publishes_bound_inbound_message(tmp_path: Path) -> None:
|
||||
async def test_local_trigger_queue_submits_bound_inbound_message(tmp_path: Path) -> None:
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
trigger = store.create(
|
||||
name="PR review",
|
||||
@@ -87,18 +88,18 @@ async def test_local_trigger_queue_publishes_bound_inbound_message(tmp_path: Pat
|
||||
origin_metadata={"webui": True, WEBUI_TURN_METADATA_KEY: "old-turn"},
|
||||
)
|
||||
store.enqueue(trigger.id, "Review PR #4502")
|
||||
published: list[InboundMessage] = []
|
||||
submitted: list[InboundMessage] = []
|
||||
|
||||
class _Bus:
|
||||
async def publish_inbound(self, msg: InboundMessage) -> None:
|
||||
published.append(msg)
|
||||
async def _submit_turn(msg: InboundMessage):
|
||||
submitted.append(msg)
|
||||
return None
|
||||
|
||||
task = asyncio.create_task(
|
||||
run_local_trigger_queue(store=store, bus=_Bus(), poll_interval_s=0.01)
|
||||
run_local_trigger_queue(store=store, submit_turn=_submit_turn, poll_interval_s=0.01)
|
||||
)
|
||||
try:
|
||||
for _ in range(100):
|
||||
if published:
|
||||
if submitted:
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
finally:
|
||||
@@ -106,8 +107,8 @@ async def test_local_trigger_queue_publishes_bound_inbound_message(tmp_path: Pat
|
||||
with suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
assert len(published) == 1
|
||||
msg = published[0]
|
||||
assert len(submitted) == 1
|
||||
msg = submitted[0]
|
||||
assert msg.channel == "websocket"
|
||||
assert msg.chat_id == "chat-1"
|
||||
assert msg.sender_id == "trigger"
|
||||
@@ -120,6 +121,10 @@ async def test_local_trigger_queue_publishes_bound_inbound_message(tmp_path: Pat
|
||||
"label": "PR review",
|
||||
}
|
||||
assert msg.metadata["_local_trigger"]["trigger_id"] == trigger.id
|
||||
assert (
|
||||
msg.metadata["_local_trigger"]["persist_content"]
|
||||
== "Local trigger received: PR review\n\nReview PR #4502"
|
||||
)
|
||||
|
||||
stored = store.get(trigger.id)
|
||||
assert stored is not None
|
||||
@@ -227,6 +232,52 @@ async def test_local_trigger_queue_requeues_when_submitted_turn_is_interrupted(
|
||||
await task
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_trigger_queue_does_not_retry_completed_agent_failure(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
trigger = store.create(
|
||||
name="CI review",
|
||||
channel="websocket",
|
||||
chat_id="chat-1",
|
||||
session_key="websocket:chat-1",
|
||||
)
|
||||
store.enqueue(trigger.id, "Review failed CI")
|
||||
started = asyncio.Event()
|
||||
|
||||
async def _submit_turn(_msg: InboundMessage):
|
||||
started.set()
|
||||
raise AutomationTurnError("model failed")
|
||||
|
||||
task = asyncio.create_task(
|
||||
run_local_trigger_queue(
|
||||
store=store,
|
||||
submit_turn=_submit_turn,
|
||||
poll_interval_s=0.01,
|
||||
)
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(started.wait(), timeout=1)
|
||||
for _ in range(100):
|
||||
stored = store.get(trigger.id)
|
||||
if stored and stored.last_status == "error":
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
stored = store.get(trigger.id)
|
||||
assert stored is not None
|
||||
assert stored.last_status == "error"
|
||||
assert stored.last_error == "model failed"
|
||||
assert store.claim_deliveries() == []
|
||||
assert not list(store.processing_dir.glob("*.json"))
|
||||
assert not list(store.failed_dir.glob("*.json"))
|
||||
finally:
|
||||
task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_trigger_queue_recovers_processing_delivery_on_start(
|
||||
tmp_path: Path,
|
||||
@@ -240,19 +291,19 @@ async def test_local_trigger_queue_recovers_processing_delivery_on_start(
|
||||
)
|
||||
store.enqueue(trigger.id, "Review PR #4591")
|
||||
assert len(store.claim_deliveries()) == 1
|
||||
published: list[InboundMessage] = []
|
||||
submitted: list[InboundMessage] = []
|
||||
|
||||
class _Bus:
|
||||
async def publish_inbound(self, msg: InboundMessage) -> None:
|
||||
published.append(msg)
|
||||
async def _submit_turn(msg: InboundMessage):
|
||||
submitted.append(msg)
|
||||
return None
|
||||
|
||||
restarted = LocalTriggerStore(tmp_path)
|
||||
task = asyncio.create_task(
|
||||
run_local_trigger_queue(store=restarted, bus=_Bus(), poll_interval_s=0.01)
|
||||
run_local_trigger_queue(store=restarted, submit_turn=_submit_turn, poll_interval_s=0.01)
|
||||
)
|
||||
try:
|
||||
for _ in range(100):
|
||||
if published:
|
||||
if submitted:
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
finally:
|
||||
@@ -260,7 +311,7 @@ async def test_local_trigger_queue_recovers_processing_delivery_on_start(
|
||||
with suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
assert len(published) == 1
|
||||
assert published[0].content == "Review PR #4591"
|
||||
assert published[0].metadata["_local_trigger"]["trigger_id"] == trigger.id
|
||||
assert len(submitted) == 1
|
||||
assert submitted[0].content == "Review PR #4591"
|
||||
assert submitted[0].metadata["_local_trigger"]["trigger_id"] == trigger.id
|
||||
assert restarted.claim_deliveries() == []
|
||||
|
||||
Reference in New Issue
Block a user