mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(webui): show actual local trigger messages (#5228)
This commit is contained in:
parent
eeecfac538
commit
f11710a578
@ -428,6 +428,7 @@ async def test_session_automations_route_lists_local_triggers(
|
|||||||
chat_id="abc",
|
chat_id="abc",
|
||||||
session_key="websocket:abc",
|
session_key="websocket:abc",
|
||||||
)
|
)
|
||||||
|
trigger_store.enqueue(trigger.id, "Review PR #4591")
|
||||||
channel = _ch(
|
channel = _ch(
|
||||||
bus,
|
bus,
|
||||||
session_manager=_seed_session(tmp_path, key="websocket:abc"),
|
session_manager=_seed_session(tmp_path, key="websocket:abc"),
|
||||||
@ -454,6 +455,7 @@ async def test_session_automations_route_lists_local_triggers(
|
|||||||
assert job["kind"] == "local_trigger"
|
assert job["kind"] == "local_trigger"
|
||||||
assert job["schedule"]["kind"] == "local"
|
assert job["schedule"]["kind"] == "local"
|
||||||
assert job["payload"]["kind"] == "local_trigger"
|
assert job["payload"]["kind"] == "local_trigger"
|
||||||
|
assert job["payload"]["message"] == "Review PR #4591"
|
||||||
assert job["payload"]["command"] == f'nanobot trigger {trigger.id} "message"'
|
assert job["payload"]["command"] == f'nanobot trigger {trigger.id} "message"'
|
||||||
assert job["state"]["pending"] is True
|
assert job["state"]["pending"] is True
|
||||||
finally:
|
finally:
|
||||||
@ -2615,6 +2617,7 @@ async def test_webui_automations_route_manages_local_triggers(
|
|||||||
by_id = {job["id"]: job for job in listed.json()["jobs"]}
|
by_id = {job["id"]: job for job in listed.json()["jobs"]}
|
||||||
assert by_id[trigger.id]["kind"] == "local_trigger"
|
assert by_id[trigger.id]["kind"] == "local_trigger"
|
||||||
assert by_id[trigger.id]["state"]["pending"] is True
|
assert by_id[trigger.id]["state"]["pending"] is True
|
||||||
|
assert by_id[trigger.id]["payload"]["message"] == "Review queued PR"
|
||||||
assert by_id[trigger.id]["trigger"]["command"] == f'nanobot trigger {trigger.id} "message"'
|
assert by_id[trigger.id]["trigger"]["command"] == f'nanobot trigger {trigger.id} "message"'
|
||||||
|
|
||||||
disabled = await _http_get(
|
disabled = await _http_get(
|
||||||
|
|||||||
@ -166,7 +166,8 @@ class LocalTriggerStore:
|
|||||||
raise ValueError("trigger message is required")
|
raise ValueError("trigger message is required")
|
||||||
self._ensure_dirs()
|
self._ensure_dirs()
|
||||||
with self._lock:
|
with self._lock:
|
||||||
trigger = self._find_unlocked(self._load_triggers_unlocked(), trigger_id)
|
triggers = self._load_triggers_unlocked()
|
||||||
|
trigger = self._find_unlocked(triggers, trigger_id)
|
||||||
if trigger is None:
|
if trigger is None:
|
||||||
raise TriggerNotFoundError(f"trigger not found: {trigger_id}")
|
raise TriggerNotFoundError(f"trigger not found: {trigger_id}")
|
||||||
if not trigger.enabled:
|
if not trigger.enabled:
|
||||||
@ -180,10 +181,20 @@ class LocalTriggerStore:
|
|||||||
path = self.inbox_dir / f"{delivery.created_at_ms}-{delivery.id}.json"
|
path = self.inbox_dir / f"{delivery.created_at_ms}-{delivery.id}.json"
|
||||||
self._atomic_write(path, json.dumps(_delivery_payload(delivery), ensure_ascii=False))
|
self._atomic_write(path, json.dumps(_delivery_payload(delivery), ensure_ascii=False))
|
||||||
delivery.path = path
|
delivery.path = path
|
||||||
|
run_record_path: Path | None = None
|
||||||
try:
|
try:
|
||||||
self.write_delivery_run_record(delivery, trigger=trigger, status="queued")
|
run_record_path = self.write_delivery_run_record(
|
||||||
|
delivery,
|
||||||
|
trigger=trigger,
|
||||||
|
status="queued",
|
||||||
|
)
|
||||||
|
trigger.last_message = _run_record_text(content)
|
||||||
|
trigger.updated_at_ms = delivery.created_at_ms
|
||||||
|
self._save_triggers_unlocked(triggers)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
path.unlink(missing_ok=True)
|
path.unlink(missing_ok=True)
|
||||||
|
if run_record_path is not None:
|
||||||
|
run_record_path.unlink(missing_ok=True)
|
||||||
delivery.path = None
|
delivery.path = None
|
||||||
raise
|
raise
|
||||||
return delivery
|
return delivery
|
||||||
|
|||||||
@ -61,6 +61,7 @@ class LocalTrigger:
|
|||||||
origin_metadata: dict[str, Any] = field(default_factory=dict)
|
origin_metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
created_at_ms: int = 0
|
created_at_ms: int = 0
|
||||||
updated_at_ms: int = 0
|
updated_at_ms: int = 0
|
||||||
|
last_message: str = ""
|
||||||
last_run_at_ms: int | None = None
|
last_run_at_ms: int | None = None
|
||||||
last_status: TriggerStatus | None = None
|
last_status: TriggerStatus | None = None
|
||||||
last_error: str | None = None
|
last_error: str | None = None
|
||||||
@ -90,6 +91,7 @@ class LocalTrigger:
|
|||||||
origin_metadata=dict(_get(data, "originMetadata", "origin_metadata", {}) or {}),
|
origin_metadata=dict(_get(data, "originMetadata", "origin_metadata", {}) or {}),
|
||||||
created_at_ms=_int_or_zero(_get(data, "createdAtMs", "created_at_ms", 0)),
|
created_at_ms=_int_or_zero(_get(data, "createdAtMs", "created_at_ms", 0)),
|
||||||
updated_at_ms=_int_or_zero(_get(data, "updatedAtMs", "updated_at_ms", 0)),
|
updated_at_ms=_int_or_zero(_get(data, "updatedAtMs", "updated_at_ms", 0)),
|
||||||
|
last_message=str(_get(data, "lastMessage", "last_message", "") or ""),
|
||||||
last_run_at_ms=_optional_int(_get(data, "lastRunAtMs", "last_run_at_ms")),
|
last_run_at_ms=_optional_int(_get(data, "lastRunAtMs", "last_run_at_ms")),
|
||||||
last_status=_get(data, "lastStatus", "last_status"), # type: ignore[arg-type]
|
last_status=_get(data, "lastStatus", "last_status"), # type: ignore[arg-type]
|
||||||
last_error=_get(data, "lastError", "last_error"),
|
last_error=_get(data, "lastError", "last_error"),
|
||||||
@ -108,6 +110,7 @@ class LocalTrigger:
|
|||||||
"originMetadata": self.origin_metadata,
|
"originMetadata": self.origin_metadata,
|
||||||
"createdAtMs": self.created_at_ms,
|
"createdAtMs": self.created_at_ms,
|
||||||
"updatedAtMs": self.updated_at_ms,
|
"updatedAtMs": self.updated_at_ms,
|
||||||
|
"lastMessage": self.last_message,
|
||||||
"lastRunAtMs": self.last_run_at_ms,
|
"lastRunAtMs": self.last_run_at_ms,
|
||||||
"lastStatus": self.last_status,
|
"lastStatus": self.last_status,
|
||||||
"lastError": self.last_error,
|
"lastError": self.last_error,
|
||||||
|
|||||||
@ -209,7 +209,7 @@ def _serialize_trigger(
|
|||||||
},
|
},
|
||||||
"payload": {
|
"payload": {
|
||||||
"kind": "local_trigger",
|
"kind": "local_trigger",
|
||||||
"message": command,
|
"message": trigger.last_message or command,
|
||||||
"command": command,
|
"command": command,
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
|
|||||||
@ -150,6 +150,36 @@ def test_enqueue_writes_trigger_run_record(tmp_path: Path) -> None:
|
|||||||
assert record["content"] == "Review PR #4591"
|
assert record["content"] == "Review PR #4591"
|
||||||
assert record["origin_metadata"] == {"webui": True}
|
assert record["origin_metadata"] == {"webui": True}
|
||||||
assert record["updated_at_ms"] > 0
|
assert record["updated_at_ms"] > 0
|
||||||
|
stored = store.get(trigger.id)
|
||||||
|
assert stored is not None
|
||||||
|
assert stored.last_message == "Review PR #4591"
|
||||||
|
|
||||||
|
|
||||||
|
def test_enqueue_rolls_back_delivery_and_audit_when_trigger_save_fails(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
store = LocalTriggerStore(tmp_path)
|
||||||
|
trigger = store.create(
|
||||||
|
name="PR review",
|
||||||
|
channel="websocket",
|
||||||
|
chat_id="chat-1",
|
||||||
|
session_key="websocket:chat-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
def fail_save(_triggers: list[LocalTrigger]) -> None:
|
||||||
|
raise OSError("store write failed")
|
||||||
|
|
||||||
|
monkeypatch.setattr(store, "_save_triggers_unlocked", fail_save)
|
||||||
|
|
||||||
|
with pytest.raises(OSError, match="store write failed"):
|
||||||
|
store.enqueue(trigger.id, "Review PR #4591")
|
||||||
|
|
||||||
|
assert list(store.inbox_dir.glob("*.json")) == []
|
||||||
|
assert list(store.runs_dir.glob("*.json")) == []
|
||||||
|
stored = LocalTriggerStore(tmp_path).get(trigger.id)
|
||||||
|
assert stored is not None
|
||||||
|
assert stored.last_message == ""
|
||||||
|
|
||||||
|
|
||||||
def test_delivery_run_record_truncates_large_content_and_response(tmp_path: Path) -> None:
|
def test_delivery_run_record_truncates_large_content_and_response(tmp_path: Path) -> None:
|
||||||
@ -168,6 +198,9 @@ def test_delivery_run_record_truncates_large_content_and_response(tmp_path: Path
|
|||||||
assert queued_record["content"].startswith("content-")
|
assert queued_record["content"].startswith("content-")
|
||||||
assert queued_record["content"].endswith("\n... (truncated)")
|
assert queued_record["content"].endswith("\n... (truncated)")
|
||||||
assert len(queued_record["content"]) < len(large_content)
|
assert len(queued_record["content"]) < len(large_content)
|
||||||
|
stored = store.get(trigger.id)
|
||||||
|
assert stored is not None
|
||||||
|
assert stored.last_message == queued_record["content"]
|
||||||
|
|
||||||
store.write_delivery_run_record(
|
store.write_delivery_run_record(
|
||||||
delivery,
|
delivery,
|
||||||
|
|||||||
@ -113,6 +113,43 @@ describe("SessionInfoPopover", () => {
|
|||||||
expect(screen.queryByText(/ago/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/ago/i)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the actual message received by a local trigger", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn().mockResolvedValue(
|
||||||
|
automationsResponse([
|
||||||
|
{
|
||||||
|
id: "trg_123",
|
||||||
|
name: "PR monitor",
|
||||||
|
enabled: true,
|
||||||
|
kind: "local_trigger",
|
||||||
|
schedule: { kind: "local" },
|
||||||
|
payload: {
|
||||||
|
kind: "local_trigger",
|
||||||
|
message: "Review PR #4591",
|
||||||
|
command: 'nanobot trigger trg_123 "message"',
|
||||||
|
},
|
||||||
|
state: { pending: false },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<SessionInfoPopover
|
||||||
|
sessionKey="websocket:chat-1"
|
||||||
|
token="tok"
|
||||||
|
title="Release work"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Session details" }));
|
||||||
|
|
||||||
|
expect(await screen.findByText("Review PR #4591")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('nanobot trigger trg_123 "message"')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("refreshes while open so completed one-shot automations disappear", async () => {
|
it("refreshes while open so completed one-shot automations disappear", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user