feat: 增强日历功能并集成 AgentScope 代理服务
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.agentscope.events.agui_codec import to_agui_wire_event
|
||||
|
||||
|
||||
def test_maps_internal_text_delta_to_agui_wire_event() -> None:
|
||||
internal = {
|
||||
"id": "e1",
|
||||
"type": "text.delta",
|
||||
"threadId": "t1",
|
||||
"runId": "r1",
|
||||
"data": {"delta": "hel"},
|
||||
}
|
||||
|
||||
result = to_agui_wire_event(internal)
|
||||
|
||||
assert result["type"] == "TEXT_MESSAGE_CONTENT"
|
||||
assert result["threadId"] == "t1"
|
||||
assert result["runId"] == "r1"
|
||||
assert result["delta"] == "hel"
|
||||
|
||||
|
||||
def test_reserved_keys_in_data_cannot_override_wire_fields() -> None:
|
||||
internal = {
|
||||
"id": "e2",
|
||||
"type": "run.started",
|
||||
"threadId": "thread-1",
|
||||
"runId": "run-1",
|
||||
"data": {
|
||||
"type": "RUN_ERROR",
|
||||
"threadId": "thread-override",
|
||||
"runId": "run-override",
|
||||
"message": "ok",
|
||||
},
|
||||
}
|
||||
|
||||
result = to_agui_wire_event(internal)
|
||||
|
||||
assert result["type"] == "RUN_STARTED"
|
||||
assert result["threadId"] == "thread-1"
|
||||
assert result["runId"] == "run-1"
|
||||
assert result["message"] == "ok"
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from core.agentscope.events.pipeline import AgentScopeEventPipeline
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipeline_orders_codec_persist_publish() -> None:
|
||||
calls: list[str] = []
|
||||
|
||||
class _Codec:
|
||||
def to_wire(self, event: dict[str, object]) -> dict[str, object]:
|
||||
calls.append("codec")
|
||||
return {"type": "RUN_STARTED", **event}
|
||||
|
||||
class _Store:
|
||||
async def persist(self, event: dict[str, object]) -> None:
|
||||
calls.append("persist")
|
||||
assert event["type"] == "RUN_STARTED"
|
||||
|
||||
class _Bus:
|
||||
async def publish(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
calls.append("publish")
|
||||
assert session_id == "thread-1"
|
||||
return "1-0"
|
||||
|
||||
pipeline = AgentScopeEventPipeline(codec=_Codec(), store=_Store(), bus=_Bus())
|
||||
cursor = await pipeline.emit(session_id="thread-1", event={"id": "evt-1"})
|
||||
|
||||
assert cursor == "1-0"
|
||||
assert calls == ["codec", "persist", "publish"]
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.agentscope.events.redis_bus import RedisStreamBus
|
||||
|
||||
|
||||
class _FakeRedis:
|
||||
def __init__(self) -> None:
|
||||
self._rows: list[tuple[str, str]] = []
|
||||
|
||||
def xadd(self, _stream: str, fields: dict[str, str]) -> str:
|
||||
cursor = f"{len(self._rows) + 1}-0"
|
||||
self._rows.append((cursor, fields["event"]))
|
||||
return cursor
|
||||
|
||||
def xread(
|
||||
self,
|
||||
streams: dict[str, str],
|
||||
count: int,
|
||||
block: int,
|
||||
) -> list[tuple[str, list[tuple[str, dict[str, str]]]]]:
|
||||
del count, block
|
||||
stream_name, last = next(iter(streams.items()))
|
||||
rows: list[tuple[str, dict[str, str]]] = []
|
||||
for cursor, payload in self._rows:
|
||||
if cursor > last:
|
||||
rows.append((cursor, {"event": payload}))
|
||||
return [(stream_name, rows)]
|
||||
|
||||
|
||||
class _FakeRedisBytes:
|
||||
def __init__(self) -> None:
|
||||
self._rows: list[tuple[str, str]] = []
|
||||
|
||||
def xadd(self, _stream: str, fields: dict[str, str]) -> str:
|
||||
cursor = f"{len(self._rows) + 1}-0"
|
||||
self._rows.append((cursor, fields["event"]))
|
||||
return cursor
|
||||
|
||||
def xread(
|
||||
self,
|
||||
streams: dict[str, str],
|
||||
count: int,
|
||||
block: int,
|
||||
) -> list[tuple[str, list[tuple[str, dict[str, bytes]]]]]:
|
||||
del count, block
|
||||
stream_name, last = next(iter(streams.items()))
|
||||
rows: list[tuple[str, dict[str, bytes]]] = []
|
||||
for cursor, payload in self._rows:
|
||||
if cursor > last:
|
||||
rows.append((cursor, {"event": payload.encode("utf-8")}))
|
||||
return [(stream_name, rows)]
|
||||
|
||||
|
||||
async def test_publish_then_read_after_cursor() -> None:
|
||||
bus = RedisStreamBus(client=_FakeRedis(), stream_prefix="agent.events")
|
||||
|
||||
first_cursor = await bus.publish(
|
||||
session_id="thread-1", event={"type": "RUN_STARTED"}
|
||||
)
|
||||
await bus.publish(session_id="thread-1", event={"type": "RUN_FINISHED"})
|
||||
|
||||
rows = await bus.read(session_id="thread-1", last_event_id=first_cursor)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["event"]["type"] == "RUN_FINISHED"
|
||||
|
||||
|
||||
async def test_read_supports_bytes_payload() -> None:
|
||||
bus = RedisStreamBus(client=_FakeRedisBytes(), stream_prefix="agent.events")
|
||||
await bus.publish(session_id="thread-1", event={"type": "RUN_STARTED"})
|
||||
rows = await bus.read(session_id="thread-1", last_event_id=None)
|
||||
assert rows[0]["event"]["type"] == "RUN_STARTED"
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from core.agentscope.events.sse import to_sse_event
|
||||
|
||||
|
||||
def test_sse_frame_contains_event_and_json_payload() -> None:
|
||||
payload = {"type": "RUN_STARTED", "threadId": "t1", "runId": "r1"}
|
||||
|
||||
frame = to_sse_event("1-0", payload)
|
||||
|
||||
assert frame.startswith("id: 1-0\n")
|
||||
assert "event: RUN_STARTED\n" in frame
|
||||
assert frame.endswith("\n\n")
|
||||
|
||||
data_line = [line for line in frame.splitlines() if line.startswith("data: ")][0]
|
||||
parsed = json.loads(data_line[len("data: ") :])
|
||||
assert parsed["threadId"] == "t1"
|
||||
|
||||
|
||||
def test_sse_sanitizes_stream_id_newlines() -> None:
|
||||
payload = {"type": "RUN_STARTED"}
|
||||
frame = to_sse_event("1-0\nmalicious: yes", payload)
|
||||
assert frame.startswith("id: 1-0malicious: yes\n")
|
||||
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agent.domain.user_context import UserAgentContext, parse_profile_settings
|
||||
from core.agentscope.runtime.agent_route_runtime import AgentRouteRuntime
|
||||
from core.agentscope.schemas import ReportOutput, RuntimeOutput
|
||||
from core.agentscope.schemas.agent_runtime import RunCommand
|
||||
from core.agentscope.schemas.execution import ExecutionBatchOutput
|
||||
from core.agentscope.schemas.intent import IntentOutput
|
||||
|
||||
|
||||
def _user_context() -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=uuid4(),
|
||||
username="tester",
|
||||
bio=None,
|
||||
settings=parse_profile_settings(
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"country": "CN",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_emits_started_text_and_finished_events() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FakeOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="DIRECT_RESPONSE",
|
||||
intent_summary="summary",
|
||||
direct_response="done",
|
||||
tasks=[],
|
||||
complexity="simple",
|
||||
),
|
||||
execution=ExecutionBatchOutput(
|
||||
task_results=[],
|
||||
overall_status="SUCCESS",
|
||||
aggregate_summary="ok",
|
||||
),
|
||||
report=ReportOutput(
|
||||
assistant_text="hello world",
|
||||
response_metadata={},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FakeOrchestrator(), pipeline=_FakePipeline()
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
assert [item["type"] for item in calls] == [
|
||||
"run.started",
|
||||
"step.start",
|
||||
"step.finish",
|
||||
"step.start",
|
||||
"step.finish",
|
||||
"step.start",
|
||||
"text.start",
|
||||
"text.delta",
|
||||
"text.end",
|
||||
"step.finish",
|
||||
"run.finished",
|
||||
]
|
||||
assert calls[1]["data"]["stepName"] == "intent"
|
||||
assert calls[2]["data"]["stepName"] == "intent"
|
||||
assert calls[3]["data"]["stepName"] == "execution"
|
||||
assert calls[4]["data"]["stepName"] == "execution"
|
||||
assert calls[5]["data"]["stepName"] == "report"
|
||||
assert calls[7]["data"]["delta"] == "hello world"
|
||||
assert calls[6]["data"]["messageId"] == calls[7]["data"]["messageId"]
|
||||
assert calls[7]["data"]["messageId"] == calls[8]["data"]["messageId"]
|
||||
assert calls[9]["data"]["stepName"] == "report"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_emits_run_error_when_orchestrator_fails() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FailOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FailOrchestrator(),
|
||||
pipeline=_FakePipeline(),
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
assert [item["type"] for item in calls] == [
|
||||
"run.started",
|
||||
"step.start",
|
||||
"run.error",
|
||||
]
|
||||
assert calls[1]["data"]["stepName"] == "intent"
|
||||
assert calls[2]["data"]["message"] == "runtime execution failed"
|
||||
@@ -0,0 +1,138 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
import core.agentscope.runtime.tasks as tasks_module
|
||||
|
||||
|
||||
def _run_input_payload() -> dict[str, Any]:
|
||||
return {
|
||||
"threadId": str(uuid4()),
|
||||
"runId": "run-1",
|
||||
"messages": [],
|
||||
"tools": [],
|
||||
"context": {},
|
||||
"forwardedProps": {},
|
||||
}
|
||||
|
||||
|
||||
class _FakeSessionCtx:
|
||||
async def __aenter__(self) -> object:
|
||||
return object()
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||||
del exc_type, exc, tb
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agentscope_task_calls_runtime_run(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
called: dict[str, int] = {"run": 0, "resume": 0}
|
||||
|
||||
class _FakeRuntime:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
del kwargs
|
||||
|
||||
async def run(self, **kwargs: object) -> object:
|
||||
del kwargs
|
||||
called["run"] += 1
|
||||
return object()
|
||||
|
||||
async def resume(self, **kwargs: object) -> object:
|
||||
del kwargs
|
||||
called["resume"] += 1
|
||||
return object()
|
||||
|
||||
async def _fake_get_redis_client() -> object:
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr(tasks_module, "AgentRouteRuntime", _FakeRuntime)
|
||||
monkeypatch.setattr(
|
||||
tasks_module,
|
||||
"get_or_init_redis_client",
|
||||
_fake_get_redis_client,
|
||||
)
|
||||
monkeypatch.setattr(tasks_module, "AsyncSessionLocal", lambda: _FakeSessionCtx())
|
||||
|
||||
result = await tasks_module.run_agentscope_task(
|
||||
{
|
||||
"command": "run",
|
||||
"owner_id": str(uuid4()),
|
||||
"run_input": _run_input_payload(),
|
||||
}
|
||||
)
|
||||
|
||||
assert result["status"] == "completed"
|
||||
assert called["run"] == 1
|
||||
assert called["resume"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agentscope_task_calls_runtime_resume(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
called: dict[str, int] = {"run": 0, "resume": 0}
|
||||
|
||||
class _FakeRuntime:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
del kwargs
|
||||
|
||||
async def run(self, **kwargs: object) -> object:
|
||||
del kwargs
|
||||
called["run"] += 1
|
||||
return object()
|
||||
|
||||
async def resume(self, **kwargs: object) -> object:
|
||||
del kwargs
|
||||
called["resume"] += 1
|
||||
return object()
|
||||
|
||||
async def _fake_get_redis_client() -> object:
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr(tasks_module, "AgentRouteRuntime", _FakeRuntime)
|
||||
monkeypatch.setattr(
|
||||
tasks_module,
|
||||
"get_or_init_redis_client",
|
||||
_fake_get_redis_client,
|
||||
)
|
||||
monkeypatch.setattr(tasks_module, "AsyncSessionLocal", lambda: _FakeSessionCtx())
|
||||
|
||||
result = await tasks_module.run_agentscope_task(
|
||||
{
|
||||
"command": "resume",
|
||||
"owner_id": str(uuid4()),
|
||||
"run_input": _run_input_payload(),
|
||||
}
|
||||
)
|
||||
|
||||
assert result["status"] == "completed"
|
||||
assert called["run"] == 0
|
||||
assert called["resume"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agentscope_task_requires_owner_id() -> None:
|
||||
with pytest.raises(ValueError, match="owner_id is required"):
|
||||
await tasks_module.run_agentscope_task(
|
||||
{
|
||||
"command": "run",
|
||||
"run_input": _run_input_payload(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agentscope_task_rejects_invalid_command_type() -> None:
|
||||
with pytest.raises(ValueError, match="invalid command type"):
|
||||
await tasks_module.run_agentscope_task(
|
||||
{
|
||||
"command": "unknown",
|
||||
"owner_id": str(uuid4()),
|
||||
"run_input": _run_input_payload(),
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from core.agentscope import schemas as exported_schemas
|
||||
from core.agentscope.schemas.agent_runtime import (
|
||||
AcceptedTaskResponse,
|
||||
AgUiWireEvent,
|
||||
HistorySnapshot,
|
||||
HistorySnapshotResponse,
|
||||
InternalRuntimeEvent,
|
||||
ResumeCommand,
|
||||
RunCommand,
|
||||
)
|
||||
|
||||
|
||||
def test_run_command_alias_roundtrip() -> None:
|
||||
payload = {
|
||||
"threadId": "thread-001",
|
||||
"runId": "run-001",
|
||||
"state": {"cursor": 1},
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [{"name": "calendar.lookup"}],
|
||||
"context": {"locale": "zh-CN"},
|
||||
"forwardedProps": {"traceId": "trace-1"},
|
||||
}
|
||||
|
||||
command = RunCommand.model_validate(payload)
|
||||
|
||||
assert command.thread_id == "thread-001"
|
||||
assert command.run_id == "run-001"
|
||||
assert command.forwarded_props == {"traceId": "trace-1"}
|
||||
|
||||
dumped = command.model_dump(mode="json", by_alias=True)
|
||||
assert dumped["threadId"] == "thread-001"
|
||||
assert dumped["runId"] == "run-001"
|
||||
assert dumped["forwardedProps"] == {"traceId": "trace-1"}
|
||||
|
||||
|
||||
def test_history_snapshot_response_shape() -> None:
|
||||
response = HistorySnapshotResponse(
|
||||
threadId="thread-123",
|
||||
snapshot=HistorySnapshot(
|
||||
threadId="thread-123",
|
||||
day="2026-03-11",
|
||||
hasMore=False,
|
||||
messages=[{"id": "msg-1"}],
|
||||
),
|
||||
)
|
||||
|
||||
dumped = response.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
assert dumped["type"] == "STATE_SNAPSHOT"
|
||||
assert dumped["threadId"] == "thread-123"
|
||||
assert dumped["snapshot"]["scope"] == "history_day"
|
||||
assert dumped["snapshot"]["hasMore"] is False
|
||||
assert dumped["snapshot"]["messages"] == [{"id": "msg-1"}]
|
||||
|
||||
|
||||
def test_runtime_event_validation_basics() -> None:
|
||||
internal = InternalRuntimeEvent(type="RUN_STARTED", data={"step": 1})
|
||||
assert internal.type == "RUN_STARTED"
|
||||
assert internal.model_dump(mode="json", by_alias=True)["data"] == {"step": 1}
|
||||
|
||||
wire = AgUiWireEvent(type="TEXT_MESSAGE_CONTENT", payload={"delta": "hello"})
|
||||
dumped = wire.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
assert dumped["type"] == "TEXT_MESSAGE_CONTENT"
|
||||
assert dumped["payload"] == {"delta": "hello"}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
InternalRuntimeEvent.model_validate({"threadId": "t-1", "data": {}})
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
AgUiWireEvent.model_validate({"payload": {"delta": "hello"}})
|
||||
|
||||
|
||||
def test_task_response_and_resume_aliases() -> None:
|
||||
accepted = AcceptedTaskResponse(
|
||||
taskId="task-1",
|
||||
threadId="thread-1",
|
||||
runId="run-1",
|
||||
created=False,
|
||||
)
|
||||
dumped = accepted.model_dump(mode="json", by_alias=True)
|
||||
assert dumped["taskId"] == "task-1"
|
||||
assert dumped["threadId"] == "thread-1"
|
||||
assert dumped["runId"] == "run-1"
|
||||
|
||||
resumed = ResumeCommand.model_validate(
|
||||
{
|
||||
"threadId": "thread-1",
|
||||
"runId": "run-2",
|
||||
"messages": [],
|
||||
"tools": [],
|
||||
"context": {},
|
||||
}
|
||||
)
|
||||
assert resumed.thread_id == "thread-1"
|
||||
assert resumed.run_id == "run-2"
|
||||
|
||||
|
||||
def test_schemas_exports_include_task_and_history_models() -> None:
|
||||
assert exported_schemas.AcceptedTaskResponse is AcceptedTaskResponse
|
||||
assert exported_schemas.TaskAccepted is AcceptedTaskResponse
|
||||
assert exported_schemas.TaskAcceptedResponse is AcceptedTaskResponse
|
||||
assert exported_schemas.HistorySnapshotResponse is HistorySnapshotResponse
|
||||
@@ -131,3 +131,50 @@ async def test_calendar_write_rejects_event_id_for_create(
|
||||
|
||||
assert result["data"]["ok"] is False
|
||||
assert result["data"]["code"] == "INVALID_ARGUMENT"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_maps_reminder_minutes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def _fake_execute(**kwargs: Any) -> dict[str, object]:
|
||||
captured.update(cast(dict[str, object], kwargs["tool_args"]))
|
||||
return {"type": "calendar_card.v1", "version": "v1", "data": {"ok": True}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
calendar_module,
|
||||
"_execute_mutate_calendar_event",
|
||||
_fake_execute,
|
||||
)
|
||||
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
|
||||
await calendar_module.calendar_write(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token-abc",
|
||||
operation="create",
|
||||
reminder_minutes=15,
|
||||
)
|
||||
|
||||
assert captured["reminderMinutes"] == 15
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_rejects_invalid_reminder_minutes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token-abc",
|
||||
operation="create",
|
||||
reminder_minutes=10081,
|
||||
)
|
||||
|
||||
assert result["data"]["ok"] is False
|
||||
assert result["data"]["code"] == "INVALID_ARGUMENT"
|
||||
|
||||
Reference in New Issue
Block a user