@linkedclaw/cli 0.1.2 → 0.1.5
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/README.md +248 -48
- package/dist/bin.js +8099 -4778
- package/dist/bin.js.map +1 -1
- package/package.json +17 -32
- package/src/arena/api.ts +154 -0
- package/src/arena/hash.ts +15 -0
- package/src/arena/types.ts +106 -0
- package/src/bin.ts +33 -0
- package/src/commands/agent.ts +264 -0
- package/src/commands/arena.ts +393 -0
- package/src/commands/auth.ts +116 -0
- package/src/commands/converge.ts +969 -0
- package/src/commands/provider.ts +245 -0
- package/src/commands/requester.ts +479 -0
- package/src/config.ts +85 -0
- package/src/context.ts +27 -0
- package/src/converge/api.ts +213 -0
- package/src/converge/hash.ts +35 -0
- package/src/converge/lock.ts +30 -0
- package/src/converge/staging.ts +83 -0
- package/src/converge/types.ts +91 -0
- package/src/converge/workspace.ts +92 -0
- package/src/errors.ts +41 -0
- package/src/handlers/subprocess.ts +185 -0
- package/src/output.ts +57 -0
- package/src/types.ts +90 -0
- package/test/agent-help.test.ts +207 -0
- package/test/arena-api.test.ts +211 -0
- package/test/arena-commands.test.ts +559 -0
- package/test/arena-hash.test.ts +33 -0
- package/test/cli-help.test.ts +82 -0
- package/test/converge-accept.test.ts +206 -0
- package/test/converge-decision.test.ts +274 -0
- package/test/converge-hash.test.ts +58 -0
- package/test/converge-help.test.ts +58 -0
- package/test/converge-lock.test.ts +48 -0
- package/test/converge-review.test.ts +135 -0
- package/test/converge-run.test.ts +286 -0
- package/test/converge-staging.test.ts +161 -0
- package/test/converge-status.test.ts +141 -0
- package/test/converge-workspace.test.ts +92 -0
- package/test/hire-flags.test.ts +55 -0
- package/test/recv-flags.test.ts +83 -0
- package/test/register-browser.test.ts +55 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +25 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ApiError } from "../src/errors.js";
|
|
3
|
+
import { makeArenaApi } from "../src/arena/api.js";
|
|
4
|
+
|
|
5
|
+
function jsonResponse(body: unknown, init: { ok?: boolean; status?: number } = {}) {
|
|
6
|
+
return {
|
|
7
|
+
ok: init.ok ?? true,
|
|
8
|
+
status: init.status ?? 200,
|
|
9
|
+
json: vi.fn(async () => body),
|
|
10
|
+
} as any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("makeArenaApi", () => {
|
|
14
|
+
const fetchMock = vi.fn();
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
fetchMock.mockResolvedValue(jsonResponse({ ok: true }));
|
|
18
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.unstubAllGlobals();
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("register posts to the contestant registration route", async () => {
|
|
27
|
+
const api = makeArenaApi("http://services.test/", "k");
|
|
28
|
+
await api.register({
|
|
29
|
+
contestant_agent_id: "agt_1",
|
|
30
|
+
mandate_id: "mand_1",
|
|
31
|
+
category: { topic: "code", subtopic: "rust" },
|
|
32
|
+
});
|
|
33
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
34
|
+
"http://services.test/api/v1/arena/contestants/register",
|
|
35
|
+
expect.objectContaining({ method: "POST" }),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("createTournamentArena posts the exact manifest with idempotency header", async () => {
|
|
40
|
+
const api = makeArenaApi("http://services.test/", "k");
|
|
41
|
+
const manifest = {
|
|
42
|
+
mode: "tournament" as const,
|
|
43
|
+
category: { topic: "code", subtopic: "rust" },
|
|
44
|
+
config: {
|
|
45
|
+
bracket_shape: "single_elim",
|
|
46
|
+
child_mode: "task_submission",
|
|
47
|
+
advancement_rule: "top_k(1)",
|
|
48
|
+
bracket_size: 4,
|
|
49
|
+
seeding: "unseeded",
|
|
50
|
+
child_config: { prompt: "ship a patch" },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
await api.createTournamentArena(manifest, { idempotencyKey: "idem_123" });
|
|
54
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
55
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
56
|
+
"http://services.test/api/v1/arena/arenas",
|
|
57
|
+
expect.objectContaining({ method: "POST" }),
|
|
58
|
+
);
|
|
59
|
+
expect(init.headers).toMatchObject({
|
|
60
|
+
Authorization: "Bearer k",
|
|
61
|
+
"X-CSRF-Token": "k",
|
|
62
|
+
"Idempotency-Key": "idem_123",
|
|
63
|
+
});
|
|
64
|
+
expect(JSON.parse(init.body)).toEqual(manifest);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("offers uses GET and accepts pending matches", async () => {
|
|
68
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({
|
|
69
|
+
offers: [],
|
|
70
|
+
pending_matches: [{ match_id: "amch_123", prompt: "ship it" }],
|
|
71
|
+
}));
|
|
72
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
73
|
+
const response = await api.listOffers();
|
|
74
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
75
|
+
"http://services.test/api/v1/arena/offers",
|
|
76
|
+
expect.objectContaining({ method: "GET" }),
|
|
77
|
+
);
|
|
78
|
+
expect(response.pending_matches?.[0].match_id).toBe("amch_123");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("acceptOffer posts to the offer accept route", async () => {
|
|
82
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
83
|
+
await api.acceptOffer("aoff_123");
|
|
84
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
85
|
+
"http://services.test/api/v1/arena/offers/aoff_123/accept",
|
|
86
|
+
expect.objectContaining({ method: "POST" }),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("submit posts to the submissions route and strips only client-only submission_hash", async () => {
|
|
91
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
92
|
+
await api.submit("arn_123", {
|
|
93
|
+
offer_id: "aoff_123",
|
|
94
|
+
raw_content: "hello",
|
|
95
|
+
match_id: "amch_123",
|
|
96
|
+
seq: 1,
|
|
97
|
+
submission_hash: "sha256:" + "a".repeat(64),
|
|
98
|
+
});
|
|
99
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
100
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
101
|
+
"http://services.test/api/v1/arena/arenas/arn_123/submissions",
|
|
102
|
+
expect.objectContaining({ method: "POST" }),
|
|
103
|
+
);
|
|
104
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
105
|
+
offer_id: "aoff_123",
|
|
106
|
+
raw_content: "hello",
|
|
107
|
+
match_id: "amch_123",
|
|
108
|
+
seq: 1,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("commitJuror posts to the juror commit route with the exact body", async () => {
|
|
113
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
114
|
+
await api.commitJuror("arn_123", {
|
|
115
|
+
juror_agent_id: "agt_juror",
|
|
116
|
+
mandate_id: "mnd_123",
|
|
117
|
+
});
|
|
118
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
119
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
120
|
+
"http://services.test/api/v1/arena/arenas/arn_123/jurors/commit",
|
|
121
|
+
expect.objectContaining({ method: "POST" }),
|
|
122
|
+
);
|
|
123
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
124
|
+
juror_agent_id: "agt_juror",
|
|
125
|
+
mandate_id: "mnd_123",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("voteTask posts to the task juror vote route with the exact body", async () => {
|
|
130
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
131
|
+
await api.voteTask("arn_123", {
|
|
132
|
+
submission_id: "asub_123",
|
|
133
|
+
score: 0.75,
|
|
134
|
+
rationale_ref: "note://1",
|
|
135
|
+
});
|
|
136
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
137
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
138
|
+
"http://services.test/api/v1/arena/arenas/arn_123/juror-votes",
|
|
139
|
+
expect.objectContaining({ method: "POST" }),
|
|
140
|
+
);
|
|
141
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
142
|
+
submission_id: "asub_123",
|
|
143
|
+
score: 0.75,
|
|
144
|
+
rationale_ref: "note://1",
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("voteMatch posts to the match juror vote route with the exact body", async () => {
|
|
149
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
150
|
+
await api.voteMatch("arn_123", "amch_123", {
|
|
151
|
+
outcome: "a",
|
|
152
|
+
rationale_ref: "note://2",
|
|
153
|
+
});
|
|
154
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
155
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
156
|
+
"http://services.test/api/v1/arena/arenas/arn_123/matches/amch_123/juror-votes",
|
|
157
|
+
expect.objectContaining({ method: "POST" }),
|
|
158
|
+
);
|
|
159
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
160
|
+
outcome: "a",
|
|
161
|
+
rationale_ref: "note://2",
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("listArenas registered uses the registered query parameter", async () => {
|
|
166
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
167
|
+
await api.listArenas({ registered: true });
|
|
168
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
169
|
+
"http://services.test/api/v1/arena/arenas?registered=true",
|
|
170
|
+
expect.objectContaining({ method: "GET" }),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("getLeaderboard hits the leaderboard route", async () => {
|
|
175
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
176
|
+
await api.getLeaderboard("arn_123");
|
|
177
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
178
|
+
"http://services.test/api/v1/arena/arenas/arn_123/leaderboard",
|
|
179
|
+
expect.objectContaining({ method: "GET" }),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("getCategoryLeaderboard hits the category leaderboard route", async () => {
|
|
184
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
185
|
+
await api.getCategoryLeaderboard({ topic: "coding", subtopic: "patches" }, "match");
|
|
186
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
187
|
+
"http://services.test/api/v1/arena/leaderboard?category_topic=coding&category_subtopic=patches&mode=match",
|
|
188
|
+
expect.objectContaining({ method: "GET" }),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("sends authorization and content-type headers", async () => {
|
|
193
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
194
|
+
await api.listOffers();
|
|
195
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
196
|
+
expect(init.headers).toMatchObject({
|
|
197
|
+
Authorization: "Bearer k",
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("turns non-2xx responses into ApiError", async () => {
|
|
203
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ detail: "arena_not_found" }, { ok: false, status: 404 }));
|
|
204
|
+
const api = makeArenaApi("http://services.test", "k");
|
|
205
|
+
await expect(api.getLeaderboard("missing")).rejects.toMatchObject({
|
|
206
|
+
name: "ApiError",
|
|
207
|
+
status: 404,
|
|
208
|
+
detail: "arena_not_found",
|
|
209
|
+
} satisfies Partial<ApiError>);
|
|
210
|
+
});
|
|
211
|
+
});
|