@openparachute/agent 0.2.3-rc.2 → 0.2.3-rc.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.
Files changed (54) hide show
  1. package/package.json +4 -1
  2. package/src/transports/vault.ts +19 -1
  3. package/src/_parked/interactive-spawn.test.ts +0 -324
  4. package/src/_parked/interactive-spawn.ts +0 -701
  5. package/src/agent-defs.test.ts +0 -1504
  6. package/src/agent-mcp-config.test.ts +0 -115
  7. package/src/agents.test.ts +0 -360
  8. package/src/auth.test.ts +0 -46
  9. package/src/backends/attached-queue.test.ts +0 -376
  10. package/src/backends/programmatic.test.ts +0 -1715
  11. package/src/backends/registry.test.ts +0 -1494
  12. package/src/backends/stream-json.test.ts +0 -570
  13. package/src/channel-backend-wiring.test.ts +0 -237
  14. package/src/credentials.test.ts +0 -274
  15. package/src/cron.test.ts +0 -342
  16. package/src/daemon-agent-def-api.test.ts +0 -166
  17. package/src/daemon-agent-defs-api.test.ts +0 -953
  18. package/src/daemon-agent-env-api.test.ts +0 -338
  19. package/src/daemon-attached-queue-store.test.ts +0 -65
  20. package/src/daemon-config-api.test.ts +0 -962
  21. package/src/daemon-jobs-api.test.ts +0 -271
  22. package/src/daemon-vault-chat.test.ts +0 -250
  23. package/src/daemon.test.ts +0 -746
  24. package/src/def-vaults.test.ts +0 -136
  25. package/src/delivery-state.test.ts +0 -110
  26. package/src/effective-env.test.ts +0 -114
  27. package/src/grants.test.ts +0 -638
  28. package/src/hub-jwt.test.ts +0 -161
  29. package/src/jobs.test.ts +0 -245
  30. package/src/mcp-http.test.ts +0 -265
  31. package/src/mint-token.test.ts +0 -152
  32. package/src/module-manifest.test.ts +0 -158
  33. package/src/programmatic-wiring.test.ts +0 -838
  34. package/src/registry.test.ts +0 -227
  35. package/src/resolve-port.test.ts +0 -64
  36. package/src/routing.test.ts +0 -184
  37. package/src/runner.test.ts +0 -506
  38. package/src/sandbox/config.test.ts +0 -150
  39. package/src/sandbox/egress.test.ts +0 -113
  40. package/src/sandbox/live-seatbelt.test.ts +0 -277
  41. package/src/sandbox/mounts.test.ts +0 -154
  42. package/src/sandbox/sandbox.test.ts +0 -168
  43. package/src/services-manifest.test.ts +0 -106
  44. package/src/spa-serve.test.ts +0 -116
  45. package/src/spawn-agent-cli.test.ts +0 -172
  46. package/src/spawn-agent.test.ts +0 -1218
  47. package/src/spawn-deps.test.ts +0 -54
  48. package/src/terminal-assets.test.ts +0 -50
  49. package/src/terminal.test.ts +0 -530
  50. package/src/transports/http-ui.test.ts +0 -455
  51. package/src/transports/telegram.test.ts +0 -174
  52. package/src/transports/vault.test.ts +0 -2012
  53. package/src/ui-kit.test.ts +0 -178
  54. package/web/ui/tsconfig.json +0 -21
