@desplega.ai/agent-swarm 1.76.2 → 1.77.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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Unit tests for `summarizeSessionForPi` in `src/providers/pi-mono-extension.ts`.
3
+ *
4
+ * Plan: thoughts/taras/plans/2026-05-10-fix-session-summarization-workers.md
5
+ * → Phase 1 § "Test coverage"
6
+ *
7
+ * Uses explicit dependency injection (the `deps` parameter on
8
+ * `summarizeSessionForPi`) instead of `bun:test`'s `mock.module()` because the
9
+ * latter installs a process-wide override that leaks across test files in the
10
+ * same `bun test` run (`buildRatingsFromLlm` siblings + Phase-0 internal-ai
11
+ * tests would break).
12
+ *
13
+ * Mocks:
14
+ * - `runSummarize` — captures args + returns canned result
15
+ * - `fetchRetrievalsForTask` — returns canned retrievals
16
+ * - `postRatings` — captures args, asserts `events:` key
17
+ * - `buildRatingsFromLlm` — minimal pass-through unless overridden
18
+ * - `globalThis.fetch` — captures `/api/memory/index` POSTs
19
+ */
20
+
21
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
22
+ import type { SummarizeSessionForPiDeps, SwarmHooksConfig } from "../providers/pi-mono-extension";
23
+ import { summarizeSessionForPi } from "../providers/pi-mono-extension";
24
+
25
+ // ── helpers ───────────────────────────────────────────────────────────────────
26
+
27
+ function makeConfig(): SwarmHooksConfig {
28
+ return {
29
+ apiUrl: "http://localhost:3013",
30
+ apiKey: "test-key",
31
+ agentId: "agent-pi-1",
32
+ taskId: "task-pi-1",
33
+ isLead: false,
34
+ };
35
+ }
36
+
37
+ /** Build a transcript with > 100 chars so the degenerate gate doesn't trip. */
38
+ function longTranscript(extra = "") {
39
+ return "User: do a thing\nAssistant: doing thing\nTool[write]: ok\n".repeat(5) + extra;
40
+ }
41
+
42
+ /**
43
+ * Write a temp file under /tmp containing `content`. The SUT's
44
+ * `Bun.file(sessionFile).text()` reads it back without further mocking.
45
+ */
46
+ async function writeTempTranscript(content: string): Promise<string> {
47
+ const path = `/tmp/pi-mono-test-transcript-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`;
48
+ await Bun.write(path, content);
49
+ return path;
50
+ }
51
+
52
+ // ── test state ────────────────────────────────────────────────────────────────
53
+
54
+ type RunSummarizeArgs = Parameters<NonNullable<SummarizeSessionForPiDeps["runSummarize"]>>[0];
55
+ type RunSummarizeResult = Awaited<
56
+ ReturnType<NonNullable<SummarizeSessionForPiDeps["runSummarize"]>>
57
+ >;
58
+ type FetchRetrievalsArgs = Parameters<
59
+ NonNullable<SummarizeSessionForPiDeps["fetchRetrievalsForTask"]>
60
+ >[0];
61
+ type FetchRetrievalsResult = Awaited<
62
+ ReturnType<NonNullable<SummarizeSessionForPiDeps["fetchRetrievalsForTask"]>>
63
+ >;
64
+ type PostRatingsArgs = Parameters<NonNullable<SummarizeSessionForPiDeps["postRatings"]>>[0];
65
+
66
+ const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
67
+ type FetchHandlerResp = {
68
+ ok: boolean;
69
+ status: number;
70
+ text: () => Promise<string>;
71
+ json: () => Promise<unknown>;
72
+ };
73
+ let fetchHandler: ((url: string, init?: RequestInit) => Promise<FetchHandlerResp>) | null = null;
74
+ const consoleErrors: unknown[][] = [];
75
+
76
+ const origFetch = globalThis.fetch;
77
+ const origConsoleError = console.error;
78
+
79
+ beforeEach(() => {
80
+ fetchCalls.length = 0;
81
+ consoleErrors.length = 0;
82
+ fetchHandler = null;
83
+ // Default fetch: 202 for /api/memory/index, 200 otherwise (so non-test fetches
84
+ // like fetchTaskDetails don't crash with an undefined handler).
85
+ fetchHandler = async (url) => {
86
+ if (url.includes("/api/memory/index")) {
87
+ return {
88
+ ok: true,
89
+ status: 202,
90
+ text: async () => "",
91
+ json: async () => ({ queued: true, memoryIds: ["mem-1"] }),
92
+ };
93
+ }
94
+ return { ok: true, status: 200, text: async () => "", json: async () => ({}) };
95
+ };
96
+ globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
97
+ const urlStr = typeof url === "string" ? url : url.toString();
98
+ fetchCalls.push({ url: urlStr, init });
99
+ if (!fetchHandler) return new Response("{}", { status: 200 });
100
+ return fetchHandler(urlStr, init) as unknown as Response;
101
+ }) as typeof fetch;
102
+ console.error = (...args: unknown[]) => {
103
+ consoleErrors.push(args);
104
+ };
105
+ delete process.env.MEMORY_RATERS;
106
+ });
107
+
108
+ afterEach(() => {
109
+ globalThis.fetch = origFetch;
110
+ console.error = origConsoleError;
111
+ });
112
+
113
+ // ── tests ─────────────────────────────────────────────────────────────────────
114
+
115
+ describe("summarizeSessionForPi", () => {
116
+ test("happy path — long transcript + valid summary → POSTs to /api/memory/index", async () => {
117
+ const transcript = longTranscript("Some real-looking work here\n");
118
+ const sessionFile = await writeTempTranscript(transcript);
119
+
120
+ let lastRunSummarizeArgs: RunSummarizeArgs | null = null;
121
+ const deps: SummarizeSessionForPiDeps = {
122
+ runSummarize: async (args) => {
123
+ lastRunSummarizeArgs = args;
124
+ return {
125
+ summary: "Learned X about Y — concrete reusable fact.",
126
+ ratings: [],
127
+ } as RunSummarizeResult;
128
+ },
129
+ };
130
+
131
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
132
+
133
+ expect(lastRunSummarizeArgs).not.toBeNull();
134
+ expect(lastRunSummarizeArgs!.harness).toBe("pi");
135
+ expect(lastRunSummarizeArgs!.taskContext.sourceTaskId).toBe("task-pi-1");
136
+ expect(lastRunSummarizeArgs!.taskContext.agentId).toBe("agent-pi-1");
137
+ expect(lastRunSummarizeArgs!.apiUrl).toBe("http://localhost:3013");
138
+ expect(lastRunSummarizeArgs!.apiKey).toBe("test-key");
139
+
140
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
141
+ expect(indexCalls.length).toBe(1);
142
+ const body = JSON.parse(indexCalls[0]!.init?.body as string) as Record<string, unknown>;
143
+ expect(body.scope).toBe("agent");
144
+ expect(body.source).toBe("session_summary");
145
+ expect(body.sourceTaskId).toBe("task-pi-1");
146
+ expect(body.agentId).toBe("agent-pi-1");
147
+ expect(body.name).toBe("session-summary");
148
+ expect(body.content).toBe("Learned X about Y — concrete reusable fact.");
149
+
150
+ expect(consoleErrors.length).toBe(0);
151
+ });
152
+
153
+ test("empty transcript (≤100 chars) → no POST, no error", async () => {
154
+ const sessionFile = await writeTempTranscript("short");
155
+
156
+ const deps: SummarizeSessionForPiDeps = {
157
+ runSummarize: async () => {
158
+ throw new Error("should not be called");
159
+ },
160
+ };
161
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
162
+
163
+ expect(fetchCalls.length).toBe(0);
164
+ expect(consoleErrors.length).toBe(0);
165
+ });
166
+
167
+ test("no sessionFile → no POST, no error", async () => {
168
+ const deps: SummarizeSessionForPiDeps = {
169
+ runSummarize: async () => {
170
+ throw new Error("should not be called");
171
+ },
172
+ };
173
+ await summarizeSessionForPi(makeConfig(), undefined, deps);
174
+
175
+ expect(fetchCalls.length).toBe(0);
176
+ expect(consoleErrors.length).toBe(0);
177
+ });
178
+
179
+ test("no credentials (runSummarize returns null) → no POST, no error log", async () => {
180
+ const sessionFile = await writeTempTranscript(longTranscript());
181
+
182
+ const deps: SummarizeSessionForPiDeps = {
183
+ runSummarize: async () => null,
184
+ };
185
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
186
+
187
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
188
+ expect(indexCalls.length).toBe(0);
189
+ // wrapper logs internally; the pi wrapper itself should not log on null return
190
+ expect(consoleErrors.length).toBe(0);
191
+ });
192
+
193
+ test("length gate — summary too short → no POST", async () => {
194
+ const sessionFile = await writeTempTranscript(longTranscript());
195
+
196
+ const deps: SummarizeSessionForPiDeps = {
197
+ runSummarize: async () => ({ summary: "tiny", ratings: [] }) as RunSummarizeResult,
198
+ };
199
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
200
+
201
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
202
+ expect(indexCalls.length).toBe(0);
203
+ expect(consoleErrors.length).toBe(0);
204
+ });
205
+
206
+ test("'no significant learnings' gate → no POST", async () => {
207
+ const sessionFile = await writeTempTranscript(longTranscript());
208
+
209
+ const deps: SummarizeSessionForPiDeps = {
210
+ runSummarize: async () =>
211
+ ({ summary: "No significant learnings.", ratings: [] }) as RunSummarizeResult,
212
+ };
213
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
214
+
215
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
216
+ expect(indexCalls.length).toBe(0);
217
+ expect(consoleErrors.length).toBe(0);
218
+ });
219
+
220
+ test("POST 500 → exactly one console.error('session_summary: /api/memory/index POST failed (pi):', ...)", async () => {
221
+ const sessionFile = await writeTempTranscript(longTranscript());
222
+
223
+ fetchHandler = async (url) => {
224
+ if (url.includes("/api/memory/index")) {
225
+ return {
226
+ ok: false,
227
+ status: 500,
228
+ text: async () => "internal server error",
229
+ json: async () => ({}),
230
+ };
231
+ }
232
+ return { ok: true, status: 200, text: async () => "", json: async () => ({}) };
233
+ };
234
+
235
+ const deps: SummarizeSessionForPiDeps = {
236
+ runSummarize: async () =>
237
+ ({
238
+ summary: "A valid long-enough summary that passes the length gate.",
239
+ ratings: [],
240
+ }) as RunSummarizeResult,
241
+ };
242
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
243
+
244
+ const matching = consoleErrors.filter(
245
+ (args) =>
246
+ typeof args[0] === "string" &&
247
+ (args[0] as string).startsWith("session_summary: /api/memory/index POST failed (pi):"),
248
+ );
249
+ expect(matching.length).toBe(1);
250
+ expect(matching[0]![1]).toBe(500);
251
+ });
252
+
253
+ test("fetch throws → exactly one console.error('session_summary failed (pi):', ...)", async () => {
254
+ const sessionFile = await writeTempTranscript(longTranscript());
255
+
256
+ fetchHandler = async (url) => {
257
+ if (url.includes("/api/memory/index")) {
258
+ throw new Error("network down");
259
+ }
260
+ return { ok: true, status: 200, text: async () => "", json: async () => ({}) };
261
+ };
262
+
263
+ const deps: SummarizeSessionForPiDeps = {
264
+ runSummarize: async () =>
265
+ ({
266
+ summary: "A valid long-enough summary that passes the length gate.",
267
+ ratings: [],
268
+ }) as RunSummarizeResult,
269
+ };
270
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
271
+
272
+ const matching = consoleErrors.filter(
273
+ (args) =>
274
+ typeof args[0] === "string" &&
275
+ (args[0] as string).startsWith("session_summary failed (pi):"),
276
+ );
277
+ expect(matching.length).toBe(1);
278
+ });
279
+
280
+ test("ratings path — MEMORY_RATERS=llm + retrievals + ratings → postRatings called with `events:` key (NOT `ratings:`)", async () => {
281
+ process.env.MEMORY_RATERS = "llm";
282
+ const sessionFile = await writeTempTranscript(longTranscript());
283
+
284
+ const retrievalRow = {
285
+ id: "mem-A",
286
+ name: "memory A",
287
+ content: "...",
288
+ };
289
+ const fetchRetrievalsMock: SummarizeSessionForPiDeps["fetchRetrievalsForTask"] = async (
290
+ _args: FetchRetrievalsArgs,
291
+ ) => [retrievalRow] as unknown as FetchRetrievalsResult;
292
+
293
+ let lastPostRatingsArgs: PostRatingsArgs | null = null;
294
+ const postRatingsMock: SummarizeSessionForPiDeps["postRatings"] = async (args) => {
295
+ lastPostRatingsArgs = args;
296
+ return { ok: true, status: 200 };
297
+ };
298
+
299
+ const deps: SummarizeSessionForPiDeps = {
300
+ runSummarize: async (args) => {
301
+ expect(args.retrievals.length).toBe(1);
302
+ expect(args.retrievals[0]!.id).toBe("mem-A");
303
+ return {
304
+ summary: "Long-enough summary with real content for the index POST.",
305
+ ratings: [{ id: "mem-A", score: 0.8, reasoning: "useful" }],
306
+ } as RunSummarizeResult;
307
+ },
308
+ fetchRetrievalsForTask: fetchRetrievalsMock,
309
+ postRatings: postRatingsMock,
310
+ buildRatingsFromLlm: (ratings, retrievals) => {
311
+ // Smoke-check: only keep ratings present in retrievals (mirrors real impl)
312
+ const allowed = new Set(retrievals.map((r) => r.id));
313
+ return ratings
314
+ .filter((r) => allowed.has(r.id))
315
+ .map((r) => ({
316
+ memoryId: r.id,
317
+ signal: 2 * r.score - 1,
318
+ weight: 0.8,
319
+ source: "llm",
320
+ reasoning: r.reasoning,
321
+ }));
322
+ },
323
+ };
324
+
325
+ await summarizeSessionForPi(makeConfig(), sessionFile, deps);
326
+
327
+ // Index POST happened
328
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
329
+ expect(indexCalls.length).toBe(1);
330
+
331
+ // postRatings was called with `events:` key, not `ratings:` — guards against
332
+ // the orchestrator-flagged plan/signature mismatch
333
+ expect(lastPostRatingsArgs).not.toBeNull();
334
+ expect(lastPostRatingsArgs!.apiUrl).toBe("http://localhost:3013");
335
+ expect(lastPostRatingsArgs!.agentId).toBe("agent-pi-1");
336
+ expect(lastPostRatingsArgs!.taskId).toBe("task-pi-1");
337
+ expect(Array.isArray(lastPostRatingsArgs!.events)).toBe(true);
338
+ expect(lastPostRatingsArgs!.events.length).toBe(1);
339
+ expect(lastPostRatingsArgs!.events[0]!.memoryId).toBe("mem-A");
340
+ expect(lastPostRatingsArgs!.events[0]!.source).toBe("llm");
341
+
342
+ // Guard against accidentally passing a `ratings:` key (plan example bug)
343
+ expect((lastPostRatingsArgs as unknown as Record<string, unknown>).ratings).toBeUndefined();
344
+
345
+ expect(consoleErrors.length).toBe(0);
346
+ });
347
+ });
@@ -225,7 +225,15 @@ describe("auto-reload debouncer", () => {
225
225
  }
226
226
  });
