@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,105 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { z } from "zod";
3
+ import type { SummaryWithRatingsSchema } from "../../be/memory/raters/llm.js";
4
+ import { summarizeSession } from "../../utils/internal-ai/summarize-session.js";
5
+
6
+ const LONG_TRANSCRIPT = `User: please refactor X
7
+ Assistant: I'll start with reading the file.
8
+ Tool[Read]: input={"file":"/tmp/x"} output="ok"
9
+ Assistant: Now I'll make the change.
10
+ Tool[Edit]: input={"file":"/tmp/x","old":"a","new":"b"} output="ok"
11
+ Assistant: Done.`.padEnd(200, "x");
12
+
13
+ describe("summarizeSession", () => {
14
+ test("pass-through: injected _completeStructured result is returned verbatim", async () => {
15
+ const fake: z.infer<typeof SummaryWithRatingsSchema> = {
16
+ summary: "Learned: X uses Y",
17
+ ratings: [{ id: "mem-1", score: 0.9, reasoning: "very useful" }],
18
+ };
19
+ const result = await summarizeSession({
20
+ harness: "pi",
21
+ transcript: LONG_TRANSCRIPT,
22
+ retrievals: [{ id: "mem-1", name: "x", content: "y" }],
23
+ taskContext: { sourceTaskId: "task-1", agentId: "agent-1" },
24
+ apiUrl: "http://localhost:3013",
25
+ apiKey: "k",
26
+ _completeStructured: (async () => fake) as any,
27
+ });
28
+ expect(result).toEqual(fake);
29
+ });
30
+
31
+ test("retrievals are injected into the userPrompt via buildSummaryWithRatingsPrompt", async () => {
32
+ let capturedUserPrompt = "";
33
+ await summarizeSession({
34
+ harness: "claude",
35
+ transcript: LONG_TRANSCRIPT,
36
+ retrievals: [{ id: "mem-abc", name: "the-name", content: "the-content" }],
37
+ taskContext: { sourceTaskId: "task-1", agentId: "agent-1" },
38
+ apiUrl: "http://localhost:3013",
39
+ apiKey: "k",
40
+ _completeStructured: (async (opts: { userPrompt: string }) => {
41
+ capturedUserPrompt = opts.userPrompt;
42
+ return { summary: "x", ratings: [] };
43
+ }) as any,
44
+ });
45
+ expect(capturedUserPrompt).toContain("mem-abc");
46
+ expect(capturedUserPrompt).toContain("the-name");
47
+ expect(capturedUserPrompt).toContain("the-content");
48
+ // Confirms BASE_SUMMARIZE_PROMPT was used.
49
+ expect(capturedUserPrompt).toContain("high-value learnings");
50
+ // Confirms transcript was included.
51
+ expect(capturedUserPrompt).toContain("Transcript:");
52
+ });
53
+
54
+ test("includes Task: line in prompt when taskContext.prompt provided", async () => {
55
+ let capturedUserPrompt = "";
56
+ await summarizeSession({
57
+ harness: "codex",
58
+ transcript: LONG_TRANSCRIPT,
59
+ retrievals: [],
60
+ taskContext: { sourceTaskId: "task-1", agentId: "agent-1", prompt: "do the thing" },
61
+ apiUrl: "http://localhost:3013",
62
+ apiKey: "k",
63
+ _completeStructured: (async (opts: { userPrompt: string }) => {
64
+ capturedUserPrompt = opts.userPrompt;
65
+ return { summary: "x", ratings: [] };
66
+ }) as any,
67
+ });
68
+ expect(capturedUserPrompt).toContain("Task: do the thing");
69
+ });
70
+
71
+ test("degenerate transcript (≤ 100 chars) returns null without invoking _completeStructured", async () => {
72
+ let invocations = 0;
73
+ const result = await summarizeSession({
74
+ harness: "opencode",
75
+ transcript: "tiny",
76
+ retrievals: [],
77
+ taskContext: { sourceTaskId: "task-1", agentId: "agent-1" },
78
+ apiUrl: "http://localhost:3013",
79
+ apiKey: "k",
80
+ _completeStructured: (async () => {
81
+ invocations++;
82
+ return { summary: "x", ratings: [] };
83
+ }) as any,
84
+ });
85
+ expect(result).toBeNull();
86
+ expect(invocations).toBe(0);
87
+ });
88
+
89
+ test("callerTag is derived as session-summary:<harness>", async () => {
90
+ let capturedTag = "";
91
+ await summarizeSession({
92
+ harness: "pi",
93
+ transcript: LONG_TRANSCRIPT,
94
+ retrievals: [],
95
+ taskContext: { sourceTaskId: "task-1", agentId: "agent-1" },
96
+ apiUrl: "http://localhost:3013",
97
+ apiKey: "k",
98
+ _completeStructured: (async (opts: { callerTag?: string }) => {
99
+ capturedTag = opts.callerTag ?? "";
100
+ return { summary: "x", ratings: [] };
101
+ }) as any,
102
+ });
103
+ expect(capturedTag).toBe("session-summary:pi");
104
+ });
105
+ });
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Unit tests for the opencode plugin's vendored summarize helpers.
3
+ *
4
+ * Plan: thoughts/taras/plans/2026-05-10-fix-session-summarization-workers.md
5
+ * → Phase 2 § "Test coverage"
6
+ *
7
+ * Uses explicit dependency injection (the `deps` parameter on
8
+ * `summarizeSessionForOpencode`) instead of `bun:test`'s `mock.module()`.
9
+ * The latter is process-wide and leaks across test files in the same
10
+ * `bun test` run (verified in Phase 1; see `pi-mono-extension.test.ts`).
11
+ *
12
+ * Test cases (per the plan):
13
+ * 1. `flattenOpencodeTranscript` snapshot with mixed parts
14
+ * 2. Happy path — long transcript + valid summary → POSTs to /api/memory/index
15
+ * 3. Empty messages → no POST, no error
16
+ * 4. `client.session.messages` throws → exactly one
17
+ * `console.error("session_summary failed (opencode):", ...)`
18
+ */
19
+
20
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
21
+ import type { Message, Part } from "@opencode-ai/sdk";
22
+ import {
23
+ flattenOpencodeTranscript,
24
+ type SummarizeSessionForOpencodeDeps,
25
+ type SwarmConfig,
26
+ summarizeSessionForOpencode,
27
+ } from "../../plugin/opencode-plugins/lib/summarize";
28
+
29
+ // ── helpers ───────────────────────────────────────────────────────────────────
30
+
31
+ function makeConfig(overrides: Partial<SwarmConfig> = {}): SwarmConfig {
32
+ return {
33
+ apiUrl: "http://localhost:3013",
34
+ apiKey: "test-key",
35
+ agentId: "agent-oc-1",
36
+ taskId: "task-oc-1",
37
+ isLead: false,
38
+ ...overrides,
39
+ };
40
+ }
41
+
42
+ /** Build a fake `client` whose `session.messages` returns the provided items. */
43
+ function fakeClient(messages: Array<{ info: Message; parts: Part[] }>): {
44
+ session: { messages: (opts: unknown) => Promise<{ data: typeof messages }> };
45
+ } {
46
+ return {
47
+ session: {
48
+ messages: async () => ({ data: messages }),
49
+ },
50
+ };
51
+ }
52
+
53
+ function makeUserText(
54
+ id: string,
55
+ sessionID: string,
56
+ text: string,
57
+ ): {
58
+ info: Message;
59
+ parts: Part[];
60
+ } {
61
+ return {
62
+ info: {
63
+ id,
64
+ sessionID,
65
+ role: "user",
66
+ time: { created: Date.now() },
67
+ } as Message,
68
+ parts: [
69
+ {
70
+ id: `${id}-p1`,
71
+ sessionID,
72
+ messageID: id,
73
+ type: "text",
74
+ text,
75
+ } as Part,
76
+ ],
77
+ };
78
+ }
79
+
80
+ function makeAssistantWithTool(
81
+ id: string,
82
+ sessionID: string,
83
+ text: string,
84
+ toolName: string,
85
+ toolInput: Record<string, unknown>,
86
+ toolOutput: string,
87
+ ): { info: Message; parts: Part[] } {
88
+ return {
89
+ info: {
90
+ id,
91
+ sessionID,
92
+ role: "assistant",
93
+ time: { created: Date.now() },
94
+ } as unknown as Message,
95
+ parts: [
96
+ {
97
+ id: `${id}-p1`,
98
+ sessionID,
99
+ messageID: id,
100
+ type: "text",
101
+ text,
102
+ } as Part,
103
+ {
104
+ id: `${id}-p2`,
105
+ sessionID,
106
+ messageID: id,
107
+ type: "tool",
108
+ callID: `${id}-c1`,
109
+ tool: toolName,
110
+ state: {
111
+ status: "completed",
112
+ input: toolInput,
113
+ output: toolOutput,
114
+ title: "ran tool",
115
+ metadata: {},
116
+ time: { start: Date.now(), end: Date.now() + 100 },
117
+ },
118
+ } as Part,
119
+ ],
120
+ };
121
+ }
122
+
123
+ // ── test state ────────────────────────────────────────────────────────────────
124
+
125
+ const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
126
+ type FetchHandlerResp = {
127
+ ok: boolean;
128
+ status: number;
129
+ text: () => Promise<string>;
130
+ json: () => Promise<unknown>;
131
+ };
132
+ let fetchHandler: ((url: string, init?: RequestInit) => Promise<FetchHandlerResp>) | null = null;
133
+ const consoleErrors: unknown[][] = [];
134
+
135
+ const origFetch = globalThis.fetch;
136
+ const origConsoleError = console.error;
137
+
138
+ beforeEach(() => {
139
+ fetchCalls.length = 0;
140
+ consoleErrors.length = 0;
141
+ fetchHandler = async (url) => {
142
+ if (url.includes("/api/memory/index")) {
143
+ return {
144
+ ok: true,
145
+ status: 202,
146
+ text: async () => "",
147
+ json: async () => ({ queued: true, memoryIds: ["mem-1"] }),
148
+ };
149
+ }
150
+ return { ok: true, status: 200, text: async () => "", json: async () => ({}) };
151
+ };
152
+ globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
153
+ const urlStr = typeof url === "string" ? url : url.toString();
154
+ fetchCalls.push({ url: urlStr, init });
155
+ if (!fetchHandler) return new Response("{}", { status: 200 });
156
+ return fetchHandler(urlStr, init) as unknown as Response;
157
+ }) as typeof fetch;
158
+ console.error = (...args: unknown[]) => {
159
+ consoleErrors.push(args);
160
+ };
161
+ delete process.env.MEMORY_RATERS;
162
+ });
163
+
164
+ afterEach(() => {
165
+ globalThis.fetch = origFetch;
166
+ console.error = origConsoleError;
167
+ });
168
+
169
+ // ── tests ─────────────────────────────────────────────────────────────────────
170
+
171
+ describe("flattenOpencodeTranscript", () => {
172
+ test("emits User: / Assistant: / Tool[..]: lines in order, ignores other parts", () => {
173
+ const sessionID = "s1";
174
+ const items = [
175
+ makeUserText("m1", sessionID, "Please update the README"),
176
+ makeAssistantWithTool(
177
+ "m2",
178
+ sessionID,
179
+ "Reading the file first",
180
+ "read",
181
+ { path: "/workspace/README.md" },
182
+ "(file contents)",
183
+ ),
184
+ makeUserText("m3", sessionID, "Looks good, ship it"),
185
+ ];
186
+ const result = flattenOpencodeTranscript(items);
187
+ expect(result).toContain("User: Please update the README");
188
+ expect(result).toContain("Assistant: Reading the file first");
189
+ expect(result).toContain('Tool[read]: input={"path":"/workspace/README.md"}');
190
+ expect(result).toContain('output="(file contents)"');
191
+ expect(result).toContain("User: Looks good, ship it");
192
+ // Order check
193
+ const lines = result.split("\n");
194
+ expect(lines[0]).toBe("User: Please update the README");
195
+ expect(lines[1]).toBe("Assistant: Reading the file first");
196
+ expect(lines[2]!.startsWith("Tool[read]:")).toBe(true);
197
+ expect(lines[3]).toBe("User: Looks good, ship it");
198
+ });
199
+
200
+ test("incomplete tool state (status=running) is dropped", () => {
201
+ const items = [
202
+ {
203
+ info: {
204
+ id: "m1",
205
+ sessionID: "s1",
206
+ role: "assistant" as const,
207
+ time: { created: 0 },
208
+ } as Message,
209
+ parts: [
210
+ {
211
+ id: "p1",
212
+ sessionID: "s1",
213
+ messageID: "m1",
214
+ type: "tool" as const,
215
+ callID: "c1",
216
+ tool: "bash",
217
+ state: {
218
+ status: "running" as const,
219
+ input: { cmd: "ls" },
220
+ time: { start: 0 },
221
+ },
222
+ } as Part,
223
+ ],
224
+ },
225
+ ];
226
+ const result = flattenOpencodeTranscript(items);
227
+ // Running tool calls should not appear.
228
+ expect(result).toBe("");
229
+ });
230
+
231
+ test("reasoning / file / step parts are ignored", () => {
232
+ const items = [
233
+ makeUserText("m1", "s1", "Do work"),
234
+ {
235
+ info: {
236
+ id: "m2",
237
+ sessionID: "s1",
238
+ role: "assistant" as const,
239
+ time: { created: 0 },
240
+ } as Message,
241
+ parts: [
242
+ {
243
+ id: "p1",
244
+ sessionID: "s1",
245
+ messageID: "m2",
246
+ type: "reasoning",
247
+ text: "thinking...",
248
+ time: { start: 0 },
249
+ } as Part,
250
+ {
251
+ id: "p2",
252
+ sessionID: "s1",
253
+ messageID: "m2",
254
+ type: "step-start",
255
+ } as Part,
256
+ {
257
+ id: "p3",
258
+ sessionID: "s1",
259
+ messageID: "m2",
260
+ type: "text",
261
+ text: "ok done",
262
+ } as Part,
263
+ ],
264
+ },
265
+ ];
266
+ const result = flattenOpencodeTranscript(items);
267
+ expect(result).toBe("User: Do work\nAssistant: ok done");
268
+ });
269
+
270
+ test("empty items array → empty string", () => {
271
+ expect(flattenOpencodeTranscript([])).toBe("");
272
+ });
273
+ });
274
+
275
+ describe("summarizeSessionForOpencode", () => {
276
+ test("happy path — long transcript + valid summary → POSTs to /api/memory/index", async () => {
277
+ const items: Array<{ info: Message; parts: Part[] }> = [];
278
+ // Generate enough lines to exceed the 100-char gate.
279
+ for (let i = 0; i < 10; i++) {
280
+ items.push(makeUserText(`m${i}u`, "s1", `Doing task ${i} with multiple details and notes`));
281
+ items.push(
282
+ makeAssistantWithTool(
283
+ `m${i}a`,
284
+ "s1",
285
+ `Working on task ${i} now in detail`,
286
+ "edit",
287
+ { path: `/file${i}` },
288
+ `result-${i}`,
289
+ ),
290
+ );
291
+ }
292
+
293
+ let runSummaryArgs: { systemPrompt: string; userPrompt: string } | null = null;
294
+ const deps: SummarizeSessionForOpencodeDeps = {
295
+ resolveAuth: async () => ({
296
+ kind: "anthropic" as const,
297
+ apiKey: "sk-test",
298
+ modelDefault: "anthropic/claude-haiku-4-5",
299
+ }),
300
+ runSummaryLlm: async (_cred, systemPrompt, userPrompt) => {
301
+ runSummaryArgs = { systemPrompt, userPrompt };
302
+ return {
303
+ summary: "Learned X about Y — concrete reusable fact about opencode.",
304
+ ratings: [],
305
+ };
306
+ },
307
+ };
308
+
309
+ await summarizeSessionForOpencode(makeConfig(), fakeClient(items) as never, "s1", deps);
310
+
311
+ expect(runSummaryArgs).not.toBeNull();
312
+ expect(runSummaryArgs!.systemPrompt).toContain("expert at extracting durable");
313
+ expect(runSummaryArgs!.userPrompt).toContain("Transcript:");
314
+
315
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
316
+ expect(indexCalls.length).toBe(1);
317
+ const body = JSON.parse(indexCalls[0]!.init?.body as string) as Record<string, unknown>;
318
+ expect(body.scope).toBe("agent");
319
+ expect(body.source).toBe("session_summary");
320
+ expect(body.sourceTaskId).toBe("task-oc-1");
321
+ expect(body.agentId).toBe("agent-oc-1");
322
+ expect(body.name).toBe("session-summary");
323
+ expect(body.content).toBe("Learned X about Y — concrete reusable fact about opencode.");
324
+
325
+ expect(consoleErrors.length).toBe(0);
326
+ });
327
+
328
+ test("empty messages array → no POST, no error", async () => {
329
+ await summarizeSessionForOpencode(makeConfig(), fakeClient([]) as never, "s1", {
330
+ // Should never be called.
331
+ resolveAuth: async () => {
332
+ throw new Error("resolveAuth should not be called for empty transcript");
333
+ },
334
+ });
335
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
336
+ expect(indexCalls.length).toBe(0);
337
+ expect(consoleErrors.length).toBe(0);
338
+ });
339
+
340
+ test("transcript ≤100 chars after flattening → no POST, no error", async () => {
341
+ const items = [makeUserText("m1", "s1", "hi")];
342
+ await summarizeSessionForOpencode(makeConfig(), fakeClient(items) as never, "s1", {
343
+ resolveAuth: async () => {
344
+ throw new Error("resolveAuth should not be called for short transcript");
345
+ },
346
+ });
347
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
348
+ expect(indexCalls.length).toBe(0);
349
+ expect(consoleErrors.length).toBe(0);
350
+ });
351
+
352
+ test("client.session.messages throws → exactly one console.error('session_summary failed (opencode):', ...)", async () => {
353
+ const fakeClientThrows = {
354
+ session: {
355
+ messages: async () => {
356
+ throw new Error("opencode SDK boom");
357
+ },
358
+ },
359
+ };
360
+
361
+ await summarizeSessionForOpencode(makeConfig(), fakeClientThrows as never, "s1", {
362
+ resolveAuth: async () => {
363
+ throw new Error("resolveAuth should not be called when SDK throws");
364
+ },
365
+ });
366
+
367
+ // No index POST should fire.
368
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
369
+ expect(indexCalls.length).toBe(0);
370
+
371
+ // Exactly one error log with the documented prefix.
372
+ const opencodeErrors = consoleErrors.filter((args) =>
373
+ String(args[0] ?? "").includes("session_summary failed (opencode):"),
374
+ );
375
+ expect(opencodeErrors.length).toBe(1);
376
+ });
377
+
378
+ test("resolveAuth returns null → no POST, no error log (graceful no-op)", async () => {
379
+ const items: Array<{ info: Message; parts: Part[] }> = [];
380
+ for (let i = 0; i < 5; i++) {
381
+ items.push(makeUserText(`m${i}`, "s1", `long enough transcript line ${i} with detail`));
382
+ }
383
+ await summarizeSessionForOpencode(makeConfig(), fakeClient(items) as never, "s1", {
384
+ resolveAuth: async () => null,
385
+ runSummaryLlm: async () => {
386
+ throw new Error("runSummaryLlm should not be called when no creds");
387
+ },
388
+ });
389
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
390
+ expect(indexCalls.length).toBe(0);
391
+ expect(consoleErrors.length).toBe(0);
392
+ });
393
+
394
+ test("summary contains 'no significant learnings' → no POST", async () => {
395
+ const items: Array<{ info: Message; parts: Part[] }> = [];
396
+ for (let i = 0; i < 5; i++) {
397
+ items.push(makeUserText(`m${i}`, "s1", `long enough transcript line ${i} with detail`));
398
+ }
399
+ await summarizeSessionForOpencode(makeConfig(), fakeClient(items) as never, "s1", {
400
+ resolveAuth: async () => ({
401
+ kind: "openrouter" as const,
402
+ apiKey: "sk-test",
403
+ modelDefault: "openrouter/google/gemini-3-flash-preview",
404
+ }),
405
+ runSummaryLlm: async () => ({
406
+ summary: "No significant learnings.",
407
+ ratings: [],
408
+ }),
409
+ });
410
+ const indexCalls = fetchCalls.filter((c) => c.url.endsWith("/api/memory/index"));
411
+ expect(indexCalls.length).toBe(0);
412
+ });
413
+
414
+ test("/api/memory/index POST 500 → console.error with documented prefix, no throw", async () => {
415
+ fetchHandler = async (url) => {
416
+ if (url.includes("/api/memory/index")) {
417
+ return {
418
+ ok: false,
419
+ status: 500,
420
+ text: async () => "server boom",
421
+ json: async () => ({}),
422
+ };
423
+ }
424
+ return { ok: true, status: 200, text: async () => "", json: async () => ({}) };
425
+ };
426
+
427
+ const items: Array<{ info: Message; parts: Part[] }> = [];
428
+ for (let i = 0; i < 5; i++) {
429
+ items.push(makeUserText(`m${i}`, "s1", `long enough transcript line ${i} with detail`));
430
+ }
431
+ await summarizeSessionForOpencode(makeConfig(), fakeClient(items) as never, "s1", {
432
+ resolveAuth: async () => ({
433
+ kind: "openrouter" as const,
434
+ apiKey: "sk-test",
435
+ modelDefault: "openrouter/google/gemini-3-flash-preview",
436
+ }),
437
+ runSummaryLlm: async () => ({
438
+ summary: "A concrete reusable fact about opencode.",
439
+ ratings: [],
440
+ }),
441
+ });
442
+
443
+ const postFailures = consoleErrors.filter((args) =>
444
+ String(args[0] ?? "").includes("session_summary: /api/memory/index POST failed (opencode):"),
445
+ );
446
+ expect(postFailures.length).toBe(1);
447
+ });
448
+
449
+ test("ratings path — MEMORY_RATERS=llm + retrievals + ratings → postRatings called with events: key", async () => {
450
+ process.env.MEMORY_RATERS = "llm";
451
+
452
+ const retrievals = [
453
+ { id: "mem-a", name: "memA", content: "content A" },
454
+ { id: "mem-b", name: "memB", content: "content B" },
455
+ ];
456
+
457
+ let postRatingsArgs: { events: unknown[]; taskId?: string; agentId?: string } | null = null;
458
+
459
+ const items: Array<{ info: Message; parts: Part[] }> = [];
460
+ for (let i = 0; i < 5; i++) {
461
+ items.push(makeUserText(`m${i}`, "s1", `long enough transcript line ${i} with detail`));
462
+ }
463
+
464
+ await summarizeSessionForOpencode(makeConfig(), fakeClient(items) as never, "s1", {
465
+ resolveAuth: async () => ({
466
+ kind: "openrouter" as const,
467
+ apiKey: "sk-test",
468
+ modelDefault: "openrouter/google/gemini-3-flash-preview",
469
+ }),
470
+ runSummaryLlm: async () => ({
471
+ summary: "Learned that mem-a is highly relevant and mem-b is irrelevant.",
472
+ ratings: [
473
+ { id: "mem-a", score: 0.9, reasoning: "directly used" },
474
+ { id: "mem-b", score: 0.1, reasoning: "off-topic" },
475
+ ],
476
+ }),
477
+ fetchRetrievalsForTask: async () => retrievals,
478
+ postRatings: async (args) => {
479
+ postRatingsArgs = {
480
+ events: args.events,
481
+ taskId: args.taskId,
482
+ agentId: args.agentId,
483
+ };
484
+ return { ok: true, status: 200 };
485
+ },
486
+ });
487
+
488
+ expect(postRatingsArgs).not.toBeNull();
489
+ expect(postRatingsArgs!.agentId).toBe("agent-oc-1");
490
+ // Critical: events:, not ratings: — Phase 1 errata.
491
+ expect(Array.isArray(postRatingsArgs!.events)).toBe(true);
492
+ expect(postRatingsArgs!.events.length).toBe(2);
493
+ // Task ID is passed for cross-referencing.
494
+ expect(postRatingsArgs!.taskId).toBe("task-oc-1");
495
+ });
496
+ });