@desplega.ai/agent-swarm 1.88.0 → 1.90.0

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 (64) hide show
  1. package/README.md +7 -0
  2. package/openapi.json +41 -1
  3. package/package.json +3 -2
  4. package/plugin/skills/composio/SKILL.md +173 -0
  5. package/plugin/skills/composio-gmail/SKILL.md +83 -0
  6. package/plugin/skills/composio-google-calendar/SKILL.md +81 -0
  7. package/plugin/skills/composio-google-docs/SKILL.md +71 -0
  8. package/src/be/db.ts +353 -2
  9. package/src/be/migrations/081_metrics.sql +39 -0
  10. package/src/be/migrations/082_user_audit_fields.sql +120 -0
  11. package/src/be/modelsdev-cache.json +3413 -1423
  12. package/src/be/seed-skills/index.ts +7 -0
  13. package/src/cli.tsx +18 -0
  14. package/src/commands/runner.ts +153 -22
  15. package/src/commands/x.ts +118 -0
  16. package/src/github/handlers.ts +40 -1
  17. package/src/heartbeat/heartbeat.ts +80 -12
  18. package/src/http/active-sessions.ts +32 -1
  19. package/src/http/auth.ts +36 -0
  20. package/src/http/core.ts +20 -16
  21. package/src/http/db-query.ts +20 -0
  22. package/src/http/index.ts +2 -0
  23. package/src/http/metrics.ts +447 -0
  24. package/src/http/operator-actor.ts +9 -0
  25. package/src/http/poll.ts +11 -1
  26. package/src/http/tasks.ts +6 -1
  27. package/src/http/workflows.ts +5 -1
  28. package/src/metrics/version.ts +26 -0
  29. package/src/prompts/base-prompt.ts +8 -0
  30. package/src/prompts/session-templates.ts +23 -0
  31. package/src/providers/opencode-adapter.ts +22 -6
  32. package/src/server.ts +10 -1
  33. package/src/tasks/worker-follow-up.ts +19 -1
  34. package/src/tests/base-prompt.test.ts +35 -0
  35. package/src/tests/budget-claim-gate.test.ts +26 -0
  36. package/src/tests/core-auth.test.ts +8 -1
  37. package/src/tests/events-http.test.ts +6 -2
  38. package/src/tests/github-handlers-cancel-config.test.ts +262 -0
  39. package/src/tests/heartbeat-supersede-resume.test.ts +91 -1
  40. package/src/tests/heartbeat.test.ts +84 -3
  41. package/src/tests/http-api-integration.test.ts +3 -1
  42. package/src/tests/metrics-http.test.ts +247 -0
  43. package/src/tests/opencode-adapter.test.ts +90 -30
  44. package/src/tests/runner-repo-autostash.test.ts +117 -0
  45. package/src/tests/runner-requester-profile.test.ts +25 -0
  46. package/src/tests/runner-skills-refresh.test.ts +1 -1
  47. package/src/tests/swarm-x-tool.test.ts +90 -0
  48. package/src/tests/system-default-skills.test.ts +3 -0
  49. package/src/tests/ui-logs-parser.test.ts +271 -0
  50. package/src/tests/user-token-rest-auth.test.ts +129 -0
  51. package/src/tests/workflow-async-v2.test.ts +23 -0
  52. package/src/tests/x-composio.test.ts +122 -0
  53. package/src/tools/create-metric.ts +191 -0
  54. package/src/tools/swarm-x.ts +116 -0
  55. package/src/tools/tool-config.ts +6 -0
  56. package/src/types.ts +120 -0
  57. package/src/utils/request-auth-context.ts +28 -0
  58. package/src/utils/skills-refresh.ts +2 -2
  59. package/src/workflows/engine.ts +24 -2
  60. package/src/workflows/executors/agent-task.ts +2 -0
  61. package/src/x/composio.ts +295 -0
  62. package/templates/skills/attio-interaction/SKILL.md +279 -0
  63. package/templates/skills/attio-interaction/config.json +14 -0
  64. package/templates/skills/attio-interaction/content.md +272 -0
