From f6ceff2eb9d835bbb83ced44a1ba0ac1469ab44c Mon Sep 17 00:00:00 2001 From: jahabeebs <47253537+jahabeebs@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:54:16 -0500 Subject: [PATCH] fix: pr comments --- apps/agent/README.md | 2 +- apps/agent/config.example.yml | 3 + packages/automated-dispute/src/config.ts | 2 +- .../src/exceptions/errorHandler.ts | 8 +- .../src/interfaces/notificationService.ts | 33 ++- .../src/services/discordNotifier.ts | 73 +++-- .../src/services/eboProcessor.ts | 104 ++----- .../automated-dispute/src/types/errorTypes.ts | 1 + .../tests/mocks/eboProcessor.mocks.ts | 4 +- .../tests/services/discordNotifier.spec.ts | 259 +++++++++--------- .../tests/services/eboProcessor.spec.ts | 4 +- 11 files changed, 247 insertions(+), 246 deletions(-) diff --git a/apps/agent/README.md b/apps/agent/README.md index 0c7abe59..4543bb9b 100644 --- a/apps/agent/README.md +++ b/apps/agent/README.md @@ -37,7 +37,7 @@ cp .env.example .env | `BLOCK_NUMBER_RPC_URLS_MAP` | JSON map of chain IDs to arrays of RPC URLs for Block Number calculations | Yes | | `BLOCK_NUMBER_BLOCKMETA_TOKEN` | Bearer token for the Blockmeta service (see notes below on how to obtain) | Yes | | `EBO_AGENT_CONFIG_FILE_PATH` | Path to the agent YAML configuration file | Yes | -| `DISCORD_WEBHOOK` | Your Discord channel webhook for notifications | Yes | +| `DISCORD_WEBHOOK` | Your Discord channel webhook for notifications | No | **Notes:** diff --git a/apps/agent/config.example.yml b/apps/agent/config.example.yml index da0268b8..60f0e14e 100644 --- a/apps/agent/config.example.yml +++ b/apps/agent/config.example.yml @@ -31,3 +31,6 @@ processor: requestModule: "0x1234567890123456789012345678901234567890" # Address of the Request module responseModule: "0x1234567890123456789012345678901234567890" # Address of the Response module escalationModule: "0x1234567890123456789012345678901234567890" # Address of the Escalation module + +notifier: + discordDefaultAvatarUrl: "https://cryptologos.cc/logos/the-graph-grt-logo.png" # Default avatar URL for Discord notifications diff --git a/packages/automated-dispute/src/config.ts b/packages/automated-dispute/src/config.ts index 1274eedf..14d9f5c4 100644 --- a/packages/automated-dispute/src/config.ts +++ b/packages/automated-dispute/src/config.ts @@ -1,7 +1,7 @@ import { z } from "zod"; const ConfigSchema = z.object({ - DISCORD_WEBHOOK: z.string().min(1), + DISCORD_WEBHOOK: z.string().url().optional(), }); export const config = ConfigSchema.parse(process.env); diff --git a/packages/automated-dispute/src/exceptions/errorHandler.ts b/packages/automated-dispute/src/exceptions/errorHandler.ts index dfb7169a..1ca073d4 100644 --- a/packages/automated-dispute/src/exceptions/errorHandler.ts +++ b/packages/automated-dispute/src/exceptions/errorHandler.ts @@ -24,13 +24,11 @@ export class ErrorHandler { this.logger.error(`Error executing custom action: ${actionError}`); } finally { if (strategy.shouldNotify) { - const errorMessage = this.notificationService.createErrorMessage( - error, - context, + await this.notificationService.sendError( "An error occurred in the custom contract", + context, + error, ); - - await this.notificationService.send(errorMessage); } if (strategy.shouldReenqueue && context.reenqueueEvent) { diff --git a/packages/automated-dispute/src/interfaces/notificationService.ts b/packages/automated-dispute/src/interfaces/notificationService.ts index 6b2f41b7..9e881f35 100644 --- a/packages/automated-dispute/src/interfaces/notificationService.ts +++ b/packages/automated-dispute/src/interfaces/notificationService.ts @@ -40,13 +40,36 @@ export interface NotificationService { */ send(message: IMessage): Promise; + /** + * Sends a notification message and throws an exception if sending fails. + * + * @param {IMessage} message - The message to send. + * @returns {Promise} A promise that resolves when the message is sent. + * @throws {NotificationFailureException} If sending the message fails. + */ + sendOrThrow(message: IMessage): Promise; + /** * Creates an IMessage from an error. * - * @param err - The error object of type unknown. - * @param context - Additional context for the error. - * @param defaultMessage - A default message describing the error context. - * @returns An IMessage object ready to be sent via the notifier. + * @param {string} defaultMessage - A default message describing the error context. + * @param {unknown} [context] - Additional context for the error. + * @param {unknown} [err] - The error object. + * @returns {IMessage} An IMessage object ready to be sent via the notifier. + */ + createErrorMessage(defaultMessage: string, context?: unknown, err?: unknown): IMessage; + + /** + * Sends an error notification message. + * + * @param {string} defaultMessage - A default message describing the error context. + * @param {Record} [context] - Additional context for the error. + * @param {unknown} [err] - The error object. + * @returns {Promise} A promise that resolves when the message is sent. */ - createErrorMessage(err: unknown, context: unknown, defaultMessage: string): IMessage; + sendError( + defaultMessage: string, + context?: Record, + err?: unknown, + ): Promise; } diff --git a/packages/automated-dispute/src/services/discordNotifier.ts b/packages/automated-dispute/src/services/discordNotifier.ts index 9511b9e1..2c78f794 100644 --- a/packages/automated-dispute/src/services/discordNotifier.ts +++ b/packages/automated-dispute/src/services/discordNotifier.ts @@ -1,5 +1,6 @@ import { isNativeError } from "util/types"; import type { APIEmbed, JSONEncodable } from "discord.js"; +import { Logger } from "@ebo-agent/shared"; import { WebhookClient, WebhookMessageCreateOptions } from "discord.js"; import { NotificationFailureException } from "../exceptions/index.js"; @@ -14,18 +15,23 @@ export type WebhookMessage = WebhookMessageCreateOptions & { */ export class DiscordNotifier implements NotificationService { private client: WebhookClient; + private defaultAvatarUrl?: string; + private logger: Logger; /** * Creates an instance of DiscordNotifier. * * @param {string} url - The Discord webhook URL. + * @param {string} [defaultAvatarUrl] - The default avatar URL to use if none is provided. */ - constructor(url: string) { + constructor(url: string, defaultAvatarUrl?: string) { this.client = new WebhookClient({ url }); + this.defaultAvatarUrl = defaultAvatarUrl; + this.logger = Logger.getInstance(); } /** - * Sends a notification message to Discord. + * Sends a notification message to Discord. Errors are logged but not thrown. * * @param {IMessage} message - The message to send. * @returns {Promise} A promise that resolves when the message is sent. @@ -34,7 +40,26 @@ export class DiscordNotifier implements NotificationService { const webhookMessage = this.buildWebhookMessage(message); try { await this.client.send(webhookMessage); - console.error(this.client, this.client); + } catch (error) { + this.logger.error( + `Failed to send Discord notification: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + + /** + * Sends a notification message to Discord. Throws an exception if sending fails. + * + * @param {IMessage} message - The message to send. + * @returns {Promise} A promise that resolves when the message is sent. + * @throws {NotificationFailureException} If sending the message fails. + */ + async sendOrThrow(message: IMessage): Promise { + const webhookMessage = this.buildWebhookMessage(message); + try { + await this.client.send(webhookMessage); } catch (error) { throw new NotificationFailureException("An error occurred with the Discord client."); } @@ -50,7 +75,7 @@ export class DiscordNotifier implements NotificationService { return { username: message.username || "EBO Agent", content: message.title, - avatarURL: message.avatarUrl || "https://cryptologos.cc/logos/the-graph-grt-logo.png", + avatarURL: message.avatarUrl || this.defaultAvatarUrl, embeds: message.subtitle || message.description ? [this.buildWebhookMessageEmbed(message)] @@ -76,24 +101,24 @@ export class DiscordNotifier implements NotificationService { /** * Creates an IMessage from an error. * - * @param err - The error object of type unknown. - * @param context - Additional context for the error. - * @param defaultMessage - A default message describing the error context. - * @returns An IMessage object ready to be sent via the notifier. + * @param {string} defaultMessage - A default message describing the error context. + * @param {unknown} [context={}] - Additional context for the error. + * @param {unknown} [err] - The error object. + * @returns {IMessage} An IMessage object ready to be sent via the notifier. */ public createErrorMessage( - err: unknown, - context: Record, defaultMessage: string, + context: unknown = {}, + err?: unknown, ): IMessage { + const contextObject = typeof context === "object" && context !== null ? context : {}; if (isNativeError(err)) { return { title: `**Error:** ${err.name} - ${err.message}`, description: `**Context:**\n\`\`\`json\n${JSON.stringify( { - message: defaultMessage, + ...contextObject, stack: err.stack, - ...context, }, null, 2, @@ -101,12 +126,11 @@ export class DiscordNotifier implements NotificationService { }; } else { return { - title: `**Error:** Unknown error`, + title: `**Error:** ${defaultMessage}`, description: `**Context:**\n\`\`\`json\n${JSON.stringify( { - message: defaultMessage, - error: String(err), - ...context, + ...contextObject, + error: err ? JSON.stringify(err, null, 2) : undefined, }, null, 2, @@ -114,4 +138,21 @@ export class DiscordNotifier implements NotificationService { }; } } + + /** + * Sends an error notification message to Discord. + * + * @param {string} defaultMessage - A default message describing the error context. + * @param {Record} [context={}] - Additional context for the error. + * @param {unknown} [err] - The error object. + * @returns {Promise} A promise that resolves when the message is sent. + */ + public async sendError( + defaultMessage: string, + context: Record = {}, + err?: unknown, + ): Promise { + const errorMessage = this.createErrorMessage(defaultMessage, context, err); + await this.send(errorMessage); + } } diff --git a/packages/automated-dispute/src/services/eboProcessor.ts b/packages/automated-dispute/src/services/eboProcessor.ts index feb2fd23..ed09a68b 100644 --- a/packages/automated-dispute/src/services/eboProcessor.ts +++ b/packages/automated-dispute/src/services/eboProcessor.ts @@ -62,14 +62,12 @@ export class EboProcessor { } catch (err) { this.logger.error(`Unhandled error during the event loop: ${err}`); - const errorMessage = this.notifier.createErrorMessage( - err, - { message: "Unhandled error during the event loop" }, + await this.notifier.sendError( "Unhandled error during the event loop", + { message: "Unhandled error during the event loop" }, + err, ); - await this.notifier.send(errorMessage); - clearInterval(this.eventsInterval); throw err; @@ -148,7 +146,7 @@ export class EboProcessor { if (err instanceof Error) { this.onActorError(requestId, err); } else { - this.onActorError(requestId, new Error(String(err))); + this.onActorError(requestId, new Error(stringify(err))); } } }); @@ -167,13 +165,11 @@ export class EboProcessor { this.logger.error(`Sync failed: ${err}`); } - const errorMessage = this.notifier.createErrorMessage( - err, - { message: "Error during synchronization" }, + await this.notifier.sendError( "Error during synchronization", + { message: "Error during synchronization" }, + err, ); - - await this.notifier.send(errorMessage); } } @@ -338,18 +334,12 @@ export class EboProcessor { } else { this.logger.warn(`Chain ${chainId} not supported by the agent. Skipping...`); - const errorMessage = this.notifier.createErrorMessage( - new Error("Unsupported chain"), - { - message: `Chain ${chainId} not supported by the agent. Skipping...`, - chainId, - requestId, - }, + this.notifier.sendError( `Chain ${chainId} not supported by the agent. Skipping...`, + { chainId, requestId }, + new Error("Unsupported chain"), ); - this.notifier.send(errorMessage); - return null; } } else { @@ -383,16 +373,7 @@ export class EboProcessor { `The request ${requestId} will stop being tracked by the system.`, ); - const errorMessage = this.notifier.createErrorMessage( - error, - { - message: `Actor error for request ${requestId}`, - requestId, - }, - `Actor error for request ${requestId}`, - ); - - this.notifier.send(errorMessage); + this.notifier.sendError(`Actor error for request ${requestId}`, { requestId }, error); this.terminateActor(requestId); } @@ -459,41 +440,15 @@ export class EboProcessor { } } - if (err instanceof Error) { - this.logger.error( - `Could not create a request for epoch ${epoch} and chain ${chain}.`, - ); - - this.logger.error(err); - - const errorMessage = this.notifier.createErrorMessage( - err, - { - message: `Could not create a request for epoch ${epoch} and chain ${chain}.`, - epoch, - chain, - }, - `Could not create a request for epoch ${epoch} and chain ${chain}.`, - ); - - await this.notifier.send(errorMessage); - } else { - this.logger.error( - `Could not create a request for epoch ${epoch} and chain ${chain}: ${err}`, - ); - - const errorMessage = this.notifier.createErrorMessage( - err, - { - message: `Could not create a request for epoch ${epoch} and chain ${chain}.`, - epoch, - chain, - }, - `Could not create a request for epoch ${epoch} and chain ${chain}.`, - ); - - await this.notifier.send(errorMessage); - } + this.logger.error( + `Could not create a request for epoch ${epoch} and chain ${chain}.`, + ); + + this.notifier.sendError( + `Could not create a request for epoch ${epoch} and chain ${chain}.`, + { epoch, chain }, + err, + ); } }); @@ -507,13 +462,7 @@ export class EboProcessor { this.logger.error(`Requests creation failed: ${err}`); } - const errorMessage = this.notifier.createErrorMessage( - err, - { epoch }, - "Error creating missing requests", - ); - - await this.notifier.send(errorMessage); + this.notifier.sendError("Error creating missing requests", { epoch }, err); } } @@ -532,16 +481,11 @@ export class EboProcessor { } else { this.logger.warn(alreadyDeletedActorWarning(requestId)); - const errorMessage = this.notifier.createErrorMessage( - new Error("Actor already deleted"), - { - message: `Actor handling request ${requestId} was already terminated.`, - requestId, - }, + this.notifier.sendError( `Actor handling request ${requestId} was already terminated.`, + { requestId }, + new Error("Actor already deleted"), ); - - this.notifier.send(errorMessage); } } } diff --git a/packages/automated-dispute/src/types/errorTypes.ts b/packages/automated-dispute/src/types/errorTypes.ts index 9e574334..a807c0c5 100644 --- a/packages/automated-dispute/src/types/errorTypes.ts +++ b/packages/automated-dispute/src/types/errorTypes.ts @@ -17,6 +17,7 @@ export type TimeBasedError = BaseErrorStrategy; export type ErrorHandlingStrategy = BaseErrorStrategy; export interface ErrorContext { + [key: string]: unknown; request: Request; response?: Response; dispute?: Dispute; diff --git a/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts b/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts index e2459b49..89cd81ab 100644 --- a/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts +++ b/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts @@ -65,7 +65,9 @@ export function buildEboProcessor( if (!notifier) { notifier = { send: vi.fn(), - createErrorMessage: vi.fn((context, defaultMessage) => { + sendOrThrow: vi.fn(), + sendError: vi.fn(), + createErrorMessage: vi.fn((defaultMessage, context) => { return { title: defaultMessage, description: JSON.stringify(context, null, 2), diff --git a/packages/automated-dispute/tests/services/discordNotifier.spec.ts b/packages/automated-dispute/tests/services/discordNotifier.spec.ts index 28638640..6fdd6500 100644 --- a/packages/automated-dispute/tests/services/discordNotifier.spec.ts +++ b/packages/automated-dispute/tests/services/discordNotifier.spec.ts @@ -5,59 +5,57 @@ import { NotificationFailureException } from "../../src/exceptions/index.js"; import { DiscordNotifier } from "../../src/index.js"; import { IMessage } from "../../src/interfaces/index.js"; -// Change this to `true` to test your Discord webhook URL set below rather than mocking it -const useRealDiscordWebhook = false; -const webhookUrl = "https://discord.com/api/webhooks/KEEP-THIS-SECRET/KEEP-THIS-PART-SECRET-TOO"; +vi.mock("discord.js", () => { + const mockSend = vi.fn(); -if (!useRealDiscordWebhook) { - vi.mock("discord.js", async () => { - const actualDiscord = await vi.importActual("discord.js"); + const MockWebhookClient = vi.fn(() => ({ + send: mockSend, + })); - const mockSend = vi.fn(); - - const MockWebhookClient = vi.fn(() => ({ - send: mockSend, - })); + return { + WebhookClient: MockWebhookClient, + }; +}); - return { - ...actualDiscord, - WebhookClient: MockWebhookClient, - }; - }); -} else { - vi.unmock("discord.js"); -} +vi.mock("../../src/Logger", () => { + const mockLogger = { + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }; + return { + Logger: { + getInstance: () => mockLogger, + }, + }; +}); describe("DiscordNotifier", () => { let notifier: DiscordNotifier; + const webhookUrl = "https://discord.com/api/webhooks/TEST/TEST"; beforeEach(() => { - if (!useRealDiscordWebhook) { - vi.clearAllMocks(); - } + vi.clearAllMocks(); notifier = new DiscordNotifier(webhookUrl); }); - it("initializes the WebhookClient with the correct URL", () => { - if (!useRealDiscordWebhook) { + describe("send", () => { + it("initializes the WebhookClient with the correct URL", () => { const WebhookClientMock = WebhookClient as unknown as vi.Mock; expect(WebhookClientMock).toHaveBeenCalledWith({ url: webhookUrl }); - } - }); + }); + + it("sends a message successfully", async () => { + const message: IMessage = { + title: "Test message", + subtitle: "Test subtitle", + description: "Test description", + username: "TestUserWithoutAvatar", + avatarUrl: "https://example.com/avatar.png", + actionUrl: "https://example.com/action", + }; - it("sends a message successfully", async () => { - const message: IMessage = { - title: "Test message", - subtitle: "Test subtitle", - description: "Test description", - username: "TestUserWithoutAvatar", - avatarUrl: "https://example.com/avatar.png", - actionUrl: "https://example.com/action", - }; - - if (useRealDiscordWebhook) { - await expect(notifier.send(message)).resolves.not.toThrow(); - } else { await notifier.send(message); const WebhookClientMock = WebhookClient as unknown as vi.Mock; @@ -74,127 +72,116 @@ describe("DiscordNotifier", () => { title: "Test subtitle", description: "Test description", color: 2326507, - url: "https://cryptologos.cc/logos/the-graph-grt-logo.png", + url: "https://example.com/action", }, ], }), ); - } - }); - - it("throws NotificationFailureException when send fails", async () => { - if (useRealDiscordWebhook) { - return; - } - - const message: IMessage = { - title: "Test message", - }; - - const WebhookClientMock = WebhookClient as unknown as vi.Mock; - const webhookClientInstance = WebhookClientMock.mock.results[0].value; - const sendMock = webhookClientInstance.send as vi.Mock; - sendMock.mockRejectedValueOnce(new Error("Send failed")); - - await expect(notifier.send(message)).rejects.toThrow(NotificationFailureException); + }); }); - it("builds the webhook message correctly without embed when subtitle and description are not provided", async () => { - const message: IMessage = { - title: "Test message", - }; + describe("sendOrThrow", () => { + it("sends a message successfully", async () => { + const message: IMessage = { + title: "Test message", + }; - if (useRealDiscordWebhook) { - await expect(notifier.send(message)).resolves.not.toThrow(); - } else { - await notifier.send(message); + await notifier.sendOrThrow(message); const WebhookClientMock = WebhookClient as unknown as vi.Mock; const webhookClientInstance = WebhookClientMock.mock.results[0].value; const sendMock = webhookClientInstance.send as vi.Mock; - expect(sendMock).toHaveBeenCalledWith({ - username: "EBO Agent", - content: "Test message", - avatarURL: "https://cryptologos.cc/logos/the-graph-grt-logo.png", - }); - } - }); + expect(sendMock).toHaveBeenCalledWith( + expect.objectContaining({ + username: "EBO Agent", + content: "Test message", + avatarURL: undefined, + }), + ); + }); - it("builds the webhook message with embed when subtitle or description are provided", async () => { - const message: IMessage = { - title: "Test message", - subtitle: "Test subtitle", - description: "Test description", - actionUrl: "https://example.com/action", - }; - - if (useRealDiscordWebhook) { - await expect(notifier.send(message)).resolves.not.toThrow(); - } else { - await notifier.send(message); + it("throws NotificationFailureException when send fails", async () => { + const message: IMessage = { + title: "Test message", + }; const WebhookClientMock = WebhookClient as unknown as vi.Mock; const webhookClientInstance = WebhookClientMock.mock.results[0].value; const sendMock = webhookClientInstance.send as vi.Mock; + sendMock.mockRejectedValueOnce(new Error("Send failed")); - expect(sendMock).toHaveBeenCalledWith( - expect.objectContaining({ - username: "EBO Agent", - content: "Test message", - avatarURL: "https://cryptologos.cc/logos/the-graph-grt-logo.png", - embeds: [ - { - title: "Test subtitle", - description: "Test description", - color: 2326507, - url: "https://example.com/action", - }, - ], - }), + await expect(notifier.sendOrThrow(message)).rejects.toThrow( + NotificationFailureException, ); - } + }); }); - it("creates an error message from an Error object", () => { - const error = new Error("Test error"); - const context = { foo: "bar" }; - const defaultMessage = "Default error message"; - - const errorMessage = notifier.createErrorMessage(error, context, defaultMessage); - - expect(errorMessage).toEqual({ - title: `**Error:** ${error.name} - ${error.message}`, - description: `**Context:**\n\`\`\`json\n${JSON.stringify( - { - message: defaultMessage, - stack: error.stack, - ...context, - }, - null, - 2, - )}\n\`\`\``, + describe("createErrorMessage", () => { + it("creates an error message from an Error object", () => { + const error = new Error("Test error"); + const context = { foo: "bar" }; + const defaultMessage = "Default error message"; + + const errorMessage = notifier.createErrorMessage(defaultMessage, context, error); + + expect(errorMessage).toEqual({ + title: `**Error:** Error - Test error`, + description: `**Context:**\n\`\`\`json\n${JSON.stringify( + { + foo: "bar", + stack: error.stack, + }, + null, + 2, + )}\n\`\`\``, + }); + }); + + it("creates an error message from a non-Error object", () => { + const error = { message: "String error" }; + const context = { foo: "bar" }; + const defaultMessage = "Default error message"; + + const errorMessage = notifier.createErrorMessage(defaultMessage, context, error); + + expect(errorMessage).toEqual({ + title: `**Error:** Default error message`, + description: `**Context:**\n\`\`\`json\n${JSON.stringify( + { + foo: "bar", + error: JSON.stringify(error, null, 2), + }, + null, + 2, + )}\n\`\`\``, + }); }); }); - it("creates an error message from a non-Error object", () => { - const error = "String error"; - const context = { foo: "bar" }; - const defaultMessage = "Default error message"; - - const errorMessage = notifier.createErrorMessage(error, context, defaultMessage); - - expect(errorMessage).toEqual({ - title: `**Error:** Unknown error`, - description: `**Context:**\n\`\`\`json\n${JSON.stringify( - { - message: defaultMessage, - error: String(error), - ...context, - }, - null, - 2, - )}\n\`\`\``, + describe("sendError", () => { + it("sends an error message successfully", async () => { + const error = new Error("Test error"); + const context = { foo: "bar" }; + const defaultMessage = "Default error message"; + + const sendSpy = vi.spyOn(notifier, "send").mockResolvedValueOnce(); + + await notifier.sendError(defaultMessage, context, error); + + expect(sendSpy).toHaveBeenCalledWith({ + title: `**Error:** Error - Test error`, + description: `**Context:**\n\`\`\`json\n${JSON.stringify( + { + foo: "bar", + stack: error.stack, + }, + null, + 2, + )}\n\`\`\``, + }); + + sendSpy.mockRestore(); }); }); }); diff --git a/packages/automated-dispute/tests/services/eboProcessor.spec.ts b/packages/automated-dispute/tests/services/eboProcessor.spec.ts index d4b9f3f9..74274dda 100644 --- a/packages/automated-dispute/tests/services/eboProcessor.spec.ts +++ b/packages/automated-dispute/tests/services/eboProcessor.spec.ts @@ -39,7 +39,9 @@ describe("EboProcessor", () => { vi.useFakeTimers(); notifier = { send: vi.fn().mockResolvedValue(undefined), - createErrorMessage: vi.fn((context, defaultMessage) => { + sendOrThrow: vi.fn().mockResolvedValue(undefined), + sendError: vi.fn().mockResolvedValue(undefined), + createErrorMessage: vi.fn((defaultMessage, context) => { return { title: defaultMessage, description: JSON.stringify(context, null, 2),