import { describe, test, expect, beforeAll, afterAll } from "vitest"; import type { PrismaClient } from "@trigger.dev/database"; import type * as DeliverErrorGroupAlertModule from "../app/v3/services/alerts/deliverErrorGroupAlert.server.js"; import type * as SecretStoreModule from "../app/services/secrets/secretStore.server.js"; let DeliverErrorGroupAlertService: typeof DeliverErrorGroupAlertModule.DeliverErrorGroupAlertService; let prisma: PrismaClient; let getSecretStore: typeof SecretStoreModule.getSecretStore; type ErrorAlertPayload = { channelId: string; projectId: string; classification: "new_issue" | "regression" | "unignored"; error: { fingerprint: string; environmentId: string; environmentSlug: string; environmentName: string; taskIdentifier: string; errorType: string; errorMessage: string; sampleStackTrace: string; firstSeen: string; lastSeen: string; occurrenceCount: number; }; }; let testChannelId: string; let testProjectId: string; let testOrganizationId: string; let testSecretKey: string; let testSecretReferenceId: string; // Helper to create mock error payloads function createMockErrorPayload( overrides: Partial> & { error?: Partial; } = {} ): ErrorAlertPayload { const { error: errorOverrides, ...payloadOverrides } = overrides; const defaultError: ErrorAlertPayload["error"] = { fingerprint: "fp_test_" + Date.now(), environmentId: "env_test_dev", environmentSlug: "dev", environmentName: "Development", taskIdentifier: "process-payment", errorType: "TypeError", errorMessage: "Cannot read property 'id' of undefined", sampleStackTrace: `TypeError: Cannot read property 'id' of undefined at processPayment (src/tasks/payment.ts:42:15) at Object.run (src/tasks/payment.ts:15:20) at TaskExecutor.execute (node_modules/@trigger.dev/core/dist/index.js:234:18)`, firstSeen: Date.now().toString(), lastSeen: Date.now().toString(), occurrenceCount: 42, ...errorOverrides, }; return { channelId: testChannelId, projectId: testProjectId, classification: "new_issue", ...payloadOverrides, error: defaultError, }; } // Skip tests if Slack credentials not configured const hasSlackCredentials = !!process.env.TEST_SLACK_CHANNEL_ID && !!process.env.TEST_SLACK_BOT_TOKEN; describe.skipIf(!hasSlackCredentials)("Slack Error Alert Visual Tests", () => { beforeAll(async () => { const dbModule = await import("../app/db.server.js"); prisma = dbModule.prisma; const secretModule = await import("../app/services/secrets/secretStore.server.js"); getSecretStore = secretModule.getSecretStore; const alertModule = await import("../app/v3/services/alerts/deliverErrorGroupAlert.server.js"); DeliverErrorGroupAlertService = alertModule.DeliverErrorGroupAlertService; const organization = await prisma.organization.create({ data: { title: "Slack Test Org", slug: "slack-test-org-" + Date.now(), }, }); testOrganizationId = organization.id; // Create test project const project = await prisma.project.create({ data: { name: "Slack Test Project", slug: "slack-test-project-" + Date.now(), externalRef: "proj_slack_test_" + Date.now(), organizationId: organization.id, }, }); testProjectId = project.id; const secretStore = getSecretStore("DATABASE"); testSecretKey = `slack-test-token-${Date.now()}`; await secretStore.setSecret(testSecretKey, { botAccessToken: process.env.TEST_SLACK_BOT_TOKEN!, }); const secretReference = await prisma.secretReference.create({ data: { key: testSecretKey, provider: "DATABASE", }, }); testSecretReferenceId = secretReference.id; // Create Slack organization integration const integration = await prisma.organizationIntegration.create({ data: { friendlyId: "integration_test_" + Date.now(), organizationId: organization.id, service: "SLACK", integrationData: {}, tokenReferenceId: secretReference.id, }, }); // Create alert channel const channel = await prisma.projectAlertChannel.create({ data: { friendlyId: "channel_test_" + Date.now(), name: "Test Slack Channel", type: "SLACK", enabled: true, projectId: project.id, integrationId: integration.id, properties: { channelId: process.env.TEST_SLACK_CHANNEL_ID!, channelName: "test-slack-alerts", integrationId: integration.id, }, }, }); testChannelId = channel.id; }); afterAll(async () => { if (testChannelId) { await prisma.projectAlertChannel.deleteMany({ where: { id: testChannelId }, }); } if (testOrganizationId) { await prisma.organizationIntegration.deleteMany({ where: { organizationId: testOrganizationId }, }); } if (testSecretReferenceId) { await prisma.secretReference.deleteMany({ where: { id: testSecretReferenceId }, }); } if (testSecretKey) { const secretStore = getSecretStore("DATABASE"); await secretStore.deleteSecret(testSecretKey); } if (testProjectId) { await prisma.project.deleteMany({ where: { id: testProjectId }, }); } if (testOrganizationId) { await prisma.organization.deleteMany({ where: { id: testOrganizationId }, }); } }); test("new_issue classification", async () => { const payload = createMockErrorPayload({ classification: "new_issue", error: { taskIdentifier: "process-order", errorMessage: "Failed to process order due to invalid payment method", errorType: "PaymentError", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); // Message sent - check Slack channel visually expect(true).toBe(true); }); test("regression classification", async () => { const payload = createMockErrorPayload({ classification: "regression", error: { taskIdentifier: "send-email", errorMessage: "SMTP connection timeout after 30 seconds", errorType: "TimeoutError", occurrenceCount: 156, }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("unignored (resurfaced) classification", async () => { const payload = createMockErrorPayload({ classification: "unignored", error: { taskIdentifier: "sync-database", errorMessage: "Connection pool exhausted", errorType: "DatabaseError", occurrenceCount: 99, }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("short error message", async () => { const payload = createMockErrorPayload({ error: { errorMessage: "Not found", errorType: "NotFoundError", sampleStackTrace: "NotFoundError: Not found\n at findUser (src/db.ts:10:5)", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("long stack trace", async () => { const longStackTrace = `ReferenceError: processData is not defined at handler (src/tasks/data-processor.ts:125:15) at async TaskRunner.execute (node_modules/@trigger.dev/sdk/dist/runner.js:89:12) at async WorkerThread.processTask (node_modules/@trigger.dev/sdk/dist/worker.js:234:18) at async WorkerPool.run (src/lib/worker-pool.ts:56:10) at async TaskQueue.dequeue (src/lib/queue.ts:142:8) at async Orchestrator.processNextTask (src/orchestrator.ts:98:5) at async Orchestrator.start (src/orchestrator.ts:45:7) at async main (src/index.ts:12:3) at Object. (src/index.ts:20:1) at Module._compile (node:internal/modules/cjs/loader:1376:14) at Module._extensions..js (node:internal/modules/cjs/loader:1435:10) at Module.load (node:internal/modules/cjs/loader:1207:32) at Module._load (node:internal/modules/cjs/loader:1023:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12) at node:internal/main/run_main_module:28:49`; const payload = createMockErrorPayload({ error: { errorType: "ReferenceError", errorMessage: "processData is not defined", sampleStackTrace: longStackTrace, taskIdentifier: "data-processor", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("very long error message (triggers truncation)", async () => { // Create a message that's over 3000 characters const longMessage = "x".repeat(3500); const longStackTrace = `Error: ${longMessage} at verylongfunctionname (src/tasks/long-task.ts:1:1) ${" at stackframe (file.ts:1:1)\n".repeat(100)}`; const payload = createMockErrorPayload({ error: { errorMessage: longMessage, sampleStackTrace: longStackTrace, taskIdentifier: "long-error-task", errorType: "Error", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); // Should see truncation message in Slack expect(true).toBe(true); }); test("special characters in error", async () => { const payload = createMockErrorPayload({ error: { errorMessage: "Unexpected token `<` in JSON at position 0", errorType: "SyntaxError", sampleStackTrace: `SyntaxError: Unexpected token \`<\` in JSON at position 0 at JSON.parse () at parseResponse (src/api/client.ts:42:15) at fetch("https://api.example.com/data?query=test&limit=10")`, taskIdentifier: "api-fetch-task", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("unicode and emoji in error", async () => { const payload = createMockErrorPayload({ error: { errorMessage: "Failed to process emoji πŸ”₯ in message: Hello δΈ–η•Œ", errorType: "EncodingError", sampleStackTrace: `EncodingError: Failed to process emoji πŸ”₯ in message: Hello δΈ–η•Œ at encodeMessage (src/utils/encoding.ts:15:10) at sendMessage (src/tasks/messaging.ts:42:8)`, taskIdentifier: "messaging-task", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("different error types - TypeError", async () => { const payload = createMockErrorPayload({ error: { errorType: "TypeError", errorMessage: "Cannot call method 'map' on undefined", sampleStackTrace: `TypeError: Cannot call method 'map' on undefined at transformData (src/transformers/data.ts:18:25)`, }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("different error types - ReferenceError", async () => { const payload = createMockErrorPayload({ error: { errorType: "ReferenceError", errorMessage: "userConfig is not defined", sampleStackTrace: `ReferenceError: userConfig is not defined at initializeApp (src/app.ts:32:10)`, }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("different error types - Custom Error", async () => { const payload = createMockErrorPayload({ error: { errorType: "InvalidConfigurationError", errorMessage: "API key is missing or invalid", sampleStackTrace: `InvalidConfigurationError: API key is missing or invalid at validateConfig (src/config/validator.ts:45:11) at loadConfig (src/config/loader.ts:23:5)`, taskIdentifier: "config-loader", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); test("error with no stack trace", async () => { const payload = createMockErrorPayload({ error: { errorMessage: "An unknown error occurred", errorType: "Error", sampleStackTrace: "", }, }); const service = new DeliverErrorGroupAlertService(); await service.call(payload); expect(true).toBe(true); }); });