Skip to content

Commit 001d1a5

Browse files
Copilotfuxingloh
andcommitted
refactor: remove bare web command, use Headers class, real server in tests
Co-authored-by: fuxingloh <4266087+fuxingloh@users.noreply.github.com>
1 parent 68e5beb commit 001d1a5

3 files changed

Lines changed: 101 additions & 105 deletions

File tree

packages/use-agently/src/cli.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { whoamiCommand } from "./commands/whoami";
44
import { balanceCommand } from "./commands/balance";
55
import { agentsCommand } from "./commands/agents";
66
import { a2aCommand, a2aCardCommand } from "./commands/a2a";
7-
import { webCommand, webGetCommand, webPutCommand, webDeleteCommand } from "./commands/web";
7+
import { webGetCommand, webPutCommand, webDeleteCommand } from "./commands/web";
88
import { doctorCommand } from "./commands/doctor";
99
import { updateCommand } from "./commands/update";
1010

@@ -34,7 +34,6 @@ cli.addCommand(agentsCommand.helpGroup("Discovery"));
3434
// Protocols
3535
cli.addCommand(a2aCommand.helpGroup("Protocols"));
3636
cli.addCommand(a2aCardCommand.helpGroup("Protocols"));
37-
cli.addCommand(webCommand.helpGroup("Protocols"));
3837
cli.addCommand(webGetCommand.helpGroup("Protocols"));
3938
cli.addCommand(webPutCommand.helpGroup("Protocols"));
4039
cli.addCommand(webDeleteCommand.helpGroup("Protocols"));
Lines changed: 92 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,130 @@
1-
import { describe, expect, mock, test } from "bun:test";
1+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
22
import { captureOutput, mockConfigModule } from "../testing";
33

44
mockConfigModule();
55

6-
// Mock the createPaymentFetch to return a controlled fetch
7-
mock.module("../client", () => ({
8-
createPaymentFetch: () => mockFetch,
9-
createA2AClient: async () => ({}),
10-
}));
11-
12-
let mockFetch: typeof fetch;
13-
146
const { cli } = await import("../cli");
157

