@nookplot/runtime 0.5.70 → 0.5.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,563 +1,287 @@
1
1
  /**
2
- * Action dispatch tests for AutonomousAgent.handleActionRequest.
2
+ * Action dispatch tests for AutonomousAgent — unified dispatch via gateway.
3
3
  *
4
- * Tests that each action type calls the correct runtime manager method
5
- * with the right arguments. Uses mock runtime + mock prepareSignRelay.
4
+ * After the thin dispatch refactor, all actions go through
5
+ * `POST /v1/actions/execute { toolName, payload }`. These tests verify:
6
+ * - Correct toolName + payload are sent to the gateway
7
+ * - `completed` results are handled
8
+ * - `sign_required` results trigger local signing + relay
9
+ * - `client_side_required` results are handled locally
10
+ * - `error` results reject delegated actions
11
+ * - Approval gate still works for on-chain actions
12
+ * - actionId lifecycle (completeAction / rejectDelegatedAction)
6
13
  */
7
14
  import { describe, it, expect, vi, beforeEach } from "vitest";
8
15
  import { createMockRuntime } from "./helpers/mockRuntime.js";
9
16
  import { AutonomousAgent } from "../autonomous.js";
10
- // Mock prepareSignRelay at module level
17
+ // Mock the signing module
11
18
  vi.mock("../signing.js", () => ({
12
19
  prepareSignRelay: vi.fn().mockResolvedValue({ txHash: "0xRELAY_TX" }),
13
- signForwardRequest: vi.fn(),
20
+ signForwardRequest: vi.fn().mockResolvedValue("0xSIGNATURE"),
14
21
  }));
15
- import { prepareSignRelay } from "../signing.js";
16
- const mockPrepareSignRelay = vi.mocked(prepareSignRelay);
17
22
  let runtime;
18
23
  let callbacks;
19
24
  let agent;
25
+ let requestMock;
20
26
  beforeEach(() => {
21
27
  vi.clearAllMocks();
22
28
  const mock = createMockRuntime();
23
29
  runtime = mock.runtime;
24
30
  callbacks = mock.callbacks;
31
+ // Default mock: return `completed` for POST /v1/actions/execute
32
+ requestMock = runtime.connection.request;
33
+ requestMock.mockImplementation(async (method, path) => {
34
+ if (method === "POST" && path === "/v1/actions/execute") {
35
+ return { status: "completed", result: { success: true } };
36
+ }
37
+ return { success: true };
38
+ });
25
39
  agent = new AutonomousAgent(runtime, { verbose: false });
26
40
  agent.start();
27
41
  });
28
42
  /** Helper: dispatch an action request and wait for processing */
29
43
  async function dispatchAction(actionType, payload, suggestedContent, actionId) {
30
- const event = { agentId: "agent_1", actionType, payload, suggestedContent, actionId };
31
- callbacks.actionCb(event);
32
- // Allow async handler to settle
33
- await new Promise((r) => setTimeout(r, 10));
44
+ callbacks.actionCb({ agentId: "agent_1", actionType, payload, suggestedContent, actionId });
45
+ await new Promise((r) => setTimeout(r, 20));
34
46
  }
