@notionhq/workers 0.3.0 → 0.5.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 (62) hide show
  1. package/dist/block.d.ts +1 -1
  2. package/dist/block.d.ts.map +1 -1
  3. package/dist/builder.d.ts +7 -1
  4. package/dist/builder.d.ts.map +1 -1
  5. package/dist/builder.js +13 -0
  6. package/dist/capabilities/ai_connector.js +1 -1
  7. package/dist/capabilities/automation.js +1 -1
  8. package/dist/capabilities/context.d.ts +2 -1
  9. package/dist/capabilities/context.d.ts.map +1 -1
  10. package/dist/capabilities/context.js +21 -3
  11. package/dist/capabilities/stateful-capability.js +1 -1
  12. package/dist/capabilities/sync.d.ts +11 -1
  13. package/dist/capabilities/sync.d.ts.map +1 -1
  14. package/dist/capabilities/sync.js +25 -5
  15. package/dist/capabilities/tool.d.ts +21 -0
  16. package/dist/capabilities/tool.d.ts.map +1 -1
  17. package/dist/capabilities/tool.js +3 -2
  18. package/dist/capabilities/webhook.js +1 -1
  19. package/dist/capabilities/workflow.d.ts +50 -0
  20. package/dist/capabilities/workflow.d.ts.map +1 -0
  21. package/dist/capabilities/workflow.js +40 -0
  22. package/dist/capabilities/workflow.test.d.ts +2 -0
  23. package/dist/capabilities/workflow.test.d.ts.map +1 -0
  24. package/dist/error.d.ts +36 -0
  25. package/dist/error.d.ts.map +1 -1
  26. package/dist/error.js +14 -0
  27. package/dist/index.d.ts +5 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +10 -1
  30. package/dist/schema-builder.d.ts.map +1 -1
  31. package/dist/triggers.d.ts +365 -0
  32. package/dist/triggers.d.ts.map +1 -0
  33. package/dist/triggers.generated.d.ts +246 -0
  34. package/dist/triggers.generated.d.ts.map +1 -0
  35. package/dist/triggers.generated.js +239 -0
  36. package/dist/triggers.js +0 -0
  37. package/dist/types.d.ts +18 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/worker.d.ts +33 -2
  40. package/dist/worker.d.ts.map +1 -1
  41. package/dist/worker.js +34 -0
  42. package/package.json +9 -2
  43. package/src/block.ts +40 -40
  44. package/src/builder.ts +20 -0
  45. package/src/capabilities/ai_connector.ts +1 -1
  46. package/src/capabilities/automation.ts +1 -1
  47. package/src/capabilities/context.ts +45 -3
  48. package/src/capabilities/stateful-capability.ts +1 -1
  49. package/src/capabilities/sync.test.ts +172 -1
  50. package/src/capabilities/sync.ts +46 -5
  51. package/src/capabilities/tool.test.ts +63 -0
  52. package/src/capabilities/tool.ts +23 -1
  53. package/src/capabilities/webhook.ts +1 -1
  54. package/src/capabilities/workflow.test.ts +184 -0
  55. package/src/capabilities/workflow.ts +119 -0
  56. package/src/error.ts +40 -0
  57. package/src/index.ts +22 -2
  58. package/src/schema-builder.ts +1 -0
  59. package/src/triggers.generated.ts +489 -0
  60. package/src/triggers.ts +504 -0
  61. package/src/types.ts +20 -0
  62. package/src/worker.ts +101 -8
@@ -8,7 +8,7 @@ import {
8
8
  vi,
9
9
  } from "vitest";
10
10
  import * as Builder from "../builder.js";
11
- import { ExecutionError } from "../error.js";
11
+ import { ExecutionError, RateLimitError } from "../error.js";
12
12
  import { getPacerState } from "../pacer_internal.js";
13
13
  import * as Schema from "../schema.js";
14
14
  import { Worker } from "../worker.js";
@@ -59,6 +59,34 @@ describe("Worker.sync", () => {
59
59
  });
60
60
  });
61
61
 
