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",
|
||||
session_key="websocket:abc",
|
||||
)
|
||||
trigger_store.enqueue(trigger.id, "Review PR #4591")
|
||||
channel = _ch(
|
||||
bus,
|
||||
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["schedule"]["kind"] == "local"
|
||||
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["state"]["pending"] is True
|
||||
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"]}
|
||||
assert by_id[trigger.id]["kind"] == "local_trigger"
|
||||
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"'
|
||||
|
||||
disabled = await _http_get(
|
||||
|
||||
@ -166,7 +166,8 @@ class LocalTriggerStore:
|
||||
raise ValueError("trigger message is required")
|
||||
self._ensure_dirs()
|
||||
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:
|
||||
raise TriggerNotFoundError(f"trigger not found: {trigger_id}")
|
||||
if not trigger.enabled:
|
||||
@ -180,10 +181,20 @@ class LocalTriggerStore:
|
||||
path = self.inbox_dir / f"{delivery.created_at_ms}-{delivery.id}.json"
|
||||
self._atomic_write(path, json.dumps(_delivery_payload(delivery), ensure_ascii=False))
|
||||
delivery.path = path
|
||||
run_record_path: Path | None = None
|
||||
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:
|
||||
path.unlink(missing_ok=True)
|
||||
if run_record_path is not None:
|
||||
run_record_path.unlink(missing_ok=True)
|
||||
delivery.path = None
|
||||
raise
|
||||
return delivery
|
||||
|
||||
@ -61,6 +61,7 @@ class LocalTrigger:
|
||||
origin_metadata: dict[str, Any] = field(default_factory=dict)
|
||||
created_at_ms: int = 0
|
||||
updated_at_ms: int = 0
|
||||
last_message: str = ""
|
||||
last_run_at_ms: int | None = None
|
||||
last_status: TriggerStatus | None = None
|
||||
last_error: str | None = None
|
||||
@ -90,6 +91,7 @@ class LocalTrigger:
|
||||
origin_metadata=dict(_get(data, "originMetadata", "origin_metadata", {}) or {}),
|
||||
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)),
|
||||
last_message=str(_get(data, "lastMessage", "last_message", "") or ""),
|
||||
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_error=_get(data, "lastError", "last_error"),
|
||||
@ -108,6 +110,7 @@ class LocalTrigger:
|
||||
"originMetadata": self.origin_metadata,
|
||||
"createdAtMs": self.created_at_ms,
|
||||
"updatedAtMs": self.updated_at_ms,
|
||||
"lastMessage": self.last_message,
|
||||
"lastRunAtMs": self.last_run_at_ms,
|
||||
"lastStatus": self.last_status,
|
||||
"lastError": self.last_error,
|
||||
|
||||
@ -209,7 +209,7 @@ def _serialize_trigger(
|
||||
},
|
||||
"payload": {
|
||||
"kind": "local_trigger",
|
||||
"message": command,
|
||||
"message": trigger.last_message or command,
|
||||
"command": command,
|
||||
},
|
||||
"state": {
|
||||
|
||||
@ -150,6 +150,36 @@ def test_enqueue_writes_trigger_run_record(tmp_path: Path) -> None:
|
||||
assert record["content"] == "Review PR #4591"
|
||||
assert record["origin_metadata"] == {"webui": True}
|
||||
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:
|
||||
@ -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"].endswith("\n... (truncated)")
|
||||
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(
|
||||
delivery,
|
||||
|
||||
@ -113,6 +113,43 @@ describe("SessionInfoPopover", () => {
|
||||
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 () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user