@agentwonderland/mcp 0.1.54 → 0.1.55
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.
- package/dist/core/__tests__/api-client.test.js +25 -1
- package/dist/core/__tests__/link-cli.test.js +95 -0
- package/dist/core/api-client.d.ts +3 -1
- package/dist/core/api-client.js +21 -2
- package/dist/core/link-cli.d.ts +17 -0
- package/dist/core/link-cli.js +165 -0
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/__tests__/playbooks.test.js +114 -34
- package/dist/tools/playbooks.js +146 -42
- package/package.json +1 -1
- package/src/core/__tests__/api-client.test.ts +33 -0
- package/src/core/__tests__/link-cli.test.ts +110 -0
- package/src/core/api-client.ts +25 -1
- package/src/core/link-cli.ts +189 -0
- package/src/core/version.ts +1 -1
- package/src/index.ts +1 -1
- package/src/tools/__tests__/playbooks.test.ts +125 -46
- package/src/tools/playbooks.ts +177 -56
package/src/core/link-cli.ts
CHANGED
|
@@ -21,6 +21,15 @@ export class LinkApprovalRequiredError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface ApprovedLinkSpendRequestParams {
|
|
25
|
+
amount: string;
|
|
26
|
+
currency: string;
|
|
27
|
+
context: string;
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
networkId: string;
|
|
30
|
+
paymentMethodId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
function formatLinkApprovalRequiredMessage(spendRequestId: string, approvalUrl?: string): string {
|
|
25
34
|
return [
|
|
26
35
|
"Link approval required.",
|
|
@@ -137,6 +146,16 @@ function extractSpendRequestApproval(output: unknown): { id: string; approvalUrl
|
|
|
137
146
|
return null;
|
|
138
147
|
}
|
|
139
148
|
|
|
149
|
+
function extractSpendRequestStatus(output: unknown): { id?: string; approvalUrl?: string; status?: string } {
|
|
150
|
+
const record = Array.isArray(output) ? asRecord(output[0]) : asRecord(output);
|
|
151
|
+
if (!record) return {};
|
|
152
|
+
return {
|
|
153
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
154
|
+
approvalUrl: typeof record.approval_url === "string" ? record.approval_url : undefined,
|
|
155
|
+
status: typeof record.status === "string" ? record.status : undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
140
159
|
function isMatchingPendingSpendRequest(
|
|
141
160
|
pending: PendingLinkSpendRequest | null,
|
|
142
161
|
params: {
|
|
@@ -162,6 +181,14 @@ function isProjectedSpendCapError(message: string): boolean {
|
|
|
162
181
|
return /projected daily spend|projected spend|exceeds limit/i.test(message);
|
|
163
182
|
}
|
|
164
183
|
|
|
184
|
+
function isApprovedSpendRequestStatus(status?: string): boolean {
|
|
185
|
+
return /approved|active|ready/i.test(status ?? "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isPendingSpendRequestStatus(status?: string): boolean {
|
|
189
|
+
return /pending/i.test(status ?? "");
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
function recordLinkCooldown(reason: string): void {
|
|
166
193
|
const now = new Date();
|
|
167
194
|
setLinkCooldown({
|
|
@@ -370,6 +397,168 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
370
397
|
}
|
|
371
398
|
}
|
|
372
399
|
|
|
400
|
+
export async function ensureApprovedLinkSpendRequest(params: ApprovedLinkSpendRequestParams): Promise<string> {
|
|
401
|
+
const existing = getPendingLinkSpendRequest();
|
|
402
|
+
if (isMatchingPendingSpendRequest(existing, params)) {
|
|
403
|
+
const retrieved = await runLinkCli([
|
|
404
|
+
"spend-request",
|
|
405
|
+
"retrieve",
|
|
406
|
+
existing.id,
|
|
407
|
+
], 30_000);
|
|
408
|
+
const status = extractSpendRequestStatus(retrieved);
|
|
409
|
+
if (isApprovedSpendRequestStatus(status.status)) {
|
|
410
|
+
return existing.id;
|
|
411
|
+
}
|
|
412
|
+
if (isPendingSpendRequestStatus(status.status)) {
|
|
413
|
+
throw new LinkApprovalRequiredError(existing.id, existing.approvalUrl ?? status.approvalUrl);
|
|
414
|
+
}
|
|
415
|
+
setPendingLinkSpendRequest(null);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const args = [
|
|
419
|
+
"spend-request",
|
|
420
|
+
"create",
|
|
421
|
+
"--credential-type",
|
|
422
|
+
"shared_payment_token",
|
|
423
|
+
"--network-id",
|
|
424
|
+
params.networkId,
|
|
425
|
+
"--amount",
|
|
426
|
+
params.amount,
|
|
427
|
+
"--currency",
|
|
428
|
+
params.currency,
|
|
429
|
+
"--payment-method-id",
|
|
430
|
+
params.paymentMethodId,
|
|
431
|
+
"--context",
|
|
432
|
+
params.context,
|
|
433
|
+
"--request-approval",
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
if (process.env.AGENTWONDERLAND_LINK_TEST_MODE === "1") {
|
|
437
|
+
args.push("--test");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let output: unknown;
|
|
441
|
+
try {
|
|
442
|
+
output = await runLinkCli(args);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
445
|
+
if (isProjectedSpendCapError(message)) {
|
|
446
|
+
recordLinkCooldown(message);
|
|
447
|
+
throw new Error(
|
|
448
|
+
[
|
|
449
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
450
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
451
|
+
"Use USDC for now, wait for the rolling Link window to clear, or ask Stripe to raise/clear the merchant projected-spend cap.",
|
|
452
|
+
"",
|
|
453
|
+
message,
|
|
454
|
+
].join("\n"),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const status = extractSpendRequestStatus(output);
|
|
461
|
+
const id = status.id ?? extractSpendRequestApproval(output)?.id;
|
|
462
|
+
if (id && isApprovedSpendRequestStatus(status.status)) {
|
|
463
|
+
setPendingLinkSpendRequest({
|
|
464
|
+
id,
|
|
465
|
+
approvalUrl: status.approvalUrl,
|
|
466
|
+
amount: params.amount,
|
|
467
|
+
currency: params.currency,
|
|
468
|
+
context: params.context,
|
|
469
|
+
expiresAt: params.expiresAt,
|
|
470
|
+
networkId: params.networkId,
|
|
471
|
+
paymentMethodId: params.paymentMethodId,
|
|
472
|
+
createdAt: new Date().toISOString(),
|
|
473
|
+
});
|
|
474
|
+
return id;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const approval = extractSpendRequestApproval(output);
|
|
478
|
+
if (approval?.id && isPendingSpendRequestStatus(approval.status)) {
|
|
479
|
+
setPendingLinkSpendRequest({
|
|
480
|
+
id: approval.id,
|
|
481
|
+
approvalUrl: approval.approvalUrl,
|
|
482
|
+
amount: params.amount,
|
|
483
|
+
currency: params.currency,
|
|
484
|
+
context: params.context,
|
|
485
|
+
expiresAt: params.expiresAt,
|
|
486
|
+
networkId: params.networkId,
|
|
487
|
+
paymentMethodId: params.paymentMethodId,
|
|
488
|
+
createdAt: new Date().toISOString(),
|
|
489
|
+
});
|
|
490
|
+
if (approval.approvalUrl) {
|
|
491
|
+
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
492
|
+
}
|
|
493
|
+
throw new LinkApprovalRequiredError(approval.id, approval.approvalUrl);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
throw new Error("Link spend request did not return an approved or pending approval state.");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function extractMppPayBody(output: unknown): unknown {
|
|
500
|
+
if (typeof output === "string") {
|
|
501
|
+
try {
|
|
502
|
+
return JSON.parse(output);
|
|
503
|
+
} catch {
|
|
504
|
+
return output;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (Array.isArray(output)) {
|
|
508
|
+
return output.length === 1 ? extractMppPayBody(output[0]) : output;
|
|
509
|
+
}
|
|
510
|
+
const record = asRecord(output);
|
|
511
|
+
if (!record) return output;
|
|
512
|
+
for (const key of ["body", "json", "data", "response_body"]) {
|
|
513
|
+
const value = record[key];
|
|
514
|
+
if (value !== undefined) return extractMppPayBody(value);
|
|
515
|
+
}
|
|
516
|
+
const response = asRecord(record.response);
|
|
517
|
+
if (response) return extractMppPayBody(response.body ?? response.json ?? response.data ?? response);
|
|
518
|
+
return output;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export async function payMppWithApprovedLinkSpendRequest(params: {
|
|
522
|
+
url: string;
|
|
523
|
+
method: string;
|
|
524
|
+
headers: Record<string, string>;
|
|
525
|
+
body?: unknown;
|
|
526
|
+
spendRequestId: string;
|
|
527
|
+
}): Promise<unknown> {
|
|
528
|
+
const args = [
|
|
529
|
+
"mpp",
|
|
530
|
+
"pay",
|
|
531
|
+
params.url,
|
|
532
|
+
"--spend-request-id",
|
|
533
|
+
params.spendRequestId,
|
|
534
|
+
"--method",
|
|
535
|
+
params.method,
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
for (const [name, value] of Object.entries(params.headers)) {
|
|
539
|
+
args.push("--header", `${name}: ${value}`);
|
|
540
|
+
}
|
|
541
|
+
if (params.body !== undefined) {
|
|
542
|
+
args.push("--data", typeof params.body === "string" ? params.body : JSON.stringify(params.body));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const output = await runLinkCli(args);
|
|
546
|
+
return extractMppPayBody(output);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function decodeMppNetworkId(challenge: string): Promise<string> {
|
|
550
|
+
const output = await runLinkCli(["mpp", "decode", "--challenge", challenge], 30_000);
|
|
551
|
+
const found = walk(output, (value, key) => {
|
|
552
|
+
if (typeof value !== "string") return null;
|
|
553
|
+
if (key && /network[_-]?id/i.test(key)) return value;
|
|
554
|
+
return null;
|
|
555
|
+
});
|
|
556
|
+
if (!found) {
|
|
557
|
+
throw new Error("Could not decode Link MPP network_id from payment challenge.");
|
|
558
|
+
}
|
|
559
|
+
return found;
|
|
560
|
+
}
|
|
561
|
+
|
|
373
562
|
async function retrieveSharedPaymentToken(spendRequestId: string, approvalUrl?: string): Promise<string> {
|
|
374
563
|
const retrieved = await runLinkCli([
|
|
375
564
|
"spend-request",
|
package/src/core/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const MCP_PACKAGE_VERSION = "0.1.55";
|
package/src/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
52
52
|
" For multi-agent workflows, use search_playbooks() → get_playbook() → run_playbook().",
|
|
53
53
|
"2. If the agent returns status 'processing', poll with get_job(). Async runs resolve automatically.",
|
|
54
54
|
"3. After a successful agent run, rate_agent() and optionally tip_agent() if the result was useful.",
|
|
55
|
-
"
|
|
55
|
+
" Playbooks return one run-level receipt; do not ask users to rate or tip individual child-agent steps.",
|
|
56
56
|
"4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
|
|
57
57
|
"",
|
|
58
58
|
"PAYMENT:",
|
|
@@ -2,20 +2,38 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
const mockApiGet = vi.fn();
|
|
4
4
|
const mockApiPost = vi.fn();
|
|
5
|
+
const mockApiPostWithApprovedLinkSpendRequest = vi.fn();
|
|
5
6
|
const mockApiPostWithPayment = vi.fn();
|
|
6
7
|
const mockUploadLocalFiles = vi.fn();
|
|
7
8
|
const mockHasWalletConfigured = vi.fn();
|
|
8
9
|
const mockGetConfiguredMethods = vi.fn();
|
|
9
10
|
const mockGetWalletAddress = vi.fn();
|
|
11
|
+
const mockGetLinkConfig = vi.fn();
|
|
12
|
+
const mockSetPendingLinkSpendRequest = vi.fn();
|
|
10
13
|
const mockRequiresSpendConfirmation = vi.fn();
|
|
11
14
|
const mockCanSpend = vi.fn();
|
|
12
15
|
const mockRecordSpend = vi.fn();
|
|
13
16
|
const mockRequiresPolicyConfirmation = vi.fn();
|
|
14
17
|
const mockStoreFeedbackToken = vi.fn();
|
|
18
|
+
const mockDecodeMppNetworkId = vi.fn();
|
|
19
|
+
const mockEnsureApprovedLinkSpendRequest = vi.fn();
|
|
20
|
+
|
|
21
|
+
class MockLinkApprovalRequiredError extends Error {
|
|
22
|
+
approvalUrl: string;
|
|
23
|
+
spendRequestId: string;
|
|
24
|
+
|
|
25
|
+
constructor(approvalUrl: string, spendRequestId = "lsrq_test_123") {
|
|
26
|
+
super(`Link approval required: ${approvalUrl}`);
|
|
27
|
+
this.name = "LinkApprovalRequiredError";
|
|
28
|
+
this.approvalUrl = approvalUrl;
|
|
29
|
+
this.spendRequestId = spendRequestId;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
vi.mock("../../core/api-client.js", () => ({
|
|
17
34
|
apiGet: mockApiGet,
|
|
18
35
|
apiPost: mockApiPost,
|
|
36
|
+
apiPostWithApprovedLinkSpendRequest: mockApiPostWithApprovedLinkSpendRequest,
|
|
19
37
|
apiPostWithPayment: mockApiPostWithPayment,
|
|
20
38
|
}));
|
|
21
39
|
|
|
@@ -30,6 +48,8 @@ vi.mock("../../core/payments.js", () => ({
|
|
|
30
48
|
}));
|
|
31
49
|
|
|
32
50
|
vi.mock("../../core/config.js", () => ({
|
|
51
|
+
getLinkConfig: mockGetLinkConfig,
|
|
52
|
+
setPendingLinkSpendRequest: mockSetPendingLinkSpendRequest,
|
|
33
53
|
requiresSpendConfirmation: mockRequiresSpendConfirmation,
|
|
34
54
|
}));
|
|
35
55
|
|
|
@@ -43,6 +63,12 @@ vi.mock("../_token-cache.js", () => ({
|
|
|
43
63
|
storeFeedbackToken: mockStoreFeedbackToken,
|
|
44
64
|
}));
|
|
45
65
|
|
|
66
|
+
vi.mock("../../core/link-cli.js", () => ({
|
|
67
|
+
decodeMppNetworkId: mockDecodeMppNetworkId,
|
|
68
|
+
ensureApprovedLinkSpendRequest: mockEnsureApprovedLinkSpendRequest,
|
|
69
|
+
LinkApprovalRequiredError: MockLinkApprovalRequiredError,
|
|
70
|
+
}));
|
|
71
|
+
|
|
46
72
|
function flattenToolText(result: unknown): string {
|
|
47
73
|
const content = (result as { content?: Array<{ type?: string; text?: string }> })?.content ?? [];
|
|
48
74
|
return content
|
|
@@ -116,6 +142,11 @@ describe("playbook MCP tools", () => {
|
|
|
116
142
|
mockHasWalletConfigured.mockReturnValue(true);
|
|
117
143
|
mockGetConfiguredMethods.mockReturnValue(["card"]);
|
|
118
144
|
mockGetWalletAddress.mockResolvedValue("0xabc");
|
|
145
|
+
mockGetLinkConfig.mockReturnValue({
|
|
146
|
+
paymentMethodId: "csmrpd_test_123",
|
|
147
|
+
});
|
|
148
|
+
mockDecodeMppNetworkId.mockResolvedValue("profile_test");
|
|
149
|
+
mockEnsureApprovedLinkSpendRequest.mockResolvedValue("lsrq_test_123");
|
|
119
150
|
mockRequiresSpendConfirmation.mockReturnValue(true);
|
|
120
151
|
mockRequiresPolicyConfirmation.mockReturnValue(false);
|
|
121
152
|
mockCanSpend.mockReturnValue({ ok: true, message: "" });
|
|
@@ -273,51 +304,15 @@ describe("playbook MCP tools", () => {
|
|
|
273
304
|
expect(text).toContain("Saved competitor-ads to favorites");
|
|
274
305
|
});
|
|
275
306
|
|
|
276
|
-
it("
|
|
277
|
-
mockApiPost.mockResolvedValueOnce({
|
|
278
|
-
ok: true,
|
|
279
|
-
feedback_id: "feedback-row-1",
|
|
280
|
-
playbook_slug: "competitor-ads",
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const { registerPlaybookTools } = await import("../playbooks.js");
|
|
284
|
-
const harness = makeServerHarness();
|
|
285
|
-
registerPlaybookTools(harness.server as never);
|
|
286
|
-
|
|
287
|
-
const result = await harness.handlers.get("rate_playbook")!({
|
|
288
|
-
slug: "competitor-ads",
|
|
289
|
-
run_id: "22222222-2222-4222-8222-222222222222",
|
|
290
|
-
rating: 5,
|
|
291
|
-
useful: true,
|
|
292
|
-
comment: "Useful.",
|
|
293
|
-
});
|
|
294
|
-
const text = flattenToolText(result);
|
|
295
|
-
|
|
296
|
-
expect(mockApiPost).toHaveBeenCalledWith(
|
|
297
|
-
"/playbooks/competitor-ads/feedback",
|
|
298
|
-
{
|
|
299
|
-
run_id: "22222222-2222-4222-8222-222222222222",
|
|
300
|
-
rating: 5,
|
|
301
|
-
useful: true,
|
|
302
|
-
comment: "Useful.",
|
|
303
|
-
},
|
|
304
|
-
{ ensureConsumerPrincipal: true },
|
|
305
|
-
);
|
|
306
|
-
expect(text).toContain("Recorded feedback for competitor-ads");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("validates favorite and feedback inputs before calling the gateway", async () => {
|
|
307
|
+
it("does not expose per-playbook feedback as an MCP tool", async () => {
|
|
310
308
|
const { registerPlaybookTools } = await import("../playbooks.js");
|
|
311
309
|
const harness = makeServerHarness();
|
|
312
310
|
registerPlaybookTools(harness.server as never);
|
|
313
311
|
|
|
314
312
|
const favoriteResult = await harness.handlers.get("favorite_playbook")!({});
|
|
315
|
-
const emptyFeedbackResult = await harness.handlers.get("rate_playbook")!({ slug: "competitor-ads" });
|
|
316
|
-
const missingIdFeedbackResult = await harness.handlers.get("rate_playbook")!({ rating: 5 });
|
|
317
313
|
|
|
318
314
|
expect(flattenToolText(favoriteResult)).toContain("Provide slug or playbook_id.");
|
|
319
|
-
expect(
|
|
320
|
-
expect(flattenToolText(missingIdFeedbackResult)).toContain("Provide slug or playbook_id.");
|
|
315
|
+
expect(harness.handlers.has("rate_playbook")).toBe(false);
|
|
321
316
|
expect(mockApiPost).not.toHaveBeenCalled();
|
|
322
317
|
});
|
|
323
318
|
|
|
@@ -618,16 +613,100 @@ describe("playbook MCP tools", () => {
|
|
|
618
613
|
failure_message: null,
|
|
619
614
|
}),
|
|
620
615
|
);
|
|
621
|
-
expect(mockStoreFeedbackToken).
|
|
622
|
-
"33333333-3333-4333-8333-333333333333",
|
|
623
|
-
"token",
|
|
624
|
-
"11111111-1111-4111-8111-111111111111",
|
|
625
|
-
);
|
|
616
|
+
expect(mockStoreFeedbackToken).not.toHaveBeenCalled();
|
|
626
617
|
expect(text).toContain("Playbook run 22222222-2222-4222-8222-222222222222");
|
|
627
618
|
expect(text).toContain("Charged: $1.25");
|
|
628
|
-
expect(text).toContain("Was this playbook useful?");
|
|
629
|
-
expect(text).toContain("favorite_playbook({ slug: \"competitor-ads\" })");
|
|
630
|
-
expect(text).toContain("rate_playbook
|
|
619
|
+
expect(text).not.toContain("Was this playbook useful?");
|
|
620
|
+
expect(text).not.toContain("favorite_playbook({ slug: \"competitor-ads\" })");
|
|
621
|
+
expect(text).not.toContain("rate_playbook");
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("uses one approved Link spend request across paid playbook child steps", async () => {
|
|
625
|
+
mockRequiresSpendConfirmation.mockReturnValue(false);
|
|
626
|
+
mockGetConfiguredMethods.mockReturnValue(["link"]);
|
|
627
|
+
mockApiGet
|
|
628
|
+
.mockResolvedValueOnce(playbook)
|
|
629
|
+
.mockResolvedValueOnce({
|
|
630
|
+
run_id: "22222222-2222-4222-8222-222222222222",
|
|
631
|
+
status: "completed",
|
|
632
|
+
playbook_id: playbook.id,
|
|
633
|
+
playbook_slug: playbook.slug,
|
|
634
|
+
playbook_version: 1,
|
|
635
|
+
budget_usd: 5,
|
|
636
|
+
quoted_cost_usd: 1.25,
|
|
637
|
+
charged_usd: 1.25,
|
|
638
|
+
refunded_usd: 0,
|
|
639
|
+
remaining_budget_usd: 3.75,
|
|
640
|
+
steps: [{
|
|
641
|
+
playbook_step_id: "ads",
|
|
642
|
+
step_index: 0,
|
|
643
|
+
node_type: "aw_agent",
|
|
644
|
+
agent_slug: "ad-strategy-intel",
|
|
645
|
+
agent_id: "11111111-1111-4111-8111-111111111111",
|
|
646
|
+
provider_id: null,
|
|
647
|
+
job_id: "33333333-3333-4333-8333-333333333333",
|
|
648
|
+
status: "succeeded",
|
|
649
|
+
quoted_cost_usd: 1.25,
|
|
650
|
+
charged_usd: 1.25,
|
|
651
|
+
refunded_usd: 0,
|
|
652
|
+
}],
|
|
653
|
+
});
|
|
654
|
+
mockApiPost.mockImplementation(async (path: string) => {
|
|
655
|
+
if (path === "/playbook-runs") {
|
|
656
|
+
return {
|
|
657
|
+
run_id: "22222222-2222-4222-8222-222222222222",
|
|
658
|
+
steps: playbook.current_quote.steps,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (path === "/agents/11111111-1111-4111-8111-111111111111/run") {
|
|
662
|
+
throw {
|
|
663
|
+
status: 402,
|
|
664
|
+
message: "payment required",
|
|
665
|
+
headers: new Headers({ "www-authenticate": "Payment challenge" }),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return { ok: true };
|
|
669
|
+
});
|
|
670
|
+
mockApiPostWithApprovedLinkSpendRequest.mockResolvedValueOnce({
|
|
671
|
+
status: "success",
|
|
672
|
+
job_id: "33333333-3333-4333-8333-333333333333",
|
|
673
|
+
agent_id: "11111111-1111-4111-8111-111111111111",
|
|
674
|
+
output: { rows: 3 },
|
|
675
|
+
cost: 1.25,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const { registerPlaybookTools } = await import("../playbooks.js");
|
|
679
|
+
const harness = makeServerHarness();
|
|
680
|
+
registerPlaybookTools(harness.server as never);
|
|
681
|
+
|
|
682
|
+
const result = await harness.handlers.get("run_playbook")!({
|
|
683
|
+
slug: "competitor-ads",
|
|
684
|
+
input: { domain: "notion.so" },
|
|
685
|
+
budget: 5,
|
|
686
|
+
pay_with: "link",
|
|
687
|
+
confirmed: true,
|
|
688
|
+
});
|
|
689
|
+
const text = flattenToolText(result);
|
|
690
|
+
|
|
691
|
+
expect(mockDecodeMppNetworkId).toHaveBeenCalledWith("Payment challenge");
|
|
692
|
+
expect(mockEnsureApprovedLinkSpendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
693
|
+
amount: "500",
|
|
694
|
+
currency: "usd",
|
|
695
|
+
networkId: "profile_test",
|
|
696
|
+
paymentMethodId: "csmrpd_test_123",
|
|
697
|
+
}));
|
|
698
|
+
expect(mockApiPostWithApprovedLinkSpendRequest).toHaveBeenCalledWith(
|
|
699
|
+
"/agents/11111111-1111-4111-8111-111111111111/run",
|
|
700
|
+
expect.objectContaining({
|
|
701
|
+
input: expect.objectContaining({
|
|
702
|
+
startUrls: [{ url: "https://notion.so" }],
|
|
703
|
+
}),
|
|
704
|
+
}),
|
|
705
|
+
"lsrq_test_123",
|
|
706
|
+
);
|
|
707
|
+
expect(mockApiPostWithPayment).not.toHaveBeenCalled();
|
|
708
|
+
expect(mockSetPendingLinkSpendRequest).toHaveBeenCalledWith(null);
|
|
709
|
+
expect(text).toContain("Playbook run 22222222-2222-4222-8222-222222222222");
|
|
631
710
|
});
|
|
632
711
|
|
|
633
712
|
it("uploads local file inputs before run creation and child execution", async () => {
|