62
+ describe("Builder.imageCover", () => {
63
+ it("creates an image cover with the default position", () => {
64
+ const value = Builder.imageCover("https://example.com/cover.jpg");
65
+
66
+ expect(value).toEqual({
67
+ type: "image",
68
+ url: "https://example.com/cover.jpg",
69
+ position: 0.5,
70
+ });
71
+ });
72
+
73
+ it("creates an image cover with a custom position", () => {
74
+ const value = Builder.imageCover("https://example.com/cover.jpg", 0.25);
75
+
76
+ expect(value).toEqual({
77
+ type: "image",
78
+ url: "https://example.com/cover.jpg",
79
+ position: 0.25,
80
+ });
81
+ });
82
+
83
+ it("rejects cover positions outside the supported range", () => {
84
+ expect(() =>
85
+ Builder.imageCover("https://example.com/cover.jpg", 1.5),
86
+ ).toThrow("Expected a number between 0 and 1");
87
+ });
88
+ });
89
+
62
90
  describe("SyncedObject with relation properties", () => {
63
91
  it("allows relation values in synced objects", async () => {
64
92
  const worker = new Worker();
@@ -107,6 +135,42 @@ describe("Worker.sync", () => {
107
135
  });
108
136
  });
109
137
 
138
+ describe("SyncedObject with page covers", () => {
139
+ it("allows cover values in synced objects", async () => {
140
+ const worker = new Worker();
141
+ const tasks = worker.database("tasks", {
142
+ type: "managed",
143
+ initialTitle: "Tasks",
144
+ primaryKeyProperty: "Task ID",
145
+ schema: {
146
+ properties: {
147
+ "Task ID": Schema.title(),
148
+ },
149
+ },
150
+ });
151
+
152
+ const capability = createSyncCapability("tasksSync", {
153
+ database: tasks,
154
+ execute: async () => ({
155
+ changes: [
156
+ {
157
+ type: "upsert",
158
+ key: "task-1",
159
+ properties: {
160
+ "Task ID": Builder.title("TASK-001"),
161
+ },
162
+ cover: Builder.imageCover("https://example.com/cover.jpg"),
163
+ },
164
+ ],
165
+ hasMore: false,
166
+ }),
167
+ });
168
+
169
+ expect(capability._tag).toBe("sync");
170
+ expect(capability.config.databaseKey).toBe("tasks");
171
+ });
172
+ });
173
+
110
174
  describe("pacer local fallback", () => {
111
175
  it("seeds pacer state from declarations when runtime context has no pacers", async () => {
112
176
  const worker = new Worker();
@@ -251,6 +315,113 @@ describe("Worker.sync", () => {
251
315
  );
252
316
  });
253
317
 
318
+ it("wraps RateLimitError in error envelope with _tag rate_limit", async () => {
319
+ const worker = new Worker();
320
+ const db = worker.database("test", {
321
+ type: "managed",
322
+ initialTitle: "Test",
323
+ primaryKeyProperty: "ID",
324
+ schema: {
325
+ properties: {
326
+ ID: Schema.title(),
327
+ },
328
+ },
329
+ });
330
+
331
+ const capability = createSyncCapability("rateLimitSync", {
332
+ database: db,
333
+ execute: async () => {
334
+ throw new RateLimitError();
335
+ },
336
+ });
337
+
338
+ await expect(capability.handler()).rejects.toThrow(RateLimitError);
339
+
340
+ expect(stdoutSpy).toHaveBeenCalledWith(
341
+ expect.stringContaining(`"_tag":"rate_limit"`),
342
+ );
343
+ expect(stdoutSpy).toHaveBeenCalledWith(
344
+ expect.stringContaining(`"name":"RateLimitError"`),
345
+ );
346
+ });
347
+
348
+ it("includes retryAfter in rate_limit error envelope when provided", async () => {
349
+ const worker = new Worker();
350
+ const db = worker.database("test", {
351
+ type: "managed",
352
+ initialTitle: "Test",
353
+ primaryKeyProperty: "ID",
354
+ schema: {
355
+ properties: {
356
+ ID: Schema.title(),
357
+ },
358
+ },
359
+ });
360
+
361
+ const capability = createSyncCapability("rateLimitSync", {
362
+ database: db,
363
+ execute: async () => {
364
+ throw new RateLimitError({ retryAfter: 30 });
365
+ },
366
+ });
367
+
368
+ await expect(capability.handler()).rejects.toThrow(RateLimitError);
369
+
370
+ const output = stdoutSpy.mock.calls
371
+ .map((call) => String(call[0]))
372
+ .find((s) => s.includes(`"_tag":"rate_limit"`)) as string;
373
+ const parsed = JSON.parse(
374
+ output.replace(
375
+ /\n<__notion_output__>(.*)<\/__notion_output__>\n/,
376
+ "$1",
377
+ ),
378
+ );
379
+ expect(parsed).toEqual({
380
+ _tag: "error",
381
+ error: {
382
+ _tag: "rate_limit",
383
+ name: "RateLimitError",
384
+ message:
385
+ "Error during worker execution: Rate limited by external API",
386
+ retryAfter: 30,
387
+ },
388
+ });
389
+ });
390
+
391
+ it("wraps RateLimitError without retryAfter correctly", async () => {
392
+ const worker = new Worker();
393
+ const db = worker.database("test", {
394
+ type: "managed",
395
+ initialTitle: "Test",
396
+ primaryKeyProperty: "ID",
397
+ schema: {
398
+ properties: {
399
+ ID: Schema.title(),
400
+ },
401
+ },
402
+ });
403
+
404
+ const capability = createSyncCapability("rateLimitSync", {
405
+ database: db,
406
+ execute: async () => {
407
+ throw new RateLimitError();
408
+ },
409
+ });
410
+
411
+ await expect(capability.handler()).rejects.toThrow(RateLimitError);
412
+
413
+ const output = stdoutSpy.mock.calls
414
+ .map((call) => String(call[0]))
415
+ .find((s) => s.includes(`"_tag":"rate_limit"`)) as string;
416
+ const parsed = JSON.parse(
417
+ output.replace(
418
+ /\n<__notion_output__>(.*)<\/__notion_output__>\n/,
419
+ "$1",
420
+ ),
421
+ );
422
+ expect(parsed.error.retryAfter).toBeUndefined();
423
+ });
424
+
254
425
  it("emits sync output with key and default targetDatabaseKey", async () => {
255
426
  const worker = new Worker();
256
427
  const tasks = worker.database("tasks", {
@@ -1,4 +1,4 @@
1
- import { ExecutionError } from "../error.js";
1
+ import { ExecutionError, RateLimitError } from "../error.js";
2
2
  import {
3
3
  getPacerState,
4
4
  initPacerState,
@@ -10,6 +10,7 @@ import type {
10
10
  Schema,
11
11
  } from "../schema.js";
12
12
  import type {
13
+ Cover,
13
14
  HandlerOptions,
14
15
  Icon,
15
16
  PeopleValue,
@@ -28,6 +29,18 @@ import {
28
29
  type RuntimeContext as StatefulRuntimeContext,
29
30
  } from "./stateful-capability.js";
30
31
 
32
+ type SyncErrorEnvelope =
33
+ | { _tag: "error"; error: { _tag: "generic"; name: string; message: string } }
34
+ | {
35
+ _tag: "error";
36
+ error: {
37
+ _tag: "rate_limit";
38
+ name: string;
39
+ message: string;
40
+ retryAfter?: number;
41
+ };
42
+ };
43
+
31
44
  /**
32
45
  * Maps a property configuration to its corresponding value type.
33
46
  */
@@ -86,6 +99,11 @@ export type SyncChangeUpsert<
86
99
  * Use the `Builder.emojiIcon()`, `Builder.notionIcon()`, or `Builder.imageIcon()` helpers.
87
100
  */
88
101
  icon?: Icon;
102
+ /**
103
+ * Optional cover to use as the cover for this row's page.
104
+ * Use the `Builder.imageCover()` helper.
105
+ */
106
+ cover?: Cover;
89
107
  /**
90
108
  * Optional markdown content to add to the page body.
91
109
  * This will be converted to Notion blocks and added as page content.
@@ -239,7 +257,7 @@ export function createSyncCapability<
239
257
  runtimeContext?: StatefulRuntimeContext<Context>,
240
258
  options?: HandlerOptions,
241
259
  ) {
242
- const capabilityContext = createCapabilityContext();
260
+ const capabilityContext = createCapabilityContext("sync");
243
261
  const state = runtimeContext?.state ?? runtimeContext?.userContext;
244
262
 
245
263
  initPacerState(runtimeContext?.pacers, pacerDeclarations);
@@ -251,12 +269,35 @@ export function createSyncCapability<
251
269
  capabilityContext,
252
270
  );
253
271
  } catch (err) {
272
+ if (err instanceof RateLimitError) {
273
+ if (!options?.concreteOutput) {
274
+ const envelope: SyncErrorEnvelope = {
275
+ _tag: "error",
276
+ error: {
277
+ _tag: "rate_limit",
278
+ name: err.name,
279
+ message: err.message,
280
+ ...(err.retryAfter !== undefined
281
+ ? { retryAfter: err.retryAfter }
282
+ : {}),
283
+ },
284
+ };
285
+ writeOutput(envelope);
286
+ }
287
+ throw err;
288
+ }
289
+
254
290
  const error = new ExecutionError(err);
255
291
  if (!options?.concreteOutput) {
256
- writeOutput({
292
+ const envelope: SyncErrorEnvelope = {
257
293
  _tag: "error",
258
- error: { name: error.name, message: error.message },
259
- });
294
+ error: {
295
+ _tag: "generic",
296
+ name: error.name,
297
+ message: error.message,
298
+ },
299
+ };
300
+ writeOutput(envelope);
260
301
  }
261
302
  throw error;
262
303
  }
@@ -222,6 +222,69 @@ describe("Worker.tool", () => {
222
222
  expect(capability.config.schema).toBeDefined();
223
223
  });
224
224
 
225
+ describe("hints", () => {
226
+ it("omits hints from config when not specified (existing tools unchanged)", () => {
227
+ const capability = createToolCapability<{ name: string }, string>(
228
+ "noHints",
229
+ {
230
+ title: "No Hints",
231
+ description: "A tool without hints",
232
+ schema: j.object({ name: j.string() }),
233
+ execute: ({ name }) => `Hello, ${name}`,
234
+ },
235
+ );
236
+
237
+ expect(capability.config.hints).toBeUndefined();
238
+ });
239
+
240
+ it("flows readOnlyHint=true through to config", () => {
241
+ const capability = createToolCapability<{ query: string }, string>(
242
+ "readOnly",
243
+ {
244
+ title: "Read Only",
245
+ description: "A read-only tool",
246
+ schema: j.object({ query: j.string() }),
247
+ hints: { readOnlyHint: true },
248
+ execute: ({ query }) => query,
249
+ },
250
+ );
251
+
252
+ expect(capability.config.hints).toEqual({ readOnlyHint: true });
253
+ });
254
+
255
+ it("flows readOnlyHint=false through to config", () => {
256
+ const capability = createToolCapability<{ pageId: string }, string>(
257
+ "writeTool",
258
+ {
259
+ title: "Write Tool",
260
+ description: "A write tool",
261
+ schema: j.object({ pageId: j.string() }),
262
+ hints: { readOnlyHint: false },
263
+ execute: ({ pageId }) => pageId,
264
+ },
265
+ );
266
+
267
+ expect(capability.config.hints).toEqual({ readOnlyHint: false });
268
+ });
269
+
270
+ it("does not affect execution behavior", async () => {
271
+ const capability = createToolCapability<{ x: number }, number>(
272
+ "doubleReadOnly",
273
+ {
274
+ title: "Double",
275
+ description: "Doubles a number",
276
+ schema: j.object({ x: j.number() }),
277
+ hints: { readOnlyHint: true },
278
+ execute: ({ x }) => x * 2,
279
+ },
280
+ );
281
+
282
+ const result = await capability.handler({ x: 7 });
283
+
284
+ expect(result).toEqual({ _tag: "success", value: 14 });
285
+ });
286
+ });
287
+
225
288
  it("invalid output with custom output schema", async () => {
226
289
  const capability = createToolCapability<
227
290
  { value: number },
@@ -70,6 +70,26 @@ function validateRequiredProperties(
70
70
  }
71
71
  }
72
72
 
73
+ /**
74
+ * Hints describing a tool's behavior. Advisory metadata read by the platform
75
+ * (not the model) to decide whether a call is safe to auto-execute or
76
+ * requires user confirmation.
77
+ *
78
+ * Field names mirror the MCP tool annotation shape
79
+ * (`McpServerToolAnnotations`) so worker tools flow through the same
80
+ * confirmation / auto-execute machinery as MCP tools.
81
+ *
82
+ * Only hints with active consumers in the Notion client are included.
83
+ */
84
+ export interface ToolHints {
85
+ /**
86
+ * The tool only reads state and has no side effects. Safe to call
87
+ * repeatedly; safe to auto-execute under default policy. Tools without
88
+ * this hint are treated as write tools and prompt for confirmation.
89
+ */
90
+ readOnlyHint?: boolean;
91
+ }
92
+
73
93
  export interface ToolConfiguration<
74
94
  I extends JSONValue,
75
95
  O extends JSONValue = JSONValue,
@@ -78,6 +98,7 @@ export interface ToolConfiguration<
78
98
  description: string;
79
99
  schema: SchemaBuilder<I>;
80
100
  outputSchema?: SchemaBuilder<O>;
101
+ hints?: ToolHints;
81
102
  execute: (input: I, context: CapabilityContext) => O | Promise<O>;
82
103
  }
83
104
 
@@ -218,7 +239,7 @@ export function createToolCapability<
218
239
  }
219
240
 
220
241
  try {
221
- const capabilityContext = createCapabilityContext();
242
+ const capabilityContext = createCapabilityContext("tool");
222
243
  const result = await config.execute(input, capabilityContext);
223
244
  if (validateOutput && !validateOutput(result)) {
224
245
  const error = new InvalidToolOutputError(
@@ -281,6 +302,7 @@ export function createToolCapability<
281
302
  description: config.description,
282
303
  schema: inputSchema,
283
304
  outputSchema: outputSchema,
305
+ hints: config.hints,
284
306
  },
285
307
  handler,
286
308
  };
@@ -118,7 +118,7 @@ export function createWebhookCapability(
118
118
  options?: HandlerOptions,
119
119
  ): Promise<{ status: "success" } | undefined> {
120
120
  try {
121
- const capabilityContext = createCapabilityContext();
121
+ const capabilityContext = createCapabilityContext("webhook");
122
122
  await config.execute(events, capabilityContext);
123
123
 
124
124
  if (options?.concreteOutput) {
@@ -0,0 +1,184 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ expectTypeOf,
7
+ it,
8
+ type Mock,
9
+ vi,
10
+ } from "vitest";
11
+ import { ExecutionError } from "../error.js";
12
+ import {
13
+ type CapabilityContext,
14
+ Worker,
15
+ type WorkflowConfiguration,
16
+ } from "../index.js";
17
+ import {
18
+ type NotionPageCreatedEvent,
19
+ type NotionPageCreatedTrigger,
20
+ triggers,
21
+ } from "../triggers.generated.js";
22
+ import { createWorkflowCapability } from "./workflow.js";
23
+
24
+ const pageCreatedEvent: NotionPageCreatedEvent = {
25
+ type: "notion.page.created",
26
+ vendor: "notion",
27
+ message: "Page created",
28
+ actor: {
29
+ id: "user-1",
30
+ displayName: "Ada Lovelace",
31
+ email: "ada@example.com",
32
+ vendorActor: {
33
+ vendor: "notion",
34
+ id: "user-1",
35
+ displayName: "Ada Lovelace",
36
+ handle: null,
37
+ type: "user",
38
+ url: null,
39
+ },
40
+ },
41
+ url: "https://www.notion.so/page-1",
42
+ timestamp: "2026-06-26T12:00:00.000Z",
43
+ page: {
44
+ id: "page-1",
45
+ Name: "Welcome",
46
+ Status: "New",
47
+ },
48
+ content: "# Welcome\n\nHello, world!",
49
+ edits: [
50
+ {
51
+ before: null,
52
+ after: {
53
+ id: "page-1",
54
+ Name: "Welcome",
55
+ Status: "New",
56
+ },
57
+ afterContent: "# Welcome\n\nHello, world!",
58
+ contentChanged: true,
59
+ timestamp: 1_782_477_600_000,
60
+ editedBy: null,
61
+ },
62
+ ],
63
+ };
64
+
65
+ describe("createWorkflowCapability", () => {
66
+ let stdoutSpy: Mock<typeof process.stdout.write>;
67
+
68
+ beforeEach(() => {
69
+ stdoutSpy = vi
70
+ .spyOn(process.stdout, "write")
71
+ .mockImplementation(() => true);
72
+ });
73
+
74
+ afterEach(() => {
75
+ vi.restoreAllMocks();
76
+ });
77
+
78
+ it("declares a page-created trigger without a database", () => {
79
+ expect(triggers.notionPageCreated()).toEqual({
80
+ type: "notion.page.created",
81
+ });
82
+ });
83
+
84
+ it("requires trigger declarations in workflow configuration", () => {
85
+ type PageAddedConfiguration = WorkflowConfiguration<
86
+ readonly [NotionPageCreatedTrigger]
87
+ >;
88
+ type ConfigurationWithoutTriggers = Omit<
89
+ PageAddedConfiguration,
90
+ "triggers"
91
+ >;
92
+
93
+ expectTypeOf<ConfigurationWithoutTriggers>().not.toMatchTypeOf<PageAddedConfiguration>();
94
+ });
95
+
96
+ it("infers the canonical page-created event and capability context", () => {
97
+ createWorkflowCapability("pageAdded", {
98
+ title: "Page Added",
99
+ description: "Runs when a page is added",
100
+ triggers: [triggers.notionPageCreated()],
101
+ execute: (event, context) => {
102
+ expectTypeOf(event).toEqualTypeOf<NotionPageCreatedEvent>();
103
+ expectTypeOf(event.type).toEqualTypeOf<"notion.page.created">();
104
+ expectTypeOf(context).toEqualTypeOf<CapabilityContext>();
105
+ },
106
+ });
107
+ });
108
+
109
+ it("includes triggers in capability config and the worker manifest", () => {
110
+ const worker = new Worker();
111
+ worker.workflow("pageAdded", {
112
+ title: "Page Added",
113
+ description: "Runs when a page is added",
114
+ triggers: [triggers.notionPageCreated()],
115
+ execute: () => {},
116
+ });
117
+
118
+ expect(worker.manifest.capabilities).toEqual([
119
+ {
120
+ _tag: "workflow",
121
+ key: "pageAdded",
122
+ config: {
123
+ title: "Page Added",
124
+ description: "Runs when a page is added",
125
+ triggers: [{ type: "notion.page.created" }],
126
+ },
127
+ },
128
+ ]);
129
+ });
130
+
131
+ it("executes with the public event and capability context", async () => {
132
+ const execute = vi.fn();
133
+ const capability = createWorkflowCapability("pageAdded", {
134
+ title: "Page Added",
135
+ description: "Runs when a page is added",
136
+ triggers: [triggers.notionPageCreated()],
137
+ execute,
138
+ });
139
+
140
+ await expect(
141
+ capability.handler(pageCreatedEvent, { concreteOutput: true }),
142
+ ).resolves.toEqual({ status: "success" });
143
+ expect(execute).toHaveBeenCalledWith(
144
+ pageCreatedEvent,
145
+ expect.objectContaining({ notion: expect.any(Object) }),
146
+ );
147
+ expect(stdoutSpy).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it("writes the success envelope by default", async () => {
151
+ const capability = createWorkflowCapability("pageAdded", {
152
+ title: "Page Added",
153
+ description: "Runs when a page is added",
154
+ triggers: [triggers.notionPageCreated()],
155
+ execute: () => {},
156
+ });
157
+
158
+ await capability.handler(pageCreatedEvent);
159
+
160
+ expect(stdoutSpy).toHaveBeenCalledWith(
161
+ '\n<__notion_output__>{"_tag":"success","value":{"status":"success"}}</__notion_output__>\n',
162
+ );
163
+ });
164
+
165
+ it("writes and throws an execution error", async () => {
166
+ const capability = createWorkflowCapability("pageAdded", {
167
+ title: "Page Added",
168
+ description: "Runs when a page is added",
169
+ triggers: [triggers.notionPageCreated()],
170
+ execute: () => {
171
+ throw new Error("Something went wrong");
172
+ },
173
+ });
174
+
175
+ await expect(capability.handler(pageCreatedEvent)).rejects.toThrow(
176
+ ExecutionError,
177
+ );
178
+ expect(stdoutSpy).toHaveBeenCalledWith(
179
+ expect.stringContaining(
180
+ '"_tag":"error","error":{"name":"ExecutionError","message":"Error during worker execution: Error: Something went wrong"',
181
+ ),
182
+ );
183
+ });
184
+ });