35
- // ── On-chain via runtime managers ──────────────────────────────
36
- describe("on-chain actions via runtime managers", () => {
37
- it("create_post / publish calls memory.publishKnowledge", async () => {
38
- await dispatchAction("create_post", { community: "general", title: "Test" }, "Post body");
39
- expect(runtime.memory.publishKnowledge).toHaveBeenCalledWith(expect.objectContaining({ title: "Test", body: "Post body", community: "general" }));
40
- });
41
- it("publish is alias for create_post", async () => {
42
- await dispatchAction("publish", { community: "sci" }, "My insight");
43
- expect(runtime.memory.publishKnowledge).toHaveBeenCalled();
44
- });
45
- it("post_reply calls memory.publishComment", async () => {
46
- await dispatchAction("post_reply", { parentCid: "Qm_parent", community: "general" }, "Nice post!");
47
- expect(runtime.memory.publishComment).toHaveBeenCalledWith(expect.objectContaining({ parentCid: "Qm_parent", body: "Nice post!", community: "general" }));
48
- });
49
- it("vote calls memory.vote", async () => {
50
- await dispatchAction("vote", { cid: "Qm_cid", voteType: "up" });
51
- expect(runtime.memory.vote).toHaveBeenCalledWith(expect.objectContaining({ cid: "Qm_cid", type: "up" }));
52
- });
53
- it("follow_agent calls social.follow", async () => {
54
- await dispatchAction("follow_agent", { targetAddress: "0xTARGET" });
55
- expect(runtime.social.follow).toHaveBeenCalledWith("0xTARGET");
56
- });
57
- it("attest_agent calls social.attest", async () => {
58
- await dispatchAction("attest_agent", { targetAddress: "0xTARGET" }, "Great work!");
59
- expect(runtime.social.attest).toHaveBeenCalledWith("0xTARGET", "Great work!");
60
- });
61
- it("list_service calls marketplace.createListing", async () => {
62
- await dispatchAction("list_service", { category: "dev" }, "Build APIs");
63
- expect(runtime.marketplace.createListing).toHaveBeenCalledWith(expect.objectContaining({ title: "Build APIs", category: "dev" }));
64
- });
65
- it("create_agreement calls marketplace.createAgreement", async () => {
66
- await dispatchAction("create_agreement", { listingId: 42 }, "Build me an API");
67
- expect(runtime.marketplace.createAgreement).toHaveBeenCalledWith(expect.objectContaining({ listingId: 42, terms: "Build me an API" }));
68
- });
69
- it("deliver_work calls marketplace.deliver", async () => {
70
- await dispatchAction("deliver_work", { agreementId: 5 }, "Qm_delivery_cid");
71
- expect(runtime.marketplace.deliver).toHaveBeenCalledWith(5, "Qm_delivery_cid");
72
- });
73
- it("settle_agreement calls marketplace.settle", async () => {
74
- await dispatchAction("settle_agreement", { agreementId: 5 });
75
- expect(runtime.marketplace.settle).toHaveBeenCalledWith(5);
76
- });
77
- it("dispute_agreement calls marketplace.dispute", async () => {
78
- await dispatchAction("dispute_agreement", { agreementId: 5 });
79
- expect(runtime.marketplace.dispute).toHaveBeenCalledWith(5);
80
- });
81
- it("cancel_agreement calls marketplace.cancel", async () => {
82
- await dispatchAction("cancel_agreement", { agreementId: 5 });
83
- expect(runtime.marketplace.cancel).toHaveBeenCalledWith(5);
84
- });
85
- it("expire_dispute calls marketplace.expireDispute", async () => {
86
- await dispatchAction("expire_dispute", { agreementId: 5 });
87
- expect(runtime.marketplace.expireDispute).toHaveBeenCalledWith(5);
88
- });
89
- it("expire_delivered calls marketplace.expireDelivered", async () => {
90
- await dispatchAction("expire_delivered", { agreementId: 5 });
91
- expect(runtime.marketplace.expireDelivered).toHaveBeenCalledWith(5);
92
- });
93
- });
94
- // ── On-chain via prepareSignRelay ────────────────────────────
95
- describe("on-chain actions via prepareSignRelay", () => {
96
- it("create_community calls prepareSignRelay with /v1/prepare/community", async () => {
97
- await dispatchAction("create_community", { slug: "ai", name: "AI Agents" }, "An AI community");
98
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/community", expect.objectContaining({ slug: "ai", name: "AI Agents" }));
99
- });
100
- it("propose_guild calls prepareSignRelay with /v1/prepare/guild", async () => {
101
- await dispatchAction("propose_guild", { name: "BuilderGuild", members: ["0xA", "0xB", "0xC"] }, "A builder guild");
102
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/guild", expect.objectContaining({ name: "BuilderGuild", members: ["0xA", "0xB", "0xC"] }));
103
- });
104
- it("propose_clique is alias for propose_guild", async () => {
105
- await dispatchAction("propose_clique", { name: "Clique", members: ["0xA", "0xB"] }, "desc");
106
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/guild", expect.anything());
107
- });
108
- it("create_project does 2-step discover + prepare", async () => {
109
- vi.mocked(runtime.connection.request).mockResolvedValueOnce({ discoveryId: "disc_1" });
110
- await dispatchAction("create_project", { projectId: "proj_1", name: "My Project" }, "Project desc");
111
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/projects/discover", expect.anything());
112
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/project", expect.objectContaining({ discoveryId: "disc_1", projectId: "proj_1" }));
113
- });
114
- it("claim_bounty calls prepareSignRelay with /v1/prepare/bounty/:id/claim", async () => {
115
- await dispatchAction("claim_bounty", { bountyId: "42" });
116
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty/42/claim", {});
117
- });
118
- it("claim is alias for claim_bounty", async () => {
119
- await dispatchAction("claim", { bountyId: "99" });
120
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty/99/claim", {});
121
- });
122
- it("create_bounty calls prepareSignRelay with /v1/prepare/bounty", async () => {
123
- await dispatchAction("create_bounty", { tokenRewardAmount: "100" }, "Fix the bug");
124
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty", expect.objectContaining({ title: "Fix the bug", tokenRewardAmount: "100" }));
125
- });
126
- it("create_bundle calls prepareSignRelay with /v1/prepare/bundle", async () => {
127
- await dispatchAction("create_bundle", { name: "Research Bundle" });
128
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bundle", expect.objectContaining({ name: "Research Bundle" }));
129
- });
130
- it("approve_bounty_claimer calls prepareSignRelay with /v1/prepare/bounty/:id/approve-claimer", async () => {
131
- await dispatchAction("approve_bounty_claimer", { bountyId: "10", claimer: "0xCLAIMER" });
132
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty/10/approve-claimer", { claimer: "0xCLAIMER" });
133
- });
134
- it("approve_bounty_work calls prepareSignRelay with /v1/prepare/bounty/:id/approve", async () => {
135
- await dispatchAction("approve_bounty_work", { bountyId: "10" });
136
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty/10/approve", {});
137
- });
138
- it("dispute_bounty_work calls prepareSignRelay with /v1/prepare/bounty/:id/dispute", async () => {
139
- await dispatchAction("dispute_bounty_work", { bountyId: "10" });
140
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty/10/dispute", {});
141
- });
142
- it("cancel_bounty calls prepareSignRelay with /v1/prepare/bounty/:id/cancel", async () => {
143
- await dispatchAction("cancel_bounty", { bountyId: "10" });
144
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty/10/cancel", {});
145
- });
146
- it("unclaim_bounty calls prepareSignRelay with /v1/prepare/bounty/:id/unclaim", async () => {
147
- await dispatchAction("unclaim_bounty", { bountyId: "10" });
148
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/bounty/10/unclaim", {});
149
- });
150
- it("deploy_preview calls prepareSignRelay with /v1/prepare/project/:id/deployment", async () => {
151
- await dispatchAction("deploy_preview", { projectId: "proj_1" });
152
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/project/proj_1/deployment", expect.objectContaining({ prepaidHours: 2 }));
153
- });
154
- it("join_guild / approve_guild calls prepareSignRelay with /v1/prepare/guild/:id/approve", async () => {
155
- await dispatchAction("join_guild", { guildId: 5 });
156
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/guild/5/approve", {});
157
- });
158
- it("reject_guild calls prepareSignRelay with /v1/prepare/guild/:id/reject", async () => {
159
- await dispatchAction("reject_guild", { guildId: 5 });
160
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/guild/5/reject", {});
161
- });
162
- it("leave_guild calls prepareSignRelay with /v1/prepare/guild/:id/leave", async () => {
163
- await dispatchAction("leave_guild", { guildId: 5 });
164
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/guild/5/leave", {});
165
- });
166
- it("update_service calls prepareSignRelay with /v1/prepare/service/update", async () => {
167
- await dispatchAction("update_service", { listingId: "list_1", title: "Updated" });
168
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/service/update", expect.objectContaining({ listingId: "list_1" }));
169
- });
170
- it("create_listing calls prepareSignRelay with /v1/prepare/service/list", async () => {
171
- await dispatchAction("create_listing", { title: "Service", category: "dev" });
172
- expect(mockPrepareSignRelay).toHaveBeenCalledWith(runtime.connection, "/v1/prepare/service/list", expect.anything());
173
- });
174
- });
175
- // ── Off-chain messaging ──────────────────────────────────────
176
- describe("off-chain messaging actions", () => {
177
- it("reply with channelId sends to channel", async () => {
178
- await dispatchAction("reply", { channelId: "ch_1" }, "Hello channel!");
179
- expect(runtime.channels.send).toHaveBeenCalledWith("ch_1", "Hello channel!");
180
- });
181
- it("reply with to sends DM", async () => {
182
- await dispatchAction("reply", { to: "0xTARGET" }, "Hello DM!");
183
- expect(runtime.inbox.send).toHaveBeenCalledWith(expect.objectContaining({ to: "0xTARGET", content: "Hello DM!" }));
184
- });
185
- it("send_dm calls inbox.send", async () => {
186
- await dispatchAction("send_dm", { recipientAddress: "0xTARGET" }, "Hey!");
187
- expect(runtime.inbox.send).toHaveBeenCalledWith(expect.objectContaining({ to: "0xTARGET", content: "Hey!" }));
188
- });
189
- it("send_message with to sends DM", async () => {
190
- await dispatchAction("send_message", { to: "0xTARGET" }, "Collaboration time!");
191
- expect(runtime.inbox.send).toHaveBeenCalled();
192
- });
193
- it("send_message with channelId sends to channel", async () => {
194
- await dispatchAction("send_message", { channelId: "ch_2" }, "Hi team!");
195
- expect(runtime.channels.send).toHaveBeenCalledWith("ch_2", "Hi team!");
196
- });
197
- it("acknowledge sends to project channel", async () => {
198
- await dispatchAction("acknowledge", { projectId: "proj_1" });
199
- expect(runtime.channels.sendToProject).toHaveBeenCalledWith("proj_1", expect.any(String));
200
- });
201
- it("accept sends to project channel", async () => {
202
- await dispatchAction("accept", { projectId: "proj_1" });
203
- expect(runtime.channels.sendToProject).toHaveBeenCalledWith("proj_1", expect.any(String));
204
- });
205
- it("execute with channelId sends to channel", async () => {
206
- await dispatchAction("execute", { channelId: "ch_1" }, "Executing task...");
207
- expect(runtime.channels.send).toHaveBeenCalledWith("ch_1", "Executing task...");
208
- });
209
- it("execute with to sends DM", async () => {
210
- await dispatchAction("execute", { to: "0xTARGET" }, "Done!");
211
- expect(runtime.inbox.send).toHaveBeenCalled();
212
- });
213
- it("propose_collab sends DM", async () => {
214
- await dispatchAction("propose_collab", { targetAddress: "0xTARGET" }, "Let's work together!");
215
- expect(runtime.inbox.send).toHaveBeenCalledWith(expect.objectContaining({ to: "0xTARGET", content: "Let's work together!" }));
216
- });
217
- });
218
- // ── Off-chain social ─────────────────────────────────────────
219
- describe("off-chain social actions", () => {
220
- it("follow calls social.follow", async () => {
47
+ // ── Unified dispatch to gateway ──────────────────────────────
48
+ describe("unified dispatch via POST /v1/actions/execute", () => {
49
+ it("dispatches action with correct toolName and payload", async () => {
50
+ await dispatchAction("send_message", { to: "0xTARGET", content: "Hello!" });
51
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({
52
+ toolName: "nookplot_send_message",
53
+ payload: expect.objectContaining({ to: "0xTARGET", content: "Hello!" }),
54
+ }));
55
+ });
56
+ it("includes suggestedContent in payload when present", async () => {
57
+ await dispatchAction("create_post", { community: "general", title: "Test" }, "Post body here");
58
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({
59
+ toolName: "nookplot_create_post",
60
+ payload: expect.objectContaining({
61
+ community: "general",
62
+ title: "Test",
63
+ suggestedContent: "Post body here",
64
+ }),
65
+ }));
66
+ });
67
+ it("follows dispatches as nookplot_follow", async () => {
221
68
  await dispatchAction("follow", { targetAddress: "0xTARGET" });
222
- expect(runtime.social.follow).toHaveBeenCalledWith("0xTARGET");
223
- });
224
- it("follow_back calls social.follow", async () => {
225
- await dispatchAction("follow_back", { senderAddress: "0xFOLLOWER" });
226
- expect(runtime.social.follow).toHaveBeenCalledWith("0xFOLLOWER");
227
- });
228
- it("attest calls social.attest", async () => {
229
- await dispatchAction("attest", { targetAddress: "0xTARGET" }, "Excellent contributor");
230
- expect(runtime.social.attest).toHaveBeenCalledWith("0xTARGET", "Excellent contributor");
231
- });
232
- it("attest_back calls social.attest", async () => {
233
- await dispatchAction("attest_back", { senderAddress: "0xATTESTER" });
234
- expect(runtime.social.attest).toHaveBeenCalledWith("0xATTESTER", "Valued collaborator");
69
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({ toolName: "nookplot_follow" }));
70
+ });
71
+ it("vote dispatches as nookplot_vote", async () => {
72
+ await dispatchAction("vote", { contentCid: "Qm_cid", isUpvote: true });
73
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({
74
+ toolName: "nookplot_vote",
75
+ payload: expect.objectContaining({ contentCid: "Qm_cid", isUpvote: true }),
76
+ }));
77
+ });
78
+ it("reply action dispatches correctly", async () => {
79
+ await dispatchAction("reply", { channelId: "ch_1" }, "My reply");
80
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({
81
+ toolName: "nookplot_reply",
82
+ payload: expect.objectContaining({ channelId: "ch_1", suggestedContent: "My reply" }),
83
+ }));
235
84
  });
236
85
  });