package/src/cron.test.ts DELETED
@@ -1,342 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { parseCron, nextRunAfter, CronParseError, type ParsedCron } from "./cron.ts";
3
-
4
- /** Helper: the wall-clock fields of an instant in a tz (for asserting matches). */
5
- function wall(date: Date, tz: string): { y: number; mo: number; d: number; h: number; mi: number; wd: number } {
6
- const p = new Intl.DateTimeFormat("en-US", {
7
- timeZone: tz,
8
- hour12: false,
9
- weekday: "short",
10
- year: "numeric",
11
- month: "numeric",
12
- day: "numeric",
13
- hour: "numeric",
14
- minute: "numeric",
15
- }).formatToParts(date);
16
- const g = (t: string) => p.find((x) => x.type === t)?.value ?? "";
17
- const WD: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
18
- let h = parseInt(g("hour"), 10);
19
- if (h === 24) h = 0;
20
- return {
21
- y: parseInt(g("year"), 10),
22
- mo: parseInt(g("month"), 10),
23
- d: parseInt(g("day"), 10),
24
- h,
25
- mi: parseInt(g("minute"), 10),
26
- wd: WD[g("weekday")] ?? -1,
27
- };
28
- }
29
-
30
- describe("parseCron — field parsing", () => {
31
- test("a fully-wild expression matches every minute", () => {
32
- const p = parseCron("* * * * *");
33
- expect(p.minute.size).toBe(60);
34
- expect(p.hour.size).toBe(24);
35
- expect(p.dom.size).toBe(31);
36
- expect(p.month.size).toBe(12);
37
- expect(p.dow.size).toBe(7);
38
- expect(p.domStar).toBe(true);
39
- expect(p.dowStar).toBe(true);
40
- });
41
-
42
- test("concrete fields parse to single-value sets", () => {
43
- const p = parseCron("53 7 * * *");
44
- expect([...p.minute]).toEqual([53]);
45
- expect([...p.hour]).toEqual([7]);
46
- expect(p.domStar).toBe(true);
47
- expect(p.dowStar).toBe(true);
48
- });
49
-
50
- test("step (*​/15) expands to the stepped set", () => {
51
- const p = parseCron("*/15 * * * *");
52
- expect([...p.minute].sort((a, b) => a - b)).toEqual([0, 15, 30, 45]);
53
- // A stepped minute field is NOT a bare star — but minute has no union rule,
54
- // so what matters is the value set. (domStar/dowStar only track dom/dow.)
55
- });
56
-
57
- test("ranges expand inclusively (1-5)", () => {
58
- const p = parseCron("0 9 * * 1-5");
59
- expect([...p.dow].sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5]);
60
- expect(p.dowStar).toBe(false);
61
- });
62
-
63
- test("comma lists union their elements (0,30)", () => {
64
- const p = parseCron("0,30 * * * *");
65
- expect([...p.minute].sort((a, b) => a - b)).toEqual([0, 30]);
66
- });
67
-
68
- test("range + step (0-30/10)", () => {
69
- const p = parseCron("0-30/10 * * * *");
70
- expect([...p.minute].sort((a, b) => a - b)).toEqual([0, 10, 20, 30]);
71
- });
72
-
73
- test("bare number with step extends to field max (5/2 in hour)", () => {
74
- const p = parseCron("0 5/2 * * *");
75
- expect([...p.hour].sort((a, b) => a - b)).toEqual([5, 7, 9, 11, 13, 15, 17, 19, 21, 23]);
76
- });
77
- });
78
-
79
- describe("parseCron — error cases", () => {
80
- test("wrong field count throws", () => {
81
- expect(() => parseCron("* * * *")).toThrow(CronParseError);
82
- expect(() => parseCron("* * * * * *")).toThrow(/5 fields/);
83
- });
84
- test("out-of-range value throws (minute 60)", () => {
85
- expect(() => parseCron("60 * * * *")).toThrow(/out of range/);
86
- });
87
- test("out-of-range hour throws (24)", () => {
88
- expect(() => parseCron("0 24 * * *")).toThrow(/out of range/);
89
- });
90
- test("dow 7 is rejected in v1 (Sunday is 0 only)", () => {
91
- expect(() => parseCron("0 0 * * 7")).toThrow(/out of range/);
92
- });
93
- test("non-numeric value throws", () => {
94
- expect(() => parseCron("MON * * * *")).toThrow(CronParseError);
95
- });
96
- test("descending range throws", () => {
97
- expect(() => parseCron("0 9-5 * * *")).toThrow(/descending/);
98
- });
99
- test("zero step throws", () => {
100
- expect(() => parseCron("*/0 * * * *")).toThrow(/step/);
101
- });
102
- test("empty field throws", () => {
103
- expect(() => parseCron("0 , * * *")).toThrow(CronParseError);
104
- });
105
- });
106
-
107
- describe("nextRunAfter — strict advance + concrete schedules", () => {
108
- const LA = "America/Los_Angeles";
109
-
110
- test("'53 7 * * *' in LA returns 07:53 LA, strictly after `from`", () => {
111
- // from = 2026-06-17 06:00 LA → next is 07:53 the SAME day.
112
- const from = new Date("2026-06-17T13:00:00Z"); // 06:00 LA (PDT, UTC-7)
113
- const next = nextRunAfter("53 7 * * *", LA, from)!;
114
- expect(next).not.toBeNull();
115
- const w = wall(next, LA);
116
- expect(w.h).toBe(7);
117
- expect(w.mi).toBe(53);
118
- expect(w.d).toBe(17);
119
- expect(next.getTime()).toBeGreaterThan(from.getTime());
120
- });
121
-
122
- test("'53 7 * * *' when already past today rolls to tomorrow", () => {
123
- const from = new Date("2026-06-17T15:00:00Z"); // 08:00 LA, past 07:53
124
- const next = nextRunAfter("53 7 * * *", LA, from)!;
125
- const w = wall(next, LA);
126
- expect(w.h).toBe(7);
127
- expect(w.mi).toBe(53);
128
- expect(w.d).toBe(18); // tomorrow
129
- });
130
-
131
- test("hourly '0 * * * *' returns the next top-of-hour", () => {
132
- const from = new Date("2026-06-17T10:17:00Z");
133
- const next = nextRunAfter("0 * * * *", "UTC", from)!;
134
- expect(next.toISOString()).toBe("2026-06-17T11:00:00.000Z");
135
- });
136
-
137
- test("never returns `from` itself even when `from` is an exact match (no double-fire)", () => {
138
- // from is exactly 11:00:00 UTC, which '0 * * * *' matches — must advance to 12:00.
139
- const from = new Date("2026-06-17T11:00:00Z");
140
- const next = nextRunAfter("0 * * * *", "UTC", from)!;
141
- expect(next.toISOString()).toBe("2026-06-17T12:00:00.000Z");
142
- expect(next.getTime()).toBeGreaterThan(from.getTime());
143
- });
144
-
145
- test("'*​/15 * * * *' steps to the next quarter-hour", () => {
146
- const from = new Date("2026-06-17T10:07:30Z");
147
- const next = nextRunAfter("*/15 * * * *", "UTC", from)!;
148
- expect(next.toISOString()).toBe("2026-06-17T10:15:00.000Z");
149
- });
150
-
151
- test("'0 9 * * 1-5' (weekday 9am) skips the weekend", () => {
152
- // 2026-06-19 is a Friday; from = Fri 10:00 UTC → next match is Mon 09:00.
153
- const fri = new Date("2026-06-19T10:00:00Z");
154
- expect(wall(fri, "UTC").wd).toBe(5); // Friday
155
- const next = nextRunAfter("0 9 * * 1-5", "UTC", fri)!;
156
- const w = wall(next, "UTC");
157
- expect(w.wd).toBe(1); // Monday
158
- expect(w.h).toBe(9);
159
- expect(w.d).toBe(22); // 2026-06-22 is the Monday
160
- });
161
-
162
- test("end-of-month rollover ('0 0 1 * *' → first of next month)", () => {
163
- const from = new Date("2026-01-31T12:00:00Z");
164
- const next = nextRunAfter("0 0 1 * *", "UTC", from)!;
165
- expect(next.toISOString()).toBe("2026-02-01T00:00:00.000Z");
166
- });
167
-
168
- test("end-of-year rollover ('0 0 1 1 *' → next Jan 1)", () => {
169
- const from = new Date("2026-12-31T23:59:00Z");
170
- const next = nextRunAfter("0 0 1 1 *", "UTC", from)!;
171
- expect(next.toISOString()).toBe("2027-01-01T00:00:00.000Z");
172
- });
173
-
174
- test("leap day ('0 0 29 2 *' fires on Feb 29 2028, skips non-leap years)", () => {
175
- // From mid-2026, the next Feb 29 is in 2028 (2026/2027 are not leap years).
176
- const from = new Date("2026-06-01T00:00:00Z");
177
- const next = nextRunAfter("0 0 29 2 *", "UTC", from)!;
178
- const w = wall(next, "UTC");
179
- expect(w.y).toBe(2028);
180
- expect(w.mo).toBe(2);
181
- expect(w.d).toBe(29);
182
- });
183
-
184
- test("dom AND dow both restricted → matches on EITHER (cron union rule)", () => {
185
- // '0 0 13 * 5' = midnight on the 13th OR any Friday. From 2026-02-01:
186
- // the first Friday (the 6th) comes before the 13th.
187
- const from = new Date("2026-02-01T00:00:00Z");
188
- const next = nextRunAfter("0 0 13 * 5", "UTC", from)!;
189
- const w = wall(next, "UTC");
190
- // 2026-02-06 is a Friday → that's the union hit before the 13th.
191
- expect(w.d).toBe(6);
192
- expect(w.wd).toBe(5);
193
- });
194
-
195
- test("a tz offset actually shifts the fire instant (UTC vs LA differ)", () => {
196
- const from = new Date("2026-06-17T00:00:00Z");
197
- const utc = nextRunAfter("0 12 * * *", "UTC", from)!;
198
- const la = nextRunAfter("0 12 * * *", LA, from)!;
199
- // 12:00 LA (PDT, UTC-7) is 19:00Z; 12:00 UTC is 12:00Z. They must differ by 7h.
200
- expect(utc.toISOString()).toBe("2026-06-17T12:00:00.000Z");
201
- expect(la.toISOString()).toBe("2026-06-17T19:00:00.000Z");
202
- });
203
-
204
- test("defaults: no tz uses local zone; no `from` uses now (returns a future instant)", () => {
205
- const next = nextRunAfter("* * * * *");
206
- expect(next).not.toBeNull();
207
- expect(next!.getTime()).toBeGreaterThan(Date.now() - 1000);
208
- });
209
-
210
- test("an invalid IANA tz throws (not silently UTC)", () => {
211
- expect(() => nextRunAfter("* * * * *", "Not/AZone", new Date())).toThrow();
212
- });
213
- });
214
-
215
- describe("nextRunAfter — DST behavior (documented v1)", () => {
216
- const LA = "America/Los_Angeles";
217
-
218
- test("spring-forward: a 02:30 spec is SKIPPED on the gap day, fires next day", () => {
219
- // 2026-03-08 is the US spring-forward (02:00 → 03:00 PST→PDT). 02:30 never
220
- // occurs that day. from = 2026-03-08 00:00 LA.
221
- const from = new Date("2026-03-08T08:00:00Z"); // 00:00 LA (PST, UTC-8)
222
- const next = nextRunAfter("30 2 * * *", LA, from)!;
223
- const w = wall(next, LA);
224
- // The 02:30 wall time does not exist on the 8th → first match is the 9th.
225
- expect(w.d).toBe(9);
226
- expect(w.h).toBe(2);
227
- expect(w.mi).toBe(30);
228
- });
229
-
230
- test("fall-back: a 01:30 spec yields a valid instant (both repeats are real instants)", () => {
231
- // 2026-11-01 is the US fall-back (02:00 → 01:00 PDT→PST). 01:30 occurs twice.
232
- // We only assert nextRunAfter returns a real matching instant (v1 fires on the
233
- // first one it walks to); the dual-fire behavior is documented, not policed.
234
- const from = new Date("2026-11-01T07:00:00Z"); // 00:00 LA (PDT, UTC-7)
235
- const next = nextRunAfter("30 1 * * *", LA, from)!;
236
- const w = wall(next, LA);
237
- expect(w.d).toBe(1);
238
- expect(w.h).toBe(1);
239
- expect(w.mi).toBe(30);
240
- });
241
- });
242
-
243
- // ===========================================================================
244
- // REGRESSION GUARD — day-skip in a NEGATIVE-OFFSET timezone (the morning-miss bug).
245
- //
246
- // The day-skip fast path (taken whenever dom/dow restricts the date) used to jump
247
- // to UTC-midnight, which in America/Los_Angeles is ~17:00 of the wall-day; the
248
- // forward-only crawl then started in the EVENING and could never reach that
249
- // wall-day's MORNING. Result: morning weekday/sparse-dom jobs in PT were missed
250
- // (skipped to a later day) or never fired at all (returned null). These cases
251
- // FAIL on the old UTC-midnight code and PASS on the wall-midnight fix. They're
252
- // all in a negative-offset tz (where UTC-midnight ≠ wall-midnight) — the only
253
- // place the bug is visible — and they all exercise the day-skip (restricted dom
254
- // or dow), unlike the `*`-dom/dow cases above which never enter the skip path.
255
- // ===========================================================================
256
- describe("nextRunAfter — day-skip in a negative-offset tz (morning-miss regression)", () => {
257
- const LA = "America/Los_Angeles";
258
-
259
- test("'0 9 * * 1-5' from a Saturday → MONDAY 09:00 PT (not Tuesday — the morning isn't skipped)", () => {
260
- // 2026-06-20 is a Saturday. The next weekday-9am is MONDAY the 22nd at 09:00 PT.
261
- // The OLD code skipped Monday's morning (landed at Sun 17:00 wall after the
262
- // UTC-midnight jump, crawled past Mon 09:00 having started Monday at 17:00…),
263
- // returning TUESDAY. The fix returns Monday.
264
- const sat = new Date("2026-06-20T18:00:00Z"); // ~11:00 Sat LA
265
- expect(wall(sat, LA).wd).toBe(6); // Saturday
266
- const next = nextRunAfter("0 9 * * 1-5", LA, sat)!;
267
- expect(next).not.toBeNull();
268
- const w = wall(next, LA);
269
- expect(w.wd).toBe(1); // MONDAY (not 2/Tuesday)
270
- expect(w.h).toBe(9);
271
- expect(w.mi).toBe(0);
272
- expect(w.d).toBe(22); // 2026-06-22 is the Monday
273
- });
274
-
275
- test("'0 6 1 * *' (sparse dom, morning) in PT → the 1st at 06:00 PT, NOT null", () => {
276
- // The OLD code returned NULL here: every month's 1st has its morning stranded
277
- // behind the UTC-midnight jump, so the sparse-dom search exhausted → null. The
278
- // canonical "any sparse-dom morning job in PT never runs" failure.
279
- const midMonth = new Date("2026-06-15T19:00:00Z"); // ~12:00 Jun 15 LA
280
- const next = nextRunAfter("0 6 1 * *", LA, midMonth);
281
- expect(next).not.toBeNull();
282
- const w = wall(next!, LA);
283
- expect(w.d).toBe(1);
284
- expect(w.h).toBe(6);
285
- expect(w.mi).toBe(0);
286
- expect(w.mo).toBe(7); // the next 1st is July 1
287
- });
288
-
289
- test("'30 7 15 * *' (restricted-dom morning generic) in PT → the 15th at 07:30 PT", () => {
290
- const from = new Date("2026-06-10T19:00:00Z"); // ~12:00 Jun 10 LA, before the 15th
291
- const next = nextRunAfter("30 7 15 * *", LA, from);
292
- expect(next).not.toBeNull();
293
- const w = wall(next!, LA);
294
- expect(w.d).toBe(15);
295
- expect(w.h).toBe(7);
296
- expect(w.mi).toBe(30);
297
- expect(w.mo).toBe(6); // June 15
298
- });
299
-
300
- test("the canonical morning weave '53 7 * * 1-5' (weekday, restricted dow) fires Mon 07:53 PT", () => {
301
- // dow restricted → day-skip path active (unlike the all-`*` '53 7 * * *' which
302
- // works even on the buggy code). From a Sunday → Monday 07:53 PT.
303
- const sun = new Date("2026-06-21T18:00:00Z"); // ~11:00 Sun LA
304
- expect(wall(sun, LA).wd).toBe(0); // Sunday
305
- const next = nextRunAfter("53 7 * * 1-5", LA, sun)!;
306
- const w = wall(next, LA);
307
- expect(w.wd).toBe(1); // Monday
308
- expect(w.h).toBe(7);
309
- expect(w.mi).toBe(53);
310
- });
311
-
312
- test("strictly-after still holds across a negative-offset day-skip", () => {
313
- // from is exactly a matching morning instant; must advance to the NEXT match.
314
- const at0900 = new Date("2026-06-22T16:00:00Z"); // 09:00 Mon LA (PDT) — matches '0 9 * * 1-5'
315
- expect(wall(at0900, LA)).toMatchObject({ wd: 1, h: 9, mi: 0 });
316
- const next = nextRunAfter("0 9 * * 1-5", LA, at0900)!;
317
- expect(next.getTime()).toBeGreaterThan(at0900.getTime());
318
- const w = wall(next, LA);
319
- expect(w.wd).toBe(2); // the next weekday match is Tuesday 09:00
320
- expect(w.h).toBe(9);
321
- });
322
-
323
- test("day-skip works in a POSITIVE-offset tz too (Tokyo, UTC+9) — morning not skipped", () => {
324
- // Symmetric check: in a positive-offset zone UTC-midnight is ~09:00 wall, a
325
- // different failure shape; assert the fix is correct here as well.
326
- const TOKYO = "Asia/Tokyo";
327
- const from = new Date("2026-06-14T03:00:00Z"); // 12:00 Jun 14 Tokyo
328
- const next = nextRunAfter("0 6 15 * *", TOKYO, from)!;
329
- const w = wall(next, TOKYO);
330
- expect(w.d).toBe(15);
331
- expect(w.h).toBe(6);
332
- expect(w.mi).toBe(0);
333
- });
334
- });
335
-
336
- describe("nextRunAfter — accepts a pre-parsed ParsedCron", () => {
337
- test("reuses a ParsedCron without re-parsing", () => {
338
- const parsed: ParsedCron = parseCron("0 0 * * *");
339
- const next = nextRunAfter(parsed, "UTC", new Date("2026-06-17T12:00:00Z"))!;
340
- expect(next.toISOString()).toBe("2026-06-18T00:00:00.000Z");
341
- });
342
- });
@@ -1,166 +0,0 @@
1
- /**
2
- * Daemon route test for the vault-native agent-def RELOAD webhook
3
- * (POST /api/vault/agent-def, design 2026-06-17-vault-native-agents Phase 4a).
4
- *
5
- * Auth mirrors /api/vault/inbound: hub JWT, scope agent:send, uniform-401. The
6
- * AgentDefRegistry is injected with a recorder for its `reload`, so we assert the
7
- * route's dispatch (auth → parse → route to vault → reload) without a real vault.
8
- * Uses the same sentinel-token `mock.module("./hub-jwt.ts")` harness as the other
9
- * daemon route tests (file-scoped).
10
- */
11
- import { describe, test, expect, mock } from "bun:test";
12
-
13
- const SEND_TOKEN = "test-send-token"; // agent:send
14
- import { HubJwtError, looksLikeJwt } from "@openparachute/scope-guard";
15
- mock.module("./hub-jwt.ts", () => ({
16
- AGENT_AUDIENCE: "agent",
17
- CHANNEL_AUDIENCE: "channel",
18
- async validateHubJwt(token: string) {
19
- const base = { sub: "test", aud: "agent", jti: undefined, clientId: undefined, vaultScope: undefined };
20
- if (token === SEND_TOKEN) return { ...base, scopes: ["agent:read", "agent:send"] };
21
- throw new HubJwtError("issuer", "invalid token");
22
- },
23
- HubJwtError,
24
- looksLikeJwt,
25
- resetJwksCache() {},
26
- resetRevocationCache() {},
27
- }));
28
-
29
- import { createFetchHandler } from "./daemon.ts";
30
- import { ClientRegistry } from "./routing.ts";
31
- import { AgentDefRegistry, type InstantiateDeps } from "./agent-defs.ts";
32
- import type { Channel } from "./registry.ts";
33
-
34
- /** A registry whose `reload` is recorded; one bound def-vault by default. */
35
- function recordingRegistry(opts?: { vaults?: string[] }) {
36
- const reloads: Array<{ vault: string; noteId: string; event?: string }> = [];
37
- const noopDeps: InstantiateDeps = {
38
- ensureChannel: async () => {},
39
- setupAndRegister: async () => {},
40
- deregister: async () => true,
41
- removeChannel: async () => true,
42
- };
43
- const reg = new AgentDefRegistry(noopDeps, {
44
- bindings: (opts?.vaults ?? ["default"]).map((v) => ({ vault: v, token: "t" })),
45
- });
46
- // Override reload to record (avoid any vault I/O).
47
- reg.reload = (async (vault: string, noteId: string, event?: "created" | "updated" | "deleted") => {
48
- reloads.push({ vault, noteId, event });
49
- return "instantiated";
50
- }) as typeof reg.reload;
51
- return { reg, reloads };
52
- }
53
-
54
- function serverWith(channels: Map<string, Channel>, agentDefs?: AgentDefRegistry) {
55
- const registry = new ClientRegistry();
56
- const srv = Bun.serve({
57
- port: 0,
58
- hostname: "127.0.0.1",
59
- idleTimeout: 0,
60
- fetch: createFetchHandler(channels, registry, agentDefs ? { agentDefs } : undefined),
61
- });
62
- return { srv, base: `http://127.0.0.1:${srv.port}` };
63
- }
64
-
65
- const emptyChannels = () => new Map<string, Channel>();
66
- const auth = { authorization: `Bearer ${SEND_TOKEN}`, "content-type": "application/json" };
67
-
68
- describe("POST /api/vault/agent-def", () => {
69
- test("no Authorization → 401", async () => {
70
- const { reg } = recordingRegistry();
71
- const { srv, base } = serverWith(emptyChannels(), reg);
72
- const res = await fetch(`${base}/api/vault/agent-def`, {
73
- method: "POST",
74
- headers: { "content-type": "application/json" },
75
- body: JSON.stringify({ note: { id: "n" } }),
76
- });
77
- expect(res.status).toBe(401);
78
- srv.stop();
79
- });
80
-
81
- test("authed reload routes to the registry with vault + noteId + event (single-vault default)", async () => {
82
- const { reg, reloads } = recordingRegistry();
83
- const { srv, base } = serverWith(emptyChannels(), reg);
84
- const res = await fetch(`${base}/api/vault/agent-def`, {
85
- method: "POST",
86
- headers: auth,
87
- body: JSON.stringify({ event: "updated", note: { id: "Agents/uni-dev" } }),
88
- });
89
- expect(res.status).toBe(200);
90
- const body = (await res.json()) as { ok: boolean; reloaded: string };
91
- expect(body.ok).toBe(true);
92
- expect(body.reloaded).toBe("instantiated");
93
- // vault defaulted to the sole bound def-vault.
94
- expect(reloads).toEqual([{ vault: "default", noteId: "Agents/uni-dev", event: "updated" }]);
95
- srv.stop();
96
- });
97
-
98
- // Connector 1: the two def-reload triggers (note.created + note.updated) the
99
- // hub provisions send `event: "created"` / `event: "updated"` here. Both must
100
- // route through to reload() — the webhook coercion accepts BOTH, not just one.
101
- test("a note.created trigger payload routes to reload (created → instantiate)", async () => {
102
- const { reg, reloads } = recordingRegistry();
103
- const { srv, base } = serverWith(emptyChannels(), reg);
104
- const res = await fetch(`${base}/api/vault/agent-def`, {
105
- method: "POST",
106
- headers: auth,
107
- body: JSON.stringify({ event: "created", note: { id: "Agents/new-agent" } }),
108
- });
109
- expect(res.status).toBe(200);
110
- const body = (await res.json()) as { ok: boolean; reloaded: string };
111
- expect(body.ok).toBe(true);
112
- expect(reloads).toEqual([{ vault: "default", noteId: "Agents/new-agent", event: "created" }]);
113
- srv.stop();
114
- });
115
-
116
- test("explicit body.vault is honored", async () => {
117
- const { reg, reloads } = recordingRegistry({ vaults: ["default", "research"] });
118
- const { srv, base } = serverWith(emptyChannels(), reg);
119
- const res = await fetch(`${base}/api/vault/agent-def`, {
120
- method: "POST",
121
- headers: auth,
122
- body: JSON.stringify({ event: "deleted", vault: "research", note: { id: "Agents/r" } }),
123
- });
124
- expect(res.status).toBe(200);
125
- expect(reloads).toEqual([{ vault: "research", noteId: "Agents/r", event: "deleted" }]);
126
- srv.stop();
127
- });
128
-
129
- test("multiple def-vaults + no explicit vault → 400 (ambiguous)", async () => {
130
- const { reg, reloads } = recordingRegistry({ vaults: ["default", "research"] });
131
- const { srv, base } = serverWith(emptyChannels(), reg);
132
- const res = await fetch(`${base}/api/vault/agent-def`, {
133
- method: "POST",
134
- headers: auth,
135
- body: JSON.stringify({ note: { id: "n" } }),
136
- });
137
- expect(res.status).toBe(400);
138
- expect(reloads).toHaveLength(0);
139
- srv.stop();
140
- });
141
-
142
- test("missing note.id → 400", async () => {
143
- const { reg } = recordingRegistry();
144
- const { srv, base } = serverWith(emptyChannels(), reg);
145
- const res = await fetch(`${base}/api/vault/agent-def`, {
146
- method: "POST",
147
- headers: auth,
148
- body: JSON.stringify({ event: "created" }),
149
- });
150
- expect(res.status).toBe(400);
151
- srv.stop();
152
- });
153
-
154
- test("no agentDefs configured → clean no-op ack (200, reloaded: skipped)", async () => {
155
- const { srv, base } = serverWith(emptyChannels()); // no registry
156
- const res = await fetch(`${base}/api/vault/agent-def`, {
157
- method: "POST",
158
- headers: auth,
159
- body: JSON.stringify({ note: { id: "n" } }),
160
- });
161
- expect(res.status).toBe(200);
162
- const body = (await res.json()) as { ok: boolean; reloaded: string };
163
- expect(body.reloaded).toBe("skipped");
164
- srv.stop();
165
- });
166
- });