227
227
 
228
- beforeEach(() => {
228
+ beforeEach(async () => {
229
+ // Drain any reload state that leaked from earlier test files in the full
230
+ // suite (e.g. swarm-config-reserved-keys.test.ts does global PUT/DELETE on
231
+ // /api/config, which schedules a 250ms reload). If we reset() while a
232
+ // prior timer was still mid-flight, the leaked .finally() can race against
233
+ // our test body and stomp the module state — first symptom is
234
+ // `expect(pending).toBe(true)` failing because `inFlightReload` was still
235
+ // truthy when `scheduleIntegrationsReload` ran. Flush first, then reset.
236
+ await flushPendingIntegrationsReload();
229
237
  _resetAutoReloadForTests();
230
238
  });
231
239
 
@@ -77,6 +77,7 @@ async function removeDbFiles(path: string): Promise<void> {
77
77
  const ENV_KEYS_TO_RESET = [
78
78
  "SWARM_CLOUD",
79
79
  "SWARM_ORG_NAME",
80
+ "SWARM_ORG_ID",
80
81
  "SWARM_ORG_LOGO_URL",
81
82
  "SWARM_BRAND_COLOR",
82
83
  "SWARM_MARKETING_URL",
@@ -161,12 +162,14 @@ describe("buildStatusPayload — identity", () => {
161
162
  is_cloud: false,
162
163
  marketing_url: null,
163
164
  hide_cloud_promo: false,
165
+ org_id: null,
164
166
  });
165
167
  });
166
168
 
167
169
  test("reflects SWARM_* envs when all set", () => {
168
170
  process.env.SWARM_CLOUD = "true";
169
171
  process.env.SWARM_ORG_NAME = "Acme";
172
+ process.env.SWARM_ORG_ID = "org_acme_123";
170
173
  process.env.SWARM_ORG_LOGO_URL = "https://acme.example/logo.png";
171
174
  process.env.SWARM_BRAND_COLOR = "#ff5500";
172
175
  process.env.SWARM_MARKETING_URL = "https://swarm.acme.example";
@@ -180,6 +183,7 @@ describe("buildStatusPayload — identity", () => {
180
183
  is_cloud: true,
181
184
  marketing_url: "https://swarm.acme.example",
182
185
  hide_cloud_promo: true,
186
+ org_id: "org_acme_123",
183
187
  });
184
188
  });
185
189
 
@@ -1,8 +1,9 @@
1
- import { beforeEach, describe, expect, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import {
3
3
  _getInstallationIdForTests,
4
4
  _resetTelemetryStateForTests,
5
5
  initTelemetry,
6
+ track,
6
7
  } from "../telemetry";
7
8
 
8
9
  // initTelemetry no-ops when ANONYMIZED_TELEMETRY=false. The CI env or local
@@ -76,6 +77,141 @@ describe("initTelemetry", () => {
76
77
  expect(writes).toEqual([]);
77
78
  });
78
79
 
80
+ describe("track() org identity in metadata", () => {
81
+ const originalFetch = globalThis.fetch;
82
+ let captured: Record<string, unknown> | null = null;
83
+
84
+ beforeEach(() => {
85
+ captured = null;
86
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
87
+ captured = init?.body ? JSON.parse(init.body) : null;
88
+ return new Response(null, { status: 204 });
89
+ }) as typeof fetch;
90
+ });
91
+
92
+ afterEach(() => {
93
+ globalThis.fetch = originalFetch;
94
+ delete process.env.SWARM_ORG_ID;
95
+ delete process.env.SWARM_ORG_NAME;
96
+ delete process.env.SWARM_CLOUD;
97
+ });
98
+
99
+ test("omits organization_* keys from metadata when SWARM_ORG_* unset", async () => {
100
+ delete process.env.SWARM_ORG_ID;
101
+ delete process.env.SWARM_ORG_NAME;
102
+ await initTelemetry(
103
+ "api-server",
104
+ async () => undefined,
105
+ async () => {},
106
+ {
107
+ generateIfMissing: true,
108
+ },
109
+ );
110
+
111
+ track({ event: "test.event", properties: {} });
112
+ // Wait one microtask for the fire-and-forget fetch.
113
+ await new Promise((r) => setTimeout(r, 0));
114
+
115
+ const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
116
+ expect(metadata.organization_id).toBeUndefined();
117
+ expect(metadata.organization_name).toBeUndefined();
118
+ });
119
+
120
+ test("includes organization_id + organization_name when SWARM_ORG_* set", async () => {
121
+ process.env.SWARM_ORG_ID = "org_acme_123";
122
+ process.env.SWARM_ORG_NAME = "Acme Engineering";
123
+ await initTelemetry(
124
+ "api-server",
125
+ async () => undefined,
126
+ async () => {},
127
+ {
128
+ generateIfMissing: true,
129
+ },
130
+ );
131
+
132
+ track({ event: "test.event", properties: {} });
133
+ await new Promise((r) => setTimeout(r, 0));
134
+
135
+ const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
136
+ expect(metadata.organization_id).toBe("org_acme_123");
137
+ expect(metadata.organization_name).toBe("Acme Engineering");
138
+ });
139
+
140
+ test("metadata.is_cloud === false when SWARM_CLOUD unset", async () => {
141
+ delete process.env.SWARM_CLOUD;
142
+ await initTelemetry(
143
+ "api-server",
144
+ async () => undefined,
145
+ async () => {},
146
+ {
147
+ generateIfMissing: true,
148
+ },
149
+ );
150
+
151
+ track({ event: "test.event", properties: {} });
152
+ await new Promise((r) => setTimeout(r, 0));
153
+
154
+ const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
155
+ expect(metadata.is_cloud).toBe(false);
156
+ });
157
+
158
+ test("metadata.is_cloud === true when SWARM_CLOUD=true", async () => {
159
+ process.env.SWARM_CLOUD = "true";
160
+ await initTelemetry(
161
+ "api-server",
162
+ async () => undefined,
163
+ async () => {},
164
+ {
165
+ generateIfMissing: true,
166
+ },
167
+ );
168
+
169
+ track({ event: "test.event", properties: {} });
170
+ await new Promise((r) => setTimeout(r, 0));
171
+
172
+ const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
173
+ expect(metadata.is_cloud).toBe(true);
174
+ });
175
+
176
+ test("metadata.is_cloud === true when SWARM_CLOUD=1 (mirrors buildIdentity)", async () => {
177
+ process.env.SWARM_CLOUD = "1";
178
+ await initTelemetry(
179
+ "api-server",
180
+ async () => undefined,
181
+ async () => {},
182
+ {
183
+ generateIfMissing: true,
184
+ },
185
+ );
186
+
187
+ track({ event: "test.event", properties: {} });
188
+ await new Promise((r) => setTimeout(r, 0));
189
+
190
+ const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
191
+ expect(metadata.is_cloud).toBe(true);
192
+ });
193
+
194
+ test("includes only the keys that are set (org_id alone)", async () => {
195
+ process.env.SWARM_ORG_ID = "org_solo";
196
+ delete process.env.SWARM_ORG_NAME;
197
+ await initTelemetry(
198
+ "api-server",
199
+ async () => undefined,
200
+ async () => {},
201
+ {
202
+ generateIfMissing: true,
203
+ },
204
+ );
205
+
206
+ track({ event: "test.event", properties: {} });
207
+ await new Promise((r) => setTimeout(r, 0));
208
+
209
+ const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
210
+ expect(metadata.organization_id).toBe("org_solo");
211
+ expect(metadata.organization_name).toBeUndefined();
212
+ });
213
+ });
214
+
79
215
  test("existing config → reuses regardless of generateIfMissing flag", async () => {
80
216
  const existing = "install_deadbeefcafebabe";
81
217
 
@@ -29,6 +29,7 @@ function makeStatus(overrides: {
29
29
  is_cloud: false,
30
30
  marketing_url: null,
31
31
  hide_cloud_promo: false,
32
+ org_id: null,
32
33
  },
33
34
  setup: [
34
35
  { id: "harness", label: "Harness", state: "unverified" },