From aa6ce60125851669bb885449af6d41a554de27b7 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:40:39 +0000 Subject: [PATCH 1/3] chore(lint): Enable type-aware Oxlint rules disabled in #1556 Turn the nine type-aware rules back on and fix the violations they report: drop awaits on non-thenables, mark intentional fire-and-forget promises with void, await forgotten assertions, give sorts explicit comparators, stop stringifying unknown values to "[object Object]", and remove redundant type constituents. no-floating-promises allows node:test's test/describe helpers, restrict-template-expressions allows never, and unbound-method stays off in tests, where mock and identity assertions trip it. Fixes #1573 Co-Authored-By: BYK --- .github/tests/pr-risk-labels.test.mjs | 5 +- .oxlintrc.json | 33 +++++-- packages/cli/script/build.ts | 2 +- .../cli/src/commands/alert/issues/create.ts | 12 ++- .../cli/src/commands/alert/issues/edit.ts | 10 ++- .../src/commands/alert/issues/rule-resolve.ts | 2 +- .../cli/src/commands/alert/metrics/create.ts | 6 +- .../cli/src/commands/alert/metrics/edit.ts | 4 +- .../commands/alert/metrics/rule-resolve.ts | 2 +- packages/cli/src/commands/api.ts | 8 +- packages/cli/src/commands/auth/refresh.ts | 2 +- packages/cli/src/commands/issue/explain.ts | 6 +- packages/cli/src/commands/issue/view.ts | 8 +- .../cli/src/commands/local/control-plane.ts | 26 +++--- packages/cli/src/commands/local/server.ts | 18 +++- .../src/commands/release/propose-version.ts | 2 +- packages/cli/src/lib/api/explore.ts | 7 +- packages/cli/src/lib/api/projects.ts | 5 +- packages/cli/src/lib/banner.ts | 2 +- packages/cli/src/lib/custom-ca.ts | 5 +- packages/cli/src/lib/delta-upgrade.ts | 3 +- packages/cli/src/lib/errors.ts | 6 +- packages/cli/src/lib/fetch-input.ts | 19 ++++ packages/cli/src/lib/formatters/dashboard.ts | 3 +- packages/cli/src/lib/formatters/human.ts | 21 +++-- packages/cli/src/lib/formatters/local.ts | 13 +-- packages/cli/src/lib/formatters/log.ts | 3 +- packages/cli/src/lib/formatters/meta-table.ts | 3 +- .../cli/src/lib/formatters/pixel-canvas.ts | 2 +- packages/cli/src/lib/formatters/trace.ts | 3 +- packages/cli/src/lib/hex-id-recovery.ts | 9 +- packages/cli/src/lib/init/ui/ink-ui.ts | 2 +- packages/cli/src/lib/oauth.ts | 4 +- packages/cli/src/lib/sdk-invoke.ts | 3 +- packages/cli/src/lib/sentry-client.ts | 18 +--- packages/cli/src/lib/snapshots/images.ts | 6 +- packages/cli/src/lib/version-check.ts | 2 +- .../test/commands/alert/issues/list.test.ts | 2 +- .../test/commands/alert/metrics/list.test.ts | 2 +- .../cli/test/commands/api.property.test.ts | 72 ++++++++-------- packages/cli/test/commands/cli.test.ts | 3 +- .../cli/test/commands/cli/upgrade.test.ts | 35 ++++---- packages/cli/test/commands/issue/list.test.ts | 6 +- .../issue/short-id-resolution.test.ts | 2 +- .../cli/test/commands/issue/utils.test.ts | 6 +- .../test/commands/log/view.property.test.ts | 22 ++--- .../cli/test/commands/project/list.test.ts | 16 ++-- .../commands/release/propose-version.test.ts | 12 +-- .../test/commands/trace/view.property.test.ts | 20 ++--- packages/cli/test/fixture.ts | 2 +- packages/cli/test/helpers.ts | 11 +-- packages/cli/test/lib/agent-skills.test.ts | 2 +- .../cli/test/lib/api-client.coverage.test.ts | 2 +- .../test/lib/api-client.multiregion.test.ts | 2 +- .../test/lib/api-client.seer-trial.test.ts | 5 +- packages/cli/test/lib/api-client.seer.test.ts | 2 +- packages/cli/test/lib/api-client.test.ts | 4 +- packages/cli/test/lib/api/alerts.test.ts | 2 +- .../cli/test/lib/api/events-overshoot.test.ts | 2 +- .../cli/test/lib/api/infrastructure.test.ts | 2 +- .../cli/test/lib/api/issue-app-links.test.ts | 2 +- packages/cli/test/lib/api/logs.test.ts | 2 +- .../cli/test/lib/api/organizations.test.ts | 2 +- .../cli/test/lib/api/project-dsns.test.ts | 2 +- packages/cli/test/lib/api/projects.test.ts | 2 +- packages/cli/test/lib/api/releases.test.ts | 2 +- packages/cli/test/lib/api/replays.test.ts | 10 +-- .../cli/test/lib/arg-parsing.property.test.ts | 78 ++++++++--------- .../test/lib/auto-paginate.property.test.ts | 12 +-- packages/cli/test/lib/binary.test.ts | 11 +-- packages/cli/test/lib/config.test.ts | 42 ++++----- .../cli/test/lib/credential-redaction.test.ts | 9 +- packages/cli/test/lib/db/auth.test.ts | 2 +- packages/cli/test/lib/db/concurrent-worker.ts | 2 +- .../cli/test/lib/delta-upgrade.mocked.test.ts | 11 +-- packages/cli/test/lib/delta-upgrade.test.ts | 27 +++--- .../cli/test/lib/dif/scan.property.test.ts | 4 +- .../lib/formatters/human.property.test.ts | 32 +++---- .../test/lib/formatters/human.utils.test.ts | 10 +-- .../test/lib/formatters/log.property.test.ts | 18 ++-- packages/cli/test/lib/ghcr.test.ts | 25 ++++-- packages/cli/test/lib/help.test.ts | 2 +- .../test/lib/hex-id-recovery.adapters.test.ts | 2 +- .../cli/test/lib/init/ui/logging-ui.test.ts | 6 +- .../init/verify-setup-windows.mocked.test.ts | 4 +- .../lib/input-validation.property.test.ts | 28 +++--- packages/cli/test/lib/region.test.ts | 4 +- packages/cli/test/lib/release-notes.test.ts | 9 +- .../test/lib/resolve-effective-org.test.ts | 2 +- packages/cli/test/lib/resolve-target.test.ts | 28 +++--- .../lib/sentry-client.invalidation.test.ts | 3 +- .../lib/sentry-client.response-body.test.ts | 2 +- packages/cli/test/lib/sentry-client.test.ts | 2 +- .../lib/sentry-url-parser.property.test.ts | 16 ++-- .../cli/test/lib/sentry-urls.property.test.ts | 86 +++++++++---------- packages/cli/test/lib/telemetry.test.ts | 2 +- packages/cli/test/lib/upgrade.test.ts | 65 +++++++------- packages/cli/test/lib/version-check.test.ts | 7 +- packages/cli/test/mocks/server.ts | 7 +- .../animation/TerminalAnimation.tsx | 2 +- .../src/client/components/chat/chat.tsx | 6 +- .../components/ui/interactive-markdown.tsx | 2 +- .../src/client/hooks/use-mcp-metadata.ts | 2 +- .../src/server/lib/approval-dialog.ts | 4 +- .../src/server/lib/constraint-utils.ts | 16 +++- .../src/server/lib/mcp-handler.ts | 5 +- .../mcp-cloudflare/src/server/routes/chat.ts | 4 +- .../src/server/sentry.config.ts | 23 ----- .../scripts/validate-skills-mapping.ts | 2 +- packages/mcp-core/src/api-client/client.ts | 14 +-- packages/mcp-core/src/api-client/schema.ts | 74 ++++++++-------- packages/mcp-core/src/internal/formatting.ts | 34 +++++--- .../mcp-core/src/internal/tool-helpers/api.ts | 8 +- .../internal/tool-helpers/enhance-error.ts | 25 ++++-- .../src/internal/unknown-formatting.ts | 21 +++++ packages/mcp-core/src/server.test.ts | 2 +- packages/mcp-core/src/server.ts | 4 +- .../src/tools/catalog/get-alert-rule.test.ts | 2 +- .../tools/catalog/get-latest-base-snapshot.ts | 13 ++- .../src/tools/catalog/get-replay-details.ts | 2 +- .../tools/catalog/support/api-formatting.ts | 18 +--- .../src/tools/catalog/update-issue.ts | 14 ++- .../src/tools/support/issue-linking/index.ts | 2 +- .../src/tools/support/search-events/utils.ts | 6 +- .../src/tools/support/snapshots/formatting.ts | 10 +++ .../src/tools/support/snapshots/handlers.ts | 20 +++-- packages/mcp-core/src/tools/surfaces.ts | 4 +- .../src/evals/utils/structuredOutputScorer.ts | 4 +- packages/mcp-server/src/cli/commands/auth.ts | 4 +- packages/mcp-test-client/src/agent.ts | 2 +- .../src/mcp-test-client-remote.ts | 2 +- packages/smoke-tests/src/smoke.test.ts | 22 +++-- scripts/ci-projects.mjs | 2 +- 133 files changed, 822 insertions(+), 652 deletions(-) create mode 100644 packages/cli/src/lib/fetch-input.ts create mode 100644 packages/mcp-core/src/internal/unknown-formatting.ts diff --git a/.github/tests/pr-risk-labels.test.mjs b/.github/tests/pr-risk-labels.test.mjs index 34f47d1c1..39c925f14 100644 --- a/.github/tests/pr-risk-labels.test.mjs +++ b/.github/tests/pr-risk-labels.test.mjs @@ -119,7 +119,10 @@ for (const risk of ["low", "medium", "high"]) { }, async ({ run, labels }) => { await run(); - assert.deepEqual([...labels].sort(), ["area: auth", `risk: ${risk}`]); + assert.deepEqual( + [...labels].sort((a, b) => a.localeCompare(b)), + ["area: auth", `risk: ${risk}`], + ); }, ); }); diff --git a/.oxlintrc.json b/.oxlintrc.json index 561c13381..20b8b0a8e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -20,18 +20,33 @@ "rules": { "no-unused-vars": "off", "typescript/return-await": ["error", "always"], - "typescript/await-thenable": "off", - "typescript/no-base-to-string": "off", - "typescript/no-duplicate-type-constituents": "off", - "typescript/no-floating-promises": "off", - "typescript/no-misused-spread": "off", - "typescript/no-redundant-type-constituents": "off", - "typescript/require-array-sort-compare": "off", - "typescript/restrict-template-expressions": "off", - "typescript/unbound-method": "off", + "typescript/no-floating-promises": [ + "error", + { + "allowForKnownSafeCalls": [ + { + "from": "package", + "package": "node:test", + "name": ["test", "it", "describe", "suite"] + } + ] + } + ], + "typescript/restrict-template-expressions": [ + "error", + { + "allowNever": true + } + ], "jsx-a11y/interactive-supports-focus": "off" }, "overrides": [ + { + "files": ["**/*.test.{ts,tsx,mjs}", "**/test/**/*.{ts,tsx}"], + "rules": { + "typescript/unbound-method": "off" + } + }, { "files": ["packages/mcp-cloudflare/src/client/**/*.tsx"], "rules": { diff --git a/packages/cli/script/build.ts b/packages/cli/script/build.ts index 8cb6b6c84..918bf82f4 100644 --- a/packages/cli/script/build.ts +++ b/packages/cli/script/build.ts @@ -418,7 +418,7 @@ async function compileAllTargets( successes += 1; } catch (error) { console.error( - ` Post-processing ${getPackageName(target)} failed: ${error}`, + ` Post-processing ${getPackageName(target)} failed: ${String(error)}`, ); failures += 1; } diff --git a/packages/cli/src/commands/alert/issues/create.ts b/packages/cli/src/commands/alert/issues/create.ts index e0025373f..d322200d6 100644 --- a/packages/cli/src/commands/alert/issues/create.ts +++ b/packages/cli/src/commands/alert/issues/create.ts @@ -11,7 +11,11 @@ import { } from "../../../lib/api-client.js"; import { parseOrgProjectArg } from "../../../lib/arg-parsing.js"; import { buildCommand, numberParser } from "../../../lib/command.js"; -import { ContextError, ValidationError } from "../../../lib/errors.js"; +import { + ContextError, + stringifyUnknown, + ValidationError, +} from "../../../lib/errors.js"; import { CommandOutput } from "../../../lib/formatters/output.js"; import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../../lib/mutate-command.js"; import { resolveProjectBoundTargets } from "../../../lib/resolve-target.js"; @@ -244,9 +248,9 @@ export const createCommand = buildCommand({ yield new CommandOutput({ org: target.org, project: target.project, - id: String(created.id ?? ""), - name: String(created.name ?? flags.name), - status: String( + id: stringifyUnknown(created.id ?? ""), + name: stringifyUnknown(created.name ?? flags.name), + status: stringifyUnknown( created.status ?? (created.enabled === false ? "disabled" : "active"), ), } satisfies CreateResult); diff --git a/packages/cli/src/commands/alert/issues/edit.ts b/packages/cli/src/commands/alert/issues/edit.ts index c05c00954..d4d91d1eb 100644 --- a/packages/cli/src/commands/alert/issues/edit.ts +++ b/packages/cli/src/commands/alert/issues/edit.ts @@ -12,7 +12,11 @@ import { } from "../../../lib/api-client.js"; import { parseOrgProjectArg } from "../../../lib/arg-parsing.js"; import { buildCommand, numberParser } from "../../../lib/command.js"; -import { ContextError, ValidationError } from "../../../lib/errors.js"; +import { + ContextError, + stringifyUnknown, + ValidationError, +} from "../../../lib/errors.js"; import { CommandOutput } from "../../../lib/formatters/output.js"; import { resolveProjectBoundTargets } from "../../../lib/resolve-target.js"; import { @@ -284,10 +288,10 @@ export const editCommand = buildCommand({ ...updated, org: target.org, project: target.project, - id: String(updated.id ?? rule.id), + id: stringifyUnknown(updated.id ?? rule.id), // The workflows endpoint returns `enabled` rather than `status`; map it // back to a status label so human/JSON output matches the create path. - status: String( + status: stringifyUnknown( updated.status ?? (updated.enabled === false ? "disabled" : "active"), ), } satisfies EditResult); diff --git a/packages/cli/src/commands/alert/issues/rule-resolve.ts b/packages/cli/src/commands/alert/issues/rule-resolve.ts index 4580d6d8d..f1ebe6c82 100644 --- a/packages/cli/src/commands/alert/issues/rule-resolve.ts +++ b/packages/cli/src/commands/alert/issues/rule-resolve.ts @@ -42,7 +42,7 @@ export function parseIssueRuleArg( ); } - const slashCount = [...trimmed].filter((c) => c === "/").length; + const slashCount = trimmed.split("/").length - 1; if (slashCount === 0) { return { ref: trimmed, targetArg: undefined }; } diff --git a/packages/cli/src/commands/alert/metrics/create.ts b/packages/cli/src/commands/alert/metrics/create.ts index 79390420a..df62856f8 100644 --- a/packages/cli/src/commands/alert/metrics/create.ts +++ b/packages/cli/src/commands/alert/metrics/create.ts @@ -7,7 +7,7 @@ import type { SentryContext } from "../../../context.js"; import { createMetricAlertRule } from "../../../lib/api-client.js"; import { buildCommand, numberParser } from "../../../lib/command.js"; -import { ValidationError } from "../../../lib/errors.js"; +import { stringifyUnknown, ValidationError } from "../../../lib/errors.js"; import { warning } from "../../../lib/formatters/colors.js"; import { CommandOutput } from "../../../lib/formatters/output.js"; import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../../lib/mutate-command.js"; @@ -222,8 +222,8 @@ export const createCommand = buildCommand({ const created = await createMetricAlertRule(orgSlug, body); yield new CommandOutput({ org: orgSlug, - id: String(created.id ?? ""), - name: String(created.name ?? flags.name), + id: stringifyUnknown(created.id ?? ""), + name: stringifyUnknown(created.name ?? flags.name), status: metricAlertStatusLabel(created.status), } satisfies CreateResult); }, diff --git a/packages/cli/src/commands/alert/metrics/edit.ts b/packages/cli/src/commands/alert/metrics/edit.ts index 6bba1666a..372351285 100644 --- a/packages/cli/src/commands/alert/metrics/edit.ts +++ b/packages/cli/src/commands/alert/metrics/edit.ts @@ -10,7 +10,7 @@ import { putMetricAlertRule, } from "../../../lib/api-client.js"; import { buildCommand, numberParser } from "../../../lib/command.js"; -import { ValidationError } from "../../../lib/errors.js"; +import { stringifyUnknown, ValidationError } from "../../../lib/errors.js"; import { warning } from "../../../lib/formatters/colors.js"; import { CommandOutput } from "../../../lib/formatters/output.js"; import { resolveOrgOnlyFromArg } from "../../../lib/resolve-target.js"; @@ -290,7 +290,7 @@ export const editCommand = buildCommand({ yield new CommandOutput({ ...updated, org: orgSlug, - id: String(updated.id ?? rule.id), + id: stringifyUnknown(updated.id ?? rule.id), status: metricAlertStatusLabel(updated.status), } satisfies EditResult); }, diff --git a/packages/cli/src/commands/alert/metrics/rule-resolve.ts b/packages/cli/src/commands/alert/metrics/rule-resolve.ts index 46917305a..26efe2f4e 100644 --- a/packages/cli/src/commands/alert/metrics/rule-resolve.ts +++ b/packages/cli/src/commands/alert/metrics/rule-resolve.ts @@ -45,7 +45,7 @@ export function parseMetricRuleArg( return { ref: trimmed, targetArg: undefined }; } - const slashCount = [...trimmed].filter((c) => c === "/").length; + const slashCount = trimmed.split("/").length - 1; if (slashCount > 1) { throw new ValidationError( `Metric alerts are org-scoped — use '/', not '//'.\nUse: ${usageHint}`, diff --git a/packages/cli/src/commands/api.ts b/packages/cli/src/commands/api.ts index 6933557a7..5c6bb6d31 100644 --- a/packages/cli/src/commands/api.ts +++ b/packages/cli/src/commands/api.ts @@ -11,7 +11,11 @@ import * as Sentry from "@sentry/node-core/light"; import type { SentryContext } from "../context.js"; import { appendSearchParams, rawApiRequest } from "../lib/api-client.js"; import { buildCommand } from "../lib/command.js"; -import { OutputError, ValidationError } from "../lib/errors.js"; +import { + OutputError, + stringifyUnknown, + ValidationError, +} from "../lib/errors.js"; import { filterFields } from "../lib/formatters/json.js"; import { CommandOutput } from "../lib/formatters/output.js"; import { validateEndpoint } from "../lib/input-validation.js"; @@ -1071,7 +1075,7 @@ export function formatApiResponse(data: unknown): string { if (typeof data === "object") { return JSON.stringify(data, null, 2); } - return String(data); + return stringifyUnknown(data); } /** diff --git a/packages/cli/src/commands/auth/refresh.ts b/packages/cli/src/commands/auth/refresh.ts index 20c99059b..62c2d80e3 100644 --- a/packages/cli/src/commands/auth/refresh.ts +++ b/packages/cli/src/commands/auth/refresh.ts @@ -179,7 +179,7 @@ Examples: } // Standard token refresh path (no scope change). - const auth = await getAuthConfig(); + const auth = getAuthConfig(); if (!auth?.refreshToken && auth?.token) { throw new AuthError( "invalid", diff --git a/packages/cli/src/commands/issue/explain.ts b/packages/cli/src/commands/issue/explain.ts index bc91da295..ab970fa12 100644 --- a/packages/cli/src/commands/issue/explain.ts +++ b/packages/cli/src/commands/issue/explain.ts @@ -6,7 +6,7 @@ import type { SentryContext } from "../../context.js"; import { buildCommand } from "../../lib/command.js"; -import { ApiError, ContextError } from "../../lib/errors.js"; +import { ApiError, ContextError, stringifyUnknown } from "../../lib/errors.js"; import { CommandOutput } from "../../lib/formatters/output.js"; import { formatIssueExplain, @@ -145,7 +145,9 @@ export const explainCommand = buildCommand({ issueArgs, (issueArg) => analyzeIssue(issueArg, cwd, flags, flags.json || isBatch), (issueArg, reason) => { - log.warn(`Failed to analyze issue ${issueArg}: ${reason}`); + log.warn( + `Failed to analyze issue ${issueArg}: ${stringifyUnknown(reason)}`, + ); }, ); diff --git a/packages/cli/src/commands/issue/view.ts b/packages/cli/src/commands/issue/view.ts index 2bdaed703..1ef587fcb 100644 --- a/packages/cli/src/commands/issue/view.ts +++ b/packages/cli/src/commands/issue/view.ts @@ -9,7 +9,7 @@ import { getLatestEvent, listReplayIdsForIssue } from "../../lib/api-client.js"; import { spansFlag } from "../../lib/arg-parsing.js"; import { openInBrowser } from "../../lib/browser.js"; import { buildCommand } from "../../lib/command.js"; -import { ContextError } from "../../lib/errors.js"; +import { ContextError, stringifyUnknown } from "../../lib/errors.js"; import { plainSafeMuted } from "../../lib/formatters/human.js"; import { formatIssueView, @@ -187,7 +187,9 @@ export function fetchMultipleIssueViews( issueArgs, (issueArg) => buildSingleIssueViewData(issueArg, cwd, spans), (issueArg, reason) => { - log.warn(`Failed to fetch issue ${issueArg}: ${reason}`); + log.warn( + `Failed to fetch issue ${issueArg}: ${stringifyUnknown(reason)}`, + ); }, ); } @@ -222,7 +224,7 @@ async function openIssuesInBrowser( return issue; }, (issueArg, reason) => { - log.warn(`Failed to open issue ${issueArg}: ${reason}`); + log.warn(`Failed to open issue ${issueArg}: ${stringifyUnknown(reason)}`); }, ); diff --git a/packages/cli/src/commands/local/control-plane.ts b/packages/cli/src/commands/local/control-plane.ts index 1525e8524..d66d9a28a 100644 --- a/packages/cli/src/commands/local/control-plane.ts +++ b/packages/cli/src/commands/local/control-plane.ts @@ -11,9 +11,15 @@ import { type Context, Hono } from "hono"; import { cors } from "hono/cors"; import { HTTPException } from "hono/http-exception"; import { streamSSE } from "hono/streaming"; +import { stringifyUnknown } from "../../lib/errors.js"; import { SENTRY_CONTENT_TYPE } from "../../lib/formatters/local.js"; import { formatLocalServerUrl } from "./ui.js"; -import { buildSSEHandler, isHostedUiOrigin, MAX_BODY_BYTES } from "./server.js"; +import { + buildSSEHandler, + isHostedUiOrigin, + MAX_BODY_BYTES, + spotlightEnvelopeId, +} from "./server.js"; import { logger } from "../../lib/logger.js"; import { createLocalSessionStore, @@ -92,14 +98,11 @@ function eventSummary(parsed: { return id ? [id] : []; })[0]; return { - envelopeId: String(header.__spotlight_envelope_id ?? ""), - eventId: - eventIdFrom(header) ?? - itemEventId ?? - String(header.__spotlight_envelope_id ?? ""), + envelopeId: spotlightEnvelopeId(header), + eventId: eventIdFrom(header) ?? itemEventId ?? spotlightEnvelopeId(header), itemTypes: items.map((item) => Array.isArray(item) && typeof item[0] === "object" && item[0] - ? String((item[0] as { type?: unknown }).type ?? "unknown") + ? stringifyUnknown((item[0] as { type?: unknown }).type ?? "unknown") : "unknown", ), }; @@ -441,17 +444,16 @@ export function createLocalControlPlane({ .read({ all: true }) .map((container) => { const parsed = container.getParsedEnvelope(); - const header = (parsed?.envelope[0] ?? {}) as { - __spotlight_envelope_id?: unknown; - }; return { - id: String(header.__spotlight_envelope_id ?? ""), + id: spotlightEnvelopeId(parsed?.envelope[0] ?? {}), contentType: container.getContentType(), itemTypes: parsed?.envelope[1] .map((item) => Array.isArray(item) && typeof item[0] === "object" && item[0] - ? String((item[0] as { type?: unknown }).type ?? "unknown") + ? stringifyUnknown( + (item[0] as { type?: unknown }).type ?? "unknown", + ) : "unknown", ) .filter(Boolean) ?? [], diff --git a/packages/cli/src/commands/local/server.ts b/packages/cli/src/commands/local/server.ts index 5df5560cf..2f36982e5 100644 --- a/packages/cli/src/commands/local/server.ts +++ b/packages/cli/src/commands/local/server.ts @@ -143,6 +143,19 @@ const LOCAL_UI_PREVIEW_HOST_RE = /^sentry-local-git-[a-z0-9-]+\.sentry\.dev$/; export { isLoopbackHost } from "./loopback.js"; +/** + * Read Spotlight's `__spotlight_envelope_id` envelope header as a string. + * Spotlight stores a `uuidv7` UUID object there, which renders through its + * own `toString()`. + */ +export function spotlightEnvelopeId(header: Record): string { + const id = header.__spotlight_envelope_id as + | { toString(): string } + | null + | undefined; + return id == null ? "" : id.toString(); +} + type LocalReceiverOptions = { /** Enables stateful UI routes only when the receiver itself is loopback-bound. */ uiActions?: boolean; @@ -225,11 +238,10 @@ export function buildSSEHandler( if (!parsed) { return; } - const header = parsed.envelope[0]; - const envelopeId = header.__spotlight_envelope_id; + const envelopeId = spotlightEnvelopeId(parsed.envelope[0]); stream .writeSSE({ - id: envelopeId ? String(envelopeId) : undefined, + id: envelopeId || undefined, event: `${container.getContentType()}${base64Indicator}`, data: JSON.stringify(parsed.envelope), }) diff --git a/packages/cli/src/commands/release/propose-version.ts b/packages/cli/src/commands/release/propose-version.ts index 86af35174..53ab09f31 100644 --- a/packages/cli/src/commands/release/propose-version.ts +++ b/packages/cli/src/commands/release/propose-version.ts @@ -108,7 +108,7 @@ export const proposeVersionCommand = buildCommand({ } // Fall back to git HEAD SHA - const sha = await getHeadCommit(cwd); + const sha = getHeadCommit(cwd); yield new CommandOutput({ version: sha, source: "git" }); return { hint: "Detected from git HEAD" }; }, diff --git a/packages/cli/src/lib/api/explore.ts b/packages/cli/src/lib/api/explore.ts index 54bbcf7ce..75178d855 100644 --- a/packages/cli/src/lib/api/explore.ts +++ b/packages/cli/src/lib/api/explore.ts @@ -8,6 +8,7 @@ import type { EventsTableResponse } from "../../types/dashboard.js"; import { EventsTableResponseSchema } from "../../types/dashboard.js"; +import { stringifyUnknown } from "../errors.js"; import { resolveOrgRegion } from "../region.js"; import { API_MAX_PER_PAGE, @@ -156,9 +157,9 @@ export async function queryMetricsMeta( } return allRows.map((row) => ({ - name: String(row["metric.name"] ?? ""), - type: String(row["metric.type"] ?? "distribution"), - unit: String(row["metric.unit"] ?? "none"), + name: stringifyUnknown(row["metric.name"] ?? ""), + type: stringifyUnknown(row["metric.type"] ?? "distribution"), + unit: stringifyUnknown(row["metric.unit"] ?? "none"), })); } diff --git a/packages/cli/src/lib/api/projects.ts b/packages/cli/src/lib/api/projects.ts index 7c084d9d8..e4fd27875 100644 --- a/packages/cli/src/lib/api/projects.ts +++ b/packages/cli/src/lib/api/projects.ts @@ -459,9 +459,8 @@ export async function findProjectsBySlug( * Uses native RegExp.escape if available (Node.js 23.6+, Bun), otherwise polyfills. */ const escapeRegex: (str: string) => string = - typeof RegExp.escape === "function" - ? RegExp.escape - : (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + RegExp.escape?.bind(RegExp) ?? + ((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); /** * Check if two strings match with word-boundary semantics (bidirectional). diff --git a/packages/cli/src/lib/banner.ts b/packages/cli/src/lib/banner.ts index 1d569c3bc..3554468f5 100644 --- a/packages/cli/src/lib/banner.ts +++ b/packages/cli/src/lib/banner.ts @@ -101,7 +101,7 @@ export const FULL_BANNER_LINES: BannerLine[] = colorize(BANNER_ROWS_FULL); export function bannerLinesWidth(lines: readonly BannerLine[]): number { let max = 0; for (const { content } of lines) { - const width = [...content].length; + const width = Array.from(content).length; if (width > max) { max = width; } diff --git a/packages/cli/src/lib/custom-ca.ts b/packages/cli/src/lib/custom-ca.ts index aec8a466f..e62037b9c 100644 --- a/packages/cli/src/lib/custom-ca.ts +++ b/packages/cli/src/lib/custom-ca.ts @@ -23,6 +23,7 @@ import { rootCertificates } from "node:tls"; const _require = createRequire(import.meta.url); import { getDefaultCaCert } from "./db/defaults.js"; import { getEnv } from "./env.js"; +import { stringifyUnknown } from "./errors.js"; import { logger } from "./logger.js"; import { isSentrySaasUrl } from "./sentry-urls.js"; @@ -108,7 +109,9 @@ function injectIntoNodeTls(customPem: string): void { setDefaultCACertificates([...rootCertificates, customPem]); log.debug("Injected custom CA into Node.js TLS trust store"); } catch (err) { - log.debug(`Failed to set Node.js default CA certificates: ${err}`); + log.debug( + `Failed to set Node.js default CA certificates: ${stringifyUnknown(err)}`, + ); } } diff --git a/packages/cli/src/lib/delta-upgrade.ts b/packages/cli/src/lib/delta-upgrade.ts index 6ca6355ea..a1ef58ca5 100644 --- a/packages/cli/src/lib/delta-upgrade.ts +++ b/packages/cli/src/lib/delta-upgrade.ts @@ -43,6 +43,7 @@ import { import { CLI_VERSION } from "./constants.js"; import { customFetch } from "./custom-ca.js"; import { getConfigDir } from "./db/index.js"; +import { fetchInputUrl } from "./fetch-input.js"; import { formatBytes } from "./formatters/numbers.js"; import { logger } from "./logger.js"; import { makeByteProgress, type SetMessage } from "./progress.js"; @@ -177,7 +178,7 @@ function stableSource(source: UpgradeSource): SourceStrategy { const releasesUrl = getGitHubReleasesUrl(source); const sourceFetch: typeof customFetch = async (input, init) => { const response = await customFetch(input, init); - if (!(response.ok && String(input).startsWith(`${releasesUrl}?`))) { + if (!(response.ok && fetchInputUrl(input).startsWith(`${releasesUrl}?`))) { return response; } const data: unknown = await response.json(); diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index 34d680fd2..487761345 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -141,7 +141,7 @@ export class HostScopeError extends CliError { constructor( sourceOrMessage: string, destinationUrl?: string, - tokenHost?: string | undefined, + tokenHost?: string, ) { if (destinationUrl === undefined) { super(sourceOrMessage, EXIT.AUTH_HOST_SCOPE); @@ -783,12 +783,12 @@ export function stringifyUnknown(value: unknown): string { } if (value && typeof value === "object") { // JSON.stringify can throw on circular references or BigInt values. - // Fall back to String() which is always safe. + // Fall back to the tag string (e.g. `[object Object]`), which is always safe. // oxlint-disable-next-line sentry-cli/no-silent-catch -- grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { return JSON.stringify(value); } catch { - return String(value); + return Object.prototype.toString.call(value); } } return String(value); diff --git a/packages/cli/src/lib/fetch-input.ts b/packages/cli/src/lib/fetch-input.ts new file mode 100644 index 000000000..ef0f33626 --- /dev/null +++ b/packages/cli/src/lib/fetch-input.ts @@ -0,0 +1,19 @@ +/** + * Helpers for the `input` argument of `fetch()`. + */ + +/** + * Extract the full URL string from a fetch input. + * + * `String(request)` yields `"[object Request]"`, so `Request` inputs need + * their `.url` read explicitly. + */ +export function fetchInputUrl(input: Request | string | URL): string { + if (typeof input === "string") { + return input; + } + if (input instanceof URL) { + return input.href; + } + return input.url; +} diff --git a/packages/cli/src/lib/formatters/dashboard.ts b/packages/cli/src/lib/formatters/dashboard.ts index ba14fecd5..24554d6a0 100644 --- a/packages/cli/src/lib/formatters/dashboard.ts +++ b/packages/cli/src/lib/formatters/dashboard.ts @@ -19,6 +19,7 @@ import type { TimeseriesResult, WidgetDataResult, } from "../../types/dashboard.js"; +import { stringifyUnknown } from "../errors.js"; import { encodeImageToKitty } from "../kitty-image.js"; import { logger } from "../logger.js"; import { @@ -871,7 +872,7 @@ function renderTableContent(data: TableResult, innerWidth: number): string[] { if (typeof val === "number") { return formatWithUnit(val, col.unit); } - return String(val); + return stringifyUnknown(val); }), ); diff --git a/packages/cli/src/lib/formatters/human.ts b/packages/cli/src/lib/formatters/human.ts index 2ce0fb69e..565594723 100644 --- a/packages/cli/src/lib/formatters/human.ts +++ b/packages/cli/src/lib/formatters/human.ts @@ -29,6 +29,7 @@ import type { Writer, } from "../../types/index.js"; import { resolveOrgDisplayName } from "../api-client.js"; +import { stringifyUnknown } from "../errors.js"; import { getReplayIdFromEvent } from "../replay-search.js"; import { withSerializeSpan } from "../telemetry.js"; import { tipsSuppressed } from "../tips.js"; @@ -851,8 +852,12 @@ export function formatIssueDetails(issue: SentryIssue): string { const firstReleaseVersion = issue.firstRelease?.shortVersion; const lastReleaseVersion = issue.lastRelease?.shortVersion; if (firstReleaseVersion || lastReleaseVersion) { - const first = escapeMarkdownInline(String(firstReleaseVersion ?? "")); - const last = escapeMarkdownInline(String(lastReleaseVersion ?? "")); + const first = escapeMarkdownInline( + stringifyUnknown(firstReleaseVersion ?? ""), + ); + const last = escapeMarkdownInline( + stringifyUnknown(lastReleaseVersion ?? ""), + ); if (firstReleaseVersion && lastReleaseVersion) { if (firstReleaseVersion === lastReleaseVersion) { kvRows.push(["Release", first]); @@ -872,14 +877,14 @@ export function formatIssueDetails(issue: SentryIssue): string { if (issue.firstSeen) { let firstSeenLine = new Date(issue.firstSeen).toLocaleString(); if (firstReleaseVersion) { - firstSeenLine += ` (in ${escapeMarkdownCell(String(firstReleaseVersion))})`; + firstSeenLine += ` (in ${escapeMarkdownCell(stringifyUnknown(firstReleaseVersion))})`; } kvRows.push(["First seen", firstSeenLine]); } if (issue.lastSeen) { let lastSeenLine = new Date(issue.lastSeen).toLocaleString(); if (lastReleaseVersion && lastReleaseVersion !== firstReleaseVersion) { - lastSeenLine += ` (in ${escapeMarkdownCell(String(lastReleaseVersion))})`; + lastSeenLine += ` (in ${escapeMarkdownCell(stringifyUnknown(lastReleaseVersion))})`; } kvRows.push(["Last seen", lastSeenLine]); } @@ -1012,10 +1017,12 @@ function buildBreadcrumbsMarkdown(breadcrumbsEntry: BreadcrumbsEntry): string { if (!message && breadcrumb.data) { const data = breadcrumb.data as Record; if (data.url && data.method) { - const status = data.status_code ? ` → ${data.status_code}` : ""; - message = `${data.method} ${data.url}${status}`; + const status = data.status_code + ? ` → ${stringifyUnknown(data.status_code)}` + : ""; + message = `${stringifyUnknown(data.method)} ${stringifyUnknown(data.url)}${status}`; } else if (data.from && data.to) { - message = `${data.from} → ${data.to}`; + message = `${stringifyUnknown(data.from)} → ${stringifyUnknown(data.to)}`; } else if (data.arguments && Array.isArray(data.arguments)) { message = String(data.arguments[0] || "").slice(0, 60); } diff --git a/packages/cli/src/lib/formatters/local.ts b/packages/cli/src/lib/formatters/local.ts index 99d42be60..3660fcc73 100644 --- a/packages/cli/src/lib/formatters/local.ts +++ b/packages/cli/src/lib/formatters/local.ts @@ -1,5 +1,6 @@ /** Tail formatters for the local dev server. */ +import { stringifyUnknown } from "../errors.js"; import { logger } from "../logger.js"; import { blue, bold, cyan, green, muted, red, yellow } from "./colors.js"; import { stripAnsi } from "./plain-detect.js"; @@ -244,7 +245,7 @@ export function formatErrorItem( const first = exception?.values?.at(-1); const errorType = sanitize(String(first?.type ?? "Error")); const errorValue = sanitize( - String(first?.value ?? event.message ?? "Unknown error"), + stringifyUnknown(first?.value ?? event.message ?? "Unknown error"), ); let msg = `${errorType}: ${errorValue}`; @@ -388,10 +389,10 @@ function formatAttrValue(value: unknown): string { return sanitize(JSON.stringify(value)); } catch (err) { log.debug("Failed to JSON-encode attribute value", err); - return sanitize(String(value)); + return sanitize(Object.prototype.toString.call(value)); } } - return sanitize(String(value)); + return sanitize(stringifyUnknown(value)); } /** @@ -733,7 +734,7 @@ function jsonSafeIdentifier(value: unknown): string | undefined { } try { - const identifier = value.toString(); + const identifier = (value as { toString(): string }).toString(); if (identifier === "[object Object]") { return; } @@ -861,7 +862,9 @@ function formatTransactionJson( const attrs = mergeTransactionAttributes(payload); const semantic = formatSemanticSpanDisplay( attrs, - String(payload.transaction ?? trace?.description ?? "Transaction"), + stringifyUnknown( + payload.transaction ?? trace?.description ?? "Transaction", + ), ); const start = payload.start_timestamp as number | undefined; const end = payload.timestamp as number | undefined; diff --git a/packages/cli/src/lib/formatters/log.ts b/packages/cli/src/lib/formatters/log.ts index b7d3040b5..8dbfb50f8 100644 --- a/packages/cli/src/lib/formatters/log.ts +++ b/packages/cli/src/lib/formatters/log.ts @@ -9,6 +9,7 @@ import type { SentryLog, TraceItemAttribute, } from "../../types/index.js"; +import { stringifyUnknown } from "../errors.js"; import { buildTraceUrl } from "../sentry-web-urls.js"; import { colorTag, @@ -157,7 +158,7 @@ export function buildLogRowCells( const val = log[field]; cells.push( escapeMarkdownCell( - val !== null && val !== undefined ? String(val) : "", + val !== null && val !== undefined ? stringifyUnknown(val) : "", ), ); } diff --git a/packages/cli/src/lib/formatters/meta-table.ts b/packages/cli/src/lib/formatters/meta-table.ts index d8fb69f56..2507edc2b 100644 --- a/packages/cli/src/lib/formatters/meta-table.ts +++ b/packages/cli/src/lib/formatters/meta-table.ts @@ -8,6 +8,7 @@ * for numeric fields. */ +import { stringifyUnknown } from "../errors.js"; import { escapeMarkdownCell } from "./markdown.js"; import { appendUnitSuffix, formatNumber } from "./numbers.js"; import type { Column } from "./table.js"; @@ -47,7 +48,7 @@ export function formatCellValue( } return formatNumber(value); } - return escapeMarkdownCell(String(value)); + return escapeMarkdownCell(stringifyUnknown(value)); } /** diff --git a/packages/cli/src/lib/formatters/pixel-canvas.ts b/packages/cli/src/lib/formatters/pixel-canvas.ts index c55b8dd2f..1ffceaa4f 100644 --- a/packages/cli/src/lib/formatters/pixel-canvas.ts +++ b/packages/cli/src/lib/formatters/pixel-canvas.ts @@ -216,7 +216,7 @@ function* normalizeBitmapText(text: string): Iterable { .replace(COMBINING_MARK_RE, ""); if ( decomposed.length > 0 && - [...decomposed].every((candidate) => hasSpleenGlyph(candidate)) + Array.from(decomposed).every((candidate) => hasSpleenGlyph(candidate)) ) { yield* decomposed; continue; diff --git a/packages/cli/src/lib/formatters/trace.ts b/packages/cli/src/lib/formatters/trace.ts index 865fb9a8d..1b94b9dfd 100644 --- a/packages/cli/src/lib/formatters/trace.ts +++ b/packages/cli/src/lib/formatters/trace.ts @@ -12,6 +12,7 @@ import type { } from "../../types/index.js"; import type { TraceItemDetail } from "../api/traces.js"; import { REDUNDANT_DETAIL_ATTRS } from "../api/traces.js"; +import { stringifyUnknown } from "../errors.js"; import { colorTag, escapeMarkdownCell, @@ -470,7 +471,7 @@ function buildExtraColumns(extraColumns: string[]): Column[] { if (v === undefined || v === null) { return "—"; } - return escapeMarkdownCell(String(v)); + return escapeMarkdownCell(stringifyUnknown(v)); }, })); } diff --git a/packages/cli/src/lib/hex-id-recovery.ts b/packages/cli/src/lib/hex-id-recovery.ts index 88256b261..3f78448d4 100644 --- a/packages/cli/src/lib/hex-id-recovery.ts +++ b/packages/cli/src/lib/hex-id-recovery.ts @@ -32,7 +32,12 @@ import { type ParsedOrgProject, ProjectSpecificationType, } from "./arg-parsing.js"; -import { AuthError, ResolutionError, ValidationError } from "./errors.js"; +import { + AuthError, + ResolutionError, + stringifyUnknown, + ValidationError, +} from "./errors.js"; import type { HexEntityType } from "./hex-id.js"; import { ALPHA_SEGMENT_RE, @@ -460,7 +465,7 @@ const eventAdapter: FuzzyLookupAdapter = async (ctx) => { sort: "-timestamp", statsPeriod: ctx.period ?? SCAN_PERIODS.event, }); - return data.data.map((row) => String(row.id ?? "")).filter(Boolean); + return data.data.map((row) => stringifyUnknown(row.id ?? "")).filter(Boolean); }; const traceAdapter: FuzzyLookupAdapter = async (ctx) => { diff --git a/packages/cli/src/lib/init/ui/ink-ui.ts b/packages/cli/src/lib/init/ui/ink-ui.ts index 0dbc854aa..369041f83 100644 --- a/packages/cli/src/lib/init/ui/ink-ui.ts +++ b/packages/cli/src/lib/init/ui/ink-ui.ts @@ -49,6 +49,7 @@ import { spawn } from "node:child_process"; import { openSync } from "node:fs"; import { createRequire } from "node:module"; +import { join } from "node:path"; import { ReadStream } from "node:tty"; const _require = createRequire(import.meta.url); @@ -285,7 +286,6 @@ async function loadInkApp(): Promise { // oxlint-disable-next-line typescript/no-explicit-any -- node:sea types not yet in @types/node const sea = _require("node:sea") as any; const { writeFileSync, mkdtempSync } = await import("node:fs"); - const { join } = await import("node:path"); const { tmpdir } = await import("node:os"); const tmpDir = mkdtempSync(join(tmpdir(), "sentry-ink-")); const tmpFile = join(tmpDir, "ink-app.js"); diff --git a/packages/cli/src/lib/oauth.ts b/packages/cli/src/lib/oauth.ts index eb19580b4..5e1373a81 100644 --- a/packages/cli/src/lib/oauth.ts +++ b/packages/cli/src/lib/oauth.ts @@ -510,7 +510,7 @@ export async function completeOAuthFlow( tokenResponse: TokenResponse, options: StoreTokenOptions = {}, ): Promise { - await setAuthToken( + setAuthToken( tokenResponse.access_token, tokenResponse.expires_in, tokenResponse.refresh_token, @@ -534,7 +534,7 @@ export async function setApiToken( token: string, options: StoreTokenOptions = {}, ): Promise { - await setAuthToken(token, undefined, undefined, { + setAuthToken(token, undefined, undefined, { host: getSentryUrl(), newAccount: options.newAccount, }); diff --git a/packages/cli/src/lib/sdk-invoke.ts b/packages/cli/src/lib/sdk-invoke.ts index 25cbb66a9..1cb859571 100644 --- a/packages/cli/src/lib/sdk-invoke.ts +++ b/packages/cli/src/lib/sdk-invoke.ts @@ -19,6 +19,7 @@ import type { Span } from "@sentry/core"; import type { Writer } from "../types/index.js"; import { type AsyncChannel, createAsyncChannel } from "./async-channel.js"; import { withEnv } from "./env.js"; +import { stringifyUnknown } from "./errors.js"; import { SentryError, type SentryOptions } from "./sdk-types.js"; /** CLI flag names/aliases that trigger infinite streaming output. */ @@ -217,7 +218,7 @@ function buildSdkError( const message = stderrStr.replace(ANSI_RE, "").trim() || (thrown instanceof Error ? thrown.message : undefined) || - (thrown !== undefined ? String(thrown) : undefined) || + (thrown !== undefined ? stringifyUnknown(thrown) : undefined) || `Command failed with exit code ${exitCode}`; return new SentryError(message, exitCode, stderrStr); } diff --git a/packages/cli/src/lib/sentry-client.ts b/packages/cli/src/lib/sentry-client.ts index 6d28f5ffc..69e4a430d 100644 --- a/packages/cli/src/lib/sentry-client.ts +++ b/packages/cli/src/lib/sentry-client.ts @@ -43,6 +43,7 @@ import { MalformedAuthTokenError, TimeoutError, } from "./errors.js"; +import { fetchInputUrl } from "./fetch-input.js"; import { logger } from "./logger.js"; import { clearLastCacheHitAge, @@ -355,7 +356,7 @@ async function fetchWithTimeout({ // extra property is harmless (Node honors NODE_EXTRA_CA_CERTS natively). const customTls = getCustomTlsOptions(); if (customTls) { - warnIfSaasWithEnvCa(extractFullUrl(input)); + warnIfSaasWithEnvCa(fetchInputUrl(input)); } const response = await fetch(input, { @@ -467,20 +468,9 @@ function handleFetchError( return { action: "retry" }; } -/** Extract the full URL string from a fetch input */ -function extractFullUrl(input: Request | string | URL): string { - if (typeof input === "string") { - return input; - } - if (input instanceof URL) { - return input.href; - } - return input.url; -} - /** Extract the URL pathname for span naming */ function extractUrlPath(input: Request | string | URL): string { - const raw = extractFullUrl(input); + const raw = fetchInputUrl(input); // oxlint-disable-next-line sentry-cli/no-silent-catch -- grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { return new URL(raw).pathname; @@ -765,7 +755,7 @@ function createAuthenticatedFetch( `${method} ${urlPath}`, "http.client", async (span) => { - const fullUrl = extractFullUrl(input); + const fullUrl = fetchInputUrl(input); const startTime = performance.now(); // Check cache before auth/retry for GET requests. diff --git a/packages/cli/src/lib/snapshots/images.ts b/packages/cli/src/lib/snapshots/images.ts index 3e580a50e..b22581ee9 100644 --- a/packages/cli/src/lib/snapshots/images.ts +++ b/packages/cli/src/lib/snapshots/images.ts @@ -9,7 +9,7 @@ import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; -import { ValidationError } from "../errors.js"; +import { stringifyUnknown, ValidationError } from "../errors.js"; import { logger } from "../logger.js"; import { walkFiles } from "../scan/walker.js"; @@ -202,7 +202,9 @@ export async function collectImages(dir: string): Promise { width = size.width; height = size.height; } catch (err) { - log.warn(`Could not read dimensions from ${entry.relativePath}: ${err}`); + log.warn( + `Could not read dimensions from ${entry.relativePath}: ${stringifyUnknown(err)}`, + ); continue; } if (!(width && height)) { diff --git a/packages/cli/src/lib/version-check.ts b/packages/cli/src/lib/version-check.ts index a535a8306..f574380ce 100644 --- a/packages/cli/src/lib/version-check.ts +++ b/packages/cli/src/lib/version-check.ts @@ -277,7 +277,7 @@ function checkForUpdateInBackgroundImpl(): void { const channel = getReleaseChannel(); - Sentry.startSpanManual( + void Sentry.startSpanManual( { name: "version-check", op: "version.check", diff --git a/packages/cli/test/commands/alert/issues/list.test.ts b/packages/cli/test/commands/alert/issues/list.test.ts index 692998161..631f888bd 100644 --- a/packages/cli/test/commands/alert/issues/list.test.ts +++ b/packages/cli/test/commands/alert/issues/list.test.ts @@ -66,7 +66,7 @@ describe("alert issues list pagination", () => { beforeEach(async () => { originalFetch = globalThis.fetch; func = (await listCommand.loader()) as unknown as ListFunc; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); setDefaultOrganization("test-org"); setDefaultProject("test-project"); diff --git a/packages/cli/test/commands/alert/metrics/list.test.ts b/packages/cli/test/commands/alert/metrics/list.test.ts index 12873a310..633f8b05e 100644 --- a/packages/cli/test/commands/alert/metrics/list.test.ts +++ b/packages/cli/test/commands/alert/metrics/list.test.ts @@ -112,7 +112,7 @@ describe("alert metrics list pagination", () => { beforeEach(async () => { originalFetch = globalThis.fetch; func = (await listCommand.loader()) as unknown as ListFunc; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); openInBrowserSpy = vi.spyOn(browser, "openInBrowser"); warnSpy = vi.spyOn(logger, "warn"); diff --git a/packages/cli/test/commands/api.property.test.ts b/packages/cli/test/commands/api.property.test.ts index 142195239..ff000b301 100644 --- a/packages/cli/test/commands/api.property.test.ts +++ b/packages/cli/test/commands/api.property.test.ts @@ -143,7 +143,7 @@ const plainStringArb = stringMatching(/^[a-zA-Z][a-zA-Z0-9 ]{0,20}$/).filter( describe("normalizeEndpoint properties", () => { test("result never has leading slash (except for root)", async () => { - await fcAssert( + fcAssert( property(endpointArb, (endpoint) => { const result = normalizeEndpoint(endpoint); // Root "/" is the only case where leading slash is allowed @@ -156,7 +156,7 @@ describe("normalizeEndpoint properties", () => { }); test("result always has trailing slash before query string", async () => { - await fcAssert( + fcAssert( property(endpointArb, (endpoint) => { const result = normalizeEndpoint(endpoint); const queryIdx = result.indexOf("?"); @@ -174,7 +174,7 @@ describe("normalizeEndpoint properties", () => { }); test("query string is always preserved unchanged", async () => { - await fcAssert( + fcAssert( property(tuple(simplePathArb, queryStringArb), ([path, query]) => { const input = path + query; const result = normalizeEndpoint(input); @@ -187,7 +187,7 @@ describe("normalizeEndpoint properties", () => { }); test("idempotent: normalizing twice equals normalizing once", async () => { - await fcAssert( + fcAssert( property(endpointArb, (endpoint) => { const once = normalizeEndpoint(endpoint); const twice = normalizeEndpoint(once); @@ -198,7 +198,7 @@ describe("normalizeEndpoint properties", () => { }); test("leading slash is always stripped", async () => { - await fcAssert( + fcAssert( property(simplePathArb, (path) => { const withLeading = `/${path}`; const withoutLeading = path; @@ -213,7 +213,7 @@ describe("normalizeEndpoint properties", () => { }); test("api/0/ prefix is always stripped (CLI-K1)", async () => { - await fcAssert( + fcAssert( property(simplePathArb, (path) => { const withPrefix = `api/0/${path}`; const withoutPrefix = path; @@ -228,7 +228,7 @@ describe("normalizeEndpoint properties", () => { }); test("result never starts with api/0/ (CLI-K1)", async () => { - await fcAssert( + fcAssert( property(endpointArb, (endpoint) => { const result = normalizeEndpoint(endpoint); expect(result.startsWith("api/0/")).toBe(false); @@ -240,7 +240,7 @@ describe("normalizeEndpoint properties", () => { describe("parseMethod properties", () => { test("valid methods always succeed and return uppercase", async () => { - await fcAssert( + fcAssert( property(validMethodArb, (method) => { const result = parseMethod(method); expect(result).toBe(method.toUpperCase() as typeof result); @@ -253,7 +253,7 @@ describe("parseMethod properties", () => { }); test("invalid methods always throw", async () => { - await fcAssert( + fcAssert( property(invalidMethodArb, (method) => { expect(() => parseMethod(method)).toThrow(/Invalid method/); }), @@ -262,7 +262,7 @@ describe("parseMethod properties", () => { }); test("result is always uppercase", async () => { - await fcAssert( + fcAssert( property(validMethodArb, (method) => { const result = parseMethod(method); expect(result).toBe((result as string).toUpperCase() as typeof result); @@ -274,7 +274,7 @@ describe("parseMethod properties", () => { describe("parseFieldKey properties", () => { test("first segment is always the base key", async () => { - await fcAssert( + fcAssert( property(nestedFieldKeyArb, (key) => { const segments = parseFieldKey(key); // First segment should be the base (before any brackets) @@ -290,7 +290,7 @@ describe("parseFieldKey properties", () => { }); test("key starting with bracket always throws", async () => { - await fcAssert( + fcAssert( property(fieldKeyBaseArb, (key) => { expect(() => parseFieldKey(`[${key}]`)).toThrow( /Invalid field key format/, @@ -301,7 +301,7 @@ describe("parseFieldKey properties", () => { }); test("segment count equals bracket count plus one", async () => { - await fcAssert( + fcAssert( property(nestedFieldKeyArb, (key) => { const segments = parseFieldKey(key); const bracketCount = (key.match(/\[/g) || []).length; @@ -314,7 +314,7 @@ describe("parseFieldKey properties", () => { describe("setNestedValue security properties", () => { test("__proto__ at any depth throws", async () => { - await fcAssert( + fcAssert( property(fieldKeyBaseArb, (key) => { const obj: Record = {}; @@ -338,7 +338,7 @@ describe("setNestedValue security properties", () => { }); test("constructor at any depth throws", async () => { - await fcAssert( + fcAssert( property(fieldKeyBaseArb, (key) => { const obj: Record = {}; @@ -355,7 +355,7 @@ describe("setNestedValue security properties", () => { }); test("prototype at any depth throws", async () => { - await fcAssert( + fcAssert( property(fieldKeyBaseArb, (key) => { const obj: Record = {}; @@ -395,7 +395,7 @@ describe("setNestedValue security properties", () => { describe("setNestedValue behavior properties", () => { test("simple key sets top-level value", async () => { - await fcAssert( + fcAssert( property(tuple(fieldKeyBaseArb, plainStringArb), ([key, value]) => { const obj: Record = {}; setNestedValue(obj, key, value); @@ -406,7 +406,7 @@ describe("setNestedValue behavior properties", () => { }); test("nested key creates nested structure", async () => { - await fcAssert( + fcAssert( property( tuple(fieldKeyBaseArb, fieldKeyBaseArb, plainStringArb), ([base, nested, value]) => { @@ -423,7 +423,7 @@ describe("setNestedValue behavior properties", () => { }); test("array push syntax creates array", async () => { - await fcAssert( + fcAssert( property(tuple(fieldKeyBaseArb, plainStringArb), ([key, value]) => { const obj: Record = {}; setNestedValue(obj, `${key}[]`, value); @@ -436,7 +436,7 @@ describe("setNestedValue behavior properties", () => { }); test("multiple array pushes append to array", async () => { - await fcAssert( + fcAssert( property( tuple(fieldKeyBaseArb, plainStringArb, plainStringArb), ([key, value1, value2]) => { @@ -456,7 +456,7 @@ describe("setNestedValue behavior properties", () => { }); test("empty brackets in middle of path throws", async () => { - await fcAssert( + fcAssert( property(tuple(fieldKeyBaseArb, fieldKeyBaseArb), ([base, end]) => { const obj: Record = {}; expect(() => setNestedValue(obj, `${base}[][${end}]`, "value")).toThrow( @@ -470,7 +470,7 @@ describe("setNestedValue behavior properties", () => { describe("parseFieldValue properties", () => { test("valid JSON is parsed correctly", async () => { - await fcAssert( + fcAssert( property(jsonValueArb, (jsonStr) => { const result = parseFieldValue(jsonStr); const expected = JSON.parse(jsonStr); @@ -481,7 +481,7 @@ describe("parseFieldValue properties", () => { }); test("non-JSON strings are returned as-is", async () => { - await fcAssert( + fcAssert( property(plainStringArb, (str) => { const result = parseFieldValue(str); expect(result).toBe(str); @@ -553,7 +553,7 @@ function captureStderr(fn: () => void): string { describe("property: normalizeFields JSON guard", () => { test("JSON objects pass through unchanged — no colon mangling", async () => { - await fcAssert( + fcAssert( property(jsonObjectStringArb, (json) => { const output = captureStderr(() => { const result = normalizeFields([json]); @@ -566,7 +566,7 @@ describe("property: normalizeFields JSON guard", () => { }); test("JSON arrays pass through unchanged", async () => { - await fcAssert( + fcAssert( property(jsonArrayStringArb, (json) => { const output = captureStderr(() => { const result = normalizeFields([json]); @@ -581,7 +581,7 @@ describe("property: normalizeFields JSON guard", () => { describe("property: parseDataBody", () => { test("round-trip: stringify(obj) → parseDataBody returns equivalent object", async () => { - await fcAssert( + fcAssert( property(jsonObjectArb, (obj) => { const result = parseDataBody(JSON.stringify(obj)); expect(result).toEqual(obj); @@ -591,7 +591,7 @@ describe("property: parseDataBody", () => { }); test("non-JSON strings are returned as-is", async () => { - await fcAssert( + fcAssert( property(plainStringArb, (s) => { const result = parseDataBody(s); expect(result).toBe(s); @@ -603,7 +603,7 @@ describe("property: parseDataBody", () => { describe("property: extractJsonBody", () => { test("key=value fields are never extracted as body", async () => { - await fcAssert( + fcAssert( property( array(keyValueFieldArb, { minLength: 1, maxLength: 5 }), (fields) => { @@ -620,7 +620,7 @@ describe("property: extractJsonBody", () => { }); test("single JSON object is always extracted as body", async () => { - await fcAssert( + fcAssert( property(jsonObjectStringArb, (json) => { const output = captureStderr(() => { const result = extractJsonBody([json]); @@ -634,7 +634,7 @@ describe("property: extractJsonBody", () => { }); test("count invariant: |remaining| + (body ? 1 : 0) === |input|", async () => { - await fcAssert( + fcAssert( property( tuple( oneof(jsonObjectStringArb, keyValueFieldArb, bareFieldArb), @@ -655,7 +655,7 @@ describe("property: extractJsonBody", () => { }); test("two JSON objects always throws ValidationError", async () => { - await fcAssert( + fcAssert( property(jsonObjectStringArb, jsonObjectStringArb, (a, b) => { expect(() => extractJsonBody([a, b])).toThrow(ValidationError); }), @@ -666,7 +666,7 @@ describe("property: extractJsonBody", () => { describe("property: buildFromFields", () => { test("GET never produces a body from JSON extraction", async () => { - await fcAssert( + fcAssert( property(jsonObjectStringArb, (json) => { const output = captureStderr(() => { // GET with a JSON field: should NOT extract body (throws instead) @@ -682,7 +682,7 @@ describe("property: buildFromFields", () => { }); test("PUT with only JSON body returns that body exactly", async () => { - await fcAssert( + fcAssert( property(jsonObjectStringArb, (json) => { captureStderr(() => { const result = buildFromFields("PUT", { "raw-field": [json] }); @@ -696,7 +696,7 @@ describe("property: buildFromFields", () => { test("PUT: JSON body + non-conflicting fields → merged body has all keys", async () => { // Generate JSON body keys and separate field keys that don't overlap - await fcAssert( + fcAssert( property( uniqueArray(fieldKeyBaseArb, { minLength: 2, maxLength: 6 }), (keys) => { @@ -734,7 +734,7 @@ describe("property: buildFromFields", () => { }); test("PUT: JSON body + conflicting field key → throws ValidationError", async () => { - await fcAssert( + fcAssert( property( record({ key: fieldKeyBaseArb, @@ -758,7 +758,7 @@ describe("property: buildFromFields", () => { }); test("PUT with only key=value fields: no hint emitted (no JSON extraction)", async () => { - await fcAssert( + fcAssert( property( array(keyValueFieldArb, { minLength: 1, maxLength: 5 }), (fields) => { diff --git a/packages/cli/test/commands/cli.test.ts b/packages/cli/test/commands/cli.test.ts index 25cef3fbd..f7ccb5b12 100644 --- a/packages/cli/test/commands/cli.test.ts +++ b/packages/cli/test/commands/cli.test.ts @@ -12,6 +12,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { feedbackCommand } from "../../src/commands/cli/feedback.js"; import type { UpgradeResult } from "../../src/commands/cli/upgrade.js"; import { upgradeCommand } from "../../src/commands/cli/upgrade.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; /** * Create a mock context with a process.stderr.write spy for capturing @@ -165,7 +166,7 @@ describe("upgradeCommand.func", () => { test("check mode with version shows versioned command", async () => { globalThis.fetch = (async (url) => { - const response = String(url).includes("/releases/tags/cli%402.0.0") + const response = fetchInputUrl(url).includes("/releases/tags/cli%402.0.0") ? { tag_name: "cli@2.0.0" } : [{ tag_name: "cli@99.0.0" }]; return new Response(JSON.stringify(response), { diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts index 8cb3419a6..2c3413f3c 100644 --- a/packages/cli/test/commands/cli/upgrade.test.ts +++ b/packages/cli/test/commands/cli/upgrade.test.ts @@ -41,6 +41,7 @@ import { setReleaseChannel, } from "../../../src/lib/db/release-channel.js"; import { setVersionCheckInfo } from "../../../src/lib/db/version-check.js"; +import { fetchInputUrl } from "../../../src/lib/fetch-input.js"; import { TEST_TMP_DIR, useTestConfigDir } from "../../helpers.js"; /** Store original fetch for restoration */ @@ -171,7 +172,7 @@ function createMockContext( */ function mockGhcrNightlyVersion(version: string): void { mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -212,7 +213,7 @@ function mockGhcrNightlyVersion(version: string): void { */ function mockGitHubVersion(version: string): void { mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: `cli@${version}` }]), { @@ -253,7 +254,7 @@ function mockGitHubVersion(version: string): void { */ function mockNightlyVersion(version: string): void { mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -366,7 +367,7 @@ describe("sentry cli upgrade", () => { test("resolves a pinned check target from its exact source", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if ( request.includes("getsentry/toolkit/releases/tags/cli%4088.88.88") @@ -462,7 +463,7 @@ describe("sentry cli upgrade", () => { const requests: string[] = []; setVersionCheckInfo("88.88.88"); mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return await response(); }); const { context, errors, getOutput, restore } = createMockContext({ @@ -543,7 +544,7 @@ describe("sentry cli upgrade", () => { test("uses the selected legacy source for the check-mode changelog", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if (request.includes("getsentry/toolkit/releases?per_page=100")) { return new Response("Not Found", { status: 404 }); @@ -581,7 +582,7 @@ describe("sentry cli upgrade", () => { test("reports error for non-existent version", async () => { // Mock: latest is 99.99.99, but 0.0.1 doesn't exist mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: "cli@99.99.99" }]), { status: 200, @@ -917,7 +918,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy const fakeContent = new Uint8Array([0x7f, 0x45, 0x4c, 0x46]); // ELF magic const gzipped = gzipSync(fakeContent); mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: `cli@${version}` }]), { status: 200, @@ -1024,7 +1025,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy async (method) => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(null, { status: 404 }); }); const { context, errors, restore } = createMockContext({ @@ -1046,7 +1047,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy for (const status of [401, 403, 429, 500]) { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(null, { status }); }); const { context, errors, restore } = createMockContext({ @@ -1075,7 +1076,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy async (method) => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return Response.json(null); }); const { context, errors, restore } = createMockContext({ @@ -1117,7 +1118,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy const fakeContent = new Uint8Array([0x7f, 0x45, 0x4c, 0x46]); const gzipped = gzipSync(fakeContent); mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: "cli@99.99.99" }]), { status: 200, @@ -1148,7 +1149,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy // GHCR flow: token exchange → manifest → blob redirect → blob download mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); capturedUrls.push(urlStr); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -1304,7 +1305,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp // Nightly is now distributed via GHCR (token → manifest → blob) mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1384,7 +1385,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp test("allows a pinned nightly for a Homebrew installation", async () => { mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1431,7 +1432,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp test("rejects a pinned stable for Homebrew before network access", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response("Unexpected", { status: 500 }); }); setReleaseChannel("nightly"); @@ -1453,7 +1454,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp test("validates an npm stable pin through npm while tracking nightly", async () => { const requests: string[] = []; mockFetch(async (url) => { - const requestUrl = new URL(String(url)); + const requestUrl = new URL(fetchInputUrl(url)); requests.push(requestUrl.href); return requestUrl.origin === "https://api.github.com" ? new Response(JSON.stringify([]), { status: 200 }) diff --git a/packages/cli/test/commands/issue/list.test.ts b/packages/cli/test/commands/issue/list.test.ts index 10d1ee64b..36cb25efe 100644 --- a/packages/cli/test/commands/issue/list.test.ts +++ b/packages/cli/test/commands/issue/list.test.ts @@ -94,7 +94,7 @@ let func: ListFunc; beforeEach(async () => { originalFetch = globalThis.fetch; func = (await listCommand.loader()) as unknown as ListFunc; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); setDefaultOrganization("test-org"); setDefaultProject("test-project"); @@ -1559,7 +1559,7 @@ describe("issue list: collapse parameter optimization", () => { resolveCursorMock.mockReset(); advancePaginationStateMock.mockReset(); - await setOrgRegion("my-org", DEFAULT_SENTRY_URL); + setOrgRegion("my-org", DEFAULT_SENTRY_URL); }); // End-to-end guard for the headline feature: when --sort is omitted, the @@ -1622,7 +1622,7 @@ describe("issue list: collapse parameter optimization", () => { test("uses date on a self-hosted instance", async () => { delete process.env.SENTRY_HOST; process.env.SENTRY_URL = "https://sentry.example.com"; - await setOrgRegion("my-org", "https://sentry.example.com"); + setOrgRegion("my-org", "https://sentry.example.com"); const options = await runOrgAllWithoutSort(); expect(listIssuesAllPagesMock).toHaveBeenCalled(); expect(options.sort).toBe("date"); diff --git a/packages/cli/test/commands/issue/short-id-resolution.test.ts b/packages/cli/test/commands/issue/short-id-resolution.test.ts index a3c4295b5..15f88bbd4 100644 --- a/packages/cli/test/commands/issue/short-id-resolution.test.ts +++ b/packages/cli/test/commands/issue/short-id-resolution.test.ts @@ -66,7 +66,7 @@ beforeEach(async () => { listedOrgs = [cachedOrg]; listingStatus = 200; issues = new Map(); - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegions([ { slug: cachedOrg.slug, diff --git a/packages/cli/test/commands/issue/utils.test.ts b/packages/cli/test/commands/issue/utils.test.ts index 9efc81db3..f1cf1fc02 100644 --- a/packages/cli/test/commands/issue/utils.test.ts +++ b/packages/cli/test/commands/issue/utils.test.ts @@ -186,7 +186,7 @@ beforeEach(async () => { async () => new Response(JSON.stringify({ detail: "Not found" }), { status: 404 }), ); - await setAuthToken("test-token"); + setAuthToken("test-token"); // Pre-populate region cache for orgs used in tests to avoid region resolution API calls setOrgRegion("test-org", DEFAULT_SENTRY_URL); setOrgRegion("my-org", DEFAULT_SENTRY_URL); @@ -2113,7 +2113,7 @@ describe("resolveIssue: numeric 404 error handling", () => { beforeEach(async () => { savedFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("my-org", DEFAULT_SENTRY_URL); }); @@ -2213,7 +2213,7 @@ describe("resolveIssue: project-search DSN shortcut", () => { beforeEach(async () => { dsnOriginalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("my-org", DEFAULT_SENTRY_URL); // Seed project cache so resolveFromDsn resolves without any API call. // orgId is "123" (DSN parser strips the "o" prefix from o123.ingest.*) diff --git a/packages/cli/test/commands/log/view.property.test.ts b/packages/cli/test/commands/log/view.property.test.ts index 51282efb4..2488c9cab 100644 --- a/packages/cli/test/commands/log/view.property.test.ts +++ b/packages/cli/test/commands/log/view.property.test.ts @@ -25,7 +25,7 @@ const slugArb = stringMatching(/^[a-z][a-z0-9-]{1,20}[a-z0-9]$/); describe("parsePositionalArgs properties", () => { test("single valid log ID: returns it in rawLogIds with undefined targetArg", async () => { - await fcAssert( + fcAssert( property(logIdArb, (input) => { const result = parsePositionalArgs([input]); expect(result.rawLogIds).toEqual([input]); @@ -36,7 +36,7 @@ describe("parsePositionalArgs properties", () => { }); test("single arg org/project/logId: splits into target and rawLogIds", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb, logIdArb), ([org, project, logId]) => { const combined = `${org}/${project}/${logId}`; const result = parsePositionalArgs([combined]); @@ -48,7 +48,7 @@ describe("parsePositionalArgs properties", () => { }); test("single arg with one slash: throws ContextError (missing log ID)", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb), ([org, project]) => { expect(() => parsePositionalArgs([`${org}/${project}`])).toThrow( ContextError, @@ -59,7 +59,7 @@ describe("parsePositionalArgs properties", () => { }); test("two args (target + logId): first is targetArg, second is in rawLogIds", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, logIdArb), ([slug, logId]) => { const result = parsePositionalArgs([slug, logId]); expect(result.targetArg).toBe(slug); @@ -70,7 +70,7 @@ describe("parsePositionalArgs properties", () => { }); test("org/project target + logId: correctly splits target and rawLogIds", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb, logIdArb), ([org, project, logId]) => { const target = `${org}/${project}`; const result = parsePositionalArgs([target, logId]); @@ -83,7 +83,7 @@ describe("parsePositionalArgs properties", () => { }); test("multiple log IDs: all IDs present in result", async () => { - await fcAssert( + fcAssert( property( tuple(slugArb, array(logIdArb, { minLength: 1, maxLength: 5 })), ([slug, ids]) => { @@ -99,7 +99,7 @@ describe("parsePositionalArgs properties", () => { }); test("newline-joined IDs in single arg: split correctly", async () => { - await fcAssert( + fcAssert( property( tuple(slugArb, array(logIdArb, { minLength: 2, maxLength: 5 })), ([slug, ids]) => { @@ -115,7 +115,7 @@ describe("parsePositionalArgs properties", () => { }); test("parsing is deterministic: same input always produces same output", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, logIdArb), ([slug, logId]) => { const result1 = parsePositionalArgs([slug, logId]); const result2 = parsePositionalArgs([slug, logId]); @@ -130,7 +130,7 @@ describe("parsePositionalArgs properties", () => { }); test("result always has non-empty rawLogIds array", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, logIdArb), ([slug, logId]) => { const result = parsePositionalArgs([slug, logId]); expect(result.rawLogIds.length).toBeGreaterThan(0); @@ -144,7 +144,7 @@ describe("parsePositionalArgs properties", () => { test("result targetArg is undefined for single ID, defined for target + ID", async () => { // Single arg case - await fcAssert( + fcAssert( property(logIdArb, (input) => { const result = parsePositionalArgs([input]); expect(result.targetArg).toBeUndefined(); @@ -153,7 +153,7 @@ describe("parsePositionalArgs properties", () => { ); // Two+ args case - await fcAssert( + fcAssert( property(tuple(slugArb, logIdArb), ([slug, logId]) => { const result = parsePositionalArgs([slug, logId]); expect(result.targetArg).toBeDefined(); diff --git a/packages/cli/test/commands/project/list.test.ts b/packages/cli/test/commands/project/list.test.ts index 9f49d358f..4fcb85e15 100644 --- a/packages/cli/test/commands/project/list.test.ts +++ b/packages/cli/test/commands/project/list.test.ts @@ -398,7 +398,7 @@ const sampleProjects: SentryProject[] = [ describe("handleExplicit", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -472,7 +472,7 @@ describe("project list: comma-separated project slugs", () => { beforeEach(async () => { originalFetch = globalThis.fetch; func = await listCommand.loader(); - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -635,7 +635,7 @@ describe("project list: comma-separated project slugs", () => { describe("handleOrgAll", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -891,7 +891,7 @@ describe("handleOrgAll", () => { describe("handleProjectSearch", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -1214,7 +1214,7 @@ describe("displayProjectTable", () => { describe("fetchOrgProjects", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("myorg", DEFAULT_SENTRY_URL); }); @@ -1244,7 +1244,7 @@ describe("fetchOrgProjects", () => { describe("fetchOrgProjectsSafe", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("myorg", DEFAULT_SENTRY_URL); }); @@ -1290,7 +1290,7 @@ describe("fetchOrgProjectsSafe", () => { describe("fetchAllOrgProjects", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -1364,7 +1364,7 @@ describe("fetchAllOrgProjects", () => { describe("handleAutoDetect", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); diff --git a/packages/cli/test/commands/release/propose-version.test.ts b/packages/cli/test/commands/release/propose-version.test.ts index d317b9338..2144907ff 100644 --- a/packages/cli/test/commands/release/propose-version.test.ts +++ b/packages/cli/test/commands/release/propose-version.test.ts @@ -37,7 +37,7 @@ describe("release propose-version", () => { }); test("outputs HEAD SHA in JSON mode when no env vars set", async () => { - getHeadCommitSpy.mockResolvedValue( + getHeadCommitSpy.mockReturnValue( "abc123def456789012345678901234567890abcd", ); @@ -51,7 +51,7 @@ describe("release propose-version", () => { }); test("outputs bare SHA in human mode", async () => { - getHeadCommitSpy.mockResolvedValue( + getHeadCommitSpy.mockReturnValue( "abc123def456789012345678901234567890abcd", ); @@ -64,7 +64,7 @@ describe("release propose-version", () => { }); test("passes cwd to getHeadCommit", async () => { - getHeadCommitSpy.mockResolvedValue("deadbeef"); + getHeadCommitSpy.mockReturnValue("deadbeef"); const { context } = createMockContext("/my/project"); const func = await proposeVersionCommand.loader(); @@ -74,7 +74,9 @@ describe("release propose-version", () => { }); test("propagates git errors", async () => { - getHeadCommitSpy.mockRejectedValue(new Error("Not a git repository")); + getHeadCommitSpy.mockImplementation(() => { + throw new Error("Not a git repository"); + }); const { context } = createMockContext(); const func = await proposeVersionCommand.loader(); @@ -113,7 +115,7 @@ describe("release propose-version", () => { }); test("skips empty env vars", async () => { - getHeadCommitSpy.mockResolvedValue("git-sha"); + getHeadCommitSpy.mockReturnValue("git-sha"); const { context, stdoutWrite } = createMockContext("/tmp", { SENTRY_RELEASE: "", diff --git a/packages/cli/test/commands/trace/view.property.test.ts b/packages/cli/test/commands/trace/view.property.test.ts index d8c4cf38c..53abd4de3 100644 --- a/packages/cli/test/commands/trace/view.property.test.ts +++ b/packages/cli/test/commands/trace/view.property.test.ts @@ -41,7 +41,7 @@ function toUuidFormat(hex: string): string { describe("parseTraceTarget properties", () => { test("single valid trace ID: returns auto-detect with correct traceId", async () => { - await fcAssert( + fcAssert( property(traceIdArb, (input) => { const result = parseTraceTarget([input], HINT); expect(result.type).toBe("auto-detect"); @@ -52,7 +52,7 @@ describe("parseTraceTarget properties", () => { }); test("single arg org/project/traceId: returns explicit with correct fields", async () => { - await fcAssert( + fcAssert( property( tuple(slugArb, slugArb, traceIdArb), ([org, project, traceId]) => { @@ -71,7 +71,7 @@ describe("parseTraceTarget properties", () => { }); test("single arg org/traceId: returns org-scoped", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, traceIdArb), ([org, traceId]) => { const combined = `${org}/${traceId}`; const result = parseTraceTarget([combined], HINT); @@ -86,7 +86,7 @@ describe("parseTraceTarget properties", () => { }); test("two args: target + trace-id uses second arg as trace ID", async () => { - await fcAssert( + fcAssert( property( tuple(slugArb, slugArb, traceIdArb), ([org, project, traceId]) => { @@ -101,7 +101,7 @@ describe("parseTraceTarget properties", () => { }); test("parsing is deterministic", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, traceIdArb), ([target, traceId]) => { const args = [target, traceId]; const result1 = parseTraceTarget(args, HINT); @@ -117,7 +117,7 @@ describe("parseTraceTarget properties", () => { }); test("result always has traceId property defined", async () => { - await fcAssert( + fcAssert( property(traceIdArb, (traceId) => { const result = parseTraceTarget([traceId], HINT); expect(result.traceId).toBeDefined(); @@ -128,7 +128,7 @@ describe("parseTraceTarget properties", () => { }); test("UUID-format trace IDs are accepted and produce 32-char hex", async () => { - await fcAssert( + fcAssert( property(traceIdArb, (hex) => { const uuid = toUuidFormat(hex); const result = parseTraceTarget([uuid], HINT); @@ -140,7 +140,7 @@ describe("parseTraceTarget properties", () => { test("invalid trace IDs always throw ValidationError", async () => { const invalidIdArb = stringMatching(/^[g-z]{10,20}$/); - await fcAssert( + fcAssert( property(invalidIdArb, (badId) => { expect(() => parseTraceTarget([badId], HINT)).toThrow(ValidationError); }), @@ -235,7 +235,7 @@ function collectSpanIds(spans: TraceSpan[]): Set { describe("flattenSpanTree properties", () => { test("result length equals total span count", async () => { - await fcAssert( + fcAssert( property(spanTreeArb, (tree) => { const result = flattenSpanTree(tree); expect(result).toHaveLength(countSpans(tree)); @@ -245,7 +245,7 @@ describe("flattenSpanTree properties", () => { }); test("all returned spans exist in original tree", async () => { - await fcAssert( + fcAssert( property(spanTreeArb, (tree) => { const result = flattenSpanTree(tree); const originalIds = collectSpanIds(tree); diff --git a/packages/cli/test/fixture.ts b/packages/cli/test/fixture.ts index 1614963e3..23d247563 100644 --- a/packages/cli/test/fixture.ts +++ b/packages/cli/test/fixture.ts @@ -165,7 +165,7 @@ export function createE2EContext( const prevDir = process.env[CONFIG_DIR_ENV_VAR]; process.env[CONFIG_DIR_ENV_VAR] = configDir; try { - await dbSetAuthToken(token, undefined, undefined, { host: serverUrl }); + dbSetAuthToken(token, undefined, undefined, { host: serverUrl }); closeDatabase(); } finally { if (prevDir !== undefined) { diff --git a/packages/cli/test/helpers.ts b/packages/cli/test/helpers.ts index 66fee55d2..e820218a5 100644 --- a/packages/cli/test/helpers.ts +++ b/packages/cli/test/helpers.ts @@ -236,15 +236,16 @@ export function extractFetchUrl(input: RequestInfo | URL): string { // --------------------------------------------------------------------------- /** A single route handler for the fetch mock. */ +/** Dynamic fetch route response; may return the body or a promise of it. */ +export type FetchRouteHandler = (url: string, init?: RequestInit) => unknown; + export type FetchRoute = { /** URL pattern — string for `includes()` match, or RegExp */ match: string | RegExp; /** HTTP method filter (default: any) */ method?: string; - /** Response body (JSON-serialized) or a handler function */ - response: - | unknown - | ((url: string, init?: RequestInit) => unknown | Promise); + /** Response body (JSON-serialized) or a {@link FetchRouteHandler} */ + response: unknown; /** HTTP status code (default: 200) */ status?: number; /** Response headers */ @@ -318,7 +319,7 @@ export function installFetchMock( const status = route.status ?? 200; const responseBody = typeof route.response === "function" - ? await route.response(url, init) + ? await (route.response as FetchRouteHandler)(url, init) : route.response; const headers: Record = { diff --git a/packages/cli/test/lib/agent-skills.test.ts b/packages/cli/test/lib/agent-skills.test.ts index d799f8a72..0f1276600 100644 --- a/packages/cli/test/lib/agent-skills.test.ts +++ b/packages/cli/test/lib/agent-skills.test.ts @@ -150,7 +150,7 @@ describe("agent-skills", () => { // makes this a real guard rather than a tautology. const writeTargets = vi .mocked(writeFile) - .mock.calls.map((call) => String(call[0])); + .mock.calls.map((call) => call[0] as string); expect(writeTargets.length).toBeGreaterThan(0); expect(writeTargets.every((target) => target.endsWith(".tmp"))).toBe( true, diff --git a/packages/cli/test/lib/api-client.coverage.test.ts b/packages/cli/test/lib/api-client.coverage.test.ts index 4687ce842..65affaed6 100644 --- a/packages/cli/test/lib/api-client.coverage.test.ts +++ b/packages/cli/test/lib/api-client.coverage.test.ts @@ -59,7 +59,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", "https://sentry.io"); }); diff --git a/packages/cli/test/lib/api-client.multiregion.test.ts b/packages/cli/test/lib/api-client.multiregion.test.ts index 51ba7a5c8..959df45be 100644 --- a/packages/cli/test/lib/api-client.multiregion.test.ts +++ b/packages/cli/test/lib/api-client.multiregion.test.ts @@ -41,7 +41,7 @@ beforeEach(async () => { delete process.env.SENTRY_TOKEN; // Set up auth token (manual token, no refresh) - await setAuthToken("test-token"); + setAuthToken("test-token"); // Clear any existing region cache clearOrgRegions(); diff --git a/packages/cli/test/lib/api-client.seer-trial.test.ts b/packages/cli/test/lib/api-client.seer-trial.test.ts index 45e94cf57..2ad45f883 100644 --- a/packages/cli/test/lib/api-client.seer-trial.test.ts +++ b/packages/cli/test/lib/api-client.seer-trial.test.ts @@ -12,6 +12,7 @@ import { } from "../../src/lib/api-client.js"; import { setAuthToken } from "../../src/lib/db/auth.js"; import { setOrgRegion } from "../../src/lib/db/regions.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import { mockFetch, useTestConfigDir } from "../helpers.js"; useTestConfigDir("test-product-trial-api-"); @@ -20,7 +21,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", "https://sentry.io"); }); @@ -199,7 +200,7 @@ describe("startProductTrial", () => { globalThis.fetch = mockFetch( async (input: RequestInfo | URL, init?: RequestInit) => { - capturedUrl = input.toString(); + capturedUrl = fetchInputUrl(input); capturedMethod = init?.method ?? "GET"; return new Response(JSON.stringify({}), { status: 200, diff --git a/packages/cli/test/lib/api-client.seer.test.ts b/packages/cli/test/lib/api-client.seer.test.ts index 8512e4a40..9e3651907 100644 --- a/packages/cli/test/lib/api-client.seer.test.ts +++ b/packages/cli/test/lib/api-client.seer.test.ts @@ -24,7 +24,7 @@ beforeEach(async () => { originalFetch = globalThis.fetch; // Set up auth token (manual token, no refresh) - await setAuthToken("test-token"); + setAuthToken("test-token"); // Pre-populate region cache to avoid region resolution API calls setOrgRegion("test-org", "https://sentry.io"); }); diff --git a/packages/cli/test/lib/api-client.test.ts b/packages/cli/test/lib/api-client.test.ts index f58e463a9..0fd11195a 100644 --- a/packages/cli/test/lib/api-client.test.ts +++ b/packages/cli/test/lib/api-client.test.ts @@ -44,7 +44,7 @@ beforeEach(async () => { originalFetch = globalThis.fetch; // Set up initial auth token with a refresh token so 401 retry can get a new token - await setAuthToken("initial-token", 3600, "test-refresh-token"); + setAuthToken("initial-token", 3600, "test-refresh-token"); }); afterEach(() => { @@ -242,7 +242,7 @@ describe("401 retry behavior", () => { test("does not retry for manual API tokens (no refresh token)", async () => { // Manual API tokens have no expiry and no refresh token - await setAuthToken("manual-api-token"); // No expiry, no refresh token + setAuthToken("manual-api-token"); // No expiry, no refresh token const requests: RequestLog[] = []; diff --git a/packages/cli/test/lib/api/alerts.test.ts b/packages/cli/test/lib/api/alerts.test.ts index 11397a5a9..546e1f3ee 100644 --- a/packages/cli/test/lib/api/alerts.test.ts +++ b/packages/cli/test/lib/api/alerts.test.ts @@ -27,7 +27,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; resetAuthenticatedFetch(); - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); diff --git a/packages/cli/test/lib/api/events-overshoot.test.ts b/packages/cli/test/lib/api/events-overshoot.test.ts index 22ff7d530..6fec75cdc 100644 --- a/packages/cli/test/lib/api/events-overshoot.test.ts +++ b/packages/cli/test/lib/api/events-overshoot.test.ts @@ -24,7 +24,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", "https://sentry.io"); }); diff --git a/packages/cli/test/lib/api/infrastructure.test.ts b/packages/cli/test/lib/api/infrastructure.test.ts index 3760bc62d..95f212f09 100644 --- a/packages/cli/test/lib/api/infrastructure.test.ts +++ b/packages/cli/test/lib/api/infrastructure.test.ts @@ -711,7 +711,7 @@ describe("rawApiRequest binary handling", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); }); afterEach(() => { diff --git a/packages/cli/test/lib/api/issue-app-links.test.ts b/packages/cli/test/lib/api/issue-app-links.test.ts index d231318b3..1584b006a 100644 --- a/packages/cli/test/lib/api/issue-app-links.test.ts +++ b/packages/cli/test/lib/api/issue-app-links.test.ts @@ -68,7 +68,7 @@ function json(data: unknown, status = 200, headers?: HeadersInit): Response { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion(ORG, "https://de.sentry.io"); resetAuthenticatedFetch(); calls = []; diff --git a/packages/cli/test/lib/api/logs.test.ts b/packages/cli/test/lib/api/logs.test.ts index c0e86150f..220c24399 100644 --- a/packages/cli/test/lib/api/logs.test.ts +++ b/packages/cli/test/lib/api/logs.test.ts @@ -19,7 +19,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("fake-token-for-test", 3600); + setAuthToken("fake-token-for-test", 3600); }); afterEach(() => { diff --git a/packages/cli/test/lib/api/organizations.test.ts b/packages/cli/test/lib/api/organizations.test.ts index 182768ee5..39bca5fae 100644 --- a/packages/cli/test/lib/api/organizations.test.ts +++ b/packages/cli/test/lib/api/organizations.test.ts @@ -17,7 +17,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("fake-token-for-test", 3600, undefined, { + setAuthToken("fake-token-for-test", 3600, undefined, { host: "https://sentry.example.com", }); }); diff --git a/packages/cli/test/lib/api/project-dsns.test.ts b/packages/cli/test/lib/api/project-dsns.test.ts index 2b76ff4d3..5ee0e6320 100644 --- a/packages/cli/test/lib/api/project-dsns.test.ts +++ b/packages/cli/test/lib/api/project-dsns.test.ts @@ -82,7 +82,7 @@ beforeEach(async () => { resetCacheState(); disableResponseCache(); resetAuthenticatedFetch(); - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", "https://de.sentry.io"); }); diff --git a/packages/cli/test/lib/api/projects.test.ts b/packages/cli/test/lib/api/projects.test.ts index ea512ce58..4d33f57a2 100644 --- a/packages/cli/test/lib/api/projects.test.ts +++ b/packages/cli/test/lib/api/projects.test.ts @@ -97,7 +97,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", "https://us.sentry.io"); projectCacheMocks.restoreActual(); }); diff --git a/packages/cli/test/lib/api/releases.test.ts b/packages/cli/test/lib/api/releases.test.ts index 1972a4ff1..59a79a670 100644 --- a/packages/cli/test/lib/api/releases.test.ts +++ b/packages/cli/test/lib/api/releases.test.ts @@ -102,7 +102,7 @@ function linkHeader(cursor: string, hasResults: boolean): string { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", "https://us.sentry.io"); }); diff --git a/packages/cli/test/lib/api/replays.test.ts b/packages/cli/test/lib/api/replays.test.ts index ab6fa75d8..381234e24 100644 --- a/packages/cli/test/lib/api/replays.test.ts +++ b/packages/cli/test/lib/api/replays.test.ts @@ -55,7 +55,7 @@ describe("listReplays", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -178,7 +178,7 @@ describe("getReplay", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -250,7 +250,7 @@ describe("resolveReplay", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -374,7 +374,7 @@ describe("getReplayRecordingSegments", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); @@ -494,7 +494,7 @@ describe("listReplayIdsForIssue", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); }); diff --git a/packages/cli/test/lib/arg-parsing.property.test.ts b/packages/cli/test/lib/arg-parsing.property.test.ts index 969a20849..570f82996 100644 --- a/packages/cli/test/lib/arg-parsing.property.test.ts +++ b/packages/cli/test/lib/arg-parsing.property.test.ts @@ -40,7 +40,7 @@ const numericIdArb = stringMatching(/^[1-9][0-9]{0,15}$/); describe("parseIssueArg properties", () => { test("numeric-only inputs always return type 'numeric'", async () => { - await fcAssert( + fcAssert( property(numericIdArb, (input) => { const result = parseIssueArg(input); expect(result.type).toBe("numeric"); @@ -53,7 +53,7 @@ describe("parseIssueArg properties", () => { }); test("suffix is always uppercase in result", async () => { - await fcAssert( + fcAssert( property(suffixArb, (suffix) => { const result = parseIssueArg(suffix); // suffix-only type (no dash, no slash, not numeric) @@ -66,7 +66,7 @@ describe("parseIssueArg properties", () => { }); test("org/project-suffix returns type 'explicit' with uppercase suffix", async () => { - await fcAssert( + fcAssert( property( tuple(orgSlugArb, projectSlugArb, suffixArb), ([org, project, suffix]) => { @@ -86,7 +86,7 @@ describe("parseIssueArg properties", () => { }); test("org/numericId returns type 'explicit-org-numeric'", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, numericIdArb), ([org, numericId]) => { const input = `${org}/${numericId}`; const result = parseIssueArg(input); @@ -102,7 +102,7 @@ describe("parseIssueArg properties", () => { }); test("org/suffix (no dash) returns type 'explicit-org-suffix' with uppercase suffix", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, suffixArb), ([org, suffix]) => { // Skip if suffix looks numeric (would be explicit-org-numeric) if (/^\d+$/.test(suffix)) return; @@ -121,7 +121,7 @@ describe("parseIssueArg properties", () => { }); test("project-suffix returns type 'project-search' with uppercase suffix", async () => { - await fcAssert( + fcAssert( property(tuple(projectSlugArb, suffixArb), ([project, suffix]) => { const input = `${project}-${suffix}`; const result = parseIssueArg(input); @@ -142,7 +142,7 @@ describe("parseIssueArg properties", () => { /^[a-zA-Z][a-zA-Z0-9]{0,9}$/, ); - await fcAssert( + fcAssert( property(alphanumericWithLetterArb, (input) => { const result = parseIssueArg(input); @@ -178,7 +178,7 @@ describe("parseIssueArg properties", () => { suffixArb, ); - await fcAssert( + fcAssert( property(validInputArb, (input) => { try { const result = parseIssueArg(input); @@ -200,7 +200,7 @@ describe("parseIssueArg properties", () => { suffixArb, ); - await fcAssert( + fcAssert( property(inputArb, (input) => { const originalInput = input; try { @@ -221,7 +221,7 @@ describe("parseIssueArg: GitHub-style # separator properties (CLI-1G1)", () => { const forbiddenCharArb = constantFrom("?", "%", " ", "\t"); test("org/project#FULL-SHORTID is equivalent to org/project/FULL-SHORTID", async () => { - await fcAssert( + fcAssert( property( tuple(orgSlugArb, projectSlugArb, suffixArb), ([org, project, suffix]) => { @@ -236,7 +236,7 @@ describe("parseIssueArg: GitHub-style # separator properties (CLI-1G1)", () => { }); test("project#FULL-SHORTID always yields project-search with extracted suffix", async () => { - await fcAssert( + fcAssert( property(tuple(projectSlugArb, suffixArb), ([project, suffix]) => { const result = parseIssueArg(`${project}#${project}-${suffix}`); expect(result).toEqual({ @@ -250,7 +250,7 @@ describe("parseIssueArg: GitHub-style # separator properties (CLI-1G1)", () => { }); test("forbidden character in fragment always throws", async () => { - await fcAssert( + fcAssert( property( tuple(projectSlugArb, suffixArb, forbiddenCharArb), ([project, suffix, bad]) => { @@ -266,7 +266,7 @@ describe("parseOrgProjectArg properties", () => { test("undefined or empty string returns type 'auto-detect'", async () => { const emptyInputArb = constantFrom(undefined, "", " ", "\t", "\n"); - await fcAssert( + fcAssert( property(emptyInputArb, (input) => { const result = parseOrgProjectArg(input); expect(result.type).toBe("auto-detect"); @@ -276,7 +276,7 @@ describe("parseOrgProjectArg properties", () => { }); test("org/project returns type 'explicit'", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, projectSlugArb), ([org, project]) => { const input = `${org}/${project}`; const result = parseOrgProjectArg(input); @@ -293,7 +293,7 @@ describe("parseOrgProjectArg properties", () => { }); test("org/a,b stays one project unless list parsing is enabled", async () => { - await fcAssert( + fcAssert( property( tuple(orgSlugArb, projectSlugArb, projectSlugArb), ([org, first, second]) => { @@ -310,7 +310,7 @@ describe("parseOrgProjectArg properties", () => { }); test("org/a,b returns both slugs when list parsing is enabled", async () => { - await fcAssert( + fcAssert( property( tuple(orgSlugArb, projectSlugArb, projectSlugArb), ([org, first, second]) => { @@ -332,7 +332,7 @@ describe("parseOrgProjectArg properties", () => { }); test("org/ (trailing slash) returns type 'org-all'", async () => { - await fcAssert( + fcAssert( property(orgSlugArb, (org) => { const input = `${org}/`; const result = parseOrgProjectArg(input); @@ -347,7 +347,7 @@ describe("parseOrgProjectArg properties", () => { }); test("project without slash returns type 'project-search'", async () => { - await fcAssert( + fcAssert( property(projectSlugArb, (project) => { const result = parseOrgProjectArg(project); @@ -361,7 +361,7 @@ describe("parseOrgProjectArg properties", () => { }); test("/project (leading slash) returns type 'project-search'", async () => { - await fcAssert( + fcAssert( property(projectSlugArb, (project) => { const input = `/${project}`; const result = parseOrgProjectArg(input); @@ -386,7 +386,7 @@ describe("parseOrgProjectArg properties", () => { projectSlugArb.map((p) => `/${p}`), ); - await fcAssert( + fcAssert( property(validInputArb, (input) => { try { const result = parseOrgProjectArg(input); @@ -400,7 +400,7 @@ describe("parseOrgProjectArg properties", () => { }); test("whitespace is trimmed from input", async () => { - await fcAssert( + fcAssert( property( tuple(orgSlugArb, projectSlugArb, constantFrom("", " ", " ")), ([org, project, ws]) => { @@ -423,7 +423,7 @@ describe("parseIssueArg and parseOrgProjectArg consistency", () => { test("parseIssueArg uses parseOrgProjectArg for dash-separated inputs", async () => { // When parseIssueArg gets "org/project-suffix", it should parse "org/project" // the same way parseOrgProjectArg would - await fcAssert( + fcAssert( property( tuple(orgSlugArb, projectSlugArb, suffixArb), ([org, project, suffix]) => { @@ -471,7 +471,7 @@ const displayNameLikeArb = stringMatching(/^[A-Za-z][A-Za-z0-9 _-]{0,20}$/); describe("normalizeSlug properties (no-op)", () => { test("always returns the input unchanged", async () => { - await fcAssert( + fcAssert( property(displayNameLikeArb, (input) => { const result = normalizeSlug(input); expect(result.slug).toBe(input); @@ -481,7 +481,7 @@ describe("normalizeSlug properties (no-op)", () => { }); test("normalized is always false", async () => { - await fcAssert( + fcAssert( property(displayNameLikeArb, (input) => { const result = normalizeSlug(input); expect(result.normalized).toBe(false); @@ -491,7 +491,7 @@ describe("normalizeSlug properties (no-op)", () => { }); test("idempotent: normalizing twice yields same result as normalizing once", async () => { - await fcAssert( + fcAssert( property(displayNameLikeArb, (input) => { const first = normalizeSlug(input); const second = normalizeSlug(first.slug); @@ -505,7 +505,7 @@ describe("normalizeSlug properties (no-op)", () => { describe("looksLikeIssueShortId properties", () => { test("all-lowercase slugs with dashes never match", async () => { - await fcAssert( + fcAssert( property(lowercaseSlugWithDashArb, (input) => { expect(looksLikeIssueShortId(input)).toBe(false); }), @@ -514,7 +514,7 @@ describe("looksLikeIssueShortId properties", () => { }); test("strings without dashes never match", async () => { - await fcAssert( + fcAssert( property(noDashAlphanumArb, (input) => { expect(looksLikeIssueShortId(input)).toBe(false); }), @@ -523,7 +523,7 @@ describe("looksLikeIssueShortId properties", () => { }); test("strings with slashes never match", async () => { - await fcAssert( + fcAssert( property(withSlashArb, (input) => { expect(looksLikeIssueShortId(input)).toBe(false); }), @@ -532,7 +532,7 @@ describe("looksLikeIssueShortId properties", () => { }); test("two-part lowercase slugs do not match with ignoreCase", async () => { - await fcAssert( + fcAssert( property(lowercaseSlugWithDashArb, (input) => { if (input.split("-").length === 2) { expect(looksLikeIssueShortId(input, { ignoreCase: true })).toBe( @@ -545,7 +545,7 @@ describe("looksLikeIssueShortId properties", () => { }); test("three-plus-part lowercase slugs without alphanumeric final segment do not match with ignoreCase", async () => { - await fcAssert( + fcAssert( property(lowercaseSlugWithDashArb, (input) => { const parts = input.split("-"); const lastPart = parts.at(-1) ?? ""; @@ -562,7 +562,7 @@ describe("looksLikeIssueShortId properties", () => { }); test("three-plus-part lowercase slugs with alphanumeric final segment match with ignoreCase", async () => { - await fcAssert( + fcAssert( property(lowercaseSlugWithDashArb, (input) => { const parts = input.split("-"); const lastPart = parts.at(-1) ?? ""; @@ -593,7 +593,7 @@ describe("parseSelector properties", () => { ); test("recognized selectors always return a canonical value", async () => { - await fcAssert( + fcAssert( property(selectorVariantArb, (input) => { const result = parseSelector(input); expect(result).toBeDefined(); @@ -604,7 +604,7 @@ describe("parseSelector properties", () => { }); test("non-@ strings never match", async () => { - await fcAssert( + fcAssert( property(orgSlugArb, (input) => { expect(parseSelector(input)).toBeUndefined(); }), @@ -613,7 +613,7 @@ describe("parseSelector properties", () => { }); test("bare @selector parses to type 'selector'", async () => { - await fcAssert( + fcAssert( property(selectorVariantArb, (input) => { const result = parseIssueArg(input); expect(result.type).toBe("selector"); @@ -627,7 +627,7 @@ describe("parseSelector properties", () => { }); test("org/@selector parses to type 'selector' with org", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, selectorVariantArb), ([org, sel]) => { const input = `${org}/${sel}`; const result = parseIssueArg(input); @@ -642,7 +642,7 @@ describe("parseSelector properties", () => { }); test("canonical form is stable regardless of casing", async () => { - await fcAssert( + fcAssert( property(selectorVariantArb, (input) => { const direct = parseSelector(input); const lower = parseSelector(input.toLowerCase()); @@ -655,7 +655,7 @@ describe("parseSelector properties", () => { describe("detectSwappedViewArgs properties", () => { test("symmetric inverse: if swap(a,b) is non-null then swap(b,a) is null when exactly one has slash", async () => { - await fcAssert( + fcAssert( property(tuple(noSlashArb, withSlashArb), ([noSlash, withSlash]) => { // noSlash first, withSlash second → swapped → non-null expect(detectSwappedViewArgs(noSlash, withSlash)).not.toBeNull(); @@ -667,7 +667,7 @@ describe("detectSwappedViewArgs properties", () => { }); test("both without slashes always returns null", async () => { - await fcAssert( + fcAssert( property(tuple(noSlashArb, noSlashArb), ([a, b]) => { expect(detectSwappedViewArgs(a, b)).toBeNull(); }), @@ -676,7 +676,7 @@ describe("detectSwappedViewArgs properties", () => { }); test("both with slashes always returns null", async () => { - await fcAssert( + fcAssert( property(tuple(withSlashArb, withSlashArb), ([a, b]) => { expect(detectSwappedViewArgs(a, b)).toBeNull(); }), diff --git a/packages/cli/test/lib/auto-paginate.property.test.ts b/packages/cli/test/lib/auto-paginate.property.test.ts index 493498e95..df22c9f3b 100644 --- a/packages/cli/test/lib/auto-paginate.property.test.ts +++ b/packages/cli/test/lib/auto-paginate.property.test.ts @@ -61,8 +61,8 @@ const pageSizeArb = integer({ }); describe("property: autoPaginate multi-page contract", () => { - test("never returns more than limit items", () => { - fcAssert( + test("never returns more than limit items", async () => { + await fcAssert( asyncProperty( nat(API_MAX_PER_PAGE * 5), multiPageLimit, @@ -79,8 +79,8 @@ describe("property: autoPaginate multi-page contract", () => { ); }); - test("drops nextCursor on overshoot, but keeps a page-aligned boundary cursor", () => { - fcAssert( + test("drops nextCursor on overshoot, but keeps a page-aligned boundary cursor", async () => { + await fcAssert( asyncProperty( nat(API_MAX_PER_PAGE * 5), multiPageLimit, @@ -125,8 +125,8 @@ describe("property: autoPaginate multi-page contract", () => { ); }); - test("accumulated rows are contiguous from offset 0 (no skips/dupes)", () => { - fcAssert( + test("accumulated rows are contiguous from offset 0 (no skips/dupes)", async () => { + await fcAssert( asyncProperty( nat(API_MAX_PER_PAGE * 5), multiPageLimit, diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts index 3450bb849..ecc10787f 100644 --- a/packages/cli/test/lib/binary.test.ts +++ b/packages/cli/test/lib/binary.test.ts @@ -40,6 +40,7 @@ import { UpgradeSourceNotFoundError, } from "../../src/lib/binary.js"; import { UpgradeError } from "../../src/lib/errors.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; describe("getBinaryDownloadUrl", () => { test("builds correct URL for current platform", () => { @@ -90,7 +91,7 @@ describe("resolveUpgradeSource", () => { const resolved = await resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(JSON.stringify({ tag_name: "cli@0.45.0" }), { status: 200, }); @@ -112,7 +113,7 @@ describe("resolveUpgradeSource", () => { const resolved = await resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response( requests.length === 1 ? "Not Found" @@ -138,7 +139,7 @@ describe("resolveUpgradeSource", () => { resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response("failure", { status }); }, }), @@ -157,7 +158,7 @@ describe("resolveUpgradeSource", () => { resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); throw new TypeError("fetch failed"); }, }), @@ -174,7 +175,7 @@ describe("resolveUpgradeSource", () => { const error = await resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response("Not Found", { status: 404 }); }, }).catch((reason: unknown) => reason); diff --git a/packages/cli/test/lib/config.test.ts b/packages/cli/test/lib/config.test.ts index 5237bfb34..4c0dfac1b 100644 --- a/packages/cli/test/lib/config.test.ts +++ b/packages/cli/test/lib/config.test.ts @@ -64,54 +64,54 @@ afterEach(() => { describe("auth token management", () => { test("setAuthToken stores token", async () => { - await setAuthToken("test-token-123"); + setAuthToken("test-token-123"); - const token = await getAuthToken(); + const token = getAuthToken(); expect(token).toBe("test-token-123"); }); test("setAuthToken with expiration sets expiresAt", async () => { const before = Date.now(); - await setAuthToken("expiring-token", 3600); // 1 hour + setAuthToken("expiring-token", 3600); // 1 hour const after = Date.now(); - const auth = await getAuthConfig(); + const auth = getAuthConfig(); expect(auth?.expiresAt).toBeGreaterThanOrEqual(before + 3600 * 1000); expect(auth?.expiresAt).toBeLessThanOrEqual(after + 3600 * 1000); }); test("setAuthToken stores refresh token", async () => { - await setAuthToken("access-token", 3600, "refresh-token"); + setAuthToken("access-token", 3600, "refresh-token"); - const auth = await getAuthConfig(); + const auth = getAuthConfig(); expect(auth?.refreshToken).toBe("refresh-token"); }); test("getAuthToken returns undefined for expired token", async () => { // Set a token that expires in the future, then we'll manipulate it - await setAuthToken("expired-token", -1); // Negative expires immediately + setAuthToken("expired-token", -1); // Negative expires immediately - const token = await getAuthToken(); + const token = getAuthToken(); expect(token).toBeUndefined(); }); test("getAuthToken returns token if not expired", async () => { - await setAuthToken("valid-token", 3600); // 1 hour + setAuthToken("valid-token", 3600); // 1 hour - const token = await getAuthToken(); + const token = getAuthToken(); expect(token).toBe("valid-token"); }); test("clearAuth removes auth data", async () => { - await setAuthToken("token-to-clear"); - expect(await getAuthToken()).toBe("token-to-clear"); + setAuthToken("token-to-clear"); + expect(getAuthToken()).toBe("token-to-clear"); await clearAuth(); - expect(await getAuthToken()).toBeUndefined(); + expect(getAuthToken()).toBeUndefined(); }); test("isAuthenticated returns true with valid token", async () => { - await setAuthToken("valid-token"); + setAuthToken("valid-token"); expect(isAuthenticated()).toBe(true); }); @@ -120,7 +120,7 @@ describe("auth token management", () => { }); test("isAuthenticated returns false with expired token", async () => { - await setAuthToken("expired", -1); // Negative expires immediately + setAuthToken("expired", -1); // Negative expires immediately expect(isAuthenticated()).toBe(false); }); }); @@ -171,7 +171,7 @@ describe("defaults management", () => { describe("refreshToken error handling", () => { test("network error during refresh does not clear auth", async () => { // Set up a token that needs refresh (expired but has refresh token) - await setAuthToken("still-valid-token", -1, "my-refresh-token"); // Expired + setAuthToken("still-valid-token", -1, "my-refresh-token"); // Expired // Set required env var for OAuth process.env.SENTRY_CLIENT_ID = "test-client-id"; @@ -194,14 +194,14 @@ describe("refreshToken error handling", () => { } // Auth should NOT be cleared on network error - const auth = await getAuthConfig(); + const auth = getAuthConfig(); expect(auth?.token).toBe("still-valid-token"); expect(auth?.refreshToken).toBe("my-refresh-token"); }); test("auth error during refresh clears auth", async () => { // Set up a token that needs refresh - await setAuthToken("revoked-token", -1, "invalid-refresh-token"); // Expired + setAuthToken("revoked-token", -1, "invalid-refresh-token"); // Expired process.env.SENTRY_CLIENT_ID = "test-client-id"; @@ -226,7 +226,7 @@ describe("refreshToken error handling", () => { } // Auth SHOULD be cleared when server rejects refresh token - const auth = await getAuthConfig(); + const auth = getAuthConfig(); expect(auth).toBeUndefined(); }); }); @@ -648,10 +648,10 @@ describe("JSON to SQLite migration", () => { ); // Access the database (triggers migration) - const token = await getAuthToken(); + const token = getAuthToken(); expect(token).toBe("migrated-token"); - const auth = await getAuthConfig(); + const auth = getAuthConfig(); expect(auth?.refreshToken).toBe("migrated-refresh"); const org = getDefaultOrganization(); diff --git a/packages/cli/test/lib/credential-redaction.test.ts b/packages/cli/test/lib/credential-redaction.test.ts index 23d992986..21c4eb9d1 100644 --- a/packages/cli/test/lib/credential-redaction.test.ts +++ b/packages/cli/test/lib/credential-redaction.test.ts @@ -190,8 +190,11 @@ describe("error output boundaries", () => { expect(error.message).not.toContain("SYNTHETIC"); expect(error.stack).not.toContain("SYNTHETIC"); expect(error.stderr).not.toContain("SYNTHETIC"); - expect(JSON.stringify({ message: error.message, ...error })).not.toContain( - "SYNTHETIC", - ); + expect( + JSON.stringify({ + message: error.message, + ...Object.fromEntries(Object.entries(error)), + }), + ).not.toContain("SYNTHETIC"); }); }); diff --git a/packages/cli/test/lib/db/auth.test.ts b/packages/cli/test/lib/db/auth.test.ts index 344dde160..392be4283 100644 --- a/packages/cli/test/lib/db/auth.test.ts +++ b/packages/cli/test/lib/db/auth.test.ts @@ -233,7 +233,7 @@ describe("clearAuth: integration with per-account caches", () => { await import("../../../src/lib/db/issue-org-cache.js"); // Seed a mapping as if a previous session resolved this issue. - await setAuthToken("test-token"); + setAuthToken("test-token"); setCachedIssueOrg("12345", "previous-account-org"); expect(getCachedIssueOrg("12345")).toBe("previous-account-org"); diff --git a/packages/cli/test/lib/db/concurrent-worker.ts b/packages/cli/test/lib/db/concurrent-worker.ts index 534315600..3164e0ac1 100644 --- a/packages/cli/test/lib/db/concurrent-worker.ts +++ b/packages/cli/test/lib/db/concurrent-worker.ts @@ -182,4 +182,4 @@ async function main(): Promise { console.log(JSON.stringify(result)); } -main(); +void main(); diff --git a/packages/cli/test/lib/delta-upgrade.mocked.test.ts b/packages/cli/test/lib/delta-upgrade.mocked.test.ts index 9b356e6c7..d99c3452c 100644 --- a/packages/cli/test/lib/delta-upgrade.mocked.test.ts +++ b/packages/cli/test/lib/delta-upgrade.mocked.test.ts @@ -17,6 +17,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import { useTestConfigDir } from "../helpers.js"; useTestConfigDir("delta-upgrade-mocked-"); @@ -143,7 +144,7 @@ describe("resolveStableDelta", () => { // All sizes stay 0 — this will produce an empty output mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } @@ -203,7 +204,7 @@ describe("resolveNightlyDelta", () => { // Mock GHCR: token exchange succeeds, nightly manifest has no .gz layer mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "test-token" }), { status: 200, @@ -259,7 +260,7 @@ describe("resolveNightlyDelta", () => { // Mock GHCR: nightly manifest has .gz layer, but no patch tags mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "test-token" }), { status: 200, @@ -386,7 +387,7 @@ describe("attemptDeltaUpgrade", () => { // Return garbage patch data mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } @@ -467,7 +468,7 @@ describe("attemptDeltaUpgrade", () => { ]; mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } diff --git a/packages/cli/test/lib/delta-upgrade.test.ts b/packages/cli/test/lib/delta-upgrade.test.ts index fdb8314fe..ded31bbea 100644 --- a/packages/cli/test/lib/delta-upgrade.test.ts +++ b/packages/cli/test/lib/delta-upgrade.test.ts @@ -40,6 +40,7 @@ import { resolveStableDelta, validateChainStep, } from "../../src/lib/delta-upgrade.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import type { OciManifest } from "../../src/lib/ghcr.js"; import { useTestConfigDir } from "../helpers.js"; @@ -834,10 +835,10 @@ describe("fetchRecentReleases", () => { ]; mockFetch(async (url) => { - expect(String(url)).toContain( + expect(fetchInputUrl(url)).toContain( "api.github.com/repos/getsentry/toolkit/releases", ); - expect(String(url)).toContain("per_page="); + expect(fetchInputUrl(url)).toContain("per_page="); return new Response(JSON.stringify(releases), { status: 200 }); }); @@ -871,7 +872,7 @@ describe("fetchRecentReleases", () => { test("uses the selected legacy GitHub repository", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(String(url)); + urls.push(fetchInputUrl(url)); return new Response(JSON.stringify([]), { status: 200 }); }); @@ -906,7 +907,7 @@ describe("downloadStablePatch", () => { const patchData = new Uint8Array([1, 2, 3, 4, 5]); mockFetch(async (url) => { - expect(String(url)).toBe("https://example.com/patch.bin"); + expect(fetchInputUrl(url)).toBe("https://example.com/patch.bin"); return new Response(patchData.buffer as ArrayBuffer, { status: 200, }); @@ -953,7 +954,7 @@ describe("resolveStableChain", () => { patches: Map, ): void { mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } @@ -1004,7 +1005,7 @@ describe("resolveStableChain", () => { test("keeps stable resolution on the selected legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(String(url)); + urls.push(fetchInputUrl(url)); return new Response("Not Found", { status: 404 }); }); @@ -1151,7 +1152,7 @@ describe("resolveNightlyChain", () => { blobs: Map, ): void { mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "test-token" }), { @@ -1247,7 +1248,7 @@ describe("resolveNightlyChain", () => { test("keeps nightly resolution on the selected legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(String(url)); + urls.push(fetchInputUrl(url)); return new Response(JSON.stringify({ tags: [] }), { status: 200 }); }); @@ -1743,7 +1744,7 @@ describe("resolveNightlyDelta", () => { blobs: Map; }): void { mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); // Token exchange if (urlStr.includes("ghcr.io/token")) { @@ -1950,7 +1951,7 @@ describe("selected source affinity", () => { test("attemptDeltaUpgrade keeps stable requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(String(url)); + urls.push(fetchInputUrl(url)); return new Response("Not Found", { status: 404 }); }); const versionedDelta = await importDeltaUpgradeWithVersion("0.13.0"); @@ -1974,7 +1975,7 @@ describe("selected source affinity", () => { test("attemptDeltaUpgrade keeps nightly requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(String(url)); + urls.push(fetchInputUrl(url)); return new Response("Unauthorized", { status: 401 }); }); const versionedDelta = @@ -1999,7 +2000,7 @@ describe("selected source affinity", () => { test("prefetchStablePatches keeps requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(String(url)); + urls.push(fetchInputUrl(url)); return new Response("Not Found", { status: 404 }); }); const versionedDelta = await importDeltaUpgradeWithVersion("0.13.0"); @@ -2018,7 +2019,7 @@ describe("selected source affinity", () => { test("prefetchNightlyPatches keeps requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(String(url)); + urls.push(fetchInputUrl(url)); return new Response("Unauthorized", { status: 401 }); }); const versionedDelta = diff --git a/packages/cli/test/lib/dif/scan.property.test.ts b/packages/cli/test/lib/dif/scan.property.test.ts index ad3e17b98..c12eb7af2 100644 --- a/packages/cli/test/lib/dif/scan.property.test.ts +++ b/packages/cli/test/lib/dif/scan.property.test.ts @@ -34,7 +34,9 @@ const NIL_DEBUG_ID = "00000000-0000-0000-0000-000000000000"; const hexGroup = (length: number) => tuple( - ...Array.from({ length }, () => constantFrom(..."0123456789abcdef")), + ...Array.from({ length }, () => + constantFrom(..."0123456789abcdef".split("")), + ), ).map((chars) => chars.join("")); /** A non-nil UUID (8-4-4-4-12). May coincide with nil with negligible odds. */ diff --git a/packages/cli/test/lib/formatters/human.property.test.ts b/packages/cli/test/lib/formatters/human.property.test.ts index a8a5b0207..0ad70e5f9 100644 --- a/packages/cli/test/lib/formatters/human.property.test.ts +++ b/packages/cli/test/lib/formatters/human.property.test.ts @@ -62,7 +62,7 @@ const userIdArb = stringMatching(/^[1-9][0-9]{0,8}$/); describe("formatShortId properties", () => { test("output (stripped of ANSI) always equals input uppercased", async () => { - await fcAssert( + fcAssert( property(shortIdArb, (shortId) => { const result = formatShortId(shortId); expect(stripFormatting(result)).toBe(shortId.toUpperCase()); @@ -72,7 +72,7 @@ describe("formatShortId properties", () => { }); test("output length (stripped) always equals input length", async () => { - await fcAssert( + fcAssert( property(shortIdArb, (shortId) => { const result = formatShortId(shortId); expect(stripFormatting(result).length).toBe(shortId.length); @@ -82,7 +82,7 @@ describe("formatShortId properties", () => { }); test("with projectSlug option, output still equals input uppercased", async () => { - await fcAssert( + fcAssert( property(tuple(projectSlugArb, suffixArb), ([project, suffix]) => { const shortId = `${project}-${suffix}`; const result = formatShortId(shortId, { projectSlug: project }); @@ -93,7 +93,7 @@ describe("formatShortId properties", () => { }); test("with projectSlug and projectAlias, output still equals input uppercased", async () => { - await fcAssert( + fcAssert( property(tuple(projectSlugArb, suffixArb), ([project, suffix]) => { const shortId = `${project}-${suffix}`; const alias = project.charAt(0); // Use first char as alias @@ -108,7 +108,7 @@ describe("formatShortId properties", () => { }); test("result is always uppercase", async () => { - await fcAssert( + fcAssert( property(shortIdArb, (shortId) => { const result = stripAnsi(formatShortId(shortId)); expect(result).toBe(result.toUpperCase()); @@ -118,7 +118,7 @@ describe("formatShortId properties", () => { }); test("deterministic: same input produces same output", async () => { - await fcAssert( + fcAssert( property(shortIdArb, (shortId) => { const result1 = formatShortId(shortId); const result2 = formatShortId(shortId); @@ -129,7 +129,7 @@ describe("formatShortId properties", () => { }); test("handles case variations consistently", async () => { - await fcAssert( + fcAssert( property(tuple(projectSlugArb, suffixArb), ([project, suffix]) => { const lowerShortId = `${project.toLowerCase()}-${suffix.toLowerCase()}`; const upperShortId = `${project.toUpperCase()}-${suffix.toUpperCase()}`; @@ -150,7 +150,7 @@ describe("formatShortId properties", () => { describe("formatUserIdentity properties", () => { test("name + email formats as 'name '", async () => { - await fcAssert( + fcAssert( property(tuple(nameArb, emailArb), ([name, email]) => { const result = formatUserIdentity({ id: "1", name, email }); expect(result).toBe(`${name} <${email}>`); @@ -160,7 +160,7 @@ describe("formatUserIdentity properties", () => { }); test("username + email (no name) formats as 'username '", async () => { - await fcAssert( + fcAssert( property(tuple(usernameArb, emailArb), ([username, email]) => { const result = formatUserIdentity({ id: "1", username, email }); expect(result).toBe(`${username} <${email}>`); @@ -170,7 +170,7 @@ describe("formatUserIdentity properties", () => { }); test("name takes precedence over username", async () => { - await fcAssert( + fcAssert( property( tuple(nameArb, usernameArb, emailArb), ([name, username, email]) => { @@ -184,7 +184,7 @@ describe("formatUserIdentity properties", () => { }); test("only name returns just name", async () => { - await fcAssert( + fcAssert( property(nameArb, (name) => { const result = formatUserIdentity({ id: "1", name }); expect(result).toBe(name); @@ -194,7 +194,7 @@ describe("formatUserIdentity properties", () => { }); test("only username returns just username", async () => { - await fcAssert( + fcAssert( property(usernameArb, (username) => { const result = formatUserIdentity({ id: "1", username }); expect(result).toBe(username); @@ -204,7 +204,7 @@ describe("formatUserIdentity properties", () => { }); test("only email returns just email", async () => { - await fcAssert( + fcAssert( property(emailArb, (email) => { const result = formatUserIdentity({ id: "1", email }); expect(result).toBe(email); @@ -214,7 +214,7 @@ describe("formatUserIdentity properties", () => { }); test("only id returns 'user {id}'", async () => { - await fcAssert( + fcAssert( property(userIdArb, (id) => { const result = formatUserIdentity({ id }); expect(result).toBe(`user ${id}`); @@ -224,7 +224,7 @@ describe("formatUserIdentity properties", () => { }); test("only userId returns 'user {userId}'", async () => { - await fcAssert( + fcAssert( property(userIdArb, (userId) => { const result = formatUserIdentity({ userId }); expect(result).toBe(`user ${userId}`); @@ -234,7 +234,7 @@ describe("formatUserIdentity properties", () => { }); test("id takes precedence over userId for fallback", async () => { - await fcAssert( + fcAssert( property(tuple(userIdArb, userIdArb), ([id, userId]) => { const result = formatUserIdentity({ id, userId }); expect(result).toBe(`user ${id}`); diff --git a/packages/cli/test/lib/formatters/human.utils.test.ts b/packages/cli/test/lib/formatters/human.utils.test.ts index 636a80e35..fa80683c3 100644 --- a/packages/cli/test/lib/formatters/human.utils.test.ts +++ b/packages/cli/test/lib/formatters/human.utils.test.ts @@ -164,7 +164,7 @@ describe("maskToken", () => { test("property: masked token never reveals middle characters", async () => { const longTokenArb = stringMatching(/^[a-zA-Z0-9_]{13,50}$/); - await fcAssert( + fcAssert( property(longTokenArb, (token) => { const masked = maskToken(token); // Should show first 8, then ..., then last 4 @@ -179,7 +179,7 @@ describe("maskToken", () => { test("property: short tokens are completely masked", async () => { const shortTokenArb = stringMatching(/^[a-zA-Z0-9]{1,12}$/); - await fcAssert( + fcAssert( property(shortTokenArb, (token) => { expect(maskToken(token)).toBe("****"); }), @@ -261,7 +261,7 @@ describe("formatDuration", () => { }); test("property: duration formatting always contains a time unit", async () => { - await fcAssert( + fcAssert( property(nat({ max: 86_400 * 60 }), (seconds) => { const result = formatDuration(seconds); expect(result).toMatch(/minute|hour|day|week/); @@ -297,7 +297,7 @@ describe("formatExpiration", () => { }); test("property: expired times always return 'Expired'", async () => { - await fcAssert( + fcAssert( property(integer({ min: 1, max: 1_000_000 }), (msAgo) => { const pastTime = Date.now() - msAgo; expect(formatExpiration(pastTime)).toBe("Expired"); @@ -307,7 +307,7 @@ describe("formatExpiration", () => { }); test("property: future times always include 'remaining'", async () => { - await fcAssert( + fcAssert( property(integer({ min: 60_000, max: 86_400_000 }), (msAhead) => { const futureTime = Date.now() + msAhead; const result = formatExpiration(futureTime); diff --git a/packages/cli/test/lib/formatters/log.property.test.ts b/packages/cli/test/lib/formatters/log.property.test.ts index f684d525f..2ba4fa78f 100644 --- a/packages/cli/test/lib/formatters/log.property.test.ts +++ b/packages/cli/test/lib/formatters/log.property.test.ts @@ -82,7 +82,7 @@ function stripAnsi(str: string): string { describe("formatLogDetails properties", () => { test("always returns a non-empty string", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -97,7 +97,7 @@ describe("formatLogDetails properties", () => { }); test("always contains the log ID", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -111,7 +111,7 @@ describe("formatLogDetails properties", () => { }); test("always contains timestamp info", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -125,7 +125,7 @@ describe("formatLogDetails properties", () => { }); test("always contains severity info", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -139,7 +139,7 @@ describe("formatLogDetails properties", () => { }); test("trace URL only appears when trace ID is present", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -158,7 +158,7 @@ describe("formatLogDetails properties", () => { }); test("SDK section only appears when sdk.name is present", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -175,7 +175,7 @@ describe("formatLogDetails properties", () => { }); test("Source Location section only appears when code fields are present", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -194,7 +194,7 @@ describe("formatLogDetails properties", () => { }); test("formatting is deterministic: same input always produces same output", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, @@ -209,7 +209,7 @@ describe("formatLogDetails properties", () => { }); test("output is a string (not array)", async () => { - await fcAssert( + fcAssert( property( detailedLogArb, orgSlugArb, diff --git a/packages/cli/test/lib/ghcr.test.ts b/packages/cli/test/lib/ghcr.test.ts index 6d8867a14..64a6abc14 100644 --- a/packages/cli/test/lib/ghcr.test.ts +++ b/packages/cli/test/lib/ghcr.test.ts @@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { UPGRADE_SOURCES } from "../../src/lib/binary.js"; import { UpgradeError } from "../../src/lib/errors.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import { downloadLayerBlob, downloadNightlyBlob, @@ -80,7 +81,7 @@ afterEach(() => { describe("getAnonymousToken", () => { test("returns token from successful response", async () => { mockFetch(async (url) => { - expect(String(url)).toContain( + expect(fetchInputUrl(url)).toContain( `https://ghcr.io/token?scope=repository:${GHCR_REPO}:pull`, ); return new Response(JSON.stringify({ token: "test-token-abc" }), { @@ -95,7 +96,9 @@ describe("getAnonymousToken", () => { test("uses the selected source's GHCR repository", async () => { mockFetch(async (url) => { - expect(String(url)).toContain("scope=repository:getsentry/toolkit:pull"); + expect(fetchInputUrl(url)).toContain( + "scope=repository:getsentry/toolkit:pull", + ); return new Response(JSON.stringify({ token: "toolkit-token" }), { status: 200, }); @@ -217,7 +220,9 @@ describe("fetchNightlyManifest", () => { let capturedHeaders: Record = {}; mockFetch(async (url, init) => { - expect(String(url)).toContain(`/v2/${GHCR_REPO}/manifests/${GHCR_TAG}`); + expect(fetchInputUrl(url)).toContain( + `/v2/${GHCR_REPO}/manifests/${GHCR_TAG}`, + ); capturedHeaders = Object.fromEntries( new Headers(init?.headers as HeadersInit).entries(), ); @@ -240,7 +245,9 @@ describe("fetchNightlyManifest", () => { test("uses the selected source's GHCR repository", async () => { const manifest = makeManifest(); mockFetch(async (url) => { - expect(String(url)).toContain("/v2/getsentry/toolkit/manifests/nightly"); + expect(fetchInputUrl(url)).toContain( + "/v2/getsentry/toolkit/manifests/nightly", + ); return new Response(JSON.stringify(manifest), { status: 200 }); }); @@ -569,7 +576,9 @@ describe("fetchManifest", () => { const manifest = makeManifest(); mockFetch(async (url) => { - expect(String(url)).toContain(`/v2/${GHCR_REPO}/manifests/patch-0.13.0`); + expect(fetchInputUrl(url)).toContain( + `/v2/${GHCR_REPO}/manifests/patch-0.13.0`, + ); return new Response(JSON.stringify(manifest), { status: 200, headers: { @@ -628,7 +637,7 @@ describe("listTags", () => { }); test("returns all tags when no prefix filter", async () => { mockFetch(async (url) => { - expect(String(url)).toContain(`/v2/${GHCR_REPO}/tags/list`); + expect(fetchInputUrl(url)).toContain(`/v2/${GHCR_REPO}/tags/list`); return new Response( JSON.stringify({ tags: ["nightly", "patch-0.13.0", "patch-0.14.0"] }), { status: 200 }, @@ -750,7 +759,9 @@ describe("downloadLayerBlob", () => { mockFetch(async (url) => { requestCount += 1; if (requestCount === 1) { - expect(String(url)).toContain(`/v2/${GHCR_REPO}/blobs/sha256:abc123`); + expect(fetchInputUrl(url)).toContain( + `/v2/${GHCR_REPO}/blobs/sha256:abc123`, + ); return Response.redirect("https://blob.storage.azure.com/file", 307); } return new Response(content, { status: 200 }); diff --git a/packages/cli/test/lib/help.test.ts b/packages/cli/test/lib/help.test.ts index 35c56dd5e..760c6dbb4 100644 --- a/packages/cli/test/lib/help.test.ts +++ b/packages/cli/test/lib/help.test.ts @@ -23,7 +23,7 @@ function maxLineWidth(banner: string): number { 0, ...stripAnsi(banner) .split("\n") - .map((line) => [...line].length), + .map((line) => Array.from(line).length), ); } diff --git a/packages/cli/test/lib/hex-id-recovery.adapters.test.ts b/packages/cli/test/lib/hex-id-recovery.adapters.test.ts index ad5742815..694a58c9f 100644 --- a/packages/cli/test/lib/hex-id-recovery.adapters.test.ts +++ b/packages/cli/test/lib/hex-id-recovery.adapters.test.ts @@ -25,7 +25,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", "https://sentry.io"); }); diff --git a/packages/cli/test/lib/init/ui/logging-ui.test.ts b/packages/cli/test/lib/init/ui/logging-ui.test.ts index 158948e1a..f4318ca2d 100644 --- a/packages/cli/test/lib/init/ui/logging-ui.test.ts +++ b/packages/cli/test/lib/init/ui/logging-ui.test.ts @@ -177,7 +177,7 @@ describe("LoggingUI spinner", () => { describe("LoggingUI prompts throw", () => { test("select rejects with LoggingUIPromptError", async () => { const { ui } = createUI(); - expect( + await expect( ui.select({ message: "Pick one", options: [{ value: "a", label: "A" }], @@ -187,7 +187,7 @@ describe("LoggingUI prompts throw", () => { test("multiselect rejects with LoggingUIPromptError", async () => { const { ui } = createUI(); - expect( + await expect( ui.multiselect({ message: "Pick many", options: [{ value: "a", label: "A" }], @@ -197,7 +197,7 @@ describe("LoggingUI prompts throw", () => { test("confirm rejects with LoggingUIPromptError", async () => { const { ui } = createUI(); - expect(ui.confirm({ message: "Sure?" })).rejects.toBeInstanceOf( + await expect(ui.confirm({ message: "Sure?" })).rejects.toBeInstanceOf( LoggingUIPromptError, ); }); diff --git a/packages/cli/test/lib/init/verify-setup-windows.mocked.test.ts b/packages/cli/test/lib/init/verify-setup-windows.mocked.test.ts index ecc56f3b1..7c333270c 100644 --- a/packages/cli/test/lib/init/verify-setup-windows.mocked.test.ts +++ b/packages/cli/test/lib/init/verify-setup-windows.mocked.test.ts @@ -29,7 +29,9 @@ vi.mock("node:child_process", async (importOriginal) => { spawn: vi.fn( (_command: string, _args: string[], options: { detached?: boolean }) => { mocks.spawnCalls.push({ options }); - const child = new EventEmitter() as EventEmitter & { + const child = new EventEmitter() as InstanceType< + typeof EventEmitter + > & { exitCode: number | null; kill: (signal: NodeJS.Signals) => boolean; pid: number; diff --git a/packages/cli/test/lib/input-validation.property.test.ts b/packages/cli/test/lib/input-validation.property.test.ts index 8e5bb4e28..99375cb5b 100644 --- a/packages/cli/test/lib/input-validation.property.test.ts +++ b/packages/cli/test/lib/input-validation.property.test.ts @@ -73,7 +73,7 @@ const controlCharArb = constantFrom( describe("rejectControlChars properties", () => { test("valid slugs never contain control characters and always pass", async () => { - await fcAssert( + fcAssert( property(validSlugArb, (input) => { // Should not throw rejectControlChars(input, "test"); @@ -83,7 +83,7 @@ describe("rejectControlChars properties", () => { }); test("any string with an embedded control character always throws", async () => { - await fcAssert( + fcAssert( property( tuple(validSlugArb, controlCharArb, validSlugArb), ([prefix, ctrl, suffix]) => { @@ -96,7 +96,7 @@ describe("rejectControlChars properties", () => { }); test("error message includes the label", async () => { - await fcAssert( + fcAssert( property(controlCharArb, (ctrl) => { try { rejectControlChars(`abc${ctrl}def`, "organization slug"); @@ -119,7 +119,7 @@ describe("rejectControlChars properties", () => { test("printable ASCII strings always pass", async () => { // Generate strings of printable ASCII (0x20-0x7E) const printableArb = stringMatching(/^[\x20-\x7e]{1,50}$/); - await fcAssert( + fcAssert( property(printableArb, (input) => { // Should not throw — all printable ASCII is valid rejectControlChars(input, "test"); @@ -131,7 +131,7 @@ describe("rejectControlChars properties", () => { describe("rejectPreEncoded properties", () => { test("valid slugs without percent signs always pass", async () => { - await fcAssert( + fcAssert( property(validSlugArb, (input) => { rejectPreEncoded(input, "test"); }), @@ -140,7 +140,7 @@ describe("rejectPreEncoded properties", () => { }); test("any string with %XX hex pattern always throws", async () => { - await fcAssert( + fcAssert( property(tuple(validSlugArb, preEncodedArb), ([prefix, encoded]) => { const input = `${prefix}${encoded}`; expect(() => rejectPreEncoded(input, "test")).toThrow( @@ -160,7 +160,7 @@ describe("rejectPreEncoded properties", () => { describe("validateResourceId properties", () => { test("valid slugs always pass", async () => { - await fcAssert( + fcAssert( property(validSlugArb, (input) => { validateResourceId(input, "test"); }), @@ -169,7 +169,7 @@ describe("validateResourceId properties", () => { }); test("valid issue suffixes always pass", async () => { - await fcAssert( + fcAssert( property(validSuffixArb, (input) => { validateResourceId(input, "test"); }), @@ -178,7 +178,7 @@ describe("validateResourceId properties", () => { }); test("slug with injection character always throws", async () => { - await fcAssert( + fcAssert( property(tuple(validSlugArb, injectionCharArb), ([slug, injection]) => { const input = `${slug}${injection}`; expect(() => validateResourceId(input, "test")).toThrow(/Invalid/); @@ -188,7 +188,7 @@ describe("validateResourceId properties", () => { }); test("slug with control character always throws", async () => { - await fcAssert( + fcAssert( property(tuple(validSlugArb, controlCharArb), ([slug, ctrl]) => { const input = `${slug}${ctrl}`; expect(() => validateResourceId(input, "test")).toThrow(/Invalid/); @@ -227,7 +227,7 @@ describe("validateResourceId properties", () => { describe("validateEndpoint properties", () => { test("valid endpoints always pass", async () => { - await fcAssert( + fcAssert( property(validEndpointArb, (input) => { validateEndpoint(input); }), @@ -244,7 +244,7 @@ describe("validateEndpoint properties", () => { constantFrom("..", "../admin", "../../admin/settings/"), ); - await fcAssert( + fcAssert( property(traversalArb, (input) => { expect(() => validateEndpoint(input)).toThrow(/path traversal/); }), @@ -253,7 +253,7 @@ describe("validateEndpoint properties", () => { }); test("endpoints with control characters always throw", async () => { - await fcAssert( + fcAssert( property(tuple(validEndpointArb, controlCharArb), ([endpoint, ctrl]) => { const input = `${endpoint}${ctrl}`; expect(() => validateEndpoint(input)).toThrow(/Invalid/); @@ -276,7 +276,7 @@ describe("validateEndpoint properties", () => { describe("cross-validator consistency", () => { test("validateResourceId catches everything rejectControlChars catches", async () => { - await fcAssert( + fcAssert( property(tuple(validSlugArb, controlCharArb), ([slug, ctrl]) => { const input = `${slug}${ctrl}`; // Both should throw diff --git a/packages/cli/test/lib/region.test.ts b/packages/cli/test/lib/region.test.ts index 1ba365192..8332d75d0 100644 --- a/packages/cli/test/lib/region.test.ts +++ b/packages/cli/test/lib/region.test.ts @@ -22,7 +22,7 @@ beforeEach(async () => { delete process.env.SENTRY_HOST; delete process.env.SENTRY_URL; // Set up auth token for API tests - await setAuthToken("test-token"); + setAuthToken("test-token"); }); afterEach(() => { @@ -296,7 +296,7 @@ describe("resolveOrgRegion", () => { // SENTRY_URL was unset at store time). Re-scope the stored token to // the self-hosted host so the fetch-layer guard admits the request. process.env.SENTRY_URL = "https://sentry.mycompany.com"; - await setAuthToken("test-token", undefined, undefined, { + setAuthToken("test-token", undefined, undefined, { host: "https://sentry.mycompany.com", }); diff --git a/packages/cli/test/lib/release-notes.test.ts b/packages/cli/test/lib/release-notes.test.ts index de6c23003..d4f1d2315 100644 --- a/packages/cli/test/lib/release-notes.test.ts +++ b/packages/cli/test/lib/release-notes.test.ts @@ -15,6 +15,7 @@ import { fetchRecentReleases, type GitHubRelease, } from "../../src/lib/delta-upgrade.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import { buildChangelogSummary, type ChangeCategory, @@ -316,7 +317,7 @@ describe("fetchChangelog source affinity", () => { test("fetches stable releases only from the explicitly selected Toolkit source", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(String(input)); + requestedUrls.push(fetchInputUrl(input)); return new Response( JSON.stringify([ makeRelease( @@ -403,7 +404,7 @@ describe("fetchChangelog source affinity", () => { test("rejects raw prefetched Toolkit releases without fetching", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(String(input)); + requestedUrls.push(fetchInputUrl(input)); return new Response("Unexpected", { status: 500 }); }); @@ -457,7 +458,7 @@ describe("fetchChangelog source affinity", () => { test("fetches stable releases only from the explicitly selected legacy source", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(String(input)); + requestedUrls.push(fetchInputUrl(input)); return new Response( JSON.stringify([ makeRelease( @@ -485,7 +486,7 @@ describe("fetchChangelog source affinity", () => { test("fetches nightly commits only from the explicitly selected Toolkit source", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(String(input)); + requestedUrls.push(fetchInputUrl(input)); return new Response( JSON.stringify([ { commit: { message: "fix: keep nightly stages affine" } }, diff --git a/packages/cli/test/lib/resolve-effective-org.test.ts b/packages/cli/test/lib/resolve-effective-org.test.ts index 152bb364f..9aa5976d3 100644 --- a/packages/cli/test/lib/resolve-effective-org.test.ts +++ b/packages/cli/test/lib/resolve-effective-org.test.ts @@ -165,7 +165,7 @@ describe("resolveEffectiveOrg with API refresh", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); }); afterEach(() => { diff --git a/packages/cli/test/lib/resolve-target.test.ts b/packages/cli/test/lib/resolve-target.test.ts index 20903f9a3..4f5e67337 100644 --- a/packages/cli/test/lib/resolve-target.test.ts +++ b/packages/cli/test/lib/resolve-target.test.ts @@ -377,7 +377,7 @@ describe("fetchProjectId", () => { }); test("returns numeric project ID on success", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); globalThis.fetch = mockFetch(async (input, init) => { const req = new Request(input, init); @@ -392,7 +392,7 @@ describe("fetchProjectId", () => { }); test("throws ResolutionError on 404", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); globalThis.fetch = mockFetch( async () => @@ -407,7 +407,7 @@ describe("fetchProjectId", () => { }); test("includes similar project suggestions on 404 when projects exist", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); // Mock: getProject returns 404, but listProjects returns available projects. @@ -448,7 +448,7 @@ describe("fetchProjectId", () => { }); test("includes numeric project ID hint on 404 for all-digit slug", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); globalThis.fetch = mockFetch( async () => @@ -470,7 +470,7 @@ describe("fetchProjectId", () => { }); test("includes project list suggestion even when listProjects fails", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); // Mock: all requests return 404 (both getProject and listProjects fail) @@ -512,7 +512,7 @@ describe("fetchProjectId", () => { }); test("returns undefined on transient server error", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); globalThis.fetch = mockFetch( async () => @@ -526,7 +526,7 @@ describe("fetchProjectId", () => { }); test("returns cached project ID without hitting the API", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); clearProjectCache(); // Seed the cache via cacheProjectsForOrg (the `list:` key path) as @@ -547,7 +547,7 @@ describe("fetchProjectId", () => { }); test("writes the response to the project cache on a cache miss", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); clearProjectCache(); globalThis.fetch = mockFetch(async (input, init) => { @@ -578,7 +578,7 @@ describe("fetchProjectId", () => { }); test("falls through to API when the cache has no projectId (legacy rows)", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); clearProjectCache(); // Seed a cache entry WITHOUT a projectId (mirrors pre-schema-v7 rows). @@ -748,7 +748,7 @@ describe("resolveLogProjectId", () => { }); test("returns an all-digit slug as a numeric ID without hitting the API", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); let apiCalled = false; globalThis.fetch = mockFetch(async () => { @@ -762,7 +762,7 @@ describe("resolveLogProjectId", () => { }); test("resolves a slug to its numeric project ID", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); clearProjectCache(); globalThis.fetch = mockFetch(async (input, init) => { @@ -778,7 +778,7 @@ describe("resolveLogProjectId", () => { }); test("returns undefined on a transient server error (falls back to slug scoping)", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); clearProjectCache(); globalThis.fetch = mockFetch( @@ -809,7 +809,7 @@ describe("resolveLogProjectId", () => { }); test("re-throws ResolutionError on a genuine 404", async () => { - await setAuthToken("test-token"); + setAuthToken("test-token"); setOrgRegion("test-org", DEFAULT_SENTRY_URL); clearProjectCache(); globalThis.fetch = mockFetch( @@ -837,7 +837,7 @@ describe("tryFuzzyProjectRecovery", () => { beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); }); afterEach(() => { diff --git a/packages/cli/test/lib/sentry-client.invalidation.test.ts b/packages/cli/test/lib/sentry-client.invalidation.test.ts index 80da1f885..eaacf057e 100644 --- a/packages/cli/test/lib/sentry-client.invalidation.test.ts +++ b/packages/cli/test/lib/sentry-client.invalidation.test.ts @@ -10,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { setAuthToken } from "../../src/lib/db/auth.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import { getCachedResponse, storeCachedResponse, @@ -82,7 +83,7 @@ describe("HTTP-layer auto-invalidation", () => { installMockFetch(async (input, init) => { expect(init?.method).toBe("PUT"); - expect(String(input)).toBe(DETAIL_URL); + expect(fetchInputUrl(input)).toBe(DETAIL_URL); return makeResponse({ id: "12345", status: "resolved" }); }); const response = await runAuthenticatedFetch(DETAIL_URL, "PUT"); diff --git a/packages/cli/test/lib/sentry-client.response-body.test.ts b/packages/cli/test/lib/sentry-client.response-body.test.ts index 1773fc0d3..703ec0b34 100644 --- a/packages/cli/test/lib/sentry-client.response-body.test.ts +++ b/packages/cli/test/lib/sentry-client.response-body.test.ts @@ -31,7 +31,7 @@ let originalFetch: typeof globalThis.fetch; beforeEach(async () => { originalFetch = globalThis.fetch; - await setAuthToken("test-token"); + setAuthToken("test-token"); resetAuthenticatedFetch(); }); diff --git a/packages/cli/test/lib/sentry-client.test.ts b/packages/cli/test/lib/sentry-client.test.ts index 029c834a4..89df80c7d 100644 --- a/packages/cli/test/lib/sentry-client.test.ts +++ b/packages/cli/test/lib/sentry-client.test.ts @@ -22,7 +22,7 @@ const REGION_URL = "https://us.sentry.io"; beforeEach(async () => { originalFetch = globalThis.fetch; // Non-expiring token — refreshToken() becomes a no-op. - await setAuthToken("test-token"); + setAuthToken("test-token"); resetAuthenticatedFetch(); }); diff --git a/packages/cli/test/lib/sentry-url-parser.property.test.ts b/packages/cli/test/lib/sentry-url-parser.property.test.ts index afeb8d02f..64df25fd3 100644 --- a/packages/cli/test/lib/sentry-url-parser.property.test.ts +++ b/packages/cli/test/lib/sentry-url-parser.property.test.ts @@ -51,7 +51,7 @@ const eventIdArb = stringMatching(/^[0-9a-f]{32}$/); describe("parseSentryUrl round-trip properties", () => { test("buildOrgUrl → parseSentryUrl extracts org", async () => { - await fcAssert( + fcAssert( property(orgSlugArb, (org) => { const url = buildOrgUrl(org); const parsed = parseSentryUrl(url); @@ -67,7 +67,7 @@ describe("parseSentryUrl round-trip properties", () => { }); test("buildProjectUrl → parseSentryUrl extracts org and project", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, projectSlugArb), ([org, project]) => { const url = buildProjectUrl(org, project); const parsed = parseSentryUrl(url); @@ -82,7 +82,7 @@ describe("parseSentryUrl round-trip properties", () => { }); test("buildTraceUrl → parseSentryUrl extracts org and traceId", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, traceIdArb), ([org, traceId]) => { const url = buildTraceUrl(org, traceId); const parsed = parseSentryUrl(url); @@ -97,7 +97,7 @@ describe("parseSentryUrl round-trip properties", () => { }); test("buildReplayUrl → parseSentryUrl extracts org and replayId", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, replayIdArb), ([org, replayId]) => { const url = buildReplayUrl(org, replayId); const parsed = parseSentryUrl(url); @@ -112,7 +112,7 @@ describe("parseSentryUrl round-trip properties", () => { }); test("issue URL round-trip: org and numeric issueId extracted", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, numericIdArb), ([org, issueId]) => { // Construct the URL pattern that Sentry uses for issues const url = `https://sentry.io/organizations/${org}/issues/${issueId}/`; @@ -128,7 +128,7 @@ describe("parseSentryUrl round-trip properties", () => { }); test("event URL round-trip: org, issueId, and eventId extracted", async () => { - await fcAssert( + fcAssert( property( tuple(orgSlugArb, numericIdArb, eventIdArb), ([org, issueId, eventId]) => { @@ -147,7 +147,7 @@ describe("parseSentryUrl round-trip properties", () => { test("non-URL strings always return null", async () => { // Org slugs alone should never parse as URLs - await fcAssert( + fcAssert( property(orgSlugArb, (org) => { expect(parseSentryUrl(org)).toBeNull(); }), @@ -156,7 +156,7 @@ describe("parseSentryUrl round-trip properties", () => { }); test("baseUrl always contains scheme and host", async () => { - await fcAssert( + fcAssert( property(tuple(orgSlugArb, numericIdArb), ([org, issueId]) => { const url = `https://sentry.io/organizations/${org}/issues/${issueId}/`; const parsed = parseSentryUrl(url); diff --git a/packages/cli/test/lib/sentry-urls.property.test.ts b/packages/cli/test/lib/sentry-urls.property.test.ts index c3ea30d0c..f72cbde81 100644 --- a/packages/cli/test/lib/sentry-urls.property.test.ts +++ b/packages/cli/test/lib/sentry-urls.property.test.ts @@ -122,7 +122,7 @@ describe("isSentrySaasUrl properties", () => { }); test("*.sentry.io subdomains always return true", async () => { - await fcAssert( + fcAssert( property(sentryRegionArb, (region) => { const url = `https://${region}.sentry.io`; expect(isSentrySaasUrl(url)).toBe(true); @@ -132,7 +132,7 @@ describe("isSentrySaasUrl properties", () => { }); test("arbitrary subdomains of sentry.io return true", async () => { - await fcAssert( + fcAssert( property(subdomainPartArb, (subdomain) => { const url = `https://${subdomain}.sentry.io`; expect(isSentrySaasUrl(url)).toBe(true); @@ -142,7 +142,7 @@ describe("isSentrySaasUrl properties", () => { }); test("non-sentry.io domains always return false", async () => { - await fcAssert( + fcAssert( property(nonSentryDomainArb, (domain) => { const url = `https://${domain}`; expect(isSentrySaasUrl(url)).toBe(false); @@ -187,7 +187,7 @@ describe("isSentrySaasUrl properties", () => { }); test("deterministic: same input always produces same output", async () => { - await fcAssert( + fcAssert( property( oneof( constantFrom("https://sentry.io", "https://us.sentry.io"), @@ -206,7 +206,7 @@ describe("isSentrySaasUrl properties", () => { describe("buildOrgUrl properties", () => { test("output always starts with org base URL", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildOrgUrl(orgSlug); expect(result.startsWith(getOrgBaseUrl(orgSlug))).toBe(true); @@ -216,7 +216,7 @@ describe("buildOrgUrl properties", () => { }); test("output always contains the org slug", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildOrgUrl(orgSlug); expect(result).toContain(orgSlug); @@ -226,7 +226,7 @@ describe("buildOrgUrl properties", () => { }); test("output follows expected pattern", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildOrgUrl(orgSlug); expect(result).toBe(`${getOrgBaseUrl(orgSlug)}/`); @@ -236,7 +236,7 @@ describe("buildOrgUrl properties", () => { }); test("output is a valid URL", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildOrgUrl(orgSlug); expect(() => new URL(result)).not.toThrow(); @@ -248,7 +248,7 @@ describe("buildOrgUrl properties", () => { describe("buildProjectUrl properties", () => { test("output contains both org and project slugs", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb), ([orgSlug, projectSlug]) => { const result = buildProjectUrl(orgSlug, projectSlug); expect(result).toContain(orgSlug); @@ -259,7 +259,7 @@ describe("buildProjectUrl properties", () => { }); test("output follows expected pattern", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb), ([orgSlug, projectSlug]) => { const result = buildProjectUrl(orgSlug, projectSlug); expect(result).toBe( @@ -271,7 +271,7 @@ describe("buildProjectUrl properties", () => { }); test("output is a valid URL", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb), ([orgSlug, projectSlug]) => { const result = buildProjectUrl(orgSlug, projectSlug); expect(() => new URL(result)).not.toThrow(); @@ -283,7 +283,7 @@ describe("buildProjectUrl properties", () => { describe("buildEventSearchUrl properties", () => { test("output contains event ID in query string", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, eventIdArb), ([orgSlug, eventId]) => { const result = buildEventSearchUrl(orgSlug, eventId); expect(result).toContain(`event.id:${eventId}`); @@ -293,7 +293,7 @@ describe("buildEventSearchUrl properties", () => { }); test("output is a valid URL with query parameter", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, eventIdArb), ([orgSlug, eventId]) => { const result = buildEventSearchUrl(orgSlug, eventId); const url = new URL(result); @@ -306,7 +306,7 @@ describe("buildEventSearchUrl properties", () => { describe("buildProjectIssuesUrl properties", () => { test("with a project ID, output filters the Issues stream by project", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, projectIdArb), ([orgSlug, projectId]) => { const result = buildProjectIssuesUrl(orgSlug, projectId); expect(result).toBe( @@ -319,7 +319,7 @@ describe("buildProjectIssuesUrl properties", () => { }); test("without a project ID, output is the org-wide Issues stream", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildProjectIssuesUrl(orgSlug); expect(result).toBe(`${getOrgBaseUrl(orgSlug)}/issues/`); @@ -329,7 +329,7 @@ describe("buildProjectIssuesUrl properties", () => { }); test("output is a valid URL that points at the Issues stream", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, projectIdArb), ([orgSlug, projectId]) => { const result = buildProjectIssuesUrl(orgSlug, projectId); expect(() => new URL(result)).not.toThrow(); @@ -342,7 +342,7 @@ describe("buildProjectIssuesUrl properties", () => { describe("parseOrgProjectFromSettingsUrl properties", () => { test("round-trips org and project through a settings URL (SaaS)", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb), ([orgSlug, projectSlug]) => { const url = buildProjectUrl(orgSlug, projectSlug); expect(parseOrgProjectFromSettingsUrl(url)).toEqual({ @@ -356,7 +356,7 @@ describe("parseOrgProjectFromSettingsUrl properties", () => { test("round-trips org and project through a settings URL (self-hosted)", async () => { process.env.SENTRY_URL = "https://sentry.example.com"; - await fcAssert( + fcAssert( property(tuple(slugArb, slugArb), ([orgSlug, projectSlug]) => { const url = buildProjectUrl(orgSlug, projectSlug); expect(parseOrgProjectFromSettingsUrl(url)).toEqual({ @@ -375,7 +375,7 @@ describe("parseOrgProjectFromSettingsUrl properties", () => { describe("buildOrgSettingsUrl properties", () => { test("without hash, output ends with trailing slash", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildOrgSettingsUrl(orgSlug); expect(result.endsWith("/")).toBe(true); @@ -385,7 +385,7 @@ describe("buildOrgSettingsUrl properties", () => { }); test("with hash, output contains hash fragment", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, hashArb), ([orgSlug, hash]) => { const result = buildOrgSettingsUrl(orgSlug, hash); expect(result).toContain(`#${hash}`); @@ -395,7 +395,7 @@ describe("buildOrgSettingsUrl properties", () => { }); test("hash is appended at the end", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, hashArb), ([orgSlug, hash]) => { const result = buildOrgSettingsUrl(orgSlug, hash); expect(result.endsWith(`#${hash}`)).toBe(true); @@ -407,7 +407,7 @@ describe("buildOrgSettingsUrl properties", () => { describe("buildSeerSettingsUrl properties", () => { test("output contains /seer/ path", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildSeerSettingsUrl(orgSlug); expect(result).toContain("/seer/"); @@ -417,7 +417,7 @@ describe("buildSeerSettingsUrl properties", () => { }); test("output is under settings path", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildSeerSettingsUrl(orgSlug); expect(result).toContain("/settings/seer/"); @@ -429,7 +429,7 @@ describe("buildSeerSettingsUrl properties", () => { describe("buildBillingUrl properties", () => { test("without product, output has no query string", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildBillingUrl(orgSlug); expect(result.includes("?")).toBe(false); @@ -439,7 +439,7 @@ describe("buildBillingUrl properties", () => { }); test("with product, output has product query parameter", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, productArb), ([orgSlug, product]) => { const result = buildBillingUrl(orgSlug, product); expect(result).toContain(`?product=${product}`); @@ -449,7 +449,7 @@ describe("buildBillingUrl properties", () => { }); test("output contains /billing/overview/ path", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildBillingUrl(orgSlug); expect(result).toContain("/billing/overview/"); @@ -461,7 +461,7 @@ describe("buildBillingUrl properties", () => { describe("buildLogsUrl properties", () => { test("without logId, output has no query string", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildLogsUrl(orgSlug); expect(result.includes("?")).toBe(false); @@ -471,7 +471,7 @@ describe("buildLogsUrl properties", () => { }); test("with logId, output contains query parameter with log ID", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, logIdArb), ([orgSlug, logId]) => { const result = buildLogsUrl(orgSlug, logId); expect(result).toContain(`?query=sentry.item_id:${logId}`); @@ -481,7 +481,7 @@ describe("buildLogsUrl properties", () => { }); test("output contains /explore/logs/ path", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildLogsUrl(orgSlug); expect(result).toContain("/explore/logs/"); @@ -491,7 +491,7 @@ describe("buildLogsUrl properties", () => { }); test("output is a valid URL", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, logIdArb), ([orgSlug, logId]) => { expect(() => new URL(buildLogsUrl(orgSlug))).not.toThrow(); expect(() => new URL(buildLogsUrl(orgSlug, logId))).not.toThrow(); @@ -503,7 +503,7 @@ describe("buildLogsUrl properties", () => { describe("buildTraceUrl properties", () => { test("output contains /explore/traces/trace/ path with trace ID", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, traceIdArb), ([orgSlug, traceId]) => { const result = buildTraceUrl(orgSlug, traceId); expect(result).toContain(`/explore/traces/trace/${traceId}/`); @@ -513,7 +513,7 @@ describe("buildTraceUrl properties", () => { }); test("output contains the org slug", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, traceIdArb), ([orgSlug, traceId]) => { const result = buildTraceUrl(orgSlug, traceId); expect(result).toContain(orgSlug); @@ -523,7 +523,7 @@ describe("buildTraceUrl properties", () => { }); test("output is a valid URL", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, traceIdArb), ([orgSlug, traceId]) => { const result = buildTraceUrl(orgSlug, traceId); expect(() => new URL(result)).not.toThrow(); @@ -533,7 +533,7 @@ describe("buildTraceUrl properties", () => { }); test("output follows expected pattern", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, traceIdArb), ([orgSlug, traceId]) => { const result = buildTraceUrl(orgSlug, traceId); expect(result).toBe( @@ -547,7 +547,7 @@ describe("buildTraceUrl properties", () => { describe("buildDashboardsListUrl properties", () => { test("output contains /dashboards/ path", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildDashboardsListUrl(orgSlug); expect(result).toContain("/dashboards/"); @@ -557,7 +557,7 @@ describe("buildDashboardsListUrl properties", () => { }); test("output contains the org slug", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildDashboardsListUrl(orgSlug); expect(result).toContain(orgSlug); @@ -567,7 +567,7 @@ describe("buildDashboardsListUrl properties", () => { }); test("output is a valid URL", async () => { - await fcAssert( + fcAssert( property(slugArb, (orgSlug) => { const result = buildDashboardsListUrl(orgSlug); expect(() => new URL(result)).not.toThrow(); @@ -579,7 +579,7 @@ describe("buildDashboardsListUrl properties", () => { describe("buildDashboardUrl properties", () => { test("output contains /dashboard/{id}/ path", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, dashboardIdArb), ([orgSlug, dashboardId]) => { const result = buildDashboardUrl(orgSlug, dashboardId); expect(result).toContain(`/dashboard/${dashboardId}/`); @@ -589,7 +589,7 @@ describe("buildDashboardUrl properties", () => { }); test("output contains org slug and dashboard ID", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, dashboardIdArb), ([orgSlug, dashboardId]) => { const result = buildDashboardUrl(orgSlug, dashboardId); expect(result).toContain(orgSlug); @@ -600,7 +600,7 @@ describe("buildDashboardUrl properties", () => { }); test("output is a valid URL", async () => { - await fcAssert( + fcAssert( property(tuple(slugArb, dashboardIdArb), ([orgSlug, dashboardId]) => { const result = buildDashboardUrl(orgSlug, dashboardId); expect(() => new URL(result)).not.toThrow(); @@ -701,7 +701,7 @@ describe("self-hosted URLs", () => { }); test("no URL builder prepends org as subdomain", async () => { - await fcAssert( + fcAssert( property( tuple(slugArb, slugArb, eventIdArb), ([orgSlug, projectSlug, eventId]) => { @@ -731,7 +731,7 @@ describe("self-hosted URLs", () => { describe("URL building cross-function properties", () => { test("all URL builders produce valid URLs", async () => { - await fcAssert( + fcAssert( property( tuple(slugArb, slugArb, eventIdArb, hashArb, productArb), ([orgSlug, projectSlug, eventId, hash, product]) => { @@ -758,7 +758,7 @@ describe("URL building cross-function properties", () => { }); test("all URL builders are deterministic", async () => { - await fcAssert( + fcAssert( property( tuple(slugArb, slugArb, eventIdArb, hashArb, productArb), ([orgSlug, projectSlug, eventId, hash, product]) => { diff --git a/packages/cli/test/lib/telemetry.test.ts b/packages/cli/test/lib/telemetry.test.ts index e4eac3a16..e0873dc39 100644 --- a/packages/cli/test/lib/telemetry.test.ts +++ b/packages/cli/test/lib/telemetry.test.ts @@ -1164,7 +1164,7 @@ describe("createTracedDatabase", () => { // toString() requires proper 'this' binding to access native private fields. // bun:sqlite returns the SQL string; Node.js sqlite returns "[object Object]". - const sqlString = stmt.toString(); + const sqlString = (stmt as { toString(): string }).toString(); if (sqlString !== "[object Object]") { expect(sqlString).toContain("SELECT * FROM test"); } diff --git a/packages/cli/test/lib/upgrade.test.ts b/packages/cli/test/lib/upgrade.test.ts index 035b5d794..5be943edc 100644 --- a/packages/cli/test/lib/upgrade.test.ts +++ b/packages/cli/test/lib/upgrade.test.ts @@ -127,6 +127,7 @@ import { setInstallInfo, } from "../../src/lib/db/install-info.js"; import { UpgradeError } from "../../src/lib/errors.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import { isProcessRunning } from "../../src/lib/process-utils.js"; const { @@ -193,7 +194,7 @@ describe("fetchLatestFromGitHub", () => { test("selects the first CLI release after sentry-mcp is renamed to Toolkit", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if (request.endsWith("/releases?per_page=100")) { return Response.json([ @@ -224,7 +225,7 @@ describe("fetchLatestFromGitHub", () => { test("selects the latest CLI-prefixed Toolkit release", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response( JSON.stringify([ { tag_name: "mcp@9.0.0" }, @@ -246,7 +247,7 @@ describe("fetchLatestFromGitHub", () => { test("follows Toolkit release pagination to find the latest CLI release", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); if (requests.length === 1) { return new Response( JSON.stringify( @@ -318,7 +319,7 @@ describe("fetchLatestFromGitHub", () => { test("rejects GitHub release pagination outside the selected source", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(JSON.stringify([{ tag_name: "mcp@9.0.0" }]), { status: 200, headers: { @@ -353,7 +354,7 @@ describe("fetchLatestFromGitHub", () => { test("rejects cyclic GitHub release pagination", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(JSON.stringify([{ tag_name: "mcp@9.0.0" }]), { status: 200, headers: { @@ -371,7 +372,7 @@ describe("fetchLatestFromGitHub", () => { test("falls back to the legacy latest release only on Toolkit HTTP 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); if (requests.length === 1) { return new Response("Not Found", { status: 404 }); } @@ -413,7 +414,7 @@ describe("fetchLatestFromGitHub", () => { test("does not use an MCP release as the latest CLI release", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(JSON.stringify([{ tag_name: "mcp@9.0.0" }]), { status: 200, }); @@ -746,7 +747,7 @@ describe("fetchLatestVersion", () => { test("uses GHCR manifest when channel is nightly (curl method)", async () => { // Nightly version is now fetched from GHCR manifest annotation, not version.json mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -773,7 +774,7 @@ describe("fetchLatestVersion", () => { test("uses GHCR manifest when channel is nightly (npm method)", async () => { // Even npm method uses GHCR when channel=nightly (nightly is curl-only distribution) mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -886,7 +887,7 @@ describe("versionExists", () => { test("probes prefixed Toolkit tags and retains the selected source", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(JSON.stringify({ tag_name: "cli@1.0.0" }), { status: 200, }); @@ -901,7 +902,7 @@ describe("versionExists", () => { test("falls back to an unprefixed legacy tag on Toolkit HTTP 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return requests.length === 1 ? new Response("Not Found", { status: 404 }) : new Response(JSON.stringify({ tag_name: "1.0.0" }), { status: 200 }); @@ -936,7 +937,7 @@ describe("versionExists", () => { async (_name, body) => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response(body, { status: 200 }); }); @@ -956,7 +957,7 @@ describe("versionExists", () => { async (annotation) => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -990,7 +991,7 @@ describe("versionExists", () => { test("does not fall back from an explicit selected source", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(String(url)); + requests.push(fetchInputUrl(url)); return new Response("Not Found", { status: 404 }); }); @@ -1123,7 +1124,7 @@ describe("versionExists", () => { annotations: { version: "0.14.0-dev.1772661724" }, }; mockFetch(async (url) => { - const u = String(url); + const u = fetchInputUrl(url); if (u === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1142,7 +1143,7 @@ describe("versionExists", () => { test("checks GHCR for nightly version - version does not exist", async () => { mockFetch(async (url) => { - const u = String(url); + const u = fetchInputUrl(url); if (u === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1166,7 +1167,7 @@ describe("versionExists", () => { annotations: { version: "0.14.0-dev.1772661724" }, }; mockFetch(async (url) => { - const u = String(url); + const u = fetchInputUrl(url); if (u === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1185,7 +1186,7 @@ describe("versionExists", () => { test("rejects a mismatched nightly annotation for an explicit source", async () => { mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); if (request.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); } @@ -1220,7 +1221,7 @@ describe("versionExists", () => { test("does not classify nightly transport error text as not found", async () => { mockFetch(async (url) => { - if (String(url).includes("ghcr.io/token")) { + if (fetchInputUrl(url).includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); } throw new Error("HTTP 404"); @@ -1233,7 +1234,7 @@ describe("versionExists", () => { test("throws on GHCR server error for nightly version", async () => { mockFetch(async (url) => { - const u = String(url); + const u = fetchInputUrl(url); if (u.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); } @@ -1248,7 +1249,7 @@ describe("versionExists", () => { test("does not classify GHCR HTTP 403 as a missing nightly version", async () => { const requests: string[] = []; mockFetch(async (url) => { - const urlString = String(url); + const urlString = fetchInputUrl(url); requests.push(urlString); if (urlString.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); @@ -2084,7 +2085,7 @@ describe("fetchLatestNightlyVersion", () => { test("falls back to legacy when the Toolkit nightly manifest returns 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -2129,7 +2130,7 @@ describe("fetchLatestNightlyVersion", () => { test("does not fall back from a non-404 Toolkit nightly failure", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -2151,7 +2152,7 @@ describe("fetchLatestNightlyVersion", () => { test("does not fall back when nightly transport error text says HTTP 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -2177,7 +2178,7 @@ describe("fetchLatestNightlyVersion", () => { // Mock the two requests: token exchange + manifest fetch let callCount = 0; mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -2213,7 +2214,7 @@ describe("fetchLatestNightlyVersion", () => { test("throws UpgradeError when GHCR token exchange fails", async () => { mockFetch(async (url) => - String(url) === "https://api.github.com/repos/getsentry/toolkit" + fetchInputUrl(url) === "https://api.github.com/repos/getsentry/toolkit" ? new Response(null, { status: 200 }) : new Response("Unauthorized", { status: 401 }), ); @@ -2226,7 +2227,7 @@ describe("fetchLatestNightlyVersion", () => { test("throws UpgradeError when manifest has no version annotation", async () => { mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -2297,7 +2298,7 @@ describe("executeUpgrade with curl method (nightly)", () => { // Mock: token exchange + manifest + blob (200 with gzipped content) mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200, @@ -2347,7 +2348,7 @@ describe("executeUpgrade with curl method (nightly)", () => { test("rejects a mismatched versioned manifest before downloading its blob", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = String(url); + const request = fetchInputUrl(url); requests.push(request); if (request.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); @@ -2484,7 +2485,7 @@ describe("downloadBinaryToTemp verifies download integrity (CLI-1D3)", () => { // with "Executable not found in $PATH" (the CLI-1D3 symptom). const emptyGzip = gzipSync(new Uint8Array(0)); mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.endsWith(".gz")) { return new Response(emptyGzip, { status: 200 }); } @@ -2506,7 +2507,7 @@ describe("downloadBinaryToTemp verifies download integrity (CLI-1D3)", () => { test("releases the download lock when verification fails", async () => { const emptyGzip = gzipSync(new Uint8Array(0)); mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.endsWith(".gz")) { return new Response(emptyGzip, { status: 200 }); } @@ -2538,7 +2539,7 @@ describe("downloadBinaryToTemp verifies download integrity (CLI-1D3)", () => { const mockBinaryContent = new Uint8Array([0x7f, 0x45, 0x4c, 0x46]); // ELF const emptyGzip = gzipSync(new Uint8Array(0)); mockFetch(async (url) => { - const urlStr = String(url); + const urlStr = fetchInputUrl(url); if (urlStr.endsWith(".gz")) { return new Response(emptyGzip, { status: 200 }); } diff --git a/packages/cli/test/lib/version-check.test.ts b/packages/cli/test/lib/version-check.test.ts index 061596cb5..80ff8d5c2 100644 --- a/packages/cli/test/lib/version-check.test.ts +++ b/packages/cli/test/lib/version-check.test.ts @@ -17,6 +17,7 @@ import { ContextError, ValidationError, } from "../../src/lib/errors.js"; +import { fetchInputUrl } from "../../src/lib/fetch-input.js"; import { abortPendingVersionCheck, getErrorUpdateNotification, @@ -516,7 +517,7 @@ describe("maybeCheckForUpdateInBackground", () => { const toolkitSource = UPGRADE_SOURCES[0]!; const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - const url = String(input); + const url = fetchInputUrl(input); requestedUrls.push(url); if (url.includes("getsentry/cli")) { throw new Error(`Unexpected legacy access: ${url}`); @@ -548,7 +549,7 @@ describe("maybeCheckForUpdateInBackground", () => { const legacySource = UPGRADE_SOURCES[1]!; const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - const url = String(input); + const url = fetchInputUrl(input); requestedUrls.push(url); if (url.includes("getsentry/toolkit")) { return new Response("not found", { status: 404 }); @@ -586,7 +587,7 @@ describe("maybeCheckForUpdateInBackground", () => { const toolkitSource = UPGRADE_SOURCES[0]!; const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - const url = String(input); + const url = fetchInputUrl(input); requestedUrls.push(url); if (url.includes("getsentry/cli")) { throw new Error(`Unexpected legacy access: ${url}`); diff --git a/packages/cli/test/mocks/server.ts b/packages/cli/test/mocks/server.ts index 7c19640a1..2a340ca67 100644 --- a/packages/cli/test/mocks/server.ts +++ b/packages/cli/test/mocks/server.ts @@ -17,8 +17,8 @@ export type MockRoute = { method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; /** Path pattern with optional :params (e.g., "/api/0/organizations/:orgSlug/") */ path: string; - /** Static response body, or handler function for dynamic responses */ - response: unknown | RouteHandler; + /** Static response body, or a {@link RouteHandler} for dynamic responses */ + response: unknown; /** HTTP status code (default: 200) */ status?: number; }; @@ -32,7 +32,8 @@ type CompiledRoute = { method: string; pattern: RegExp; paramNames: string[]; - response: unknown | RouteHandler; + /** Static response body or {@link RouteHandler} */ + response: unknown; status: number; }; diff --git a/packages/mcp-cloudflare/src/client/components/animation/TerminalAnimation.tsx b/packages/mcp-cloudflare/src/client/components/animation/TerminalAnimation.tsx index 72936a95d..1dcf9cc40 100644 --- a/packages/mcp-cloudflare/src/client/components/animation/TerminalAnimation.tsx +++ b/packages/mcp-cloudflare/src/client/components/animation/TerminalAnimation.tsx @@ -269,7 +269,7 @@ export default function TerminalAnimation() { if (didInitRef.current) return; didInitRef.current = true; - (async () => { + void (async () => { await mountOnce(); // Start playing from the first marker setTimeout(() => { diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx index 3b2e54f28..2e4c57dc0 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat.tsx @@ -76,7 +76,7 @@ export function Chat({ isOpen, onClose, onLogout }: ChatProps) { e.preventDefault(); if (!input.trim()) return; - sendMessage({ text: input }); + void sendMessage({ text: input }); setInput(""); }, [input, sendMessage], @@ -229,7 +229,7 @@ Try asking me things like: ) { hadAuthErrorRef.current = false; // Retry the failed message - regenerate(); + void regenerate(); } // Reset retry state on successful completion (no error) @@ -344,7 +344,7 @@ Try asking me things like: } // Send the message using sendMessage (AI SDK 5+ API) - sendMessage({ text: prompt }); + void sendMessage({ text: prompt }); }, [sendMessage, handleSlashCommand], ); diff --git a/packages/mcp-cloudflare/src/client/components/ui/interactive-markdown.tsx b/packages/mcp-cloudflare/src/client/components/ui/interactive-markdown.tsx index 9936f3704..6bd049dd6 100644 --- a/packages/mcp-cloudflare/src/client/components/ui/interactive-markdown.tsx +++ b/packages/mcp-cloudflare/src/client/components/ui/interactive-markdown.tsx @@ -48,7 +48,7 @@ export function InteractiveMarkdown({ components={{ // Custom renderer for code that might contain slash commands code: ({ children, ref, ...props }) => { - const text = String(children); + const text = typeof children === "string" ? children : ""; if (text.startsWith("/") && text.match(/^\/[a-zA-Z]+$/)) { // This is a slash command, make it clickable const command = text.slice(1); diff --git a/packages/mcp-cloudflare/src/client/hooks/use-mcp-metadata.ts b/packages/mcp-cloudflare/src/client/hooks/use-mcp-metadata.ts index c5d6914d3..a713fa0a3 100644 --- a/packages/mcp-cloudflare/src/client/hooks/use-mcp-metadata.ts +++ b/packages/mcp-cloudflare/src/client/hooks/use-mcp-metadata.ts @@ -71,7 +71,7 @@ export function useMcpMetadata(enabled = true): UseMcpMetadataResult { // Fetch metadata when auth token changes or component mounts useEffect(() => { - fetchMetadata(); + void fetchMetadata(); }, [fetchMetadata]); return { diff --git a/packages/mcp-cloudflare/src/server/lib/approval-dialog.ts b/packages/mcp-cloudflare/src/server/lib/approval-dialog.ts index fc4430447..ab46f290e 100644 --- a/packages/mcp-cloudflare/src/server/lib/approval-dialog.ts +++ b/packages/mcp-cloudflare/src/server/lib/approval-dialog.ts @@ -205,7 +205,9 @@ async function getApprovedClientsFromCookie( } return approvedClients as string[]; } catch (e) { - logIssue(new Error(`Error parsing cookie payload: ${e}`, { cause: e })); + logIssue( + new Error(`Error parsing cookie payload: ${String(e)}`, { cause: e }), + ); return null; // JSON parsing failed } } diff --git a/packages/mcp-cloudflare/src/server/lib/constraint-utils.ts b/packages/mcp-cloudflare/src/server/lib/constraint-utils.ts index c3a8c6200..18df17d66 100644 --- a/packages/mcp-cloudflare/src/server/lib/constraint-utils.ts +++ b/packages/mcp-cloudflare/src/server/lib/constraint-utils.ts @@ -1,4 +1,4 @@ -import type { Constraints, ProjectCapabilities } from "@sentry/mcp-core/types"; +import type { Constraints } from "@sentry/mcp-core/types"; import { SentryApiService, ApiError } from "@sentry/mcp-core/api-client"; import { logIssue, logWarn } from "@sentry/mcp-core/telem/logging"; @@ -10,13 +10,23 @@ const CACHE_TTL_SECONDS = 900; // 15 minutes const PROJECT_TIMEOUT_CACHE_TTL_SECONDS = 60; const CACHE_KEY_VERSION = "v1"; +/** + * `ProjectCapabilities | null`, derived from `Constraints` instead of written + * as a union: root lint runs before `@sentry/mcp-core` is built, and an + * unresolved type in a union trips `no-redundant-type-constituents`. + */ +type NullableProjectCapabilities = Exclude< + Constraints["projectCapabilities"], + undefined +>; + /** * Cached constraints data stored in KV. */ export type CachedConstraints = { regionUrl: string | null; /** Null for org-only entries or a project verification timeout. */ - projectCapabilities: ProjectCapabilities | null; + projectCapabilities: NullableProjectCapabilities; cachedAt: number; }; @@ -212,7 +222,7 @@ export async function verifyConstraintsAccess( } // Verify project access if specified - let projectCapabilities: ProjectCapabilities | null = null; + let projectCapabilities: NullableProjectCapabilities = null; if (projectSlug) { try { // Fetch project with timeout to avoid blocking on slow API responses diff --git a/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts b/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts index a7d0071b6..d31978e62 100644 --- a/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts +++ b/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts @@ -344,7 +344,10 @@ async function handleAuthenticatedMcpRequest( } if (auth.kind === "oauth") { - for (const skill of Array.from(auth.grantedSkills).sort()) { + const grantedSkills = Array.from(auth.grantedSkills).sort((a, b) => + a.localeCompare(b), + ); + for (const skill of grantedSkills) { activeSpan?.setAttribute(getSkillGrantedAttributeName(skill), true); } } diff --git a/packages/mcp-cloudflare/src/server/routes/chat.ts b/packages/mcp-cloudflare/src/server/routes/chat.ts index fc62a0e24..4c827dc51 100644 --- a/packages/mcp-cloudflare/src/server/routes/chat.ts +++ b/packages/mcp-cloudflare/src/server/routes/chat.ts @@ -260,7 +260,7 @@ export default new Hono<{ Bindings: Env }>().post("/", async (c) => { // Get available tools from MCP server Object.assign(tools, await mcpClient.tools()); - logInfo(`Connected to ${mcpUrl}`, { + logInfo(`Connected to ${mcpUrl.href}`, { loggerScope: ["cloudflare", "chat", "connection"], extra: { toolCount: Object.keys(tools).length, @@ -295,7 +295,7 @@ export default new Hono<{ Bindings: Env }>().post("/", async (c) => { }); Object.assign(tools, await mcpClient.tools()); - logInfo(`Connected to ${mcpUrl} (after refresh)`, { + logInfo(`Connected to ${mcpUrl.href} (after refresh)`, { loggerScope: ["cloudflare", "chat", "connection"], extra: { toolCount: Object.keys(tools).length, diff --git a/packages/mcp-cloudflare/src/server/sentry.config.ts b/packages/mcp-cloudflare/src/server/sentry.config.ts index e2ae97b8f..f16160cf1 100644 --- a/packages/mcp-cloudflare/src/server/sentry.config.ts +++ b/packages/mcp-cloudflare/src/server/sentry.config.ts @@ -27,26 +27,3 @@ export default function getSentryConfig(env: Env): CloudflareOptions { integrations: [Sentry.zodErrorsIntegration()], }; } - -getSentryConfig.partial = (config: Partial) => { - return (env: Env) => { - const defaultConfig = getSentryConfig(env); - return { - ...defaultConfig, - ...config, - initialScope: { - ...defaultConfig.initialScope, - ...config.initialScope, - tags: { - // idk I can't typescript - ...((defaultConfig.initialScope ?? {}) as any).tags, - ...((config.initialScope ?? {}) as any).tags, - }, - attributes: { - ...((defaultConfig.initialScope ?? {}) as any).attributes, - ...((config.initialScope ?? {}) as any).attributes, - }, - }, - }; - }; -}; diff --git a/packages/mcp-core/scripts/validate-skills-mapping.ts b/packages/mcp-core/scripts/validate-skills-mapping.ts index 6a07a3828..4723e74ab 100755 --- a/packages/mcp-core/scripts/validate-skills-mapping.ts +++ b/packages/mcp-core/scripts/validate-skills-mapping.ts @@ -248,4 +248,4 @@ async function main(): Promise { } } -main(); +void main(); diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index a1da7cc1d..54e2a7c23 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -900,7 +900,7 @@ export class SentryApiService { return response; } const errorText = await response.text(); - let parsed: unknown | undefined; + let parsed: unknown; try { parsed = JSON.parse(errorText); } catch (error) { @@ -2329,7 +2329,7 @@ export class SentryApiService { ): Promise { const query = new URLSearchParams({ alert_rule_id: String(alertRuleId) }); const body = await this.requestJSON( - `${apiPath`/organizations/${organizationSlug}/alert-rule-detector/`}?${query}`, + `${apiPath`/organizations/${organizationSlug}/alert-rule-detector/`}?${query.toString()}`, undefined, opts, ); @@ -2442,7 +2442,7 @@ export class SentryApiService { if (query) search.set("query", query); if (cursor) search.set("cursor", cursor); const response = await this.request( - `${apiPath`/organizations/${organizationSlug}/detectors/`}?${search}`, + `${apiPath`/organizations/${organizationSlug}/detectors/`}?${search.toString()}`, undefined, opts, ); @@ -2474,7 +2474,7 @@ export class SentryApiService { } if (cursor) search.set("cursor", cursor); const response = await this.request( - `${apiPath`/organizations/${organizationSlug}/available-actions/`}?${search}`, + `${apiPath`/organizations/${organizationSlug}/available-actions/`}?${search.toString()}`, undefined, opts, ); @@ -2506,7 +2506,7 @@ export class SentryApiService { const search = new URLSearchParams({ group, per_page: String(limit) }); if (cursor) search.set("cursor", cursor); const response = await this.request( - `${apiPath`/organizations/${organizationSlug}/data-conditions/`}?${search}`, + `${apiPath`/organizations/${organizationSlug}/data-conditions/`}?${search.toString()}`, undefined, opts, ); @@ -3502,7 +3502,7 @@ export class SentryApiService { method?: string; headers?: Array<[string, string]>; body?: string | null; - assertion?: unknown | null; + assertion?: unknown; status?: "active" | "disabled"; owner?: string | null; environment?: string | null; @@ -3585,7 +3585,7 @@ export class SentryApiService { method?: string; headers?: Array<[string, string]>; body?: string | null; - assertion?: unknown | null; + assertion?: unknown; status?: "active" | "disabled"; owner?: string | null; environment?: string | null; diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index 3408e0ae5..3ffea37c8 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -1724,43 +1724,43 @@ export const TraceMetaSchema = z * Represents the hierarchical structure of spans with timing information, * operation details, and nested children spans. */ -export const TraceSpanSchema: z.ZodType = z.lazy(() => - z.object({ - children: z.array(TraceSpanSchema), - errors: z.array(z.any()), - occurrences: z.array(z.any()), - event_id: z.string(), - span_id: z.string().optional(), - transaction_id: z.string(), - project_id: z.union([z.string(), z.number()]), - project_slug: z.string(), - profile_id: z.string().nullable().optional(), - profiler_id: z.string().nullable().optional(), - parent_span_id: z.string().nullable(), - start_timestamp: z.number(), - end_timestamp: z.number(), - measurements: z.record(z.string(), z.number()).optional(), - duration: z.number(), - trace: z.string().optional(), - transaction: z.string().nullable().optional(), - is_transaction: z.boolean().optional(), - description: z.string().nullable().optional(), - sdk_name: z.string().nullable().optional(), - op: z.string().nullable().optional(), - name: z.string().nullable().optional(), - event_type: z.string().nullable().optional(), - additional_attributes: z.record(z.string(), z.unknown()).optional(), - hash: z.string().nullable().optional(), - exclusive_time: z.number().optional(), - status: z.string().nullable().optional(), - is_segment: z.boolean().optional(), - same_process_as_parent: z.boolean().optional(), - organization: z.unknown().nullable().optional(), - tags: z.record(z.string(), z.unknown()).optional(), - timestamp: z.union([z.string(), z.number()]).optional(), - data: z.record(z.string(), z.unknown()).optional(), - }), -); +export const TraceSpanSchema = z.object({ + get children() { + return z.array(TraceSpanSchema); + }, + errors: z.array(z.any()), + occurrences: z.array(z.any()), + event_id: z.string(), + span_id: z.string().optional(), + transaction_id: z.string(), + project_id: z.union([z.string(), z.number()]), + project_slug: z.string(), + profile_id: z.string().nullable().optional(), + profiler_id: z.string().nullable().optional(), + parent_span_id: z.string().nullable(), + start_timestamp: z.number(), + end_timestamp: z.number(), + measurements: z.record(z.string(), z.number()).optional(), + duration: z.number(), + trace: z.string().optional(), + transaction: z.string().nullable().optional(), + is_transaction: z.boolean().optional(), + description: z.string().nullable().optional(), + sdk_name: z.string().nullable().optional(), + op: z.string().nullable().optional(), + name: z.string().nullable().optional(), + event_type: z.string().nullable().optional(), + additional_attributes: z.record(z.string(), z.unknown()).optional(), + hash: z.string().nullable().optional(), + exclusive_time: z.number().optional(), + status: z.string().nullable().optional(), + is_segment: z.boolean().optional(), + same_process_as_parent: z.boolean().optional(), + organization: z.unknown().nullable().optional(), + tags: z.record(z.string(), z.unknown()).optional(), + timestamp: z.union([z.string(), z.number()]).optional(), + data: z.record(z.string(), z.unknown()).optional(), +}); /** * Schema for issue objects that can appear in trace responses. diff --git a/packages/mcp-core/src/internal/formatting.ts b/packages/mcp-core/src/internal/formatting.ts index 11ee2d264..6ce811051 100644 --- a/packages/mcp-core/src/internal/formatting.ts +++ b/packages/mcp-core/src/internal/formatting.ts @@ -30,6 +30,7 @@ import type { TraceSpan, } from "../api-client/types"; import { logIssue } from "../telem/logging"; +import { formatUnknown } from "./unknown-formatting"; import { type CodeLocation, findMostRelevantInAppFrame, @@ -882,7 +883,14 @@ function formatVariableValue(value: unknown, maxLength = 80): string { } return stringified; } - return String(value); + if ( + typeof value === "number" || + typeof value === "boolean" || + typeof value === "bigint" + ) { + return String(value); + } + return `<${typeof value}>`; } catch { // Handle circular references or other stringify errors return `<${typeof value}>`; @@ -1729,7 +1737,7 @@ function formatMetricAlertCondition( return type === "lte" ? "at or below" : "below"; } if (type != null) { - return String(type); + return formatUnknown(type); } return undefined; })(); @@ -1762,19 +1770,23 @@ function formatMetricAlertDetails( const snubaQuery = getMetricAlertSnubaQuery(evidenceData); if (snubaQuery) { if (snubaQuery.dataset != null) { - parts.push(`**Dataset**: ${String(snubaQuery.dataset)}`); + parts.push(`**Dataset**: ${formatUnknown(snubaQuery.dataset)}`); } if (snubaQuery.aggregate != null) { - parts.push(`**Aggregate**: ${String(snubaQuery.aggregate)}`); + parts.push(`**Aggregate**: ${formatUnknown(snubaQuery.aggregate)}`); } - if (snubaQuery.query != null && String(snubaQuery.query).length > 0) { - parts.push(`**Query**: \`${String(snubaQuery.query)}\``); + const query = + snubaQuery.query != null ? formatUnknown(snubaQuery.query) : ""; + if (query.length > 0) { + parts.push(`**Query**: \`${query}\``); } if (snubaQuery.time_window != null) { - parts.push(`**Interval**: ${String(snubaQuery.time_window)} second(s)`); + parts.push( + `**Interval**: ${formatUnknown(snubaQuery.time_window)} second(s)`, + ); } if (snubaQuery.environment != null) { - parts.push(`**Environment**: ${String(snubaQuery.environment)}`); + parts.push(`**Environment**: ${formatUnknown(snubaQuery.environment)}`); } parts.push(""); } @@ -1796,7 +1808,7 @@ function formatMetricAlertDetails( } if (evidenceData.alert_id != null) { - parts.push(`**Alert Rule ID**: ${String(evidenceData.alert_id)}`); + parts.push(`**Alert Rule ID**: ${formatUnknown(evidenceData.alert_id)}`); } parts.push(""); @@ -2164,7 +2176,7 @@ export function formatIssueOutput({ } output += `**Event ID**: ${event.id}\n`; - output += `**Type**: ${event.type}\n`; + output += `**Type**: ${formatUnknown(event.type)}\n`; // default: error without exception data, generic: performance/metric issue, csp: CSP violation if ( event.type === "error" || @@ -2176,7 +2188,7 @@ export function formatIssueOutput({ | z.infer | z.infer | z.infer - | any; // CSP events don't have a schema yet + | { dateCreated?: string | null }; // CSP events don't have a schema yet if (typedEvent.dateCreated) { const d = new Date(typedEvent.dateCreated); if (!isNaN(d.getTime())) { diff --git a/packages/mcp-core/src/internal/tool-helpers/api.ts b/packages/mcp-core/src/internal/tool-helpers/api.ts index 58669bc47..8b475a5a3 100644 --- a/packages/mcp-core/src/internal/tool-helpers/api.ts +++ b/packages/mcp-core/src/internal/tool-helpers/api.ts @@ -5,6 +5,7 @@ import { ApiNotFoundError, } from "../../api-client/index"; import { UserInputError } from "../../errors"; +import { formatNotFoundParams } from "./enhance-error"; import type { ServerContext } from "../../types"; import { validateRegionUrl } from "./validate-region-url"; @@ -63,12 +64,7 @@ export function handleApiError( // Special handling for 404s with parameter context if (error instanceof ApiNotFoundError && params) { - const paramsList: string[] = []; - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null && value !== "") { - paramsList.push(`${key}: '${value}'`); - } - } + const paramsList = formatNotFoundParams(params); if (paramsList.length > 0) { message = `Resource not found (404): ${error.message}\nPlease verify these parameters are correct:\n${paramsList.map((p) => ` - ${p}`).join("\n")}`; diff --git a/packages/mcp-core/src/internal/tool-helpers/enhance-error.ts b/packages/mcp-core/src/internal/tool-helpers/enhance-error.ts index 5165cf173..f9ab2e40e 100644 --- a/packages/mcp-core/src/internal/tool-helpers/enhance-error.ts +++ b/packages/mcp-core/src/internal/tool-helpers/enhance-error.ts @@ -1,4 +1,5 @@ import { ApiNotFoundError } from "../../api-client"; +import { formatUnknown } from "../unknown-formatting"; /** * Enhances a 404 error with parameter context to help users understand what went wrong. @@ -20,13 +21,7 @@ export function enhanceNotFoundError( error: ApiNotFoundError, params: Record, ): ApiNotFoundError { - const paramsList: string[] = []; - - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null && value !== "") { - paramsList.push(`${key}: '${value}'`); - } - } + const paramsList = formatNotFoundParams(params); if (paramsList.length > 0) { const enhancedMessage = `${error.message}\nPlease verify these parameters are correct:\n${paramsList.map((p) => ` - ${p}`).join("\n")}`; @@ -39,3 +34,19 @@ export function enhanceNotFoundError( return error; } + +/** + * Format the non-empty request parameters as `key: 'value'` lines for a 404 + * error message. + */ +export function formatNotFoundParams( + params: Record, +): string[] { + const paramsList: string[] = []; + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null && value !== "") { + paramsList.push(`${key}: '${formatUnknown(value)}'`); + } + } + return paramsList; +} diff --git a/packages/mcp-core/src/internal/unknown-formatting.ts b/packages/mcp-core/src/internal/unknown-formatting.ts new file mode 100644 index 000000000..a7daa0ce7 --- /dev/null +++ b/packages/mcp-core/src/internal/unknown-formatting.ts @@ -0,0 +1,21 @@ +/** + * Formats an arbitrary value for display without producing `[object Object]`. + * + * Nullish values render as `unknown`, primitives render directly, and + * everything else is JSON encoded. + */ +export function formatUnknown(value: unknown): string { + if (value === null || value === undefined) { + return "unknown"; + } + + if (typeof value === "string") { + return value; + } + + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + + return JSON.stringify(value); +} diff --git a/packages/mcp-core/src/server.test.ts b/packages/mcp-core/src/server.test.ts index c37f79f9a..a6425f7d4 100644 --- a/packages/mcp-core/src/server.test.ts +++ b/packages/mcp-core/src/server.test.ts @@ -226,7 +226,7 @@ describe("buildServer", () => { tools: { v2_tool: createMockTool("v2_tool", { inputSchema: { value: z.string() }, - handler: async ({ value }) => `v2:${value}`, + handler: async ({ value }) => `v2:${String(value)}`, }), }, sdkVersion: "v2", diff --git a/packages/mcp-core/src/server.ts b/packages/mcp-core/src/server.ts index cd596d329..fd2d3e831 100644 --- a/packages/mcp-core/src/server.ts +++ b/packages/mcp-core/src/server.ts @@ -386,7 +386,9 @@ function configureServer({ if (isStructuredToolOutput(output)) { return structuredOutputToCallToolResult(output); } - throw new Error(`Invalid tool output: ${output}`); + throw new Error( + `Invalid tool output: ${typeof output === "object" ? JSON.stringify(output) : String(output)}`, + ); } catch (error) { if (activeSpan) { activeSpan.setStatus({ diff --git a/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts b/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts index 0c457d7dc..be57114f5 100644 --- a/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts @@ -266,7 +266,7 @@ describe("get_alert_rule", () => { return HttpResponse.json({ detectorId: "789" }); }), http.get(`${organizationApi}/detectors/:id/`, ({ params }) => { - reads.push(`detector:${params.id}`); + reads.push(`detector:${String(params.id)}`); return HttpResponse.json(detector); }), ); diff --git a/packages/mcp-core/src/tools/catalog/get-latest-base-snapshot.ts b/packages/mcp-core/src/tools/catalog/get-latest-base-snapshot.ts index 4f6a5c669..7dab2f240 100644 --- a/packages/mcp-core/src/tools/catalog/get-latest-base-snapshot.ts +++ b/packages/mcp-core/src/tools/catalog/get-latest-base-snapshot.ts @@ -5,7 +5,10 @@ import { ParamOrganizationSlug, ParamRegionUrl } from "../../schema"; import { setTargetTagsAndAttributes } from "../../telem/scope"; import type { ServerContext } from "../../types"; import { isNumericId } from "../../utils/slug-validation"; -import { renderSnapshotImageTreeSection } from "../support/snapshots/formatting"; +import { + type SnapshotVcsInfo, + renderSnapshotImageTreeSection, +} from "../support/snapshots/formatting"; export default defineTool({ name: "get_latest_base_snapshot", @@ -100,8 +103,10 @@ export default defineTool({ const snapshotId = data.id as string | undefined; const images = (data.images as Array>) || []; - const appInfo = data.app_info as Record | undefined; - const vcsInfo = data.vcs_info as Record | undefined; + const appInfo = data.app_info as + | { name?: string | null; platform?: string | null } + | undefined; + const vcsInfo = data.vcs_info as SnapshotVcsInfo | undefined; const snapshotUrl = snapshotId ? apiService.getPreprodSnapshotUrl(params.organizationSlug, snapshotId) @@ -124,7 +129,7 @@ export default defineTool({ if (vcsInfo) { if (vcsInfo.head_ref) sections.push( - `- **Branch**: ${vcsInfo.head_ref} (\`${String(vcsInfo.head_sha ?? "").slice(0, 8)}\`)`, + `- **Branch**: ${vcsInfo.head_ref} (\`${(vcsInfo.head_sha ?? "").slice(0, 8)}\`)`, ); } sections.push(`- **Total Images**: ${images.length}`); diff --git a/packages/mcp-core/src/tools/catalog/get-replay-details.ts b/packages/mcp-core/src/tools/catalog/get-replay-details.ts index 518f75433..b79d86976 100644 --- a/packages/mcp-core/src/tools/catalog/get-replay-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-replay-details.ts @@ -584,7 +584,7 @@ function summarizeObject( (typeof nested === "string" || typeof nested === "number"), ) .slice(0, 3) - .map(([key, nested]) => `${key}=${nested}`); + .map(([key, nested]) => `${key}=${String(nested)}`); return entries.length > 0 ? `payload=${quoteDetail(entries.join(", "))}` diff --git a/packages/mcp-core/src/tools/catalog/support/api-formatting.ts b/packages/mcp-core/src/tools/catalog/support/api-formatting.ts index e82a74ae3..11fbca288 100644 --- a/packages/mcp-core/src/tools/catalog/support/api-formatting.ts +++ b/packages/mcp-core/src/tools/catalog/support/api-formatting.ts @@ -1,5 +1,7 @@ import { isPlainObject } from "../../../internal/type-guards"; +export { formatUnknown } from "../../../internal/unknown-formatting"; + export function formatId(value: string | number | undefined | null): string { return value === undefined || value === null ? "unknown" : String(value); } @@ -48,22 +50,6 @@ export function formatActor(value: unknown): string { ); } -export function formatUnknown(value: unknown): string { - if (value === null || value === undefined) { - return "unknown"; - } - - if (typeof value === "string") { - return value; - } - - if (typeof value === "number" || typeof value === "boolean") { - return String(value); - } - - return JSON.stringify(value); -} - export function readString( value: Record | undefined | null, key: string, diff --git a/packages/mcp-core/src/tools/catalog/update-issue.ts b/packages/mcp-core/src/tools/catalog/update-issue.ts index 3298e2e79..bdfc0dbc6 100644 --- a/packages/mcp-core/src/tools/catalog/update-issue.ts +++ b/packages/mcp-core/src/tools/catalog/update-issue.ts @@ -55,13 +55,6 @@ type IgnoreState = { message: string; }; -type IssueStatusDisplay = - | "resolved" - | "resolvedInNextRelease" - | "unresolved" - | "ignored" - | string; - type IgnoreParams = { status?: string; ignoreMode?: IgnoreMode; @@ -162,7 +155,12 @@ function formatIgnoreUntil(ignoreUntil: string): string { return `${date.toISOString().slice(0, 19).replace("T", " ")} UTC`; } -function getIssueStatusDisplay(issue: Issue): IssueStatusDisplay { +/** + * Issue status for display: `resolvedInNextRelease` for next-release + * resolutions, otherwise the raw API status (`resolved`, `unresolved`, + * `ignored`, ...). + */ +function getIssueStatusDisplay(issue: Issue): string { const statusDetails = getIssueStatusDetails(issue); if (issue.status === "resolved" && statusDetails.inNextRelease === true) { diff --git a/packages/mcp-core/src/tools/support/issue-linking/index.ts b/packages/mcp-core/src/tools/support/issue-linking/index.ts index 8968720aa..ab5397ade 100644 --- a/packages/mcp-core/src/tools/support/issue-linking/index.ts +++ b/packages/mcp-core/src/tools/support/issue-linking/index.ts @@ -23,7 +23,7 @@ function parseUrl(value: string): URL { !["https:", "http:"].includes(url.protocol) || url.username || url.password || - [...value].some( + Array.from(value).some( (character) => character.charCodeAt(0) <= 32 || character === "\\", ) ) { diff --git a/packages/mcp-core/src/tools/support/search-events/utils.ts b/packages/mcp-core/src/tools/support/search-events/utils.ts index d31b4391a..d759f30aa 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.ts @@ -748,7 +748,11 @@ export function formatEventValue( return formatObjectValue(value, maxLength); } - return truncateString(sanitizeWhitespace(String(value)), maxLength); + if (typeof value === "bigint") { + return String(value); + } + + return `<${typeof value}>`; } // Helper function to fetch custom attributes for a dataset diff --git a/packages/mcp-core/src/tools/support/snapshots/formatting.ts b/packages/mcp-core/src/tools/support/snapshots/formatting.ts index 222f10c13..65fb63b93 100644 --- a/packages/mcp-core/src/tools/support/snapshots/formatting.ts +++ b/packages/mcp-core/src/tools/support/snapshots/formatting.ts @@ -5,6 +5,16 @@ export interface SnapshotImageEntry { [key: string]: unknown; } +/** VCS metadata attached to snapshot API responses. */ +export interface SnapshotVcsInfo { + repo_name?: string | null; + head_ref?: string | null; + head_sha?: string | null; + base_ref?: string | null; + base_sha?: string | null; + pr_number?: number | null; +} + export interface SnapshotImageTreeItem { image: SnapshotImageEntry; details?: string[]; diff --git a/packages/mcp-core/src/tools/support/snapshots/handlers.ts b/packages/mcp-core/src/tools/support/snapshots/handlers.ts index d2b53675a..5bf1361c4 100644 --- a/packages/mcp-core/src/tools/support/snapshots/handlers.ts +++ b/packages/mcp-core/src/tools/support/snapshots/handlers.ts @@ -13,6 +13,7 @@ import { formatToolCallInstruction } from "../../../internal/tool-helpers/tool-c import { type SnapshotImageEntry, type SnapshotImageTreeItem, + type SnapshotVcsInfo, formatSnapshotDiffPercent, getSnapshotImageDisplayName, renderSnapshotImageContext, @@ -348,9 +349,12 @@ export async function fetchSnapshotSummary( compactMetadata: true, })) as Record; - const vcsInfo = data.vcs_info as Record | undefined; + const comparisonType = data.comparison_type as string | null | undefined; + const state = data.state as string | null | undefined; + const projectId = data.project_id as string | number | null | undefined; + const vcsInfo = data.vcs_info as SnapshotVcsInfo | undefined; const approvalInfo = data.approval_info as - | Record + | { status?: string | null; is_auto_approved?: boolean | null } | undefined; const allImages = (data.images as SnapshotImageEntry[]) || []; @@ -374,10 +378,10 @@ export async function fetchSnapshotSummary( sections.push("\n## Summary\n"); sections.push(`- **URL**: ${resolvedSnapshotUrl}`); - sections.push(`- **Type**: ${data.comparison_type ?? "unknown"}`); - sections.push(`- **State**: ${data.state ?? "unknown"}`); - if (data.project_id) { - sections.push(`- **Project ID**: ${data.project_id}`); + sections.push(`- **Type**: ${comparisonType ?? "unknown"}`); + sections.push(`- **State**: ${state ?? "unknown"}`); + if (projectId) { + sections.push(`- **Project ID**: ${projectId}`); } const totalCount = countField(data, "total_count", allImages.length); @@ -397,11 +401,11 @@ export async function fetchSnapshotSummary( if (vcsInfo.repo_name) sections.push(`- **Repo**: ${vcsInfo.repo_name}`); if (vcsInfo.head_ref) sections.push( - `- **Head**: ${vcsInfo.head_ref} (\`${String(vcsInfo.head_sha ?? "").slice(0, 8)}\`)`, + `- **Head**: ${vcsInfo.head_ref} (\`${(vcsInfo.head_sha ?? "").slice(0, 8)}\`)`, ); if (vcsInfo.base_ref) sections.push( - `- **Base**: ${vcsInfo.base_ref} (\`${String(vcsInfo.base_sha ?? "").slice(0, 8)}\`)`, + `- **Base**: ${vcsInfo.base_ref} (\`${(vcsInfo.base_sha ?? "").slice(0, 8)}\`)`, ); if (vcsInfo.pr_number) sections.push(`- **PR**: #${vcsInfo.pr_number}`); } diff --git a/packages/mcp-core/src/tools/surfaces.ts b/packages/mcp-core/src/tools/surfaces.ts index dd78a7cb7..b5f6d89e6 100644 --- a/packages/mcp-core/src/tools/surfaces.ts +++ b/packages/mcp-core/src/tools/surfaces.ts @@ -41,9 +41,7 @@ const catalogInfrastructureToolNames = new Set( CATALOG_INFRASTRUCTURE_TOOL_NAMES, ); -export type TopLevelToolName = - | (typeof TOP_LEVEL_TOOL_NAMES)[number] - | (typeof EXPERIMENTAL_TOP_LEVEL_TOOL_NAMES)[number]; +export type TopLevelToolName = (typeof TOP_LEVEL_TOOL_NAMES)[number]; export function isOutsideCatalogToolName(toolName: string): boolean { return ( diff --git a/packages/mcp-server-evals/src/evals/utils/structuredOutputScorer.ts b/packages/mcp-server-evals/src/evals/utils/structuredOutputScorer.ts index cfb1ba944..2a5c257fb 100644 --- a/packages/mcp-server-evals/src/evals/utils/structuredOutputScorer.ts +++ b/packages/mcp-server-evals/src/evals/utils/structuredOutputScorer.ts @@ -108,7 +108,9 @@ export function StructuredOutputScorer( } catch (error) { return { score: 0, - metadata: { rationale: `Failed to parse output as JSON: ${error}` }, + metadata: { + rationale: `Failed to parse output as JSON: ${String(error)}`, + }, }; } diff --git a/packages/mcp-server/src/cli/commands/auth.ts b/packages/mcp-server/src/cli/commands/auth.ts index 3ddee7834..e69b9cf33 100644 --- a/packages/mcp-server/src/cli/commands/auth.ts +++ b/packages/mcp-server/src/cli/commands/auth.ts @@ -55,7 +55,9 @@ async function login(argv: string[]): Promise { await writeCachedToken(toCachedToken(tokenResponse, sentryHost, clientId)); } catch (err) { console.error( - err instanceof Error ? err.message : `Authentication failed: ${err}`, + err instanceof Error + ? err.message + : `Authentication failed: ${String(err)}`, ); process.exit(1); } diff --git a/packages/mcp-test-client/src/agent.ts b/packages/mcp-test-client/src/agent.ts index c82a9fe69..bc5754d34 100644 --- a/packages/mcp-test-client/src/agent.ts +++ b/packages/mcp-test-client/src/agent.ts @@ -151,7 +151,7 @@ export async function runAgent( let isStreaming = false; let streamError: unknown; - const result = await streamText({ + const result = streamText({ model: languageModel, system: SYSTEM_PROMPT, messages: [{ role: "user", content: userPrompt }], diff --git a/packages/mcp-test-client/src/mcp-test-client-remote.ts b/packages/mcp-test-client/src/mcp-test-client-remote.ts index fa7a69490..8d3db54bd 100644 --- a/packages/mcp-test-client/src/mcp-test-client-remote.ts +++ b/packages/mcp-test-client/src/mcp-test-client-remote.ts @@ -88,7 +88,7 @@ export async function connectToRemoteMCPServer( span.setStatus({ code: 1 }); logSuccess( - `Connected to MCP server (${mcpUrl})`, + `Connected to MCP server (${mcpUrl.href})`, `${tools.size} tools available`, ); diff --git a/packages/smoke-tests/src/smoke.test.ts b/packages/smoke-tests/src/smoke.test.ts index 3d9d433aa..e95667fa3 100644 --- a/packages/smoke-tests/src/smoke.test.ts +++ b/packages/smoke-tests/src/smoke.test.ts @@ -31,6 +31,22 @@ const IS_LOCAL_DEV = // User-Agent for smoke tests - identifies these as automated smoke tests const SMOKE_TEST_USER_AGENT = `sentry-mcp-smoke-tests/${pkg.version}`; +/** + * Merge caller headers over the smoke-test User-Agent, then apply the + * Cloudflare version override. `HeadersInit` may be a `Headers` instance or + * an array of tuples, so it can't be object-spread. + */ +function buildSmokeTestHeaders(init?: HeadersInit): Headers { + const headers = new Headers({ "User-Agent": SMOKE_TEST_USER_AGENT }); + for (const [key, value] of new Headers(init)) { + headers.set(key, value); + } + for (const [key, value] of Object.entries(VERSION_HEADER)) { + headers.set(key, value); + } + return headers; +} + // Skip all smoke tests if PREVIEW_URL is not set const describeIfPreviewUrl = PREVIEW_URL ? describe : describe.skip; @@ -76,11 +92,7 @@ async function safeFetch( try { response = await fetch(url, { ...fetchOptions, - headers: { - "User-Agent": SMOKE_TEST_USER_AGENT, - ...fetchOptions.headers, - ...VERSION_HEADER, - }, + headers: buildSmokeTestHeaders(fetchOptions.headers), signal, }); diff --git a/scripts/ci-projects.mjs b/scripts/ci-projects.mjs index 4917dd68d..357629593 100644 --- a/scripts/ci-projects.mjs +++ b/scripts/ci-projects.mjs @@ -70,7 +70,7 @@ function optionalScript(scripts, configured, candidates) { function safeRelativePath(value, field) { const hasUnsafeDelimiter = typeof value === "string" && - [...value].some((character) => { + Array.from(value).some((character) => { const codeUnit = character.charCodeAt(0); return ( codeUnit <= 0x1f || From b75e6a180ed48c1217a0d6bdb47675c2c180d66b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:06:24 +0000 Subject: [PATCH 2/3] refactor(lint): Address review feedback on type-aware rule fixes - Count slashes with a non-allocating countChar() helper - Keep RegExp.escape unbound; it is static and never reads `this` - Let formatAttrValue() defer to stringifyUnknown() - Rename fetchInputUrl() to extractInputUrl() - Restore IssueStatusDisplay using `string & {}` - Allow URL/URLSearchParams in template literals instead of sprinkling .href/.toString() - Narrow replay payload entries with a type guard instead of String() --- .oxlintrc.json | 3 +- .../src/commands/alert/issues/rule-resolve.ts | 4 +- .../commands/alert/metrics/rule-resolve.ts | 4 +- packages/cli/src/lib/api/projects.ts | 6 +- packages/cli/src/lib/delta-upgrade.ts | 6 +- .../cli/src/lib/envelope/event-send-dsn.ts | 3 +- packages/cli/src/lib/fetch-input.ts | 2 +- packages/cli/src/lib/formatters/local.ts | 12 +--- packages/cli/src/lib/sentry-client.ts | 8 +-- packages/cli/src/lib/utils.ts | 14 ++++ packages/cli/test/commands/cli.test.ts | 6 +- .../cli/test/commands/cli/upgrade.test.ts | 36 +++++----- .../test/lib/api-client.seer-trial.test.ts | 4 +- packages/cli/test/lib/binary.test.ts | 12 ++-- .../cli/test/lib/delta-upgrade.mocked.test.ts | 12 ++-- packages/cli/test/lib/delta-upgrade.test.ts | 28 ++++---- packages/cli/test/lib/ghcr.test.ts | 16 ++--- packages/cli/test/lib/release-notes.test.ts | 10 +-- .../lib/sentry-client.invalidation.test.ts | 4 +- packages/cli/test/lib/upgrade.test.ts | 66 +++++++++---------- packages/cli/test/lib/utils.test.ts | 19 +++++- packages/cli/test/lib/version-check.test.ts | 8 +-- .../mcp-cloudflare/src/server/routes/chat.ts | 4 +- packages/mcp-core/src/api-client/client.ts | 8 +-- .../src/tools/catalog/get-replay-details.ts | 8 +-- .../src/tools/catalog/update-issue.ts | 16 +++-- .../src/mcp-test-client-remote.ts | 2 +- 27 files changed, 179 insertions(+), 142 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 20b8b0a8e..233e8d2e0 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -35,7 +35,8 @@ "typescript/restrict-template-expressions": [ "error", { - "allowNever": true + "allowNever": true, + "allow": ["URL", "URLSearchParams"] } ], "jsx-a11y/interactive-supports-focus": "off" diff --git a/packages/cli/src/commands/alert/issues/rule-resolve.ts b/packages/cli/src/commands/alert/issues/rule-resolve.ts index f1ebe6c82..db5312a92 100644 --- a/packages/cli/src/commands/alert/issues/rule-resolve.ts +++ b/packages/cli/src/commands/alert/issues/rule-resolve.ts @@ -14,7 +14,7 @@ import { ResolutionError, ValidationError } from "../../../lib/errors.js"; import { fuzzyMatch } from "../../../lib/fuzzy.js"; import { logger } from "../../../lib/logger.js"; import type { ResolvedTarget } from "../../../lib/resolve-target.js"; -import { isAllDigits } from "../../../lib/utils.js"; +import { countChar, isAllDigits } from "../../../lib/utils.js"; const log = logger.withTag("alert.issues"); @@ -42,7 +42,7 @@ export function parseIssueRuleArg( ); } - const slashCount = trimmed.split("/").length - 1; + const slashCount = countChar(trimmed, "/"); if (slashCount === 0) { return { ref: trimmed, targetArg: undefined }; } diff --git a/packages/cli/src/commands/alert/metrics/rule-resolve.ts b/packages/cli/src/commands/alert/metrics/rule-resolve.ts index 26efe2f4e..9d4dd35ee 100644 --- a/packages/cli/src/commands/alert/metrics/rule-resolve.ts +++ b/packages/cli/src/commands/alert/metrics/rule-resolve.ts @@ -13,7 +13,7 @@ import { import { ResolutionError, ValidationError } from "../../../lib/errors.js"; import { fuzzyMatch } from "../../../lib/fuzzy.js"; import { logger } from "../../../lib/logger.js"; -import { isAllDigits } from "../../../lib/utils.js"; +import { countChar, isAllDigits } from "../../../lib/utils.js"; const log = logger.withTag("alert.metrics"); @@ -45,7 +45,7 @@ export function parseMetricRuleArg( return { ref: trimmed, targetArg: undefined }; } - const slashCount = trimmed.split("/").length - 1; + const slashCount = countChar(trimmed, "/"); if (slashCount > 1) { throw new ValidationError( `Metric alerts are org-scoped — use '/', not '//'.\nUse: ${usageHint}`, diff --git a/packages/cli/src/lib/api/projects.ts b/packages/cli/src/lib/api/projects.ts index e4fd27875..5b4178cb4 100644 --- a/packages/cli/src/lib/api/projects.ts +++ b/packages/cli/src/lib/api/projects.ts @@ -459,8 +459,10 @@ export async function findProjectsBySlug( * Uses native RegExp.escape if available (Node.js 23.6+, Bun), otherwise polyfills. */ const escapeRegex: (str: string) => string = - RegExp.escape?.bind(RegExp) ?? - ((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + typeof RegExp.escape === "function" + ? // oxlint-disable-next-line typescript/unbound-method -- static method that never reads `this` + RegExp.escape + : (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); /** * Check if two strings match with word-boundary semantics (bidirectional). diff --git a/packages/cli/src/lib/delta-upgrade.ts b/packages/cli/src/lib/delta-upgrade.ts index a1ef58ca5..d2586325d 100644 --- a/packages/cli/src/lib/delta-upgrade.ts +++ b/packages/cli/src/lib/delta-upgrade.ts @@ -43,7 +43,7 @@ import { import { CLI_VERSION } from "./constants.js"; import { customFetch } from "./custom-ca.js"; import { getConfigDir } from "./db/index.js"; -import { fetchInputUrl } from "./fetch-input.js"; +import { extractInputUrl } from "./fetch-input.js"; import { formatBytes } from "./formatters/numbers.js"; import { logger } from "./logger.js"; import { makeByteProgress, type SetMessage } from "./progress.js"; @@ -178,7 +178,9 @@ function stableSource(source: UpgradeSource): SourceStrategy { const releasesUrl = getGitHubReleasesUrl(source); const sourceFetch: typeof customFetch = async (input, init) => { const response = await customFetch(input, init); - if (!(response.ok && fetchInputUrl(input).startsWith(`${releasesUrl}?`))) { + if ( + !(response.ok && extractInputUrl(input).startsWith(`${releasesUrl}?`)) + ) { return response; } const data: unknown = await response.json(); diff --git a/packages/cli/src/lib/envelope/event-send-dsn.ts b/packages/cli/src/lib/envelope/event-send-dsn.ts index 92342f174..91b5a2b02 100644 --- a/packages/cli/src/lib/envelope/event-send-dsn.ts +++ b/packages/cli/src/lib/envelope/event-send-dsn.ts @@ -12,6 +12,7 @@ import { getAuthConfig } from "../db/auth.js"; import { ConfigError } from "../errors.js"; import { logger } from "../logger.js"; import { resolveProjectBoundFromArg } from "../resolve-target.js"; +import { countChar } from "../utils.js"; import { resolveIngestDsn } from "./transport.js"; const log = logger.withTag("event.send"); @@ -100,7 +101,7 @@ function looksLikeFileArgument(value: string): boolean { if (value.startsWith(".") || isAbsolute(value)) { return true; } - const slashCount = value.split("/").length - 1; + const slashCount = countChar(value, "/"); if (slashCount > 1) { return true; } diff --git a/packages/cli/src/lib/fetch-input.ts b/packages/cli/src/lib/fetch-input.ts index ef0f33626..1ee418e74 100644 --- a/packages/cli/src/lib/fetch-input.ts +++ b/packages/cli/src/lib/fetch-input.ts @@ -8,7 +8,7 @@ * `String(request)` yields `"[object Request]"`, so `Request` inputs need * their `.url` read explicitly. */ -export function fetchInputUrl(input: Request | string | URL): string { +export function extractInputUrl(input: Request | string | URL): string { if (typeof input === "string") { return input; } diff --git a/packages/cli/src/lib/formatters/local.ts b/packages/cli/src/lib/formatters/local.ts index 3660fcc73..7525f7e3e 100644 --- a/packages/cli/src/lib/formatters/local.ts +++ b/packages/cli/src/lib/formatters/local.ts @@ -384,14 +384,6 @@ function formatAttrValue(value: unknown): string { if (value === null) { return "null"; } - if (typeof value === "object") { - try { - return sanitize(JSON.stringify(value)); - } catch (err) { - log.debug("Failed to JSON-encode attribute value", err); - return sanitize(Object.prototype.toString.call(value)); - } - } return sanitize(stringifyUnknown(value)); } @@ -734,7 +726,9 @@ function jsonSafeIdentifier(value: unknown): string | undefined { } try { - const identifier = (value as { toString(): string }).toString(); + // UUID objects override toString(); the base Object result is rejected below. + // oxlint-disable-next-line typescript/no-base-to-string + const identifier = value.toString(); if (identifier === "[object Object]") { return; } diff --git a/packages/cli/src/lib/sentry-client.ts b/packages/cli/src/lib/sentry-client.ts index 69e4a430d..ac25222c3 100644 --- a/packages/cli/src/lib/sentry-client.ts +++ b/packages/cli/src/lib/sentry-client.ts @@ -43,7 +43,7 @@ import { MalformedAuthTokenError, TimeoutError, } from "./errors.js"; -import { fetchInputUrl } from "./fetch-input.js"; +import { extractInputUrl } from "./fetch-input.js"; import { logger } from "./logger.js"; import { clearLastCacheHitAge, @@ -356,7 +356,7 @@ async function fetchWithTimeout({ // extra property is harmless (Node honors NODE_EXTRA_CA_CERTS natively). const customTls = getCustomTlsOptions(); if (customTls) { - warnIfSaasWithEnvCa(fetchInputUrl(input)); + warnIfSaasWithEnvCa(extractInputUrl(input)); } const response = await fetch(input, { @@ -470,7 +470,7 @@ function handleFetchError( /** Extract the URL pathname for span naming */ function extractUrlPath(input: Request | string | URL): string { - const raw = fetchInputUrl(input); + const raw = extractInputUrl(input); // oxlint-disable-next-line sentry-cli/no-silent-catch -- grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { return new URL(raw).pathname; @@ -755,7 +755,7 @@ function createAuthenticatedFetch( `${method} ${urlPath}`, "http.client", async (span) => { - const fullUrl = fetchInputUrl(input); + const fullUrl = extractInputUrl(input); const startTime = performance.now(); // Check cache before auth/retry for GET requests. diff --git a/packages/cli/src/lib/utils.ts b/packages/cli/src/lib/utils.ts index 4a5380dba..38ec5c4a7 100644 --- a/packages/cli/src/lib/utils.ts +++ b/packages/cli/src/lib/utils.ts @@ -21,6 +21,20 @@ export function isAllDigits(str: string): boolean { return ALL_DIGITS_PATTERN.test(str); } +/** + * Count occurrences of a single character in a string without allocating. + * + * @example + * countChar("org/project/rule", "/") // 2 + */ +export function countChar(str: string, char: string): number { + let count = 0; + for (let i = str.indexOf(char); i !== -1; i = str.indexOf(char, i + 1)) { + count += 1; + } + return count; +} + /** * Parse an absolute HTTP(S) URL without embedded credentials. * diff --git a/packages/cli/test/commands/cli.test.ts b/packages/cli/test/commands/cli.test.ts index f7ccb5b12..7506f217f 100644 --- a/packages/cli/test/commands/cli.test.ts +++ b/packages/cli/test/commands/cli.test.ts @@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { feedbackCommand } from "../../src/commands/cli/feedback.js"; import type { UpgradeResult } from "../../src/commands/cli/upgrade.js"; import { upgradeCommand } from "../../src/commands/cli/upgrade.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; /** * Create a mock context with a process.stderr.write spy for capturing @@ -166,7 +166,9 @@ describe("upgradeCommand.func", () => { test("check mode with version shows versioned command", async () => { globalThis.fetch = (async (url) => { - const response = fetchInputUrl(url).includes("/releases/tags/cli%402.0.0") + const response = extractInputUrl(url).includes( + "/releases/tags/cli%402.0.0", + ) ? { tag_name: "cli@2.0.0" } : [{ tag_name: "cli@99.0.0" }]; return new Response(JSON.stringify(response), { diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts index 2c3413f3c..ccc2d2112 100644 --- a/packages/cli/test/commands/cli/upgrade.test.ts +++ b/packages/cli/test/commands/cli/upgrade.test.ts @@ -41,7 +41,7 @@ import { setReleaseChannel, } from "../../../src/lib/db/release-channel.js"; import { setVersionCheckInfo } from "../../../src/lib/db/version-check.js"; -import { fetchInputUrl } from "../../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../../src/lib/fetch-input.js"; import { TEST_TMP_DIR, useTestConfigDir } from "../../helpers.js"; /** Store original fetch for restoration */ @@ -172,7 +172,7 @@ function createMockContext( */ function mockGhcrNightlyVersion(version: string): void { mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -213,7 +213,7 @@ function mockGhcrNightlyVersion(version: string): void { */ function mockGitHubVersion(version: string): void { mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: `cli@${version}` }]), { @@ -254,7 +254,7 @@ function mockGitHubVersion(version: string): void { */ function mockNightlyVersion(version: string): void { mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -367,7 +367,7 @@ describe("sentry cli upgrade", () => { test("resolves a pinned check target from its exact source", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if ( request.includes("getsentry/toolkit/releases/tags/cli%4088.88.88") @@ -463,7 +463,7 @@ describe("sentry cli upgrade", () => { const requests: string[] = []; setVersionCheckInfo("88.88.88"); mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return await response(); }); const { context, errors, getOutput, restore } = createMockContext({ @@ -544,7 +544,7 @@ describe("sentry cli upgrade", () => { test("uses the selected legacy source for the check-mode changelog", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if (request.includes("getsentry/toolkit/releases?per_page=100")) { return new Response("Not Found", { status: 404 }); @@ -582,7 +582,7 @@ describe("sentry cli upgrade", () => { test("reports error for non-existent version", async () => { // Mock: latest is 99.99.99, but 0.0.1 doesn't exist mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: "cli@99.99.99" }]), { status: 200, @@ -918,7 +918,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy const fakeContent = new Uint8Array([0x7f, 0x45, 0x4c, 0x46]); // ELF magic const gzipped = gzipSync(fakeContent); mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: `cli@${version}` }]), { status: 200, @@ -1025,7 +1025,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy async (method) => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(null, { status: 404 }); }); const { context, errors, restore } = createMockContext({ @@ -1047,7 +1047,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy for (const status of [401, 403, 429, 500]) { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(null, { status }); }); const { context, errors, restore } = createMockContext({ @@ -1076,7 +1076,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy async (method) => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return Response.json(null); }); const { context, errors, restore } = createMockContext({ @@ -1118,7 +1118,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy const fakeContent = new Uint8Array([0x7f, 0x45, 0x4c, 0x46]); const gzipped = gzipSync(fakeContent); mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) { return new Response(JSON.stringify([{ tag_name: "cli@99.99.99" }]), { status: 200, @@ -1149,7 +1149,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy // GHCR flow: token exchange → manifest → blob redirect → blob download mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); capturedUrls.push(urlStr); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -1305,7 +1305,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp // Nightly is now distributed via GHCR (token → manifest → blob) mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1385,7 +1385,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp test("allows a pinned nightly for a Homebrew installation", async () => { mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1432,7 +1432,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp test("rejects a pinned stable for Homebrew before network access", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response("Unexpected", { status: 500 }); }); setReleaseChannel("nightly"); @@ -1454,7 +1454,7 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp test("validates an npm stable pin through npm while tracking nightly", async () => { const requests: string[] = []; mockFetch(async (url) => { - const requestUrl = new URL(fetchInputUrl(url)); + const requestUrl = new URL(extractInputUrl(url)); requests.push(requestUrl.href); return requestUrl.origin === "https://api.github.com" ? new Response(JSON.stringify([]), { status: 200 }) diff --git a/packages/cli/test/lib/api-client.seer-trial.test.ts b/packages/cli/test/lib/api-client.seer-trial.test.ts index 2ad45f883..c84635723 100644 --- a/packages/cli/test/lib/api-client.seer-trial.test.ts +++ b/packages/cli/test/lib/api-client.seer-trial.test.ts @@ -12,7 +12,7 @@ import { } from "../../src/lib/api-client.js"; import { setAuthToken } from "../../src/lib/db/auth.js"; import { setOrgRegion } from "../../src/lib/db/regions.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import { mockFetch, useTestConfigDir } from "../helpers.js"; useTestConfigDir("test-product-trial-api-"); @@ -200,7 +200,7 @@ describe("startProductTrial", () => { globalThis.fetch = mockFetch( async (input: RequestInfo | URL, init?: RequestInit) => { - capturedUrl = fetchInputUrl(input); + capturedUrl = extractInputUrl(input); capturedMethod = init?.method ?? "GET"; return new Response(JSON.stringify({}), { status: 200, diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts index ecc10787f..eb5262d9a 100644 --- a/packages/cli/test/lib/binary.test.ts +++ b/packages/cli/test/lib/binary.test.ts @@ -40,7 +40,7 @@ import { UpgradeSourceNotFoundError, } from "../../src/lib/binary.js"; import { UpgradeError } from "../../src/lib/errors.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; describe("getBinaryDownloadUrl", () => { test("builds correct URL for current platform", () => { @@ -91,7 +91,7 @@ describe("resolveUpgradeSource", () => { const resolved = await resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(JSON.stringify({ tag_name: "cli@0.45.0" }), { status: 200, }); @@ -113,7 +113,7 @@ describe("resolveUpgradeSource", () => { const resolved = await resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response( requests.length === 1 ? "Not Found" @@ -139,7 +139,7 @@ describe("resolveUpgradeSource", () => { resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response("failure", { status }); }, }), @@ -158,7 +158,7 @@ describe("resolveUpgradeSource", () => { resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); throw new TypeError("fetch failed"); }, }), @@ -175,7 +175,7 @@ describe("resolveUpgradeSource", () => { const error = await resolveUpgradeSource({ getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source), fetch: async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response("Not Found", { status: 404 }); }, }).catch((reason: unknown) => reason); diff --git a/packages/cli/test/lib/delta-upgrade.mocked.test.ts b/packages/cli/test/lib/delta-upgrade.mocked.test.ts index d99c3452c..a6e6b2e22 100644 --- a/packages/cli/test/lib/delta-upgrade.mocked.test.ts +++ b/packages/cli/test/lib/delta-upgrade.mocked.test.ts @@ -17,7 +17,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import { useTestConfigDir } from "../helpers.js"; useTestConfigDir("delta-upgrade-mocked-"); @@ -144,7 +144,7 @@ describe("resolveStableDelta", () => { // All sizes stay 0 — this will produce an empty output mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } @@ -204,7 +204,7 @@ describe("resolveNightlyDelta", () => { // Mock GHCR: token exchange succeeds, nightly manifest has no .gz layer mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "test-token" }), { status: 200, @@ -260,7 +260,7 @@ describe("resolveNightlyDelta", () => { // Mock GHCR: nightly manifest has .gz layer, but no patch tags mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "test-token" }), { status: 200, @@ -387,7 +387,7 @@ describe("attemptDeltaUpgrade", () => { // Return garbage patch data mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } @@ -468,7 +468,7 @@ describe("attemptDeltaUpgrade", () => { ]; mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } diff --git a/packages/cli/test/lib/delta-upgrade.test.ts b/packages/cli/test/lib/delta-upgrade.test.ts index ded31bbea..898f4bc88 100644 --- a/packages/cli/test/lib/delta-upgrade.test.ts +++ b/packages/cli/test/lib/delta-upgrade.test.ts @@ -40,7 +40,7 @@ import { resolveStableDelta, validateChainStep, } from "../../src/lib/delta-upgrade.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import type { OciManifest } from "../../src/lib/ghcr.js"; import { useTestConfigDir } from "../helpers.js"; @@ -835,10 +835,10 @@ describe("fetchRecentReleases", () => { ]; mockFetch(async (url) => { - expect(fetchInputUrl(url)).toContain( + expect(extractInputUrl(url)).toContain( "api.github.com/repos/getsentry/toolkit/releases", ); - expect(fetchInputUrl(url)).toContain("per_page="); + expect(extractInputUrl(url)).toContain("per_page="); return new Response(JSON.stringify(releases), { status: 200 }); }); @@ -872,7 +872,7 @@ describe("fetchRecentReleases", () => { test("uses the selected legacy GitHub repository", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(fetchInputUrl(url)); + urls.push(extractInputUrl(url)); return new Response(JSON.stringify([]), { status: 200 }); }); @@ -907,7 +907,7 @@ describe("downloadStablePatch", () => { const patchData = new Uint8Array([1, 2, 3, 4, 5]); mockFetch(async (url) => { - expect(fetchInputUrl(url)).toBe("https://example.com/patch.bin"); + expect(extractInputUrl(url)).toBe("https://example.com/patch.bin"); return new Response(patchData.buffer as ArrayBuffer, { status: 200, }); @@ -954,7 +954,7 @@ describe("resolveStableChain", () => { patches: Map, ): void { mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.startsWith("https://api.github.com/")) { return new Response(JSON.stringify(releases), { status: 200 }); } @@ -1005,7 +1005,7 @@ describe("resolveStableChain", () => { test("keeps stable resolution on the selected legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(fetchInputUrl(url)); + urls.push(extractInputUrl(url)); return new Response("Not Found", { status: 404 }); }); @@ -1152,7 +1152,7 @@ describe("resolveNightlyChain", () => { blobs: Map, ): void { mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "test-token" }), { @@ -1248,7 +1248,7 @@ describe("resolveNightlyChain", () => { test("keeps nightly resolution on the selected legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(fetchInputUrl(url)); + urls.push(extractInputUrl(url)); return new Response(JSON.stringify({ tags: [] }), { status: 200 }); }); @@ -1744,7 +1744,7 @@ describe("resolveNightlyDelta", () => { blobs: Map; }): void { mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); // Token exchange if (urlStr.includes("ghcr.io/token")) { @@ -1951,7 +1951,7 @@ describe("selected source affinity", () => { test("attemptDeltaUpgrade keeps stable requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(fetchInputUrl(url)); + urls.push(extractInputUrl(url)); return new Response("Not Found", { status: 404 }); }); const versionedDelta = await importDeltaUpgradeWithVersion("0.13.0"); @@ -1975,7 +1975,7 @@ describe("selected source affinity", () => { test("attemptDeltaUpgrade keeps nightly requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(fetchInputUrl(url)); + urls.push(extractInputUrl(url)); return new Response("Unauthorized", { status: 401 }); }); const versionedDelta = @@ -2000,7 +2000,7 @@ describe("selected source affinity", () => { test("prefetchStablePatches keeps requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(fetchInputUrl(url)); + urls.push(extractInputUrl(url)); return new Response("Not Found", { status: 404 }); }); const versionedDelta = await importDeltaUpgradeWithVersion("0.13.0"); @@ -2019,7 +2019,7 @@ describe("selected source affinity", () => { test("prefetchNightlyPatches keeps requests on the legacy source", async () => { const urls: string[] = []; mockFetch(async (url) => { - urls.push(fetchInputUrl(url)); + urls.push(extractInputUrl(url)); return new Response("Unauthorized", { status: 401 }); }); const versionedDelta = diff --git a/packages/cli/test/lib/ghcr.test.ts b/packages/cli/test/lib/ghcr.test.ts index 64a6abc14..bdba5f524 100644 --- a/packages/cli/test/lib/ghcr.test.ts +++ b/packages/cli/test/lib/ghcr.test.ts @@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { UPGRADE_SOURCES } from "../../src/lib/binary.js"; import { UpgradeError } from "../../src/lib/errors.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import { downloadLayerBlob, downloadNightlyBlob, @@ -81,7 +81,7 @@ afterEach(() => { describe("getAnonymousToken", () => { test("returns token from successful response", async () => { mockFetch(async (url) => { - expect(fetchInputUrl(url)).toContain( + expect(extractInputUrl(url)).toContain( `https://ghcr.io/token?scope=repository:${GHCR_REPO}:pull`, ); return new Response(JSON.stringify({ token: "test-token-abc" }), { @@ -96,7 +96,7 @@ describe("getAnonymousToken", () => { test("uses the selected source's GHCR repository", async () => { mockFetch(async (url) => { - expect(fetchInputUrl(url)).toContain( + expect(extractInputUrl(url)).toContain( "scope=repository:getsentry/toolkit:pull", ); return new Response(JSON.stringify({ token: "toolkit-token" }), { @@ -220,7 +220,7 @@ describe("fetchNightlyManifest", () => { let capturedHeaders: Record = {}; mockFetch(async (url, init) => { - expect(fetchInputUrl(url)).toContain( + expect(extractInputUrl(url)).toContain( `/v2/${GHCR_REPO}/manifests/${GHCR_TAG}`, ); capturedHeaders = Object.fromEntries( @@ -245,7 +245,7 @@ describe("fetchNightlyManifest", () => { test("uses the selected source's GHCR repository", async () => { const manifest = makeManifest(); mockFetch(async (url) => { - expect(fetchInputUrl(url)).toContain( + expect(extractInputUrl(url)).toContain( "/v2/getsentry/toolkit/manifests/nightly", ); return new Response(JSON.stringify(manifest), { status: 200 }); @@ -576,7 +576,7 @@ describe("fetchManifest", () => { const manifest = makeManifest(); mockFetch(async (url) => { - expect(fetchInputUrl(url)).toContain( + expect(extractInputUrl(url)).toContain( `/v2/${GHCR_REPO}/manifests/patch-0.13.0`, ); return new Response(JSON.stringify(manifest), { @@ -637,7 +637,7 @@ describe("listTags", () => { }); test("returns all tags when no prefix filter", async () => { mockFetch(async (url) => { - expect(fetchInputUrl(url)).toContain(`/v2/${GHCR_REPO}/tags/list`); + expect(extractInputUrl(url)).toContain(`/v2/${GHCR_REPO}/tags/list`); return new Response( JSON.stringify({ tags: ["nightly", "patch-0.13.0", "patch-0.14.0"] }), { status: 200 }, @@ -759,7 +759,7 @@ describe("downloadLayerBlob", () => { mockFetch(async (url) => { requestCount += 1; if (requestCount === 1) { - expect(fetchInputUrl(url)).toContain( + expect(extractInputUrl(url)).toContain( `/v2/${GHCR_REPO}/blobs/sha256:abc123`, ); return Response.redirect("https://blob.storage.azure.com/file", 307); diff --git a/packages/cli/test/lib/release-notes.test.ts b/packages/cli/test/lib/release-notes.test.ts index d4f1d2315..bd2445fa6 100644 --- a/packages/cli/test/lib/release-notes.test.ts +++ b/packages/cli/test/lib/release-notes.test.ts @@ -15,7 +15,7 @@ import { fetchRecentReleases, type GitHubRelease, } from "../../src/lib/delta-upgrade.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import { buildChangelogSummary, type ChangeCategory, @@ -317,7 +317,7 @@ describe("fetchChangelog source affinity", () => { test("fetches stable releases only from the explicitly selected Toolkit source", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(fetchInputUrl(input)); + requestedUrls.push(extractInputUrl(input)); return new Response( JSON.stringify([ makeRelease( @@ -404,7 +404,7 @@ describe("fetchChangelog source affinity", () => { test("rejects raw prefetched Toolkit releases without fetching", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(fetchInputUrl(input)); + requestedUrls.push(extractInputUrl(input)); return new Response("Unexpected", { status: 500 }); }); @@ -458,7 +458,7 @@ describe("fetchChangelog source affinity", () => { test("fetches stable releases only from the explicitly selected legacy source", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(fetchInputUrl(input)); + requestedUrls.push(extractInputUrl(input)); return new Response( JSON.stringify([ makeRelease( @@ -486,7 +486,7 @@ describe("fetchChangelog source affinity", () => { test("fetches nightly commits only from the explicitly selected Toolkit source", async () => { const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - requestedUrls.push(fetchInputUrl(input)); + requestedUrls.push(extractInputUrl(input)); return new Response( JSON.stringify([ { commit: { message: "fix: keep nightly stages affine" } }, diff --git a/packages/cli/test/lib/sentry-client.invalidation.test.ts b/packages/cli/test/lib/sentry-client.invalidation.test.ts index eaacf057e..9b5c4d224 100644 --- a/packages/cli/test/lib/sentry-client.invalidation.test.ts +++ b/packages/cli/test/lib/sentry-client.invalidation.test.ts @@ -10,7 +10,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { setAuthToken } from "../../src/lib/db/auth.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import { getCachedResponse, storeCachedResponse, @@ -83,7 +83,7 @@ describe("HTTP-layer auto-invalidation", () => { installMockFetch(async (input, init) => { expect(init?.method).toBe("PUT"); - expect(fetchInputUrl(input)).toBe(DETAIL_URL); + expect(extractInputUrl(input)).toBe(DETAIL_URL); return makeResponse({ id: "12345", status: "resolved" }); }); const response = await runAuthenticatedFetch(DETAIL_URL, "PUT"); diff --git a/packages/cli/test/lib/upgrade.test.ts b/packages/cli/test/lib/upgrade.test.ts index 5be943edc..e9144caf0 100644 --- a/packages/cli/test/lib/upgrade.test.ts +++ b/packages/cli/test/lib/upgrade.test.ts @@ -127,7 +127,7 @@ import { setInstallInfo, } from "../../src/lib/db/install-info.js"; import { UpgradeError } from "../../src/lib/errors.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import { isProcessRunning } from "../../src/lib/process-utils.js"; const { @@ -194,7 +194,7 @@ describe("fetchLatestFromGitHub", () => { test("selects the first CLI release after sentry-mcp is renamed to Toolkit", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if (request.endsWith("/releases?per_page=100")) { return Response.json([ @@ -225,7 +225,7 @@ describe("fetchLatestFromGitHub", () => { test("selects the latest CLI-prefixed Toolkit release", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response( JSON.stringify([ { tag_name: "mcp@9.0.0" }, @@ -247,7 +247,7 @@ describe("fetchLatestFromGitHub", () => { test("follows Toolkit release pagination to find the latest CLI release", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); if (requests.length === 1) { return new Response( JSON.stringify( @@ -319,7 +319,7 @@ describe("fetchLatestFromGitHub", () => { test("rejects GitHub release pagination outside the selected source", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(JSON.stringify([{ tag_name: "mcp@9.0.0" }]), { status: 200, headers: { @@ -354,7 +354,7 @@ describe("fetchLatestFromGitHub", () => { test("rejects cyclic GitHub release pagination", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(JSON.stringify([{ tag_name: "mcp@9.0.0" }]), { status: 200, headers: { @@ -372,7 +372,7 @@ describe("fetchLatestFromGitHub", () => { test("falls back to the legacy latest release only on Toolkit HTTP 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); if (requests.length === 1) { return new Response("Not Found", { status: 404 }); } @@ -414,7 +414,7 @@ describe("fetchLatestFromGitHub", () => { test("does not use an MCP release as the latest CLI release", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(JSON.stringify([{ tag_name: "mcp@9.0.0" }]), { status: 200, }); @@ -747,7 +747,7 @@ describe("fetchLatestVersion", () => { test("uses GHCR manifest when channel is nightly (curl method)", async () => { // Nightly version is now fetched from GHCR manifest annotation, not version.json mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -774,7 +774,7 @@ describe("fetchLatestVersion", () => { test("uses GHCR manifest when channel is nightly (npm method)", async () => { // Even npm method uses GHCR when channel=nightly (nightly is curl-only distribution) mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -887,7 +887,7 @@ describe("versionExists", () => { test("probes prefixed Toolkit tags and retains the selected source", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(JSON.stringify({ tag_name: "cli@1.0.0" }), { status: 200, }); @@ -902,7 +902,7 @@ describe("versionExists", () => { test("falls back to an unprefixed legacy tag on Toolkit HTTP 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return requests.length === 1 ? new Response("Not Found", { status: 404 }) : new Response(JSON.stringify({ tag_name: "1.0.0" }), { status: 200 }); @@ -937,7 +937,7 @@ describe("versionExists", () => { async (_name, body) => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response(body, { status: 200 }); }); @@ -957,7 +957,7 @@ describe("versionExists", () => { async (annotation) => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -991,7 +991,7 @@ describe("versionExists", () => { test("does not fall back from an explicit selected source", async () => { const requests: string[] = []; mockFetch(async (url) => { - requests.push(fetchInputUrl(url)); + requests.push(extractInputUrl(url)); return new Response("Not Found", { status: 404 }); }); @@ -1124,7 +1124,7 @@ describe("versionExists", () => { annotations: { version: "0.14.0-dev.1772661724" }, }; mockFetch(async (url) => { - const u = fetchInputUrl(url); + const u = extractInputUrl(url); if (u === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1143,7 +1143,7 @@ describe("versionExists", () => { test("checks GHCR for nightly version - version does not exist", async () => { mockFetch(async (url) => { - const u = fetchInputUrl(url); + const u = extractInputUrl(url); if (u === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1167,7 +1167,7 @@ describe("versionExists", () => { annotations: { version: "0.14.0-dev.1772661724" }, }; mockFetch(async (url) => { - const u = fetchInputUrl(url); + const u = extractInputUrl(url); if (u === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -1186,7 +1186,7 @@ describe("versionExists", () => { test("rejects a mismatched nightly annotation for an explicit source", async () => { mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); if (request.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); } @@ -1221,7 +1221,7 @@ describe("versionExists", () => { test("does not classify nightly transport error text as not found", async () => { mockFetch(async (url) => { - if (fetchInputUrl(url).includes("ghcr.io/token")) { + if (extractInputUrl(url).includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); } throw new Error("HTTP 404"); @@ -1234,7 +1234,7 @@ describe("versionExists", () => { test("throws on GHCR server error for nightly version", async () => { mockFetch(async (url) => { - const u = fetchInputUrl(url); + const u = extractInputUrl(url); if (u.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); } @@ -1249,7 +1249,7 @@ describe("versionExists", () => { test("does not classify GHCR HTTP 403 as a missing nightly version", async () => { const requests: string[] = []; mockFetch(async (url) => { - const urlString = fetchInputUrl(url); + const urlString = extractInputUrl(url); requests.push(urlString); if (urlString.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); @@ -2085,7 +2085,7 @@ describe("fetchLatestNightlyVersion", () => { test("falls back to legacy when the Toolkit nightly manifest returns 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -2130,7 +2130,7 @@ describe("fetchLatestNightlyVersion", () => { test("does not fall back from a non-404 Toolkit nightly failure", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -2152,7 +2152,7 @@ describe("fetchLatestNightlyVersion", () => { test("does not fall back when nightly transport error text says HTTP 404", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if (request === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); @@ -2178,7 +2178,7 @@ describe("fetchLatestNightlyVersion", () => { // Mock the two requests: token exchange + manifest fetch let callCount = 0; mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -2214,7 +2214,7 @@ describe("fetchLatestNightlyVersion", () => { test("throws UpgradeError when GHCR token exchange fails", async () => { mockFetch(async (url) => - fetchInputUrl(url) === "https://api.github.com/repos/getsentry/toolkit" + extractInputUrl(url) === "https://api.github.com/repos/getsentry/toolkit" ? new Response(null, { status: 200 }) : new Response("Unauthorized", { status: 401 }), ); @@ -2227,7 +2227,7 @@ describe("fetchLatestNightlyVersion", () => { test("throws UpgradeError when manifest has no version annotation", async () => { mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr === "https://api.github.com/repos/getsentry/toolkit") { return new Response(null, { status: 200 }); } @@ -2298,7 +2298,7 @@ describe("executeUpgrade with curl method (nightly)", () => { // Mock: token exchange + manifest + blob (200 with gzipped content) mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200, @@ -2348,7 +2348,7 @@ describe("executeUpgrade with curl method (nightly)", () => { test("rejects a mismatched versioned manifest before downloading its blob", async () => { const requests: string[] = []; mockFetch(async (url) => { - const request = fetchInputUrl(url); + const request = extractInputUrl(url); requests.push(request); if (request.includes("ghcr.io/token")) { return new Response(JSON.stringify({ token: "tok" }), { status: 200 }); @@ -2485,7 +2485,7 @@ describe("downloadBinaryToTemp verifies download integrity (CLI-1D3)", () => { // with "Executable not found in $PATH" (the CLI-1D3 symptom). const emptyGzip = gzipSync(new Uint8Array(0)); mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.endsWith(".gz")) { return new Response(emptyGzip, { status: 200 }); } @@ -2507,7 +2507,7 @@ describe("downloadBinaryToTemp verifies download integrity (CLI-1D3)", () => { test("releases the download lock when verification fails", async () => { const emptyGzip = gzipSync(new Uint8Array(0)); mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.endsWith(".gz")) { return new Response(emptyGzip, { status: 200 }); } @@ -2539,7 +2539,7 @@ describe("downloadBinaryToTemp verifies download integrity (CLI-1D3)", () => { const mockBinaryContent = new Uint8Array([0x7f, 0x45, 0x4c, 0x46]); // ELF const emptyGzip = gzipSync(new Uint8Array(0)); mockFetch(async (url) => { - const urlStr = fetchInputUrl(url); + const urlStr = extractInputUrl(url); if (urlStr.endsWith(".gz")) { return new Response(emptyGzip, { status: 200 }); } diff --git a/packages/cli/test/lib/utils.test.ts b/packages/cli/test/lib/utils.test.ts index 6f78d5df5..84da5c5eb 100644 --- a/packages/cli/test/lib/utils.test.ts +++ b/packages/cli/test/lib/utils.test.ts @@ -8,7 +8,12 @@ */ import { describe, expect, test } from "vitest"; -import { isAllDigits, parseHttpUrl, slugify } from "../../src/lib/utils.js"; +import { + countChar, + isAllDigits, + parseHttpUrl, + slugify, +} from "../../src/lib/utils.js"; describe("slugify", () => { describe("JSDoc examples (canonical alignment)", () => { @@ -95,6 +100,18 @@ describe("slugify", () => { }); }); +describe("countChar", () => { + test("counts every occurrence", () => { + expect(countChar("org/project/rule", "/")).toBe(2); + expect(countChar("//a//", "/")).toBe(4); + }); + + test("returns 0 when the character is absent", () => { + expect(countChar("rule-name", "/")).toBe(0); + expect(countChar("", "/")).toBe(0); + }); +}); + describe("isAllDigits", () => { test("returns true for pure digits", () => { expect(isAllDigits("0")).toBe(true); diff --git a/packages/cli/test/lib/version-check.test.ts b/packages/cli/test/lib/version-check.test.ts index 80ff8d5c2..d57dab7df 100644 --- a/packages/cli/test/lib/version-check.test.ts +++ b/packages/cli/test/lib/version-check.test.ts @@ -17,7 +17,7 @@ import { ContextError, ValidationError, } from "../../src/lib/errors.js"; -import { fetchInputUrl } from "../../src/lib/fetch-input.js"; +import { extractInputUrl } from "../../src/lib/fetch-input.js"; import { abortPendingVersionCheck, getErrorUpdateNotification, @@ -517,7 +517,7 @@ describe("maybeCheckForUpdateInBackground", () => { const toolkitSource = UPGRADE_SOURCES[0]!; const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - const url = fetchInputUrl(input); + const url = extractInputUrl(input); requestedUrls.push(url); if (url.includes("getsentry/cli")) { throw new Error(`Unexpected legacy access: ${url}`); @@ -549,7 +549,7 @@ describe("maybeCheckForUpdateInBackground", () => { const legacySource = UPGRADE_SOURCES[1]!; const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - const url = fetchInputUrl(input); + const url = extractInputUrl(input); requestedUrls.push(url); if (url.includes("getsentry/toolkit")) { return new Response("not found", { status: 404 }); @@ -587,7 +587,7 @@ describe("maybeCheckForUpdateInBackground", () => { const toolkitSource = UPGRADE_SOURCES[0]!; const requestedUrls: string[] = []; globalThis.fetch = mockFetch(async (input) => { - const url = fetchInputUrl(input); + const url = extractInputUrl(input); requestedUrls.push(url); if (url.includes("getsentry/cli")) { throw new Error(`Unexpected legacy access: ${url}`); diff --git a/packages/mcp-cloudflare/src/server/routes/chat.ts b/packages/mcp-cloudflare/src/server/routes/chat.ts index 4c827dc51..fc62a0e24 100644 --- a/packages/mcp-cloudflare/src/server/routes/chat.ts +++ b/packages/mcp-cloudflare/src/server/routes/chat.ts @@ -260,7 +260,7 @@ export default new Hono<{ Bindings: Env }>().post("/", async (c) => { // Get available tools from MCP server Object.assign(tools, await mcpClient.tools()); - logInfo(`Connected to ${mcpUrl.href}`, { + logInfo(`Connected to ${mcpUrl}`, { loggerScope: ["cloudflare", "chat", "connection"], extra: { toolCount: Object.keys(tools).length, @@ -295,7 +295,7 @@ export default new Hono<{ Bindings: Env }>().post("/", async (c) => { }); Object.assign(tools, await mcpClient.tools()); - logInfo(`Connected to ${mcpUrl.href} (after refresh)`, { + logInfo(`Connected to ${mcpUrl} (after refresh)`, { loggerScope: ["cloudflare", "chat", "connection"], extra: { toolCount: Object.keys(tools).length, diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 54e2a7c23..09f675447 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -2329,7 +2329,7 @@ export class SentryApiService { ): Promise { const query = new URLSearchParams({ alert_rule_id: String(alertRuleId) }); const body = await this.requestJSON( - `${apiPath`/organizations/${organizationSlug}/alert-rule-detector/`}?${query.toString()}`, + `${apiPath`/organizations/${organizationSlug}/alert-rule-detector/`}?${query}`, undefined, opts, ); @@ -2442,7 +2442,7 @@ export class SentryApiService { if (query) search.set("query", query); if (cursor) search.set("cursor", cursor); const response = await this.request( - `${apiPath`/organizations/${organizationSlug}/detectors/`}?${search.toString()}`, + `${apiPath`/organizations/${organizationSlug}/detectors/`}?${search}`, undefined, opts, ); @@ -2474,7 +2474,7 @@ export class SentryApiService { } if (cursor) search.set("cursor", cursor); const response = await this.request( - `${apiPath`/organizations/${organizationSlug}/available-actions/`}?${search.toString()}`, + `${apiPath`/organizations/${organizationSlug}/available-actions/`}?${search}`, undefined, opts, ); @@ -2506,7 +2506,7 @@ export class SentryApiService { const search = new URLSearchParams({ group, per_page: String(limit) }); if (cursor) search.set("cursor", cursor); const response = await this.request( - `${apiPath`/organizations/${organizationSlug}/data-conditions/`}?${search.toString()}`, + `${apiPath`/organizations/${organizationSlug}/data-conditions/`}?${search}`, undefined, opts, ); diff --git a/packages/mcp-core/src/tools/catalog/get-replay-details.ts b/packages/mcp-core/src/tools/catalog/get-replay-details.ts index b79d86976..7240d1c5e 100644 --- a/packages/mcp-core/src/tools/catalog/get-replay-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-replay-details.ts @@ -579,12 +579,12 @@ function summarizeObject( const entries = Object.entries(value) .filter( - ([key, nested]) => - !excludedKeys.has(key) && - (typeof nested === "string" || typeof nested === "number"), + (entry): entry is [string, string | number] => + !excludedKeys.has(entry[0]) && + (typeof entry[1] === "string" || typeof entry[1] === "number"), ) .slice(0, 3) - .map(([key, nested]) => `${key}=${String(nested)}`); + .map(([key, nested]) => `${key}=${nested}`); return entries.length > 0 ? `payload=${quoteDetail(entries.join(", "))}` diff --git a/packages/mcp-core/src/tools/catalog/update-issue.ts b/packages/mcp-core/src/tools/catalog/update-issue.ts index bdfc0dbc6..30daf6adb 100644 --- a/packages/mcp-core/src/tools/catalog/update-issue.ts +++ b/packages/mcp-core/src/tools/catalog/update-issue.ts @@ -55,6 +55,15 @@ type IgnoreState = { message: string; }; +// `string & {}` keeps the known literals in autocomplete without collapsing +// the union to plain `string`. +type IssueStatusDisplay = + | "resolved" + | "resolvedInNextRelease" + | "unresolved" + | "ignored" + | (string & {}); + type IgnoreParams = { status?: string; ignoreMode?: IgnoreMode; @@ -155,12 +164,7 @@ function formatIgnoreUntil(ignoreUntil: string): string { return `${date.toISOString().slice(0, 19).replace("T", " ")} UTC`; } -/** - * Issue status for display: `resolvedInNextRelease` for next-release - * resolutions, otherwise the raw API status (`resolved`, `unresolved`, - * `ignored`, ...). - */ -function getIssueStatusDisplay(issue: Issue): string { +function getIssueStatusDisplay(issue: Issue): IssueStatusDisplay { const statusDetails = getIssueStatusDetails(issue); if (issue.status === "resolved" && statusDetails.inNextRelease === true) { diff --git a/packages/mcp-test-client/src/mcp-test-client-remote.ts b/packages/mcp-test-client/src/mcp-test-client-remote.ts index 8d3db54bd..fa7a69490 100644 --- a/packages/mcp-test-client/src/mcp-test-client-remote.ts +++ b/packages/mcp-test-client/src/mcp-test-client-remote.ts @@ -88,7 +88,7 @@ export async function connectToRemoteMCPServer( span.setStatus({ code: 1 }); logSuccess( - `Connected to MCP server (${mcpUrl.href})`, + `Connected to MCP server (${mcpUrl})`, `${tools.size} tools available`, ); From 0394a377f95452125759f41060f29ffc6b1496af Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:37:43 +0000 Subject: [PATCH 3/3] refactor(lint): Restore String() fallback and route response unions Per review: keep String() in stringifyUnknown's JSON failure fallback so custom toString() output survives, and keep the `unknown | Handler` unions in the test mock types for documentation. Both use targeted lint disables with reasons. --- packages/cli/src/lib/errors.ts | 5 +++-- packages/cli/test/helpers.ts | 13 +++++++------ packages/cli/test/mocks/server.ts | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index 487761345..0a7230b55 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -783,12 +783,13 @@ export function stringifyUnknown(value: unknown): string { } if (value && typeof value === "object") { // JSON.stringify can throw on circular references or BigInt values. - // Fall back to the tag string (e.g. `[object Object]`), which is always safe. + // Fall back to String() which is always safe. // oxlint-disable-next-line sentry-cli/no-silent-catch -- grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { return JSON.stringify(value); } catch { - return Object.prototype.toString.call(value); + // oxlint-disable-next-line typescript/no-base-to-string -- keep custom toString() output when JSON fails + return String(value); } } return String(value); diff --git a/packages/cli/test/helpers.ts b/packages/cli/test/helpers.ts index e820218a5..80ca43167 100644 --- a/packages/cli/test/helpers.ts +++ b/packages/cli/test/helpers.ts @@ -236,16 +236,17 @@ export function extractFetchUrl(input: RequestInfo | URL): string { // --------------------------------------------------------------------------- /** A single route handler for the fetch mock. */ -/** Dynamic fetch route response; may return the body or a promise of it. */ -export type FetchRouteHandler = (url: string, init?: RequestInit) => unknown; - export type FetchRoute = { /** URL pattern — string for `includes()` match, or RegExp */ match: string | RegExp; /** HTTP method filter (default: any) */ method?: string; - /** Response body (JSON-serialized) or a {@link FetchRouteHandler} */ - response: unknown; + /* oxlint-disable typescript/no-redundant-type-constituents -- documents the accepted shapes */ + /** Response body (JSON-serialized) or a handler function */ + response: + | unknown + | ((url: string, init?: RequestInit) => unknown | Promise); + /* oxlint-enable typescript/no-redundant-type-constituents */ /** HTTP status code (default: 200) */ status?: number; /** Response headers */ @@ -319,7 +320,7 @@ export function installFetchMock( const status = route.status ?? 200; const responseBody = typeof route.response === "function" - ? await (route.response as FetchRouteHandler)(url, init) + ? await route.response(url, init) : route.response; const headers: Record = { diff --git a/packages/cli/test/mocks/server.ts b/packages/cli/test/mocks/server.ts index 2a340ca67..035e813b6 100644 --- a/packages/cli/test/mocks/server.ts +++ b/packages/cli/test/mocks/server.ts @@ -17,8 +17,9 @@ export type MockRoute = { method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; /** Path pattern with optional :params (e.g., "/api/0/organizations/:orgSlug/") */ path: string; - /** Static response body, or a {@link RouteHandler} for dynamic responses */ - response: unknown; + /** Static response body, or handler function for dynamic responses */ + // oxlint-disable-next-line typescript/no-redundant-type-constituents -- documents the accepted shapes + response: unknown | RouteHandler; /** HTTP status code (default: 200) */ status?: number; }; @@ -32,8 +33,8 @@ type CompiledRoute = { method: string; pattern: RegExp; paramNames: string[]; - /** Static response body or {@link RouteHandler} */ - response: unknown; + // oxlint-disable-next-line typescript/no-redundant-type-constituents -- documents the accepted shapes + response: unknown | RouteHandler; status: number; };