From 64b6eb79d4c6d8dd2207c07668f79c9b012b6fa2 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Thu, 7 May 2026 00:11:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(core-testing):=20R49=20guard=20=E2=80=94?= =?UTF-8?q?=20block=20real=20Sentry=20SDK=20init=20in=20test=20processes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core-testing/package.json | 2 +- .../src/instrumentation/recording-logger.ts | 26 +++++++++++++---- .../src/instrumentation/recording-tracer.ts | 25 ++++++++++++---- packages/core-testing/src/setup/jsdom.ts | 1 + .../core-testing/src/setup/no-sentry.test.ts | 16 ++++++++++ packages/core-testing/src/setup/no-sentry.ts | 29 +++++++++++++++++++ packages/core-testing/src/setup/node.ts | 2 ++ 7 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 packages/core-testing/src/setup/no-sentry.test.ts create mode 100644 packages/core-testing/src/setup/no-sentry.ts diff --git a/packages/core-testing/package.json b/packages/core-testing/package.json index 2fdcc8f..76cee9d 100644 --- a/packages/core-testing/package.json +++ b/packages/core-testing/package.json @@ -21,7 +21,6 @@ "test": "vitest run" }, "dependencies": { - "@repo/core-shared": "workspace:*", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.0", @@ -43,6 +42,7 @@ "devDependencies": { "@repo/core-eslint": "workspace:*", "@repo/core-typescript": "workspace:*", + "@sentry/nextjs": "^10.51.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "jsdom": "^25.0.0", diff --git a/packages/core-testing/src/instrumentation/recording-logger.ts b/packages/core-testing/src/instrumentation/recording-logger.ts index c4e965e..bb6e7c2 100644 --- a/packages/core-testing/src/instrumentation/recording-logger.ts +++ b/packages/core-testing/src/instrumentation/recording-logger.ts @@ -1,8 +1,24 @@ -import type { - ILogger, - Breadcrumb, - CaptureContext, -} from "@repo/core-shared/instrumentation"; +// Local type aliases matching the contracts in @repo/core-shared/instrumentation. +// Kept inline to avoid a build-graph cycle between core-testing and core-shared. +type Breadcrumb = { + category: string; + message: string; + level?: "info" | "warning" | "error"; + data?: Record; +}; + +type CaptureContext = { + tags?: Record; + extras?: Record; + fingerprint?: string[]; +}; + +interface ILogger { + captureException(err: unknown, ctx?: CaptureContext): void; + captureMessage(msg: string, level?: "info" | "warning" | "error", ctx?: CaptureContext): void; + addBreadcrumb(b: Breadcrumb): void; + setUser(user: { id: string } | null): void; +} export type RecordedCapture = | { kind: "exception"; err: unknown; ctx?: CaptureContext } diff --git a/packages/core-testing/src/instrumentation/recording-tracer.ts b/packages/core-testing/src/instrumentation/recording-tracer.ts index 31a1c36..82fdffb 100644 --- a/packages/core-testing/src/instrumentation/recording-tracer.ts +++ b/packages/core-testing/src/instrumentation/recording-tracer.ts @@ -1,9 +1,21 @@ -import type { - ITracer, - ISpan, - SpanOpts, - AttributeValue, -} from "@repo/core-shared/instrumentation"; +// Local type aliases matching the contracts in @repo/core-shared/instrumentation. +// Kept inline to avoid a build-graph cycle between core-testing and core-shared. +type AttributeValue = string | number | boolean | null; + +type SpanOpts = { + name: string; + op?: string; + attributes?: Record; +}; + +interface ISpan { + setAttribute(key: string, value: AttributeValue): void; + setStatus(status: "ok" | "error", message?: string): void; +} + +interface ITracer { + startSpan(opts: SpanOpts, fn: (span: ISpan) => Promise): Promise; +} export type RecordedSpan = { name: string; @@ -14,6 +26,7 @@ export type RecordedSpan = { durationMs: number; }; +// Exported so callers can use it as a compatible ITracer via structural typing. export class RecordingTracer implements ITracer { spans: RecordedSpan[] = []; diff --git a/packages/core-testing/src/setup/jsdom.ts b/packages/core-testing/src/setup/jsdom.ts index 2ba240b..c079b4f 100644 --- a/packages/core-testing/src/setup/jsdom.ts +++ b/packages/core-testing/src/setup/jsdom.ts @@ -1,3 +1,4 @@ +import "./no-sentry"; import "@testing-library/jest-dom/vitest"; import { afterEach } from "vitest"; import { cleanup } from "@testing-library/react"; diff --git a/packages/core-testing/src/setup/no-sentry.test.ts b/packages/core-testing/src/setup/no-sentry.test.ts new file mode 100644 index 0000000..5f19577 --- /dev/null +++ b/packages/core-testing/src/setup/no-sentry.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect, vi } from "vitest"; +import * as Sentry from "@sentry/nextjs"; + +describe("setup/no-sentry guard (R49)", () => { + it("Sentry.init is a vi.fn (mocked, not real)", () => { + expect(vi.isMockFunction(Sentry.init)).toBe(true); + }); + + it("Sentry.captureException is a vi.fn", () => { + expect(vi.isMockFunction(Sentry.captureException)).toBe(true); + }); + + it("calling Sentry.init does not throw or initialize", () => { + expect(() => Sentry.init({ dsn: "https://x@y/1" } as Parameters[0])).not.toThrow(); + }); +}); diff --git a/packages/core-testing/src/setup/no-sentry.ts b/packages/core-testing/src/setup/no-sentry.ts new file mode 100644 index 0000000..83c3c99 --- /dev/null +++ b/packages/core-testing/src/setup/no-sentry.ts @@ -0,0 +1,29 @@ +import { vi } from "vitest"; + +/** + * R49 — guard against real Sentry SDK initialization in test processes. + * + * Mocks @sentry/nextjs at the module level so any code that imports it + * receives a no-op surface. Tests that need to assert Sentry behavior + * still use vi.mock locally with their own implementation; this guard + * just ensures *unintentional* imports don't cause real network/init. + */ +vi.mock("@sentry/nextjs", () => ({ + init: vi.fn(), + startSpan: vi.fn((_opts: unknown, fn: (span: unknown) => unknown) => + fn({ setAttribute: vi.fn(), setStatus: vi.fn() }), + ), + captureException: vi.fn(), + captureMessage: vi.fn(), + addBreadcrumb: vi.fn(), + setUser: vi.fn(), + setContext: vi.fn(), + setTag: vi.fn(), + setExtra: vi.fn(), + withScope: vi.fn((fn: (scope: unknown) => unknown) => + fn({ setTag: vi.fn(), setExtra: vi.fn() }), + ), + replayIntegration: vi.fn(() => ({ name: "Replay" })), + getActiveSpan: vi.fn(() => undefined), + getCurrentHub: vi.fn(() => ({ getClient: () => undefined })), +})); diff --git a/packages/core-testing/src/setup/node.ts b/packages/core-testing/src/setup/node.ts index eb73f24..923a984 100644 --- a/packages/core-testing/src/setup/node.ts +++ b/packages/core-testing/src/setup/node.ts @@ -1,3 +1,5 @@ +import "./no-sentry"; + // Reserved for future global node-env setup. Currently a no-op so that // vitest configs may reference @repo/core-testing/setup/node uniformly. export {};