package/src/http/poll.ts CHANGED
@@ -86,6 +86,10 @@ const pollTriggers = route({
86
86
  const CHANNEL_ACTIVITY_INTERVAL_MS = 60_000; // Check at most once per 60s
87
87
  let lastChannelActivityCheckAt = 0;
88
88
 
89
+ function getRequesterNotes(notes: string | undefined): string | undefined {
90
+ return typeof notes === "string" && notes.trim().length > 0 ? notes : undefined;
91
+ }
92
+
89
93
  // ─── Cursor Commit Endpoint ─────────────────────────────────────────────────
90
94
 
91
95
  const commitCursorsRoute = route({
@@ -256,6 +260,7 @@ export async function handlePoll(
256
260
  const requestedByUser = pendingTask.requestedByUserId
257
261
  ? getUserById(pendingTask.requestedByUserId)
258
262
  : undefined;
263
+ const requestedByNotes = getRequesterNotes(requestedByUser?.notes);
259
264
 
260
265
  return {
261
266
  trigger: {
@@ -263,7 +268,12 @@ export async function handlePoll(
263
268
  taskId: pendingTask.id,
264
269
  task: { ...pendingTask, status: "in_progress" },
265
270
  ...(requestedByUser && {
266
- requestedBy: { name: requestedByUser.name, email: requestedByUser.email },
271
+ requestedBy: {
272
+ name: requestedByUser.name,
273
+ email: requestedByUser.email,
274
+ role: requestedByUser.role,
275
+ notes: requestedByNotes,
276
+ },
267
277
  }),
268
278
  },
269
279
  };
package/src/http/tasks.ts CHANGED
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { ensure } from "@desplega.ai/business-use";
3
3
  import { z } from "zod";
4
4
  import {
5
+ backfillSupersedeTaskResumeTaskId,
5
6
  cancelTask,
6
7
  completeTask,
7
8
  failTask,
@@ -34,6 +35,7 @@ import {
34
35
  ProviderNameSchema,
35
36
  ResumeReasonSchema,
36
37
  } from "../types";
38
+ import { getRequestAuth } from "../utils/request-auth-context";
37
39
  import { route } from "./route-def";
38
40
  import { json, jsonError } from "./utils";
39
41
 
@@ -354,7 +356,9 @@ export async function handleTasks(
354
356
  // Tolerant `requestedByUserId`: prevent the deleted-user race from
355
357
  // becoming a 500 — if the referenced user doesn't exist, log and drop
356
358
  // the field rather than letting the FK fail at INSERT.
357
- let requestedByUserId = parsed.body.requestedByUserId || undefined;
359
+ const auth = getRequestAuth(req);
360
+ let requestedByUserId =
361
+ auth?.kind === "user" ? auth.userId : parsed.body.requestedByUserId || undefined;
358
362
  if (requestedByUserId && !getUserById(requestedByUserId)) {
359
363
  console.warn(
360
364
  `[tasks] requestedByUserId ${requestedByUserId} does not exist — coercing to NULL`,
@@ -902,6 +906,7 @@ export async function handleTasks(
902
906
  }
903
907
 
904
908
  const resumeTaskId = followUp.task.id;
909
+ backfillSupersedeTaskResumeTaskId(parsed.params.id, resumeTaskId);
905
910
 
906
911
  ensure({
907
912
  id: "task.superseded",
@@ -21,6 +21,7 @@ import {
21
21
  WorkflowPatchSchema,
22
22
  WorkflowRunStatusSchema,
23
23
  } from "../types";
24
+ import { getRequestAuth } from "../utils/request-auth-context";
24
25
  import { getExecutorRegistry, startWorkflowExecution } from "../workflows";
25
26
  import { applyDefinitionPatch, generateEdges, validateDefinition } from "../workflows/definition";
26
27
  import { TriggerSchemaError } from "../workflows/engine";
@@ -645,10 +646,13 @@ export async function handleWorkflows(
645
646
  return true;
646
647
  }
647
648
  const body = await parseBody<Record<string, unknown>>(req);
649
+ const auth = getRequestAuth(req);
648
650
 
649
651
  let runId: string;
650
652
  try {
651
- runId = await startWorkflowExecution(workflow, body, getExecutorRegistry());
653
+ runId = await startWorkflowExecution(workflow, body, getExecutorRegistry(), {
654
+ requestedByUserId: auth?.kind === "user" ? auth.userId : undefined,
655
+ });
652
656
  } catch (err) {
653
657
  if (err instanceof TriggerSchemaError) {
654
658
  triggerSchemaErrorResponse(res, err.message, err.validationErrors);
@@ -0,0 +1,26 @@
1
+ import { createMetricVersion, getMetric, getMetricVersions } from "../be/db";
2
+ import type { MetricSnapshot, MetricVersion } from "../types";
3
+
4
+ export function snapshotMetric(metricId: string, changedByAgentId?: string): MetricVersion {
5
+ const metric = getMetric(metricId);
6
+ if (!metric) {
7
+ throw new Error(`Metric ${metricId} not found — cannot create snapshot`);
8
+ }
9
+
10
+ const existingVersions = getMetricVersions(metricId);
11
+ const maxVersion = existingVersions.length > 0 ? existingVersions[0]!.version : 0;
12
+ const nextVersion = maxVersion + 1;
13
+
14
+ const snapshot: MetricSnapshot = {
15
+ title: metric.title,
16
+ description: metric.description,
17
+ definition: metric.definition,
18
+ };
19
+
20
+ return createMetricVersion({
21
+ metricId,
22
+ version: nextVersion,
23
+ snapshot,
24
+ changedByAgentId,
25
+ });
26
+ }
@@ -72,6 +72,7 @@ export type BasePromptArgs = {
72
72
  claudeMd?: string | null;
73
73
  clonePath: string;
74
74
  warning?: string | null;
75
+ autoStashes?: { ref: string; message: string }[];
75
76
  guidelines?: {
76
77
  prChecks: string[];
77
78
  mergeChecks: string[];
@@ -197,6 +198,13 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
197
198
  } else if (!args.repoContext.warning) {
198
199
  prompt += `Repository is cloned at \`${args.repoContext.clonePath}\` but has no CLAUDE.md file.\n`;
199
200
  }
201
+
202
+ if (args.repoContext.autoStashes && args.repoContext.autoStashes.length > 0) {
203
+ const stashes = args.repoContext.autoStashes
204
+ .map((stash) => `- ${stash.ref}: ${stash.message}`)
205
+ .join("\n");
206
+ prompt += `\nPending auto-stashed work exists in this repo:\n${stashes}\nRestore if relevant with \`git stash apply <ref>\` or \`git stash pop <ref>\`.\n`;
207
+ }
200
208
  }
201
209
 
202
210
  // Inject repo guidelines
@@ -565,6 +565,29 @@ When working in a repository, your system prompt may include a **Repository Guid
565
565
  category: "system",
566
566
  });
567
567
 
568
+ // ============================================================================
569
+ // Per-task prompt templates (category: "task_lifecycle")
570
+ // ============================================================================
571
+
572
+ registerTemplate({
573
+ eventType: "task.requester.profile",
574
+ header: "",
575
+ defaultBody: `
576
+ ## Requester Profile
577
+ This task was requested by {{requester_name}}{{requester_role_suffix}}.{{requester_notes_section}}
578
+ Honor this requester profile in tone, depth, and format where it doesn't conflict with correctness or your operating rules.
579
+ `,
580
+ variables: [
581
+ { name: "requester_name", description: "The requesting user's display name" },
582
+ { name: "requester_role_suffix", description: "Formatted role suffix, including parentheses" },
583
+ {
584
+ name: "requester_notes_section",
585
+ description: "Formatted notes section sourced from users.notes, or empty string",
586
+ },
587
+ ],
588
+ category: "task_lifecycle",
589
+ });
590
+
568
591
  // ============================================================================
569
592
  // Composite session templates (category: "session")
570
593
  // ============================================================================
@@ -219,6 +219,7 @@ export class OpencodeSession implements ProviderSession {
219
219
  private completionPromise: Promise<ProviderResult>;
220
220
  private server: { url: string; close(): void };
221
221
  private aborted = false;
222
+ private completed = false;
222
223
 
223
224
  // Running cost accumulators
224
225
  private totalCostUsd = 0;
@@ -273,6 +274,10 @@ export class OpencodeSession implements ProviderSession {
273
274
  return this._sessionId;
274
275
  }
275
276
 
277
+ get isFinished(): boolean {
278
+ return this.completed;
279
+ }
280
+
276
281
  /** Emit the synthetic session_init event. Called by the adapter immediately
277
282
  * after construction; buffers if no listener is attached yet. */
278
283
  emitSessionInit(provider: "opencode"): void {
@@ -358,7 +363,7 @@ export class OpencodeSession implements ProviderSession {
358
363
 
359
364
  /** Process a single opencode SSE event */
360
365
  handleOpencodeEvent(ev: OpencodeEvent): void {
361
- if (this.aborted) return;
366
+ if (this.aborted || this.completed) return;
362
367
 
363
368
  // Always emit the raw event as a scrubbed raw_log
364
369
  const rawContent = scrubSecrets(JSON.stringify(ev));
@@ -471,7 +476,7 @@ export class OpencodeSession implements ProviderSession {
471
476
  for (const l of this.listeners) l(resultEvent);
472
477
  const raw = scrubSecrets(JSON.stringify(resultEvent));
473
478
  this.emitDirect({ type: "raw_log", content: raw });
474
- this.completionResolve({
479
+ void this.finish({
475
480
  exitCode: 0,
476
481
  sessionId: this._sessionId,
477
482
  cost,
@@ -515,7 +520,7 @@ export class OpencodeSession implements ProviderSession {
515
520
  const raw = scrubSecrets(JSON.stringify(errorEvent));
516
521
  this.emitDirect({ type: "raw_log", content: raw });
517
522
  const cost = this.buildCostData(true);
518
- this.completionResolve({
523
+ void this.finish({
519
524
  exitCode: 1,
520
525
  sessionId: this._sessionId,
521
526
  cost,
@@ -546,12 +551,22 @@ export class OpencodeSession implements ProviderSession {
546
551
  return this.completionPromise;
547
552
  }
548
553
 
554
+ private async finish(result: ProviderResult): Promise<void> {
555
+ if (this.completed) return;
556
+ this.completed = true;
557
+ try {
558
+ this.server.close();
559
+ } catch {
560
+ // best-effort
561
+ }
562
+ await this.cleanupFiles();
563
+ this.completionResolve(result);
564
+ }
565
+
549
566
  async abort(): Promise<void> {
550
567
  if (this.aborted) return;
551
568
  this.aborted = true;
552
- this.server.close();
553
- await this.cleanupFiles();
554
- this.completionResolve({
569
+ await this.finish({
555
570
  exitCode: 1,
556
571
  sessionId: this._sessionId,
557
572
  isError: true,
@@ -760,6 +775,7 @@ export class OpencodeAdapter implements ProviderAdapter {
760
775
  .then(async ({ stream }) => {
761
776
  for await (const event of stream) {
762
777
  session.handleOpencodeEvent(event as OpencodeEvent);
778
+ if (session.isFinished) break;
763
779
  }
764
780
  // Stream ended without session.idle — treat as completion
765
781
  })
package/src/server.ts CHANGED
@@ -6,6 +6,7 @@ import { registerCancelTaskTool } from "./tools/cancel-task";
6
6
  import { registerContextDiffTool } from "./tools/context-diff";
7
7
  import { registerContextHistoryTool } from "./tools/context-history";
8
8
  import { registerCreateChannelTool } from "./tools/create-channel";
9
+ import { registerCreateMetricTool } from "./tools/create-metric";
9
10
  import { registerCreatePageTool } from "./tools/create-page";
10
11
  import { registerDbQueryTool } from "./tools/db-query";
11
12
  import { registerDeleteChannelTool } from "./tools/delete-channel";
@@ -109,6 +110,7 @@ import {
109
110
  registerListConfigTool,
110
111
  registerSetConfigTool,
111
112
  } from "./tools/swarm-config";
113
+ import { registerSwarmXTool } from "./tools/swarm-x";
112
114
  // Task pool capability
113
115
  import { registerTaskActionTool } from "./tools/task-action";
114
116
  // Tracker capability
@@ -146,7 +148,7 @@ import {
146
148
  // Capability-based feature flags
147
149
  // Default: all capabilities enabled
148
150
  const DEFAULT_CAPABILITIES =
149
- "core,task-pool,profiles,services,scheduling,memory,workflows,pages,kv";
151
+ "core,task-pool,profiles,services,scheduling,memory,workflows,pages,metrics,kv";
150
152
  const CAPABILITIES = new Set(
151
153
  (process.env.CAPABILITIES || DEFAULT_CAPABILITIES).split(",").map((s) => s.trim()),
152
154
  );
@@ -226,6 +228,9 @@ export function createServer() {
226
228
  registerScriptDeleteTool(server);
227
229
  registerScriptQueryTypesTool(server);
228
230
 
231
+ // External command routes - mirrors the `agent-swarm x ...` CLI surface.
232
+ registerSwarmXTool(server);
233
+
229
234
  // Slack integration tools (always registered, will no-op if Slack not configured)
230
235
  registerSlackReplyTool(server);
231
236
  registerSlackReadTool(server);
@@ -338,6 +343,10 @@ export function createServer() {
338
343
  registerCreatePageTool(server);
339
344
  }
340
345
 
346
+ if (hasCapability("metrics")) {
347
+ registerCreateMetricTool(server);
348
+ }
349
+
341
350
  // KV capability — namespaced Redis-like key/value (see src/be/migrations/061_kv_store.sql).
342
351
  // Enabled by default; opt out via `CAPABILITIES=...` without `kv`.
343
352
  if (hasCapability("kv")) {
@@ -22,6 +22,20 @@ export const WORKER_LIVENESS_WINDOW_SECONDS = Number(
22
22
  process.env.WORKER_LIVENESS_WINDOW_SECONDS || "30",
23
23
  );
24
24
 
25
+ export const RESUME_GENERATION_TAG_PREFIX = "resume-generation:";
26
+
27
+ export function getResumeGeneration(task: Pick<AgentTask, "tags">): number {
28
+ const tag = task.tags.find((value) => value.startsWith(RESUME_GENERATION_TAG_PREFIX));
29
+ if (!tag) return 0;
30
+
31
+ const parsed = Number(tag.slice(RESUME_GENERATION_TAG_PREFIX.length));
32
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
33
+ }
34
+
35
+ export function getNextResumeGeneration(parent: Pick<AgentTask, "tags">): number {
36
+ return getResumeGeneration(parent) + 1;
37
+ }
38
+
25
39
  function attachmentPointer(a: TaskAttachment): string {
26
40
  switch (a.kind) {
27
41
  case "url":
@@ -205,7 +219,11 @@ export function createResumeFollowUp(args: {
205
219
  ].join("\n");
206
220
 
207
221
  const priority = Math.min(100, (parent.priority ?? 50) + 10);
208
- const tags = ["auto-resume", `reason:${args.reason}`];
222
+ const tags = [
223
+ "auto-resume",
224
+ `reason:${args.reason}`,
225
+ `${RESUME_GENERATION_TAG_PREFIX}${getNextResumeGeneration(parent)}`,
226
+ ];
209
227
 
210
228
  // Identity-shaped fields (dir, VCS provider/repo/number/url/etc.,
211
229
  // outputSchema, slack channel/thread/user, agentmail, mention, contextKey,
@@ -272,6 +272,41 @@ describe("getBasePrompt — repoContext", () => {
272
272
  expect(result).toContain("Repository Guidelines (MANDATORY)");
273
273
  expect(result).toContain("Auto-merge: Allowed");
274
274
  });
275
+
276
+ test("surfaces swarm-autostash entries when present", async () => {
277
+ const result = await getBasePrompt({
278
+ ...minimalArgs,
279
+ repoContext: {
280
+ claudeMd: "Rules",
281
+ clonePath: "/workspace/my-repo",
282
+ autoStashes: [
283
+ {
284
+ ref: "stash@{0}",
285
+ message: "On main: swarm-autostash main 2026-06-01T13:00:00.000Z",
286
+ },
287
+ ],
288
+ },
289
+ });
290
+
291
+ expect(result).toContain("Pending auto-stashed work exists in this repo");
292
+ expect(result).toContain("stash@{0}: On main: swarm-autostash main");
293
+ expect(result).toContain("git stash apply <ref>");
294
+ expect(result).toContain("git stash pop <ref>");
295
+ });
296
+
297
+ test("does not mention auto-stashed work when no swarm-autostash entries exist", async () => {
298
+ const result = await getBasePrompt({
299
+ ...minimalArgs,
300
+ repoContext: {
301
+ claudeMd: "Rules",
302
+ clonePath: "/workspace/my-repo",
303
+ autoStashes: [],
304
+ },
305
+ });
306
+
307
+ expect(result).not.toContain("Pending auto-stashed work exists in this repo");
308
+ expect(result).not.toContain("git stash apply <ref>");
309
+ });
275
310
  });
276
311
 
277
312
  // ---------------------------------------------------------------------------
@@ -11,6 +11,7 @@ import {
11
11
  createAgent,
12
12
  createSessionCost,
13
13
  createTaskExtended,
14
+ createUser,
14
15
  getAgentById,
15
16
  getDb,
16
17
  incrementEmptyPollCount,
@@ -129,6 +130,31 @@ describe("Phase 3 — /api/poll budget admission gate", () => {
129
130
  expect((body.trigger as { taskId: string }).taskId).toBe(task.id);
130
131
  });
131
132
 
133
+ test("pending task trigger includes requester role and notes", async () => {
134
+ const worker = createAgent({ name: "w-requester", isLead: false, status: "idle", maxTasks: 1 });
135
+ const requester = createUser({
136
+ name: "Requester One",
137
+ email: "requester@example.com",
138
+ role: "engineering manager",
139
+ notes: "Include implementation detail and test coverage.",
140
+ });
141
+ createTaskExtended("profile-aware task", {
142
+ agentId: worker.id,
143
+ requestedByUserId: requester.id,
144
+ });
145
+
146
+ const { status, body } = await callPoll(worker.id);
147
+ expect(status).toBe(200);
148
+ if ("error" in body) throw new Error("unexpected error response");
149
+ expect(body.trigger?.type).toBe("task_assigned");
150
+ expect(body.trigger?.requestedBy).toEqual({
151
+ name: "Requester One",
152
+ email: "requester@example.com",
153
+ role: "engineering manager",
154
+ notes: "Include implementation detail and test coverage.",
155
+ });
156
+ });
157
+
132
158
  test("no budgets configured + no work → trigger=null", async () => {
133
159
  const worker = createAgent({ name: "w-empty", isLead: false, status: "idle", maxTasks: 1 });
134
160
  const { status, body } = await callPoll(worker.id);
@@ -135,8 +135,15 @@ describe("handleCore auth middleware (no API_KEY configured)", () => {
135
135
  server.close();
136
136
  });
137
137
 
138
- test("authed routes pass without Bearer when API_KEY is empty", async () => {
138
+ test("authed routes fail closed without Bearer when API_KEY is empty", async () => {
139
139
  const res = await fetch(`http://localhost:${port}/api/mcp-oauth/some-id/status`);
140
+ expect(res.status).toBe(401);
141
+ const body = await res.json();
142
+ expect(body.error).toBe("Unauthorized");
143
+ });
144
+
145
+ test("public routes still pass when API_KEY is empty", async () => {
146
+ const res = await fetch(`http://localhost:${port}/api/mcp-oauth/callback`);
140
147
  expect(res.status).not.toBe(401);
141
148
  });
142
149
  });
@@ -5,6 +5,7 @@ import type { Subprocess } from "bun";
5
5
  const TEST_PORT = 13033;
6
6
  const TEST_DB_PATH = `/tmp/test-events-http-${Date.now()}.sqlite`;
7
7
  const BASE = `http://localhost:${TEST_PORT}`;
8
+ const TEST_API_KEY = "test-events-http-key";
8
9
 
9
10
  let serverProc: Subprocess;
10
11
 
@@ -15,7 +16,10 @@ async function api(
15
16
  ): Promise<{ status: number; body: Record<string, unknown> }> {
16
17
  const res = await fetch(`${BASE}${path}`, {
17
18
  method,
18
- headers: { "Content-Type": "application/json" },
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ Authorization: `Bearer ${TEST_API_KEY}`,
22
+ },
19
23
  body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
20
24
  });
21
25
  const text = await res.text();
@@ -54,7 +58,7 @@ beforeAll(async () => {
54
58
  ...process.env,
55
59
  PORT: String(TEST_PORT),
56
60
  DATABASE_PATH: TEST_DB_PATH,
57
- API_KEY: "",
61
+ API_KEY: TEST_API_KEY,
58
62
  CAPABILITIES: "core",
59
63
  SLACK_BOT_TOKEN: "",
60
64
  GITHUB_WEBHOOK_SECRET: "",