refactor: use cron turn naming internally
This commit is contained in:
@@ -8,7 +8,7 @@ from nanobot.agent.context import ContextBuilder
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
from nanobot.bus.events import InboundMessage
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.cron.automation import AUTOMATION_HISTORY_META, AUTOMATION_TRIGGER_META
|
||||
from nanobot.cron.session_turns import CRON_HISTORY_META, CRON_TRIGGER_META
|
||||
from nanobot.providers.base import LLMResponse
|
||||
from nanobot.session.goal_state import GOAL_STATE_KEY
|
||||
from nanobot.session.manager import Session, SessionManager
|
||||
@@ -66,7 +66,7 @@ def test_agent_loop_llm_runtime_reflects_current_provider_and_model(tmp_path: Pa
|
||||
assert runtime.model == "next-model"
|
||||
|
||||
|
||||
def test_persist_automation_turn_uses_distinct_history_marker(tmp_path: Path) -> None:
|
||||
def test_persist_cron_turn_uses_distinct_history_marker(tmp_path: Path) -> None:
|
||||
loop = _make_full_loop(tmp_path)
|
||||
session = loop.sessions.get_or_create("websocket:auto")
|
||||
prompt_ref = {"id": "cron.agent_turn.reminder", "version": 1, "sha256": "abc"}
|
||||
@@ -76,14 +76,14 @@ def test_persist_automation_turn_uses_distinct_history_marker(tmp_path: Path) ->
|
||||
channel="websocket",
|
||||
sender_id="cron",
|
||||
chat_id="auto",
|
||||
content="Automation: internal prompt",
|
||||
content="Cron job: internal prompt",
|
||||
metadata={
|
||||
AUTOMATION_TRIGGER_META: {
|
||||
CRON_TRIGGER_META: {
|
||||
"job_id": "job-1",
|
||||
"job_name": "Daily check",
|
||||
"run_id": "job-1:1",
|
||||
"prompt_ref": prompt_ref,
|
||||
"persist_content": "Scheduled automation triggered: Daily check",
|
||||
"persist_content": "Scheduled cron job triggered: Daily check",
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -92,13 +92,13 @@ def test_persist_automation_turn_uses_distinct_history_marker(tmp_path: Path) ->
|
||||
|
||||
assert persisted is True
|
||||
message = session.messages[-1]
|
||||
assert message["content"] == "Scheduled automation triggered: Daily check"
|
||||
assert message[AUTOMATION_HISTORY_META] is True
|
||||
assert AUTOMATION_TRIGGER_META not in message
|
||||
assert message["automation_id"] == "job-1"
|
||||
assert message["automation_name"] == "Daily check"
|
||||
assert message["automation_run_id"] == "job-1:1"
|
||||
assert message["automation_prompt_ref"] == prompt_ref
|
||||
assert message["content"] == "Scheduled cron job triggered: Daily check"
|
||||
assert message[CRON_HISTORY_META] is True
|
||||
assert CRON_TRIGGER_META not in message
|
||||
assert message["cron_job_id"] == "job-1"
|
||||
assert message["cron_job_name"] == "Daily check"
|
||||
assert message["cron_run_id"] == "job-1:1"
|
||||
assert message["cron_prompt_ref"] == prompt_ref
|
||||
|
||||
|
||||
def test_clean_generated_title_strips_reasoning_tags() -> None:
|
||||
|
||||
@@ -617,12 +617,12 @@ async def test_followup_routed_to_pending_queue(tmp_path):
|
||||
|
||||
|
||||
@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."""
|
||||
async def test_cron_turn_deferred_while_session_active(tmp_path):
|
||||
"""Cron 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,
|
||||
from nanobot.cron.session_turns import (
|
||||
CRON_DEFER_UNTIL_IDLE_META,
|
||||
CRON_TRIGGER_META,
|
||||
)
|
||||
|
||||
loop = _make_loop(tmp_path)
|
||||
@@ -639,15 +639,15 @@ async def test_automation_turn_deferred_while_session_active(tmp_path):
|
||||
chat_id="chat-1",
|
||||
content="scheduled work",
|
||||
metadata={
|
||||
AUTOMATION_TRIGGER_META: {"run_id": "run-1"},
|
||||
AUTOMATION_DEFER_UNTIL_IDLE_META: True,
|
||||
CRON_TRIGGER_META: {"run_id": "run-1"},
|
||||
CRON_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):
|
||||
if loop._cron_turns.deferred_queues.get(session_key):
|
||||
break
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
@@ -656,12 +656,12 @@ async def test_automation_turn_deferred_while_session_active(tmp_path):
|
||||
|
||||
assert pending.empty()
|
||||
assert loop._dispatch.await_count == 0
|
||||
assert loop._deferred_automation_queues[session_key] == [msg]
|
||||
assert loop._cron_turns.deferred_queues[session_key] == [msg]
|
||||
|
||||
await loop._publish_next_deferred_automation(session_key)
|
||||
await loop._cron_turns.publish_next_deferred(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
|
||||
assert session_key not in loop._cron_turns.deferred_queues
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -10,6 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from nanobot.config.schema import AgentDefaults
|
||||
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||
|
||||
_MAX_TOOL_RESULT_CHARS = AgentDefaults().max_tool_result_chars
|
||||
|
||||
@@ -450,12 +451,12 @@ class TestSubagentAnnounceSessionKey:
|
||||
so the result matches the pending queue key."""
|
||||
mgr, bus = self._make_mgr()
|
||||
|
||||
origin = {"channel": "telegram", "chat_id": "111", "session_key": "unified:default"}
|
||||
origin = {"channel": "telegram", "chat_id": "111", "session_key": UNIFIED_SESSION_KEY}
|
||||
await mgr._announce_result("sub-1", "label", "task", "result", origin, "ok")
|
||||
|
||||
msg = await bus.consume_inbound()
|
||||
assert msg.session_key_override == "unified:default"
|
||||
assert msg.session_key == "unified:default"
|
||||
assert msg.session_key_override == UNIFIED_SESSION_KEY
|
||||
assert msg.session_key == UNIFIED_SESSION_KEY
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_announce_uses_raw_key_in_normal_mode(self):
|
||||
@@ -505,9 +506,9 @@ class TestSubagentAnnounceSessionKey:
|
||||
)
|
||||
await mgr._run_subagent(
|
||||
"sub-4", "task", "label",
|
||||
{"channel": "telegram", "chat_id": "444", "session_key": "unified:default"},
|
||||
{"channel": "telegram", "chat_id": "444", "session_key": UNIFIED_SESSION_KEY},
|
||||
status,
|
||||
)
|
||||
|
||||
msg = await bus.consume_inbound()
|
||||
assert msg.session_key_override == "unified:default"
|
||||
assert msg.session_key_override == UNIFIED_SESSION_KEY
|
||||
|
||||
@@ -25,9 +25,9 @@ from nanobot.bus.queue import MessageBus
|
||||
from nanobot.command.builtin import cmd_new, register_builtin_commands
|
||||
from nanobot.command.router import CommandContext, CommandRouter
|
||||
from nanobot.config.schema import AgentDefaults, Config
|
||||
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||
from nanobot.session.manager import Session, SessionManager
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -39,8 +39,8 @@ def _make_loop(tmp_path: Path, unified_session: bool = False) -> AgentLoop:
|
||||
provider.get_default_model.return_value = "test-model"
|
||||
|
||||
with patch("nanobot.agent.loop.SessionManager"), \
|
||||
patch("nanobot.agent.loop.SubagentManager") as MockSubMgr:
|
||||
MockSubMgr.return_value.cancel_by_session = AsyncMock(return_value=0)
|
||||
patch("nanobot.agent.loop.SubagentManager") as mock_sub_mgr:
|
||||
mock_sub_mgr.return_value.cancel_by_session = AsyncMock(return_value=0)
|
||||
loop = AgentLoop(
|
||||
bus=bus,
|
||||
provider=provider,
|
||||
@@ -415,10 +415,8 @@ class TestStopCommandWithUnifiedSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_active_tasks_use_effective_key_in_unified_mode(self, tmp_path: Path):
|
||||
"""When unified_session=True, tasks are stored under UNIFIED_SESSION_KEY."""
|
||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
||||
|
||||
loop = _make_loop(tmp_path, unified_session=True)
|
||||
|
||||
|
||||
# Create a message from telegram channel
|
||||
msg = _make_msg(channel="telegram", chat_id="123456")
|
||||
|
||||
@@ -443,7 +441,6 @@ class TestStopCommandWithUnifiedSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_command_finds_task_in_unified_mode(self, tmp_path: Path):
|
||||
"""cmd_stop can cancel tasks when unified_session=True."""
|
||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
||||
from nanobot.command.builtin import cmd_stop
|
||||
|
||||
loop = _make_loop(tmp_path, unified_session=True)
|
||||
@@ -476,7 +473,6 @@ class TestStopCommandWithUnifiedSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_command_uses_effective_key_without_session_override(self, tmp_path: Path):
|
||||
"""Priority /stop must cancel the unified session even before dispatch rewrites the message."""
|
||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
||||
from nanobot.command.builtin import cmd_stop
|
||||
|
||||
loop = _make_loop(tmp_path, unified_session=True)
|
||||
@@ -502,7 +498,6 @@ class TestStopCommandWithUnifiedSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_command_cross_channel_in_unified_mode(self, tmp_path: Path):
|
||||
"""In unified mode, /stop from one channel cancels tasks from another channel."""
|
||||
from nanobot.agent.loop import UNIFIED_SESSION_KEY
|
||||
from nanobot.command.builtin import cmd_stop
|
||||
|
||||
loop = _make_loop(tmp_path, unified_session=True)
|
||||
|
||||
@@ -749,7 +749,7 @@ async def test_session_delete_blocks_when_bound_automation_exists(
|
||||
assert body["blocked_by_automations"] is True
|
||||
assert [job["name"] for job in body["automations"]] == ["Daily check"]
|
||||
assert path.exists()
|
||||
assert cron.list_bound_agent_jobs_for_session("websocket:doomed")
|
||||
assert cron.list_bound_cron_jobs_for_session("websocket:doomed")
|
||||
finally:
|
||||
await channel.stop()
|
||||
await server_task
|
||||
@@ -792,7 +792,7 @@ async def test_session_delete_can_cascade_bound_automations(
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["deleted"] is True
|
||||
assert not path.exists()
|
||||
assert cron.list_bound_agent_jobs_for_session("websocket:doomed") == []
|
||||
assert cron.list_bound_cron_jobs_for_session("websocket:doomed") == []
|
||||
assert [job.name for job in cron.list_jobs(include_disabled=True)] == [
|
||||
"Legacy same target"
|
||||
]
|
||||
@@ -837,7 +837,7 @@ async def test_session_delete_does_not_cascade_unified_automations(
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["deleted"] is True
|
||||
assert not path.exists()
|
||||
assert [job.name for job in cron.list_bound_agent_jobs_for_session(UNIFIED_SESSION_KEY)] == [
|
||||
assert [job.name for job in cron.list_bound_cron_jobs_for_session(UNIFIED_SESSION_KEY)] == [
|
||||
"Shared daily check"
|
||||
]
|
||||
finally:
|
||||
|
||||
+11
-11
@@ -11,7 +11,7 @@ from typer.testing import CliRunner
|
||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||
from nanobot.cli.commands import _proactive_delivery_metadata, app
|
||||
from nanobot.config.schema import Config
|
||||
from nanobot.cron.automation import AUTOMATION_DEFER_UNTIL_IDLE_META, AUTOMATION_TRIGGER_META
|
||||
from nanobot.cron.session_turns import CRON_DEFER_UNTIL_IDLE_META, CRON_TRIGGER_META
|
||||
from nanobot.cron.types import CronJob, CronPayload
|
||||
from nanobot.providers.factory import ProviderSnapshot, make_provider
|
||||
from nanobot.providers.openai_codex_provider import _strip_model_prefix
|
||||
@@ -1430,8 +1430,8 @@ def test_gateway_legacy_cron_payloads_with_session_key_stay_legacy(
|
||||
content="Legacy response.",
|
||||
)
|
||||
|
||||
async def submit_automation_turn(self, _msg: InboundMessage):
|
||||
raise AssertionError("legacy cron payload must not run as bound automation")
|
||||
async def submit_cron_turn(self, _msg: InboundMessage):
|
||||
raise AssertionError("legacy cron payload must not run as bound cron turn")
|
||||
|
||||
async def close_mcp(self) -> None:
|
||||
return None
|
||||
@@ -1601,8 +1601,8 @@ def test_gateway_bound_cron_runs_as_session_turn(
|
||||
self.tools = {}
|
||||
seen["agent"] = self
|
||||
|
||||
async def submit_automation_turn(self, msg: InboundMessage):
|
||||
seen["automation_msg"] = msg
|
||||
async def submit_cron_turn(self, msg: InboundMessage):
|
||||
seen["cron_msg"] = msg
|
||||
return OutboundMessage(
|
||||
channel=msg.channel,
|
||||
chat_id=msg.chat_id,
|
||||
@@ -1646,26 +1646,26 @@ def test_gateway_bound_cron_runs_as_session_turn(
|
||||
response = asyncio.run(cron.on_job(job))
|
||||
|
||||
assert response == "Checked the repo."
|
||||
msg = seen["automation_msg"]
|
||||
msg = seen["cron_msg"]
|
||||
assert isinstance(msg, InboundMessage)
|
||||
assert msg.channel == "websocket"
|
||||
assert msg.chat_id == "chat-1"
|
||||
assert msg.sender_id == "cron"
|
||||
assert msg.session_key_override == "websocket:chat-1"
|
||||
assert "Automation: Check repository health." in msg.content
|
||||
assert "Cron job: Check repository health." in msg.content
|
||||
assert msg.metadata["webui"] is True
|
||||
assert msg.metadata["workspace_scope"]["project_path"] == str(tmp_path)
|
||||
assert msg.metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {
|
||||
"kind": "cron",
|
||||
"label": "Repo check",
|
||||
}
|
||||
trigger = msg.metadata[AUTOMATION_TRIGGER_META]
|
||||
trigger = msg.metadata[CRON_TRIGGER_META]
|
||||
assert trigger["job_id"] == "repo-check"
|
||||
assert trigger["job_name"] == "Repo check"
|
||||
assert trigger["persist_content"] == (
|
||||
"Scheduled automation triggered: Repo check\n\nCheck repository health."
|
||||
"Scheduled cron job triggered: Repo check\n\nCheck repository health."
|
||||
)
|
||||
assert msg.metadata[AUTOMATION_DEFER_UNTIL_IDLE_META] is True
|
||||
assert msg.metadata[CRON_DEFER_UNTIL_IDLE_META] is True
|
||||
statuses = [record["status"] for _run_id, record in seen["run_records"]]
|
||||
assert statuses == ["queued", "ok"]
|
||||
assert seen["run_records"][0][0] == seen["run_records"][1][0]
|
||||
@@ -1682,7 +1682,7 @@ def test_gateway_bound_cron_runs_as_session_turn(
|
||||
response = asyncio.run(cron.on_job(discord_job))
|
||||
|
||||
assert response == "Checked the repo."
|
||||
msg = seen["automation_msg"]
|
||||
msg = seen["cron_msg"]
|
||||
assert isinstance(msg, InboundMessage)
|
||||
assert msg.channel == "discord"
|
||||
assert msg.chat_id == "777"
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_list_bound_agent_jobs_excludes_legacy_delivery_payloads(tmp_path) -> No
|
||||
session_key="websocket:chat-1",
|
||||
)
|
||||
|
||||
assert service.list_bound_agent_jobs_for_session("websocket:chat-1") == [bound]
|
||||
assert service.list_bound_cron_jobs_for_session("websocket:chat-1") == [bound]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -349,7 +349,7 @@ def test_add_job_requires_session_key(tmp_path) -> None:
|
||||
|
||||
result = tool._add_job(None, "Background refresh", 60, None, None, None)
|
||||
|
||||
assert result == "Error: scheduled automations must be created from a chat session"
|
||||
assert result == "Error: scheduled cron jobs must be created from a chat session"
|
||||
assert tool._cron.list_jobs() == []
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user