237
- // ── Off-chain project actions ────────────────────────────────
238
- describe("off-chain project actions", () => {
239
- it("review calls projects.submitReview", async () => {
240
- await dispatchAction("review", { projectId: "proj_1", commitId: "abc", verdict: "approve" }, "LGTM");
241
- expect(runtime.projects.submitReview).toHaveBeenCalledWith("proj_1", "abc", "approve", "LGTM");
242
- });
243
- it("comment calls projects.submitReview with verdict=comment", async () => {
244
- await dispatchAction("comment", { projectId: "proj_1", commitId: "abc" }, "Looks fine");
245
- expect(runtime.projects.submitReview).toHaveBeenCalledWith("proj_1", "abc", "comment", "Looks fine");
246
- });
247
- it("request_ai_review calls projects.requestAIReview", async () => {
248
- await dispatchAction("request_ai_review", { projectId: "proj_1", commitId: "abc" });
249
- expect(runtime.projects.requestAIReview).toHaveBeenCalledWith("proj_1", "abc");
250
- });
251
- it("commit_files calls projects.commitFiles", async () => {
252
- const files = [{ path: "src/main.ts", content: "console.log('hi')" }];
253
- await dispatchAction("commit_files", { projectId: "proj_1", files }, "Initial commit");
254
- expect(runtime.projects.commitFiles).toHaveBeenCalledWith("proj_1", files, "Initial commit");
255
- });
256
- it("gateway_commit is alias for commit_files", async () => {
257
- const files = [{ path: "index.ts", content: "export {}" }];
258
- await dispatchAction("gateway_commit", { projectId: "proj_1", files });
259
- expect(runtime.projects.commitFiles).toHaveBeenCalled();
260
- });
261
- it("add_collaborator calls projects.addCollaborator", async () => {
262
- await dispatchAction("add_collaborator", { projectId: "proj_1", collaboratorAddress: "0xCOLLAB", role: "editor" });
263
- expect(runtime.projects.addCollaborator).toHaveBeenCalledWith("proj_1", "0xCOLLAB", "editor");
264
- });
265
- it("create_task calls projects.createTask", async () => {
266
- await dispatchAction("create_task", { projectId: "proj_1" }, "Implement feature X");
267
- expect(runtime.projects.createTask).toHaveBeenCalledWith("proj_1", expect.objectContaining({ title: "Implement feature X" }));
268
- });
269
- it("assign_task calls projects.assignTask", async () => {
270
- await dispatchAction("assign_task", { projectId: "proj_1", taskId: "task_1", assigneeAddress: "0xWORKER" });
271
- expect(runtime.projects.assignTask).toHaveBeenCalledWith("proj_1", "task_1", "0xWORKER");
272
- });
273
- it("complete_task calls projects.updateTask with status=completed", async () => {
274
- await dispatchAction("complete_task", { projectId: "proj_1", taskId: "task_1" });
275
- expect(runtime.projects.updateTask).toHaveBeenCalledWith("proj_1", "task_1", expect.objectContaining({ status: "completed" }));
276
- });
277
- it("update_task calls projects.updateTask with provided updates", async () => {
278
- await dispatchAction("update_task", { projectId: "proj_1", taskId: "task_1", status: "in_progress", priority: "high" });
279
- expect(runtime.projects.updateTask).toHaveBeenCalledWith("proj_1", "task_1", expect.objectContaining({ status: "in_progress", priority: "high" }));
280
- });
281
- it("link_project_to_guild performs 3-step linking", async () => {
282
- vi.mocked(runtime.guilds.get).mockResolvedValueOnce({ members: [{ address: "0xMEMBER", status: 2 }] });
283
- await dispatchAction("link_project_to_guild", { projectId: "proj_1", guildId: 5 });
284
- expect(runtime.guilds.linkProject).toHaveBeenCalledWith(5, "proj_1");
285
- expect(runtime.projects.setGuildAttribution).toHaveBeenCalledWith("proj_1", "5");
286
- expect(runtime.projects.addCollaborator).toHaveBeenCalledWith("proj_1", "0xMEMBER", "editor");
86
+ // ── sign_required handling ──────────────────────────────
87
+ describe("sign_required response handling", () => {
88
+ it("signs and relays when gateway returns sign_required", async () => {
89
+ // Provide a private key for signing
90
+ Object.defineProperty(runtime.connection, "privateKey", { value: "0xPRIVATE_KEY", writable: true });
91
+ requestMock.mockImplementation(async (method, path) => {
92
+ if (method === "POST" && path === "/v1/actions/execute") {
93
+ return {
94
+ status: "sign_required",
95
+ forwardRequest: { from: "0xAGENT", to: "0xCONTRACT", data: "0x..." },
96
+ domain: { name: "Nookplot" },
97
+ types: { ForwardRequest: [] },
98
+ };
99
+ }
100
+ if (method === "POST" && path === "/v1/relay") {
101
+ return { txHash: "0xRELAY_TX_HASH" };
102
+ }
103
+ return { success: true };
104
+ });
105
+ await dispatchAction("create_community", { slug: "test", name: "Test" }, undefined, "act_1");
106
+ // Should have called relay with the signed request
107
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/relay", expect.objectContaining({ signature: "0xSIGNATURE" }));
108
+ // Should complete the action with txHash
109
+ expect(runtime.proactive.completeAction).toHaveBeenCalledWith("act_1", "0xRELAY_TX_HASH", { txHash: "0xRELAY_TX_HASH" });
110
+ });
111
+ it("throws when sign_required but no private key", async () => {
112
+ Object.defineProperty(runtime.connection, "privateKey", { value: null, writable: true });
113
+ requestMock.mockImplementation(async (method, path) => {
114
+ if (method === "POST" && path === "/v1/actions/execute") {
115
+ return {
116
+ status: "sign_required",
117
+ forwardRequest: { from: "0xAGENT" },
118
+ domain: { name: "Nookplot" },
119
+ types: {},
120
+ };
121
+ }
122
+ return { success: true };
123
+ });
124
+ await dispatchAction("vote", { contentCid: "Qm_cid" }, undefined, "act_2");
125
+ // Should reject the delegated action
126
+ expect(runtime.proactive.rejectDelegatedAction).toHaveBeenCalledWith("act_2", expect.stringContaining("Private key not configured"));
287
127
  });
288
128
  });
289
- // ── Off-chain HTTP actions ───────────────────────────────────
290
- describe("off-chain HTTP actions", () => {
291
- it("apply_bounty calls connection.request POST", async () => {
292
- const applyMsg = "I can do this! I have extensive experience with this type of work and will deliver high quality results.";
293
- await dispatchAction("apply_bounty", { bountyId: "42" }, applyMsg);
294
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/bounties/42/apply", expect.objectContaining({ message: applyMsg }));
295
- });
296
- it("submit_bounty_work calls connection.request POST", async () => {
297
- await dispatchAction("submit_bounty_work", { bountyId: "42" }, "Here is my work");
298
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/bounties/42/submissions", expect.anything());
299
- });
300
- it("approve_bounty_application calls connection.request POST", async () => {
301
- await dispatchAction("approve_bounty_application", { bountyId: "42", applicationId: "app_1" });
302
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/bounties/42/applications/app_1/approve", {});
303
- });
304
- it("reject_bounty_application calls connection.request POST", async () => {
305
- await dispatchAction("reject_bounty_application", { bountyId: "42", applicationId: "app_1" });
306
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/bounties/42/applications/app_1/reject", {});
307
- });
308
- it("select_bounty_submission calls connection.request POST", async () => {
309
- await dispatchAction("select_bounty_submission", { bountyId: "42", submissionId: "sub_1" });
310
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/bounties/42/submissions/sub_1/select", {});
311
- });
312
- it("accept_invitation calls connection.request POST", async () => {
313
- await dispatchAction("accept_invitation", { invitationId: "inv_1" });
314
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/teams/invitations/inv_1/accept", {});
315
- });
316
- it("decline_invitation calls connection.request POST", async () => {
317
- await dispatchAction("decline_invitation", { invitationId: "inv_1" });
318
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/teams/invitations/inv_1/decline", {});
319
- });
320
- it("grant calls connection.request POST with grant-access", async () => {
321
- await dispatchAction("grant", { projectId: "proj_1", bountyId: "42", requestId: "req_1" });
322
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/projects/proj_1/bounties/42/grant-access", { requestId: "req_1" });
323
- });
324
- it("deny calls connection.request POST with deny-access", async () => {
325
- await dispatchAction("deny", { projectId: "proj_1", bountyId: "42", requestId: "req_1" });
326
- expect(runtime.connection.request).toHaveBeenCalledWith("POST", "/v1/projects/proj_1/bounties/42/deny-access", { requestId: "req_1" });
129
+ // ── completed response handling ──────────────────────────────
130
+ describe("completed response handling", () => {
131
+ it("completes delegated action with result", async () => {
132
+ requestMock.mockImplementation(async (method, path) => {
133
+ if (method === "POST" && path === "/v1/actions/execute") {
134
+ return { status: "completed", result: { messageId: "msg_1", sent: true } };
135
+ }
136
+ return { success: true };
137
+ });
138
+ await dispatchAction("send_message", { to: "0xTARGET", content: "Hi" }, undefined, "act_3");
139
+ expect(runtime.proactive.completeAction).toHaveBeenCalledWith("act_3", undefined, // no txHash for off-chain
140
+ { messageId: "msg_1", sent: true });
327
141
  });
328
142
  });
329
- // ── Real-world actions ───────────────────────────────────────
330
- describe("real-world actions", () => {
331
- it("egress_request calls tools.httpRequest", async () => {
332
- await dispatchAction("egress_request", { url: "https://api.example.com/data", method: "GET" });
333
- expect(runtime.tools.httpRequest).toHaveBeenCalledWith("https://api.example.com/data", "GET", expect.anything());
334
- });
335
- it("http_request is alias for egress_request", async () => {
336
- await dispatchAction("http_request", { url: "https://example.com", method: "POST" });
337
- expect(runtime.tools.httpRequest).toHaveBeenCalled();
338
- });
339
- it("execute_tool calls tools.executeTool", async () => {
340
- await dispatchAction("execute_tool", { toolName: "my_tool", args: { key: "val" } });
341
- expect(runtime.tools.executeTool).toHaveBeenCalledWith("my_tool", { key: "val" });
342
- });
343
- it("connect_mcp_server calls tools.connectMcpServer", async () => {
344
- await dispatchAction("connect_mcp_server", { serverUrl: "https://mcp.example.com", serverName: "test" });
345
- expect(runtime.tools.connectMcpServer).toHaveBeenCalledWith("https://mcp.example.com", "test");
346
- });
347
- it("disconnect_mcp_server calls tools.disconnectMcpServer", async () => {
348
- await dispatchAction("disconnect_mcp_server", { serverId: "mcp_1" });
349
- expect(runtime.tools.disconnectMcpServer).toHaveBeenCalledWith("mcp_1");
350
- });
351
- it("call_mcp_tool calls tools.executeTool", async () => {
352
- await dispatchAction("call_mcp_tool", { toolName: "mcp_tool_1", args: { x: 1 } });
353
- expect(runtime.tools.executeTool).toHaveBeenCalledWith("mcp_tool_1", { x: 1 });
354
- });
355
- it("register_webhook calls tools.registerWebhook", async () => {
356
- await dispatchAction("register_webhook", { source: "github" });
357
- expect(runtime.tools.registerWebhook).toHaveBeenCalledWith("github", expect.anything());
143
+ // ── error response handling ──────────────────────────────
144
+ describe("error response handling", () => {
145
+ it("rejects delegated action when gateway returns error", async () => {
146
+ requestMock.mockImplementation(async (method, path) => {
147
+ if (method === "POST" && path === "/v1/actions/execute") {
148
+ return { status: "error", error: "Rate limited" };
149
+ }
150
+ return { success: true };
151
+ });
152
+ await dispatchAction("send_message", { to: "0xTARGET" }, "Hi", "act_4");
153
+ expect(runtime.proactive.rejectDelegatedAction).toHaveBeenCalledWith("act_4", "Rate limited");
154
+ });
155
+ it("rejects delegated action when request throws", async () => {
156
+ requestMock.mockImplementation(async (method, path) => {
157
+ if (method === "POST" && path === "/v1/actions/execute") {
158
+ throw new Error("Network failure");
159
+ }
160
+ return { success: true };
161
+ });
162
+ await dispatchAction("vote", { contentCid: "Qm_cid" }, undefined, "act_5");
163
+ expect(runtime.proactive.rejectDelegatedAction).toHaveBeenCalledWith("act_5", "Network failure");
358
164
  });
359
165
  });
360
- // ── Strategy / knowledge actions ─────────────────────────────
361
- describe("strategy and knowledge actions", () => {
362
- it("publish_insight calls insights.publish", async () => {
363
- await dispatchAction("publish_insight", { title: "Insight", body: "Content" });
364
- expect(runtime.insights.publish).toHaveBeenCalledWith(expect.objectContaining({ title: "Insight", body: "Content" }));
365
- });
366
- it("cite_insight calls insights.cite", async () => {
367
- await dispatchAction("cite_insight", { insightId: "ins_1" });
368
- expect(runtime.insights.cite).toHaveBeenCalledWith("ins_1", undefined, undefined);
369
- });
370
- it("apply_insight calls insights.apply", async () => {
371
- await dispatchAction("apply_insight", { insightId: "ins_1" });
372
- expect(runtime.insights.apply).toHaveBeenCalledWith("ins_1", undefined, undefined);
373
- });
374
- it("workspace_create calls workspaces.create", async () => {
375
- await dispatchAction("workspace_create", { name: "My Workspace" });
376
- expect(runtime.workspaces.create).toHaveBeenCalledWith(expect.objectContaining({ name: "My Workspace" }));
377
- });
378
- it("workspace_set calls workspaces.setState", async () => {
379
- await dispatchAction("workspace_set", { workspaceId: "ws_1", key: "status", value: "active" });
380
- expect(runtime.workspaces.setState).toHaveBeenCalledWith("ws_1", "status", "active");
381
- });
382
- it("workspace_snapshot calls workspaces.createSnapshot", async () => {
383
- await dispatchAction("workspace_snapshot", { workspaceId: "ws_1" });
384
- expect(runtime.workspaces.createSnapshot).toHaveBeenCalledWith("ws_1", undefined);
385
- });
386
- it("propose_action calls workspaces.createProposal", async () => {
387
- await dispatchAction("propose_action", { workspaceId: "ws_1", title: "Upgrade deps" });
388
- expect(runtime.workspaces.createProposal).toHaveBeenCalledWith("ws_1", expect.objectContaining({ title: "Upgrade deps" }));
389
- });
390
- it("vote_proposal calls workspaces.vote", async () => {
391
- await dispatchAction("vote_proposal", { workspaceId: "ws_1", proposalId: "prop_1", vote: true });
392
- expect(runtime.workspaces.vote).toHaveBeenCalledWith("ws_1", "prop_1", true, undefined);
393
- });
394
- it("cancel_proposal calls workspaces.cancelProposal", async () => {
395
- await dispatchAction("cancel_proposal", { workspaceId: "ws_1", proposalId: "prop_1" });
396
- expect(runtime.workspaces.cancelProposal).toHaveBeenCalledWith("ws_1", "prop_1");
166
+ // ── approval gate ──────────────────────────────
167
+ describe("approval gate for on-chain actions", () => {
168
+ it("blocks on-chain action when approval handler rejects", async () => {
169
+ const agentWithApproval = new AutonomousAgent(runtime, {
170
+ verbose: false,
171
+ onApproval: async () => false,
172
+ });
173
+ agentWithApproval.start();
174
+ callbacks.actionCb({
175
+ agentId: "a1",
176
+ actionType: "create_community",
177
+ actionId: "act_6",
178
+ payload: { slug: "x", name: "X" },
179
+ });
180
+ await new Promise((r) => setTimeout(r, 20));
181
+ expect(runtime.proactive.rejectDelegatedAction).toHaveBeenCalledWith("act_6", "Rejected by approval handler");
182
+ // Should NOT call the gateway execute endpoint
183
+ expect(requestMock).not.toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.anything());
184
+ });
185
+ it("allows on-chain action when approval handler approves", async () => {
186
+ const agentWithApproval = new AutonomousAgent(runtime, {
187
+ verbose: false,
188
+ onApproval: async () => true,
189
+ });
190
+ agentWithApproval.start();
191
+ callbacks.actionCb({
192
+ agentId: "a1",
193
+ actionType: "follow",
194
+ payload: { targetAddress: "0xTARGET" },
195
+ });
196
+ await new Promise((r) => setTimeout(r, 20));
197
+ // Should have dispatched to gateway
198
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({ toolName: "nookplot_follow" }));
199
+ });
200
+ it("skips approval for off-chain actions", async () => {
201
+ const approvalFn = vi.fn().mockResolvedValue(false);
202
+ const agentWithApproval = new AutonomousAgent(runtime, {
203
+ verbose: false,
204
+ onApproval: approvalFn,
205
+ });
206
+ agentWithApproval.start();
207
+ callbacks.actionCb({
208
+ agentId: "a1",
209
+ actionType: "reply",
210
+ payload: { channelId: "ch1" },
211
+ suggestedContent: "hi",
212
+ });
213
+ await new Promise((r) => setTimeout(r, 20));
214
+ expect(approvalFn).not.toHaveBeenCalled();
215
+ // Should still dispatch to gateway
216
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({ toolName: "nookplot_reply" }));
397
217
  });
398
218
  });
399
- // ── Treasury actions ─────────────────────────────────────────
400
- describe("treasury actions", () => {
401
- it("deposit_treasury calls guilds.depositTreasury", async () => {
402
- await dispatchAction("deposit_treasury", { guildId: "5", amount: 100 });
403
- expect(runtime.guilds.depositTreasury).toHaveBeenCalledWith("5", 100, undefined);
404
- });
405
- it("withdraw_treasury calls guilds.withdrawTreasury", async () => {
406
- await dispatchAction("withdraw_treasury", { guildId: "5", amount: 50 });
407
- expect(runtime.guilds.withdrawTreasury).toHaveBeenCalledWith("5", 50, undefined);
408
- });
409
- it("fund_bounty_from_treasury calls guilds.fundBountyFromTreasury", async () => {
410
- await dispatchAction("fund_bounty_from_treasury", { guildId: "5", bountyId: "10", amount: 25 });
411
- expect(runtime.guilds.fundBountyFromTreasury).toHaveBeenCalledWith("5", "10", 25, undefined);
412
- });
413
- it("distribute_revenue calls guilds.distributeRevenue", async () => {
414
- await dispatchAction("distribute_revenue", { guildId: "5" });
415
- expect(runtime.guilds.distributeRevenue).toHaveBeenCalledWith("5", undefined);
219
+ // ── action lifecycle ──────────────────────────────
220
+ describe("action lifecycle (completeAction / rejectDelegatedAction)", () => {
221
+ it("completes action on success (no actionId = no completeAction call)", async () => {
222
+ await dispatchAction("send_message", { to: "0xA", content: "hi" });
223
+ // No actionId → no completeAction call
224
+ expect(runtime.proactive.completeAction).not.toHaveBeenCalled();
225
+ });
226
+ it("completes action with actionId on success", async () => {
227
+ await dispatchAction("send_message", { to: "0xA", content: "hi" }, undefined, "act_7");
228
+ expect(runtime.proactive.completeAction).toHaveBeenCalledWith("act_7", undefined, { success: true });
229
+ });
230
+ it("rejects action with actionId on failure", async () => {
231
+ requestMock.mockImplementation(async (method, path) => {
232
+ if (method === "POST" && path === "/v1/actions/execute") {
233
+ return { status: "error", error: "Tool not found" };
234
+ }
235
+ return { success: true };
236
+ });
237
+ await dispatchAction("nonexistent_tool", {}, undefined, "act_8");
238
+ expect(runtime.proactive.rejectDelegatedAction).toHaveBeenCalledWith("act_8", "Tool not found");
416
239
  });
417
240
  });
418
- // ── Swarm actions ────────────────────────────────────────────
419
- describe("swarm actions", () => {
420
- it("create_swarm calls swarms.create", async () => {
421
- await dispatchAction("create_swarm", { title: "Data Collection", subtasks: [{ title: "Collect A" }] });
422
- expect(runtime.swarms.create).toHaveBeenCalledWith(expect.objectContaining({ title: "Data Collection" }));
423
- });
424
- it("claim_subtask calls swarms.claimSubtask", async () => {
425
- await dispatchAction("claim_subtask", { subtaskId: "st_1" });
426
- expect(runtime.swarms.claimSubtask).toHaveBeenCalledWith("st_1");
427
- });
428
- it("submit_swarm_result calls swarms.submitResult", async () => {
429
- await dispatchAction("submit_swarm_result", { subtaskId: "st_1" });
430
- expect(runtime.swarms.submitResult).toHaveBeenCalledWith("st_1", undefined, undefined);
431
- });
432
- it("aggregate_swarm calls swarms.aggregate", async () => {
433
- await dispatchAction("aggregate_swarm", { swarmId: "swarm_1" });
434
- expect(runtime.swarms.aggregate).toHaveBeenCalledWith("swarm_1", undefined);
435
- });
241
+ // ── various action types dispatch correctly ──────────────────────────────
242
+ describe("action type → toolName mapping", () => {
243
+ const cases = [
244
+ ["create_post", { community: "general", title: "T" }],
245
+ ["post_reply", { parentCid: "Qm_p" }],
246
+ ["vote", { contentCid: "Qm_c", isUpvote: true }],
247
+ ["follow", { targetAddress: "0xT" }],
248
+ ["follow_agent", { targetAddress: "0xT" }],
249
+ ["attest", { targetAddress: "0xT" }],
250
+ ["attest_agent", { targetAddress: "0xT" }],
251
+ ["send_message", { to: "0xT", content: "hi" }],
252
+ ["send_dm", { to: "0xT", content: "hi" }],
253
+ ["send_channel_message", { channelId: "ch_1", content: "hi" }],
254
+ ["create_community", { slug: "test", name: "Test", description: "A test" }],
255
+ ["propose_guild", { name: "G1", description: "Guild" }],
256
+ ["create_bounty", { title: "B1", description: "bounty", community: "gen", rewardCredits: 100 }],
257
+ ["claim_bounty", { bountyId: "b_1" }],
258
+ ["create_bundle", { name: "Bundle", cids: ["Qm_1"] }],
259
+ ["create_project", { projectId: "my-proj", name: "My Project", description: "desc" }],
260
+ ["commit_files", { projectId: "p1", message: "init", files: [] }],
261
+ ["create_task", { projectId: "p1", title: "T1" }],
262
+ ["publish_insight", { title: "Insight", body: "text" }],
263
+ ["create_swarm", { title: "S1", subtasks: [] }],
264
+ ["create_intent", { title: "I1", description: "intent" }],
265
+ ["apply_bounty", { bountyId: "b_1", message: "applying" }],
266
+ ["egress_request", { url: "https://example.com" }],
267
+ ];
268
+ for (const [actionType, payload] of cases) {
269
+ it(`${actionType} → nookplot_${actionType}`, async () => {
270
+ await dispatchAction(actionType, payload);
271
+ expect(requestMock).toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.objectContaining({
272
+ toolName: `nookplot_${actionType}`,
273
+ payload: expect.objectContaining(payload),
274
+ }));
275
+ });
276
+ }
436
277
  });
437
- // ── Specialization actions ───────────────────────────────────
438
- describe("specialization actions", () => {
439
- it("record_gap calls specialization.recordGap", async () => {
440
- await dispatchAction("record_gap", { queryText: "How to deploy to k8s?" });
441
- expect(runtime.specialization.recordGap).toHaveBeenCalledWith(expect.objectContaining({ queryText: "How to deploy to k8s?" }));
442
- });
443
- it("update_proficiency calls specialization.updateProficiency", async () => {
444
- await dispatchAction("update_proficiency", { skillDomain: "typescript", proficiency: 85 });
445
- expect(runtime.specialization.updateProficiency).toHaveBeenCalledWith("typescript", 85);
446
- });
447
- it("generate_recommendations calls specialization.generateRecommendations", async () => {
448
- await dispatchAction("generate_recommendations", {});
449
- expect(runtime.specialization.generateRecommendations).toHaveBeenCalled();
450
- });
451
- it("dismiss_recommendation calls specialization.dismissRecommendation", async () => {
452
- await dispatchAction("dismiss_recommendation", { recommendationId: "rec_1" });
453
- expect(runtime.specialization.dismissRecommendation).toHaveBeenCalledWith("rec_1");
454
- });
455
- });
456
- // ── Intent actions ───────────────────────────────────────────
457
- describe("intent actions", () => {
458
- it("create_intent calls intents.create", async () => {
459
- await dispatchAction("create_intent", { title: "Need a logo" });
460
- expect(runtime.intents.create).toHaveBeenCalledWith(expect.objectContaining({ title: "Need a logo" }));
461
- });
462
- it("browse_intents calls intents.list", async () => {
463
- await dispatchAction("browse_intents", { status: "open" });
464
- expect(runtime.intents.list).toHaveBeenCalledWith(expect.objectContaining({ status: "open", limit: 20 }));
465
- });
466
- it("submit_proposal calls intents.submitProposal", async () => {
467
- await dispatchAction("submit_proposal", { intentId: "intent_1" }, "I can help");
468
- expect(runtime.intents.submitProposal).toHaveBeenCalledWith("intent_1", expect.objectContaining({ description: "I can help" }));
469
- });
470
- it("accept_proposal calls intents.acceptProposal", async () => {
471
- await dispatchAction("accept_proposal", { intentId: "intent_1", proposalId: "prop_1" });
472
- expect(runtime.intents.acceptProposal).toHaveBeenCalledWith("intent_1", "prop_1");
473
- });
474
- it("cancel_intent calls intents.cancel", async () => {
475
- await dispatchAction("cancel_intent", { intentId: "intent_1" });
476
- expect(runtime.intents.cancel).toHaveBeenCalledWith("intent_1");
477
- });
478
- it("complete_intent calls intents.complete", async () => {
479
- await dispatchAction("complete_intent", { intentId: "intent_1" });
480
- expect(runtime.intents.complete).toHaveBeenCalledWith("intent_1");
481
- });
482
- it("withdraw_proposal calls intents.withdrawProposal", async () => {
483
- await dispatchAction("withdraw_proposal", { intentId: "intent_1", proposalId: "prop_1" });
484
- expect(runtime.intents.withdrawProposal).toHaveBeenCalledWith("intent_1", "prop_1");
485
- });
486
- });
487
- // ── Token launch actions ─────────────────────────────────────
488
- describe("token launch actions", () => {
489
- it("report_clawnch_launch is not yet wired (clawnch not live)", async () => {
490
- await dispatchAction("report_clawnch_launch", { tokenName: "MyCoin", tokenTicker: "MYC", tokenAddress: "0xTOKEN" });
491
- expect(runtime.connection.request).not.toHaveBeenCalled();
492
- });
493
- it("get_token_analytics calls connection.request GET", async () => {
494
- await dispatchAction("get_token_analytics", { tokenAddress: "0xTOKEN" });
495
- expect(runtime.connection.request).toHaveBeenCalledWith("GET", "/v1/clawnch/analytics/token/0xTOKEN");
496
- });
497
- });
498
- // ── Oracle actions ───────────────────────────────────────────
499
- describe("oracle actions", () => {
500
- it("query_oracle project calls oracle.getProjectSignals", async () => {
501
- await dispatchAction("query_oracle", { entityType: "project", entityId: "proj_1" });
502
- expect(runtime.oracle.getProjectSignals).toHaveBeenCalledWith("proj_1");
503
- });
504
- it("query_oracle agent calls oracle.getAgentSignals", async () => {
505
- await dispatchAction("query_oracle", { entityType: "agent", entityId: "0xAGENT" });
506
- expect(runtime.oracle.getAgentSignals).toHaveBeenCalledWith("0xAGENT");
507
- });
508
- it("query_oracle intent calls oracle.getIntentSignals", async () => {
509
- await dispatchAction("query_oracle", { entityType: "intent", entityId: "intent_1" });
510
- expect(runtime.oracle.getIntentSignals).toHaveBeenCalledWith("intent_1");
511
- });
512
- it("query_oracle guild calls oracle.getGuildSignals", async () => {
513
- await dispatchAction("query_oracle", { entityType: "guild", entityId: "guild_1" });
514
- expect(runtime.oracle.getGuildSignals).toHaveBeenCalledWith("guild_1");
515
- });
516
- });
517
- // ── Marketplace review + messages ────────────────────────────
518
- describe("marketplace review and messages", () => {
519
- it("submit_review calls marketplace.submitReview", async () => {
520
- await dispatchAction("submit_review", { agreementId: 5, rating: 4 }, "Good work!");
521
- expect(runtime.marketplace.submitReview).toHaveBeenCalledWith(5, 4, "Good work!");
522
- });
523
- it("send_agreement_message calls marketplace.sendAgreementMessage", async () => {
524
- await dispatchAction("send_agreement_message", { agreementId: 5, messageType: "revision_request" }, "Please fix X");
525
- expect(runtime.marketplace.sendAgreementMessage).toHaveBeenCalledWith(5, "revision_request", "Please fix X", undefined);
526
- });
527
- });
528
- // ── Matching actions ─────────────────────────────────────────
529
- describe("matching actions", () => {
530
- it("find_agents calls matching.findAgents", async () => {
531
- await dispatchAction("find_agents", { skills: ["typescript", "react"] });
532
- expect(runtime.matching.findAgents).toHaveBeenCalledWith(["typescript", "react"], expect.anything());
533
- });
534
- it("assemble_team calls matching.assembleTeam", async () => {
535
- await dispatchAction("assemble_team", {}, "Build a web3 dashboard");
536
- expect(runtime.matching.assembleTeam).toHaveBeenCalledWith(expect.objectContaining({ description: "Build a web3 dashboard" }));
537
- });
538
- });
539
- // ── Error cases and lifecycle ────────────────────────────────
540
- describe("error cases and action lifecycle", () => {
541
- it("unknown action rejects delegated action", async () => {
542
- await dispatchAction("totally_unknown_action", {}, undefined, "action_42");
543
- expect(runtime.proactive.rejectDelegatedAction).toHaveBeenCalledWith("action_42", expect.stringContaining("Unknown"));
544
- });
545
- it("actionId triggers proactive.completeAction on success", async () => {
546
- await dispatchAction("follow", { targetAddress: "0xTARGET" }, undefined, "action_99");
547
- expect(runtime.proactive.completeAction).toHaveBeenCalledWith("action_99", "0xMOCK_TX", expect.anything());
548
- });
549
- it("missing required field throws and rejects delegated action", async () => {
550
- await dispatchAction("send_dm", {}, undefined, "action_fail");
551
- expect(runtime.proactive.rejectDelegatedAction).toHaveBeenCalledWith("action_fail", expect.stringContaining("requires"));
552
- });
553
- it("custom onAction handler bypasses default dispatch", async () => {
554
- const customHandler = vi.fn().mockResolvedValue(undefined);
555
- const customAgent = new AutonomousAgent(runtime, { onAction: customHandler, verbose: false });
556
- customAgent.start();
557
- callbacks.actionCb({ agentId: "a1", actionType: "follow", payload: { targetAddress: "0x1" } });
558
- await new Promise((r) => setTimeout(r, 10));
559
- expect(customHandler).toHaveBeenCalled();
560
- expect(runtime.social.follow).not.toHaveBeenCalled();
278
+ // ── stop prevents dispatch ──────────────────────────────
279
+ describe("stop prevents action dispatch", () => {
280
+ it("stopped agent does not dispatch actions", async () => {
281
+ agent.stop();
282
+ callbacks.actionCb({ agentId: "a1", actionType: "follow", payload: { targetAddress: "0xA" } });
283
+ await new Promise((r) => setTimeout(r, 20));
284
+ expect(requestMock).not.toHaveBeenCalledWith("POST", "/v1/actions/execute", expect.anything());
561
285
  });
562
286
  });
563
287
  //# sourceMappingURL=autonomous.actionDispatch.test.js.map