16-
describe("web command", () => {
17-
const out = captureOutput();
8+
interface RequestRecord {
9+
method: string;
10+
url: string;
11+
headers: Record<string, string>;
12+
body: string;
13+
}
14+
15+
let lastRequest: RequestRecord | null = null;
16+
let nextResponse: { status: number; body: string; contentType?: string } = {
17+
status: 200,
18+
body: JSON.stringify({ ok: true }),
19+
};
20+
21+
const server = Bun.serve({
22+
port: 0,
23+
fetch(req) {
24+
return req.text().then((body) => {
25+
lastRequest = {
26+
method: req.method,
27+
url: req.url,
28+
headers: Object.fromEntries(req.headers.entries()),
29+
body,
30+
};
31+
return new Response(nextResponse.body, {
32+
status: nextResponse.status,
33+
headers: { "content-type": nextResponse.contentType ?? "application/json" },
34+
});
35+
});
36+
},
37+
});
1838

19-
test("web GET returns JSON response as YAML text", async () => {
20-
mockFetch = async (_url: string | URL | Request, _init?: RequestInit) =>
21-
new Response(JSON.stringify({ hello: "world" }), { status: 200 });
39+
const baseUrl = `http://localhost:${server.port}`;
2240

23-
await cli.parseAsync(["test", "use-agently", "web", "https://example.com/api"]);
24-
expect(out.yaml).toEqual({ hello: "world" });
25-
});
26-
27-
test("web GET returns plain text response", async () => {
28-
mockFetch = async (_url: string | URL | Request, _init?: RequestInit) =>
29-
new Response("plain text response", { status: 200 });
41+
beforeEach(() => {
42+
lastRequest = null;
43+
nextResponse = { status: 200, body: JSON.stringify({ ok: true }) };
44+
});
3045

31-
await cli.parseAsync(["test", "use-agently", "web", "https://example.com/api"]);
32-
expect(out.stdout).toBe("plain text response");
33-
});
46+
afterAll(() => {
47+
server.stop();
3448
});
3549

3650
describe("web:get command", () => {
3751
const out = captureOutput();
3852

39-
test("GET request sends correct method", async () => {
40-
let capturedMethod: string | undefined;
41-
mockFetch = async (_url: string | URL | Request, init?: RequestInit) => {
42-
capturedMethod = init?.method;
43-
return new Response(JSON.stringify({ ok: true }), { status: 200 });
44-
};
45-
46-
await cli.parseAsync(["test", "use-agently", "web:get", "https://example.com/api"]);
47-
expect(capturedMethod).toBe("GET");
48-
expect(out.yaml).toEqual({ ok: true });
53+
test("sends GET request and outputs JSON response as YAML", async () => {
54+
nextResponse = { status: 200, body: JSON.stringify({ hello: "world" }) };
55+
await cli.parseAsync(["test", "use-agently", "web:get", `${baseUrl}/api`]);
56+
expect(lastRequest?.method).toBe("GET");
57+
expect(out.yaml).toEqual({ hello: "world" });
4958
});
5059

51-
test("GET with --header sends custom headers", async () => {
52-
let capturedHeaders: Record<string, string> = {};
53-
mockFetch = async (_url: string | URL | Request, init?: RequestInit) => {
54-
capturedHeaders = Object.fromEntries(new Headers(init?.headers as HeadersInit).entries());
55-
return new Response(JSON.stringify({}), { status: 200 });
56-
};
57-
58-
await cli.parseAsync(["test", "use-agently", "web:get", "https://example.com/api", "-H", "X-Custom: value"]);
59-
expect(capturedHeaders["x-custom"]).toBe("value");
60+
test("sends custom header", async () => {
61+
nextResponse = { status: 200, body: JSON.stringify({}) };
62+
await cli.parseAsync(["test", "use-agently", "web:get", `${baseUrl}/api`, "-H", "X-Custom: test-value"]);
63+
expect(lastRequest?.headers["x-custom"]).toBe("test-value");
6064
});
6165

62-
test("json output returns response as JSON", async () => {
63-
mockFetch = async (_url: string | URL | Request, _init?: RequestInit) =>
64-
new Response(JSON.stringify({ status: "ok" }), { status: 200 });
65-
66-
await cli.parseAsync(["test", "use-agently", "-o", "json", "web:get", "https://example.com/api"]);
66+
test("json output format", async () => {
67+
nextResponse = { status: 200, body: JSON.stringify({ status: "ok" }) };
68+
await cli.parseAsync(["test", "use-agently", "-o", "json", "web:get", `${baseUrl}/api`]);
6769
expect(out.json).toEqual({ status: "ok" });
6870
});
71+
72+
test("outputs plain text for non-JSON response", async () => {
73+
const textServer = Bun.serve({
74+
port: 0,
75+
fetch() {
76+
return new Response("plain text", { status: 200, headers: { "content-type": "text/plain" } });
77+
},
78+
});
79+
try {
80+
await cli.parseAsync(["test", "use-agently", "web:get", `http://localhost:${textServer.port}/text`]);
81+
expect(out.stdout).toBe("plain text");
82+
} finally {
83+
textServer.stop();
84+
}
85+
});
6986
});
7087

7188
describe("web:put command", () => {
7289
const out = captureOutput();
7390

74-
test("PUT request sends correct method and body", async () => {
75-
let capturedMethod: string | undefined;
76-
let capturedBody: string | undefined;
77-
mockFetch = async (_url: string | URL | Request, init?: RequestInit) => {
78-
capturedMethod = init?.method;
79-
capturedBody = init?.body as string;
80-
return new Response(JSON.stringify({ updated: true }), { status: 200 });
81-
};
82-
83-
await cli.parseAsync(["test", "use-agently", "web:put", "https://example.com/api", "-d", '{"key":"value"}']);
84-
expect(capturedMethod).toBe("PUT");
85-
expect(capturedBody).toBe('{"key":"value"}');
91+
test("sends PUT request with body", async () => {
92+
nextResponse = { status: 200, body: JSON.stringify({ updated: true }) };
93+
await cli.parseAsync(["test", "use-agently", "web:put", `${baseUrl}/resource`, "-d", '{"key":"value"}']);
94+
expect(lastRequest?.method).toBe("PUT");
95+
expect(lastRequest?.body).toBe('{"key":"value"}');
8696
expect(out.yaml).toEqual({ updated: true });
8797
});
8898

89-
test("PUT with data sets Content-Type header automatically", async () => {
90-
let capturedHeaders: Record<string, string> = {};
91-
mockFetch = async (_url: string | URL | Request, init?: RequestInit) => {
92-
capturedHeaders = Object.fromEntries(new Headers(init?.headers as HeadersInit).entries());
93-
return new Response(JSON.stringify({}), { status: 200 });
94-
};
99+
test("auto-sets content-type header when --data is provided", async () => {
100+
nextResponse = { status: 200, body: JSON.stringify({}) };
101+
await cli.parseAsync(["test", "use-agently", "web:put", `${baseUrl}/resource`, "-d", '{"a":1}']);
102+
expect(lastRequest?.headers["content-type"]).toBe("application/json");
103+
});
95104

96-
await cli.parseAsync(["test", "use-agently", "web:put", "https://example.com/api", "-d", '{"a":1}']);
97-
expect(capturedHeaders["content-type"]).toBe("application/json");
105+
test("does not override explicit content-type header", async () => {
106+
nextResponse = { status: 200, body: JSON.stringify({}) };
107+
await cli.parseAsync([
108+
"test",
109+
"use-agently",
110+
"web:put",
111+
`${baseUrl}/resource`,
112+
"-H",
113+
"content-type: text/plain",
114+
"-d",
115+
"raw body",
116+
]);
117+
expect(lastRequest?.headers["content-type"]).toBe("text/plain");
98118
});
99119
});
100120

101121
describe("web:delete command", () => {
102122
const out = captureOutput();
103123

104-
test("DELETE request sends correct method", async () => {
105-
let capturedMethod: string | undefined;
106-
mockFetch = async (_url: string | URL | Request, init?: RequestInit) => {
107-
capturedMethod = init?.method;
108-
return new Response(JSON.stringify({ deleted: true }), { status: 200 });
109-
};
110-
111-
await cli.parseAsync(["test", "use-agently", "web:delete", "https://example.com/resource/123"]);
112-
expect(capturedMethod).toBe("DELETE");
124+
test("sends DELETE request", async () => {
125+
nextResponse = { status: 200, body: JSON.stringify({ deleted: true }) };
126+
await cli.parseAsync(["test", "use-agently", "web:delete", `${baseUrl}/resource/123`]);
127+
expect(lastRequest?.method).toBe("DELETE");
113128
expect(out.yaml).toEqual({ deleted: true });
114129
});
115130
});

packages/use-agently/src/commands/web.ts

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,19 @@ interface WebOptions {
99
data?: string;
1010
}
1111

12-
function parseHeaders(headerArgs: string[]): Record<string, string> {
13-
const headers: Record<string, string> = {};
14-
for (const h of headerArgs) {
15-
const idx = h.indexOf(":");
16-
if (idx > -1) {
17-
headers[h.slice(0, idx).trim().toLowerCase()] = h.slice(idx + 1).trim();
18-
}
19-
}
20-
return headers;
21-
}
22-
2312
async function executeWebRequest(url: string, method: string, options: WebOptions, command: Command): Promise<void> {
2413
const config = await getConfigOrThrow();
2514
const wallet = loadWallet(config.wallet);
2615
const paymentFetch = createPaymentFetch(wallet);
2716

28-
const headers = parseHeaders(options.header ?? []);
29-
if (options.data && !headers["content-type"]) {
30-
headers["content-type"] = "application/json";
17+
const headers = new Headers(
18+
(options.header ?? []).flatMap((h) => {
19+
const idx = h.indexOf(":");
20+
return idx > -1 ? ([[h.slice(0, idx).trim(), h.slice(idx + 1).trim()]] as [[string, string]]) : [];
21+
}),
22+
);
23+
if (options.data && !headers.has("content-type")) {
24+
headers.set("content-type", "application/json");
3125
}
3226

3327
const init: RequestInit = { method, headers };
@@ -61,18 +55,6 @@ const webOptions = (cmd: Command) =>
6155
)
6256
.option("-d, --data <body>", "Request body");
6357

64-
export const webCommand = webOptions(
65-
new Command("web")
66-
.description("Make an HTTP GET request (x402 payments handled automatically)")
67-
.argument("<url>", "URL to request")
68-
.addHelpText(
69-
"after",
70-
'\nExamples:\n use-agently web https://example.com/api\n use-agently web https://example.com/api -H "Authorization: Bearer token"',
71-
),
72-
).action(async (url: string, options: WebOptions, command: Command) => {
73-
await executeWebRequest(url, "GET", options, command);
74-
});
75-
7658
export const webGetCommand = webOptions(
7759
new Command("web:get")
7860
.description("Make an HTTP GET request (x402 payments handled automatically)")

0 commit comments

Comments
 (0)