@desplega.ai/agent-swarm 1.80.1 → 1.80.3
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 +3 -0
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/agentmail/types.ts +1 -0
- package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
- package/src/be/scripts/db.ts +34 -8
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/extract-schema.ts +55 -0
- package/src/be/scripts/typecheck.ts +7 -1
- package/src/commands/runner.ts +81 -10
- package/src/http/scripts.ts +7 -0
- package/src/http/webhooks.ts +9 -0
- package/src/http/workflows.ts +2 -15
- package/src/providers/claude-adapter.ts +1 -0
- package/src/providers/types.ts +8 -0
- package/src/scripts-runtime/eval-harness.ts +25 -1
- package/src/scripts-runtime/executors/native.ts +3 -0
- package/src/scripts-runtime/extract-args-schema.ts +69 -0
- package/src/scripts-runtime/import-allowlist.ts +1 -1
- package/src/tests/error-tracker.test.ts +44 -0
- package/src/tests/http-api-integration.test.ts +8 -4
- package/src/tests/rate-limit-event.test.ts +292 -0
- package/src/tests/scripts-http.test.ts +53 -0
- package/src/tests/scripts-runtime.test.ts +55 -0
- package/src/tests/workflow-triggers-v2.test.ts +261 -20
- package/src/types.ts +1 -0
- package/src/utils/error-tracker.ts +58 -0
- package/src/workflows/input.ts +7 -2
- package/src/workflows/triggers.ts +89 -9
|
@@ -6,6 +6,38 @@ import {
|
|
|
6
6
|
trackErrorFromJson,
|
|
7
7
|
} from "../utils/error-tracker";
|
|
8
8
|
|
|
9
|
+
describe("SessionErrorTracker — getRateLimitResetAt", () => {
|
|
10
|
+
test("returns undefined when no rate_limit_event was processed", () => {
|
|
11
|
+
const tracker = new SessionErrorTracker();
|
|
12
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns ISO string after a rejected rate_limit_event", () => {
|
|
16
|
+
const tracker = new SessionErrorTracker();
|
|
17
|
+
const futureResetsAtSec = Math.floor(Date.now() / 1000) + 3600;
|
|
18
|
+
tracker.processRateLimitEvent({
|
|
19
|
+
type: "rate_limit_event",
|
|
20
|
+
rate_limit_info: { status: "rejected", resetsAt: futureResetsAtSec },
|
|
21
|
+
});
|
|
22
|
+
const result = tracker.getRateLimitResetAt();
|
|
23
|
+
expect(result).toBeDefined();
|
|
24
|
+
expect(() => new Date(result!).toISOString()).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns undefined after only allowed/allowed_warning events", () => {
|
|
28
|
+
const tracker = new SessionErrorTracker();
|
|
29
|
+
tracker.processRateLimitEvent({
|
|
30
|
+
type: "rate_limit_event",
|
|
31
|
+
rate_limit_info: { status: "allowed", resetsAt: 1779202200 },
|
|
32
|
+
});
|
|
33
|
+
tracker.processRateLimitEvent({
|
|
34
|
+
type: "rate_limit_event",
|
|
35
|
+
rate_limit_info: { status: "allowed_warning", resetsAt: 1779202200 },
|
|
36
|
+
});
|
|
37
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
9
41
|
describe("SessionErrorTracker", () => {
|
|
10
42
|
test("hasErrors returns false when no errors tracked", () => {
|
|
11
43
|
const tracker = new SessionErrorTracker();
|
|
@@ -263,6 +295,18 @@ describe("trackErrorFromJson", () => {
|
|
|
263
295
|
trackErrorFromJson({ type: "content_block_delta", delta: {} }, tracker);
|
|
264
296
|
expect(tracker.hasErrors()).toBe(false);
|
|
265
297
|
});
|
|
298
|
+
|
|
299
|
+
test("rate_limit_event is not treated as an error signal", () => {
|
|
300
|
+
const tracker = new SessionErrorTracker();
|
|
301
|
+
trackErrorFromJson(
|
|
302
|
+
{
|
|
303
|
+
type: "rate_limit_event",
|
|
304
|
+
rate_limit_info: { status: "rejected", resetsAt: 1779202200 },
|
|
305
|
+
},
|
|
306
|
+
tracker,
|
|
307
|
+
);
|
|
308
|
+
expect(tracker.hasErrors()).toBe(false);
|
|
309
|
+
});
|
|
266
310
|
});
|
|
267
311
|
|
|
268
312
|
describe("parseStderrForErrors", () => {
|
|
@@ -1586,10 +1586,14 @@ describe("AgentMail Webhooks (with filters)", () => {
|
|
|
1586
1586
|
expect(body).toEqual({ received: true });
|
|
1587
1587
|
});
|
|
1588
1588
|
|
|
1589
|
-
test(
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1589
|
+
test.each([
|
|
1590
|
+
"message.received",
|
|
1591
|
+
"message.received.unauthenticated",
|
|
1592
|
+
])("accepts webhook for allowed event type '%s'", async (eventType) => {
|
|
1593
|
+
const { status } = await postWebhook({
|
|
1594
|
+
...makePayload({ inboxId: "support@y.xyz", from: "bob@b.com" }),
|
|
1595
|
+
event_type: eventType,
|
|
1596
|
+
});
|
|
1593
1597
|
expect(status).toBe(200);
|
|
1594
1598
|
});
|
|
1595
1599
|
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { SessionErrorTracker, trackErrorFromJson } from "../utils/error-tracker";
|
|
3
|
+
|
|
4
|
+
// Verbatim fixture from Linear CAI-1279 (session logs for task b7fbbdb9-4922-41d9-88ec-21febd6c4fec)
|
|
5
|
+
const FIXTURE_REJECTED = {
|
|
6
|
+
type: "rate_limit_event",
|
|
7
|
+
rate_limit_info: {
|
|
8
|
+
status: "rejected",
|
|
9
|
+
resetsAt: 1779202200, // seconds since epoch — 2026-05-19T14:50:00Z
|
|
10
|
+
rateLimitType: "five_hour",
|
|
11
|
+
overageStatus: "rejected",
|
|
12
|
+
overageDisabledReason: "group_zero_credit_limit",
|
|
13
|
+
isUsingOverage: false,
|
|
14
|
+
},
|
|
15
|
+
uuid: "ff6e5299-429c-4fcb-ab34-0ce4e8fa6202",
|
|
16
|
+
session_id: "69dbe5a1-1130-45eb-983f-58a7a13c9c3c",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("SessionErrorTracker — rate_limit_event processing", () => {
|
|
20
|
+
test("stashes resetsAt (seconds) correctly as ms — verbatim CAI-1279 fixture", () => {
|
|
21
|
+
const tracker = new SessionErrorTracker();
|
|
22
|
+
tracker.processRateLimitEvent(FIXTURE_REJECTED);
|
|
23
|
+
|
|
24
|
+
const result = tracker.getRateLimitResetAt();
|
|
25
|
+
expect(result).toBeDefined();
|
|
26
|
+
|
|
27
|
+
// resetsAt: 1779202200 sec → 2026-05-19T14:50:00.000Z
|
|
28
|
+
// But since we clamp to [now+60s, now+6h] and this is a past timestamp,
|
|
29
|
+
// the value will be clamped to now+60s. What matters is the sec→ms conversion works.
|
|
30
|
+
// We verify the unit is correct by checking that 1779202200 * 1000 = ms,
|
|
31
|
+
// which is NOT the same as treating it as ms (would be 1970-01-21).
|
|
32
|
+
const parsedMs = new Date(result!).getTime();
|
|
33
|
+
const nowMs = Date.now();
|
|
34
|
+
expect(parsedMs).toBeGreaterThanOrEqual(nowMs + 59_000); // clamped to at least now+60s
|
|
35
|
+
expect(parsedMs).toBeLessThanOrEqual(nowMs + 7 * 60 * 60 * 1000); // not absurdly far
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("resetsAt treated as seconds, not milliseconds (unit conversion boundary)", () => {
|
|
39
|
+
const tracker = new SessionErrorTracker();
|
|
40
|
+
// A future resetsAt value (in seconds) — 1 hour from now
|
|
41
|
+
const oneHourFromNowSec = Math.floor(Date.now() / 1000) + 3600;
|
|
42
|
+
tracker.processRateLimitEvent({
|
|
43
|
+
type: "rate_limit_event",
|
|
44
|
+
rate_limit_info: {
|
|
45
|
+
status: "rejected",
|
|
46
|
+
resetsAt: oneHourFromNowSec,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = tracker.getRateLimitResetAt();
|
|
51
|
+
expect(result).toBeDefined();
|
|
52
|
+
|
|
53
|
+
const parsedMs = new Date(result!).getTime();
|
|
54
|
+
const nowMs = Date.now();
|
|
55
|
+
// Should be ~1h from now (not 1970 if treated as ms, not year 57,000 if multiplied wrong)
|
|
56
|
+
expect(parsedMs).toBeGreaterThanOrEqual(nowMs + 50 * 60_000); // at least 50 min from now
|
|
57
|
+
expect(parsedMs).toBeLessThanOrEqual(nowMs + 70 * 60_000); // at most 70 min from now
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("status: rejected → stashes resetsAt", () => {
|
|
61
|
+
const tracker = new SessionErrorTracker();
|
|
62
|
+
const futureResetsAtSec = Math.floor(Date.now() / 1000) + 3600;
|
|
63
|
+
tracker.processRateLimitEvent({
|
|
64
|
+
type: "rate_limit_event",
|
|
65
|
+
rate_limit_info: { status: "rejected", resetsAt: futureResetsAtSec },
|
|
66
|
+
});
|
|
67
|
+
expect(tracker.getRateLimitResetAt()).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("status: allowed → does NOT stash (no cooldown needed)", () => {
|
|
71
|
+
const tracker = new SessionErrorTracker();
|
|
72
|
+
tracker.processRateLimitEvent({
|
|
73
|
+
type: "rate_limit_event",
|
|
74
|
+
rate_limit_info: { status: "allowed", resetsAt: 1779202200 },
|
|
75
|
+
});
|
|
76
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("status: allowed_warning → does NOT stash", () => {
|
|
80
|
+
const tracker = new SessionErrorTracker();
|
|
81
|
+
tracker.processRateLimitEvent({
|
|
82
|
+
type: "rate_limit_event",
|
|
83
|
+
rate_limit_info: { status: "allowed_warning", resetsAt: 1779202200 },
|
|
84
|
+
});
|
|
85
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("malformed event (missing rate_limit_info) → does NOT stash, no throw", () => {
|
|
89
|
+
const tracker = new SessionErrorTracker();
|
|
90
|
+
tracker.processRateLimitEvent({ type: "rate_limit_event" });
|
|
91
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("malformed event (resetsAt is string) → does NOT stash, no throw", () => {
|
|
95
|
+
const tracker = new SessionErrorTracker();
|
|
96
|
+
tracker.processRateLimitEvent({
|
|
97
|
+
type: "rate_limit_event",
|
|
98
|
+
rate_limit_info: { status: "rejected", resetsAt: "not-a-number" },
|
|
99
|
+
});
|
|
100
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("malformed event (resetsAt is negative) → does NOT stash", () => {
|
|
104
|
+
const tracker = new SessionErrorTracker();
|
|
105
|
+
tracker.processRateLimitEvent({
|
|
106
|
+
type: "rate_limit_event",
|
|
107
|
+
rate_limit_info: { status: "rejected", resetsAt: -1 },
|
|
108
|
+
});
|
|
109
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("resetsAt already in the past → clamped to now+60s (clock skew defense)", () => {
|
|
113
|
+
const tracker = new SessionErrorTracker();
|
|
114
|
+
// Use a known-past timestamp (year 2020)
|
|
115
|
+
tracker.processRateLimitEvent({
|
|
116
|
+
type: "rate_limit_event",
|
|
117
|
+
rate_limit_info: { status: "rejected", resetsAt: 1577836800 }, // 2020-01-01T00:00:00Z
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = tracker.getRateLimitResetAt();
|
|
121
|
+
expect(result).toBeDefined();
|
|
122
|
+
const parsedMs = new Date(result!).getTime();
|
|
123
|
+
const nowMs = Date.now();
|
|
124
|
+
expect(parsedMs).toBeGreaterThanOrEqual(nowMs + 59_000);
|
|
125
|
+
expect(parsedMs).toBeLessThanOrEqual(nowMs + 65_000);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("resetsAt absurdly far in future → clamped to now+6h (malformed defense)", () => {
|
|
129
|
+
const tracker = new SessionErrorTracker();
|
|
130
|
+
// Year 2099 in seconds
|
|
131
|
+
tracker.processRateLimitEvent({
|
|
132
|
+
type: "rate_limit_event",
|
|
133
|
+
rate_limit_info: { status: "rejected", resetsAt: 4102444800 }, // 2100-01-01 in seconds
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = tracker.getRateLimitResetAt();
|
|
137
|
+
expect(result).toBeDefined();
|
|
138
|
+
const parsedMs = new Date(result!).getTime();
|
|
139
|
+
const nowMs = Date.now();
|
|
140
|
+
const sixHoursMs = 6 * 60 * 60 * 1000;
|
|
141
|
+
expect(parsedMs).toBeLessThanOrEqual(nowMs + sixHoursMs + 1000); // within 6h (+1s tolerance)
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("multiple rate_limit_event lines → last rejected one wins", () => {
|
|
145
|
+
const tracker = new SessionErrorTracker();
|
|
146
|
+
const firstResetsAtSec = Math.floor(Date.now() / 1000) + 1800; // 30 min from now
|
|
147
|
+
const secondResetsAtSec = Math.floor(Date.now() / 1000) + 3600; // 60 min from now
|
|
148
|
+
|
|
149
|
+
tracker.processRateLimitEvent({
|
|
150
|
+
type: "rate_limit_event",
|
|
151
|
+
rate_limit_info: { status: "rejected", resetsAt: firstResetsAtSec },
|
|
152
|
+
});
|
|
153
|
+
tracker.processRateLimitEvent({
|
|
154
|
+
type: "rate_limit_event",
|
|
155
|
+
rate_limit_info: { status: "rejected", resetsAt: secondResetsAtSec },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = tracker.getRateLimitResetAt();
|
|
159
|
+
expect(result).toBeDefined();
|
|
160
|
+
const parsedMs = new Date(result!).getTime();
|
|
161
|
+
const nowMs = Date.now();
|
|
162
|
+
// Should reflect the SECOND event (~60 min), not the first (~30 min)
|
|
163
|
+
expect(parsedMs).toBeGreaterThanOrEqual(nowMs + 55 * 60_000);
|
|
164
|
+
expect(parsedMs).toBeLessThanOrEqual(nowMs + 65 * 60_000);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("allowed event between two rejected events → last rejected wins", () => {
|
|
168
|
+
const tracker = new SessionErrorTracker();
|
|
169
|
+
const firstSec = Math.floor(Date.now() / 1000) + 1800;
|
|
170
|
+
const secondSec = Math.floor(Date.now() / 1000) + 3600;
|
|
171
|
+
|
|
172
|
+
tracker.processRateLimitEvent({
|
|
173
|
+
type: "rate_limit_event",
|
|
174
|
+
rate_limit_info: { status: "rejected", resetsAt: firstSec },
|
|
175
|
+
});
|
|
176
|
+
tracker.processRateLimitEvent({
|
|
177
|
+
type: "rate_limit_event",
|
|
178
|
+
rate_limit_info: { status: "allowed", resetsAt: 9999999999 }, // should be ignored
|
|
179
|
+
});
|
|
180
|
+
tracker.processRateLimitEvent({
|
|
181
|
+
type: "rate_limit_event",
|
|
182
|
+
rate_limit_info: { status: "rejected", resetsAt: secondSec },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const result = tracker.getRateLimitResetAt();
|
|
186
|
+
expect(result).toBeDefined();
|
|
187
|
+
const parsedMs = new Date(result!).getTime();
|
|
188
|
+
const nowMs = Date.now();
|
|
189
|
+
// Should reflect the third (second rejected) event (~60 min)
|
|
190
|
+
expect(parsedMs).toBeGreaterThanOrEqual(nowMs + 55 * 60_000);
|
|
191
|
+
expect(parsedMs).toBeLessThanOrEqual(nowMs + 65 * 60_000);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("no rate_limit_event at all → getRateLimitResetAt returns undefined", () => {
|
|
195
|
+
const tracker = new SessionErrorTracker();
|
|
196
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("trackErrorFromJson — rate_limit_event routing", () => {
|
|
201
|
+
test("routes rate_limit_event to processRateLimitEvent, stashes reset time", () => {
|
|
202
|
+
const tracker = new SessionErrorTracker();
|
|
203
|
+
const futureResetsAtSec = Math.floor(Date.now() / 1000) + 3600;
|
|
204
|
+
|
|
205
|
+
trackErrorFromJson(
|
|
206
|
+
{
|
|
207
|
+
type: "rate_limit_event",
|
|
208
|
+
rate_limit_info: { status: "rejected", resetsAt: futureResetsAtSec },
|
|
209
|
+
},
|
|
210
|
+
tracker,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(tracker.getRateLimitResetAt()).toBeDefined();
|
|
214
|
+
// rate_limit_event itself is NOT an error signal — it's informational
|
|
215
|
+
expect(tracker.hasErrors()).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("rate_limit_event with allowed status → no reset stashed, no errors", () => {
|
|
219
|
+
const tracker = new SessionErrorTracker();
|
|
220
|
+
trackErrorFromJson(
|
|
221
|
+
{
|
|
222
|
+
type: "rate_limit_event",
|
|
223
|
+
rate_limit_info: { status: "allowed", resetsAt: 1779202200 },
|
|
224
|
+
},
|
|
225
|
+
tracker,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(tracker.getRateLimitResetAt()).toBeUndefined();
|
|
229
|
+
expect(tracker.hasErrors()).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("rate_limit_event does not block subsequent event processing", () => {
|
|
233
|
+
const tracker = new SessionErrorTracker();
|
|
234
|
+
const futureResetsAtSec = Math.floor(Date.now() / 1000) + 3600;
|
|
235
|
+
|
|
236
|
+
trackErrorFromJson(
|
|
237
|
+
{
|
|
238
|
+
type: "rate_limit_event",
|
|
239
|
+
rate_limit_info: { status: "rejected", resetsAt: futureResetsAtSec },
|
|
240
|
+
},
|
|
241
|
+
tracker,
|
|
242
|
+
);
|
|
243
|
+
trackErrorFromJson(
|
|
244
|
+
{ type: "result", is_error: true, result: "Your group's usage limit is set to $0" },
|
|
245
|
+
tracker,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(tracker.getRateLimitResetAt()).toBeDefined();
|
|
249
|
+
expect(tracker.hasErrors()).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("three-tier resolver logic (unit test via clamp helper)", () => {
|
|
254
|
+
// Mirrors the clampResetTime inline helper in runner.ts
|
|
255
|
+
function clampResetTime(isoString: string): string {
|
|
256
|
+
const nowMs = Date.now();
|
|
257
|
+
const minMs = nowMs + 60_000;
|
|
258
|
+
const maxMs = nowMs + 6 * 60 * 60 * 1000;
|
|
259
|
+
const candidateMs = new Date(isoString).getTime();
|
|
260
|
+
return new Date(Math.min(Math.max(candidateMs, minMs), maxMs)).toISOString();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
test("tier 1: rateLimitResetAt from structured event → used directly (after clamp)", () => {
|
|
264
|
+
const futureResetsAtSec = Math.floor(Date.now() / 1000) + 3600;
|
|
265
|
+
const tracker = new SessionErrorTracker();
|
|
266
|
+
tracker.processRateLimitEvent({
|
|
267
|
+
type: "rate_limit_event",
|
|
268
|
+
rate_limit_info: { status: "rejected", resetsAt: futureResetsAtSec },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const rateLimitResetAt = tracker.getRateLimitResetAt();
|
|
272
|
+
expect(rateLimitResetAt).toBeDefined();
|
|
273
|
+
|
|
274
|
+
// Simulate tier-1 branch: result.rateLimitResetAt is set
|
|
275
|
+
const rateLimitedUntil = clampResetTime(rateLimitResetAt!);
|
|
276
|
+
expect(rateLimitedUntil).toBeDefined();
|
|
277
|
+
const resolvedMs = new Date(rateLimitedUntil).getTime();
|
|
278
|
+
const nowMs = Date.now();
|
|
279
|
+
expect(resolvedMs).toBeGreaterThanOrEqual(nowMs + 59_000);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("tier 3 fallback: no structured event, no parseable message → 5-min default", () => {
|
|
283
|
+
// Simulate: rateLimitResetAt is undefined, parseRateLimitResetTime returns undefined
|
|
284
|
+
const defaultCooldownMs = 5 * 60 * 1000;
|
|
285
|
+
const rateLimitedUntil = new Date(Date.now() + defaultCooldownMs).toISOString();
|
|
286
|
+
|
|
287
|
+
const resolvedMs = new Date(rateLimitedUntil).getTime();
|
|
288
|
+
const nowMs = Date.now();
|
|
289
|
+
expect(resolvedMs).toBeGreaterThanOrEqual(nowMs + 4 * 60_000);
|
|
290
|
+
expect(resolvedMs).toBeLessThanOrEqual(nowMs + 6 * 60_000);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -347,4 +347,57 @@ describe("/api/scripts HTTP", () => {
|
|
|
347
347
|
expect(await del.json()).toEqual({ deleted: true });
|
|
348
348
|
expect(getScript({ name: "lookup-helper", scope: "agent", scopeId: workerId })).toBeNull();
|
|
349
349
|
});
|
|
350
|
+
|
|
351
|
+
test("script_query_types returns argsJsonSchema for a script with argsSchema export", async () => {
|
|
352
|
+
const source = `
|
|
353
|
+
import { z } from "zod";
|
|
354
|
+
export const argsSchema = z.object({
|
|
355
|
+
repo: z.string(),
|
|
356
|
+
limit: z.number().default(10),
|
|
357
|
+
});
|
|
358
|
+
export default async (args: z.infer<typeof argsSchema>) => ({ repo: args.repo });
|
|
359
|
+
`;
|
|
360
|
+
await upsert({ name: "schema-script", source });
|
|
361
|
+
|
|
362
|
+
const types = await dispatch("/api/scripts/schema-script/types", { agentId: workerId });
|
|
363
|
+
expect(types.status).toBe(200);
|
|
364
|
+
const body = (await types.json()) as { argsJsonSchema: unknown };
|
|
365
|
+
expect(body.argsJsonSchema).not.toBeNull();
|
|
366
|
+
expect(typeof body.argsJsonSchema).toBe("object");
|
|
367
|
+
// JSON Schema should describe the repo and limit properties
|
|
368
|
+
const schema = body.argsJsonSchema as { properties?: Record<string, unknown> };
|
|
369
|
+
expect(schema.properties).toHaveProperty("repo");
|
|
370
|
+
expect(schema.properties).toHaveProperty("limit");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("script_query_types returns argsJsonSchema: null for a script without argsSchema", async () => {
|
|
374
|
+
await upsert({ name: "no-schema-script", source: validSource(3) });
|
|
375
|
+
|
|
376
|
+
const types = await dispatch("/api/scripts/no-schema-script/types", { agentId: workerId });
|
|
377
|
+
expect(types.status).toBe(200);
|
|
378
|
+
const body = (await types.json()) as { argsJsonSchema: unknown };
|
|
379
|
+
expect(body.argsJsonSchema).toBeNull();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("script_search includes argsJsonSchema in results", async () => {
|
|
383
|
+
const source = `
|
|
384
|
+
import { z } from "zod";
|
|
385
|
+
export const argsSchema = z.object({ query: z.string() });
|
|
386
|
+
export default async (args: z.infer<typeof argsSchema>) => ({ result: args.query });
|
|
387
|
+
`;
|
|
388
|
+
await upsert({ name: "search-with-schema", source, description: "search result helper" });
|
|
389
|
+
|
|
390
|
+
const search = await dispatch("/api/scripts/search", {
|
|
391
|
+
method: "POST",
|
|
392
|
+
agentId: workerId,
|
|
393
|
+
body: JSON.stringify({ query: "search result helper", limit: 5 }),
|
|
394
|
+
});
|
|
395
|
+
expect(search.status).toBe(200);
|
|
396
|
+
const body = (await search.json()) as {
|
|
397
|
+
results: Array<{ name: string; argsJsonSchema: unknown }>;
|
|
398
|
+
};
|
|
399
|
+
const result = body.results.find((r) => r.name === "search-with-schema");
|
|
400
|
+
expect(result).toBeDefined();
|
|
401
|
+
expect(result?.argsJsonSchema).not.toBeNull();
|
|
402
|
+
});
|
|
350
403
|
});
|
|
@@ -286,4 +286,59 @@ describe("runScript", () => {
|
|
|
286
286
|
await Bun.$`rm -rf ${tmpdir}`;
|
|
287
287
|
}
|
|
288
288
|
});
|
|
289
|
+
|
|
290
|
+
test("argsSchema rejects invalid args with a formatted Zod error", async () => {
|
|
291
|
+
const output = await runScript({
|
|
292
|
+
agentId: "agent-1",
|
|
293
|
+
args: {},
|
|
294
|
+
resources,
|
|
295
|
+
source: `
|
|
296
|
+
import { z } from "zod";
|
|
297
|
+
export const argsSchema = z.object({
|
|
298
|
+
repo: z.string(),
|
|
299
|
+
});
|
|
300
|
+
export default async (args: z.infer<typeof argsSchema>) => ({ repo: args.repo });
|
|
301
|
+
`,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(output.error).toBeDefined();
|
|
305
|
+
expect(output.exitCode).not.toBe(0);
|
|
306
|
+
expect(output.stderr).toContain("argsSchema validation failed");
|
|
307
|
+
expect(output.stderr).toContain("repo");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("argsSchema applies .default() values when fields are omitted", async () => {
|
|
311
|
+
const output = await runScript({
|
|
312
|
+
agentId: "agent-1",
|
|
313
|
+
args: { repo: "owner/name" },
|
|
314
|
+
resources,
|
|
315
|
+
source: `
|
|
316
|
+
import { z } from "zod";
|
|
317
|
+
export const argsSchema = z.object({
|
|
318
|
+
repo: z.string(),
|
|
319
|
+
limit: z.number().default(10),
|
|
320
|
+
});
|
|
321
|
+
export default async (args: z.infer<typeof argsSchema>) => ({ repo: args.repo, limit: args.limit });
|
|
322
|
+
`,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(output.error).toBeUndefined();
|
|
326
|
+
expect(output.result).toEqual({ repo: "owner/name", limit: 10 });
|
|
327
|
+
expect(output.exitCode).toBe(0);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("script without argsSchema still works (backward-compat)", async () => {
|
|
331
|
+
const output = await runScript({
|
|
332
|
+
agentId: "agent-1",
|
|
333
|
+
args: { value: 42 },
|
|
334
|
+
resources,
|
|
335
|
+
source: `
|
|
336
|
+
export default async (args: { value: number }) => ({ doubled: args.value * 2 });
|
|
337
|
+
`,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(output.error).toBeUndefined();
|
|
341
|
+
expect(output.result).toEqual({ doubled: 84 });
|
|
342
|
+
expect(output.exitCode).toBe(0);
|
|
343
|
+
});
|
|
289
344
|
});
|