feat(cron): bind scheduled automations to sessions
This commit is contained in:
@@ -11,6 +11,7 @@ from nanobot.bus.queue import MessageBus
|
||||
from nanobot.providers.base import LLMResponse
|
||||
from nanobot.session.goal_state import GOAL_STATE_KEY
|
||||
from nanobot.session.manager import Session, SessionManager
|
||||
from nanobot.session.routing import SESSION_ROUTING_METADATA_KEY
|
||||
from nanobot.session.turn_continuation import (
|
||||
INTERNAL_CONTINUATION_META,
|
||||
INTERNAL_CONTINUATION_RUN_STARTED_AT_META,
|
||||
@@ -827,6 +828,12 @@ async def test_process_message_uses_context_chat_id_for_runtime_prompt(tmp_path:
|
||||
assert result.chat_id == "thread-777"
|
||||
assert loop.context.build_messages.call_args.kwargs["chat_id"] == "parent-456"
|
||||
assert loop._run_agent_loop.call_args.kwargs["chat_id"] == "thread-777"
|
||||
session = loop.sessions.get_or_create("discord:parent-456:thread:thread-777")
|
||||
assert session.metadata[SESSION_ROUTING_METADATA_KEY] == {
|
||||
"channel": "discord",
|
||||
"chat_id": "thread-777",
|
||||
"metadata": {"context_chat_id": "parent-456"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -616,6 +616,54 @@ async def test_followup_routed_to_pending_queue(tmp_path):
|
||||
assert queued_msg.session_key == UNIFIED_SESSION_KEY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_automation_turn_deferred_while_session_active(tmp_path):
|
||||
"""Automation turns wait for the active session instead of becoming injections."""
|
||||
from nanobot.bus.events import InboundMessage
|
||||
from nanobot.cron.automation import (
|
||||
AUTOMATION_DEFER_UNTIL_IDLE_META,
|
||||
AUTOMATION_TRIGGER_META,
|
||||
)
|
||||
|
||||
loop = _make_loop(tmp_path)
|
||||
loop._dispatch = AsyncMock() # type: ignore[method-assign]
|
||||
|
||||
session_key = "websocket:chat-1"
|
||||
pending = asyncio.Queue(maxsize=20)
|
||||
loop._pending_queues[session_key] = pending
|
||||
|
||||
run_task = asyncio.create_task(loop.run())
|
||||
msg = InboundMessage(
|
||||
channel="websocket",
|
||||
sender_id="cron",
|
||||
chat_id="chat-1",
|
||||
content="scheduled work",
|
||||
metadata={
|
||||
AUTOMATION_TRIGGER_META: {"run_id": "run-1"},
|
||||
AUTOMATION_DEFER_UNTIL_IDLE_META: True,
|
||||
},
|
||||
session_key_override=session_key,
|
||||
)
|
||||
await loop.bus.publish_inbound(msg)
|
||||
|
||||
for _ in range(20):
|
||||
if loop._deferred_automation_queues.get(session_key):
|
||||
break
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
loop.stop()
|
||||
await asyncio.wait_for(run_task, timeout=2)
|
||||
|
||||
assert pending.empty()
|
||||
assert loop._dispatch.await_count == 0
|
||||
assert loop._deferred_automation_queues[session_key] == [msg]
|
||||
|
||||
await loop._publish_next_deferred_automation(session_key)
|
||||
queued = await asyncio.wait_for(loop.bus.consume_inbound(), timeout=0.5)
|
||||
assert queued is msg
|
||||
assert session_key not in loop._deferred_automation_queues
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending_queue_preserves_overflow_for_next_injection_cycle(tmp_path):
|
||||
"""Pending queue should leave overflow messages queued for later drains."""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from nanobot.session.manager import Session, SessionManager
|
||||
from nanobot.session.routing import SESSION_ROUTING_METADATA_KEY
|
||||
|
||||
|
||||
def _assert_no_orphans(history: list[dict]) -> None:
|
||||
@@ -432,6 +433,11 @@ def test_fork_session_before_user_index_copies_only_prefix(tmp_path):
|
||||
source.metadata["webui"] = True
|
||||
source.metadata["title"] = "Old title"
|
||||
source.metadata["goal_state"] = {"status": "active", "objective": "do not inherit"}
|
||||
source.metadata[SESSION_ROUTING_METADATA_KEY] = {
|
||||
"channel": "websocket",
|
||||
"chat_id": "source",
|
||||
"metadata": {},
|
||||
}
|
||||
source.add_message("user", "round1")
|
||||
source.add_message("assistant", "answer1")
|
||||
source.add_message("user", "round2 fork me")
|
||||
@@ -450,6 +456,7 @@ def test_fork_session_before_user_index_copies_only_prefix(tmp_path):
|
||||
assert forked.metadata["webui"] is True
|
||||
assert "title" not in forked.metadata
|
||||
assert "goal_state" not in forked.metadata
|
||||
assert SESSION_ROUTING_METADATA_KEY not in forked.metadata
|
||||
saved = manager.read_session_file("websocket:fork")
|
||||
assert [m["content"] for m in saved["messages"]] == ["round1", "answer1"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user