Files
2026-07-13 13:12:33 +08:00

302 lines
10 KiB
Python

"""RPC layer must let users set webhook delivery on a cron job.
Webhook delivery is plumbed through ``scheduler.delivery``, but the RPC
``cron.add`` payload originally only parsed channel-mode overrides. These
tests assert the wire payload can carry webhook mode end-to-end.
"""
from __future__ import annotations
from typing import Any
import pytest
from opensquilla.gateway.rpc import RpcContext
from opensquilla.gateway.rpc_cron import (
_handle_cron_add,
_handle_cron_update,
_job_to_wire,
)
from opensquilla.scheduler.payloads import AGENT_TURN_KIND, SYSTEM_EVENT_KIND
from opensquilla.scheduler.types import (
CronJob,
DeliveryConfig,
DeliveryMode,
FailureDestination,
SessionTarget,
)
class _FakeScheduler:
def __init__(self) -> None:
self.added_kwargs: dict[str, Any] | None = None
async def add_job(self, **kwargs):
self.added_kwargs = kwargs
return CronJob(
id="job-w",
name=kwargs["name"],
cron_expr=kwargs.get("schedule_value") or kwargs.get("schedule_raw", ""),
schedule_raw=kwargs.get("schedule_value") or kwargs.get("schedule_raw", ""),
handler_key=kwargs["handler_key"],
payload=kwargs["payload"],
session_target=kwargs["session_target"],
session_key=kwargs["session_key"],
origin_session_key=kwargs["origin_session_key"],
delivery=kwargs.get("delivery") or DeliveryConfig(),
)
async def update_job(self, job_id, **patch):
return None
async def test_rpc_cron_add_webhook_delivery_is_reachable() -> None:
scheduler = _FakeScheduler()
result = await _handle_cron_add(
{
"name": "Webhook job",
"expression": "*/5 * * * *",
"payloadKind": AGENT_TURN_KIND,
"text": "Run analysis",
"sessionTarget": "isolated",
"delivery": {
"mode": "webhook",
"webhookUrl": "https://hooks.example/cron",
"webhookToken": "secret",
"bestEffort": True,
},
},
RpcContext(conn_id="test", cron_scheduler=scheduler),
)
assert scheduler.added_kwargs is not None
delivery = scheduler.added_kwargs["delivery"]
assert delivery.mode == DeliveryMode.WEBHOOK
assert delivery.webhook_url == "https://hooks.example/cron"
assert delivery.webhook_token == "secret"
assert delivery.best_effort is True
# Wire payload echoes the webhook target.
assert result["delivery"]["mode"] == "webhook"
assert result["delivery"]["webhookUrl"] == "https://hooks.example/cron"
assert result["delivery"]["bestEffort"] is True
async def test_rpc_cron_add_webhook_accepts_to_alias() -> None:
"""The wire payload accepts `to` as an alias for the webhook URL."""
scheduler = _FakeScheduler()
await _handle_cron_add(
{
"name": "Alias",
"expression": "*/5 * * * *",
"payloadKind": AGENT_TURN_KIND,
"text": "x",
"sessionTarget": "isolated",
"delivery": {"mode": "webhook", "to": "https://hooks.example/x"},
},
RpcContext(conn_id="test", cron_scheduler=scheduler),
)
delivery = scheduler.added_kwargs["delivery"]
assert delivery.mode == DeliveryMode.WEBHOOK
assert delivery.webhook_url == "https://hooks.example/x"
async def test_rpc_cron_add_webhook_allowed_for_main_target() -> None:
"""Webhook delivery is allowed for any sessionTarget, including main."""
scheduler = _FakeScheduler()
await _handle_cron_add(
{
"name": "Main hook",
"expression": "0 9 * * *",
"payloadKind": SYSTEM_EVENT_KIND,
"text": "Reminder",
"sessionTarget": "main",
"delivery": {
"mode": "webhook",
"webhookUrl": "https://hooks.example/main",
},
},
RpcContext(conn_id="test", cron_scheduler=scheduler),
)
assert scheduler.added_kwargs["delivery"].mode == DeliveryMode.WEBHOOK
async def test_rpc_cron_add_webhook_with_invalid_url_raises() -> None:
scheduler = _FakeScheduler()
with pytest.raises(ValueError, match="http or https"):
await _handle_cron_add(
{
"name": "Bad",
"expression": "*/5 * * * *",
"payloadKind": AGENT_TURN_KIND,
"text": "x",
"sessionTarget": "isolated",
"delivery": {"mode": "webhook", "webhookUrl": "ftp://bad/hook"},
},
RpcContext(conn_id="test", cron_scheduler=scheduler),
)
def test_job_to_wire_includes_webhook_fields() -> None:
job = CronJob(
id="job-1",
name="hook",
cron_expr="*/5 * * * *",
schedule_raw="*/5 * * * *",
handler_key="agent_run",
payload={"kind": AGENT_TURN_KIND, "task": "x", "agent_id": "main"},
session_target=SessionTarget.ISOLATED,
delivery=DeliveryConfig(
mode=DeliveryMode.WEBHOOK,
webhook_url="https://hooks.example/cron",
best_effort=True,
),
)
wire = _job_to_wire(job)
assert wire["delivery"]["mode"] == "webhook"
assert wire["delivery"]["webhookUrl"] == "https://hooks.example/cron"
assert wire["delivery"]["bestEffort"] is True
def test_job_to_wire_preserves_webhook_for_main_session_target() -> None:
"""Main + webhook must survive _job_to_wire; only channel modes are suppressed."""
job = CronJob(
id="job-main-hook",
name="main hook",
cron_expr="0 9 * * *",
schedule_raw="0 9 * * *",
handler_key="system_event",
payload={"kind": SYSTEM_EVENT_KIND, "text": "x", "agent_id": "main"},
session_target=SessionTarget.MAIN,
delivery=DeliveryConfig(
mode=DeliveryMode.WEBHOOK,
webhook_url="https://hooks.example/main",
),
)
wire = _job_to_wire(job)
assert wire["delivery"]["mode"] == "webhook"
assert wire["delivery"]["webhookUrl"] == "https://hooks.example/main"
def test_job_to_wire_suppresses_channel_delivery_for_main() -> None:
"""Channel-mode delivery on main is still hidden from the wire payload."""
job = CronJob(
id="job-main-channel",
name="main channel",
cron_expr="0 9 * * *",
schedule_raw="0 9 * * *",
handler_key="system_event",
payload={"kind": SYSTEM_EVENT_KIND, "text": "x", "agent_id": "main"},
session_target=SessionTarget.MAIN,
delivery=DeliveryConfig(
mode=DeliveryMode.CHANNEL,
channel_name="slack",
channel_id="C-x",
),
)
wire = _job_to_wire(job)
assert wire["delivery"]["mode"] == "none"
class _UpdateScheduler:
"""Minimal scheduler stub that records `update_job` and returns a mutated job."""
def __init__(self, current: CronJob) -> None:
self._current = current
self.last_patch: dict[str, Any] | None = None
async def get_job(self, job_id: str) -> CronJob | None:
return self._current if job_id == self._current.id else None
async def update_job(self, job_id: str, **patch) -> CronJob:
self.last_patch = patch
for key, value in patch.items():
setattr(self._current, key, value)
return self._current
async def pause_job(self, job_id: str) -> CronJob: # pragma: no cover
return self._current
async def resume_job(self, job_id: str) -> CronJob: # pragma: no cover
return self._current
async def test_rpc_cron_update_patches_webhook_with_best_effort_and_fd() -> None:
"""cron.update accepts a webhook delivery patch with bestEffort + failureDestination."""
current = CronJob(
id="job-1",
name="hook",
cron_expr="*/5 * * * *",
schedule_raw="*/5 * * * *",
handler_key="agent_run",
payload={"kind": AGENT_TURN_KIND, "task": "x", "agent_id": "main"},
session_target=SessionTarget.ISOLATED,
delivery=DeliveryConfig(),
)
scheduler = _UpdateScheduler(current)
await _handle_cron_update(
{
"id": "job-1",
"delivery": {
"mode": "webhook",
"webhookUrl": "https://hooks.example/cron",
"webhookToken": "secret",
"bestEffort": True,
"failureDestination": {
"mode": "channel",
"channelName": "slack",
"channelId": "C-ops",
},
},
},
RpcContext(conn_id="t", cron_scheduler=scheduler),
)
assert scheduler.last_patch is not None
new_delivery: DeliveryConfig = scheduler.last_patch["delivery"]
assert new_delivery.mode == DeliveryMode.WEBHOOK
assert new_delivery.webhook_url == "https://hooks.example/cron"
assert new_delivery.webhook_token == "secret"
assert new_delivery.best_effort is True
fd = new_delivery.failure_destination
assert isinstance(fd, FailureDestination)
assert fd.mode == DeliveryMode.CHANNEL
assert fd.channel_name == "slack"
assert fd.channel_id == "C-ops"
async def test_rpc_cron_update_standalone_failure_destination_keeps_primary() -> None:
"""A delivery patch carrying only failureDestination preserves primary delivery."""
current = CronJob(
id="job-2",
name="hook",
cron_expr="*/5 * * * *",
schedule_raw="*/5 * * * *",
handler_key="agent_run",
payload={"kind": AGENT_TURN_KIND, "task": "x", "agent_id": "main"},
session_target=SessionTarget.ISOLATED,
delivery=DeliveryConfig(
mode=DeliveryMode.WEBHOOK,
webhook_url="https://hooks.example/primary",
best_effort=True,
),
)
scheduler = _UpdateScheduler(current)
await _handle_cron_update(
{
"id": "job-2",
"delivery": {
"failureDestination": {
"mode": "webhook",
"webhookUrl": "https://hooks.example/alert",
}
},
},
RpcContext(conn_id="t", cron_scheduler=scheduler),
)
new_delivery: DeliveryConfig = scheduler.last_patch["delivery"]
assert new_delivery.mode == DeliveryMode.WEBHOOK
assert new_delivery.webhook_url == "https://hooks.example/primary"
assert new_delivery.best_effort is True
fd = new_delivery.failure_destination
assert isinstance(fd, FailureDestination)
assert fd.mode == DeliveryMode.WEBHOOK
assert fd.webhook_url == "https://hooks.example/alert"