diff --git a/src/lib/init/ui/ink-ui.ts b/src/lib/init/ui/ink-ui.ts index 8e8d0a168..c9f032b23 100644 --- a/src/lib/init/ui/ink-ui.ts +++ b/src/lib/init/ui/ink-ui.ts @@ -48,6 +48,7 @@ import { openSync } from "node:fs"; import { ReadStream } from "node:tty"; +import { setTag } from "@sentry/node-core/light"; import { CLI_VERSION } from "../../constants.js"; import { stripAnsi } from "../../formatters/plain-detect.js"; import { formatFeedbackHint, type InitFeedbackOutcome } from "../feedback.js"; @@ -817,12 +818,16 @@ export class InkUI implements WizardUI { if (this.cancelRequested) { // Safety valve: teardown already started but hasn't finished // (or something is stuck). Force-exit so the user isn't trapped. + setTag("wizard.outcome", "abandoned"); process.exit(130); } this.cancelRequested = true; this.failureMessage = "Setup cancelled."; this.feedback("cancelled"); this.tearDown(); + // Mark as abandoned before exit so the Sentry span carries the + // outcome even though beforeExit never fires for explicit process.exit(). + setTag("wizard.outcome", "abandoned"); // Match the SIGINT convention so shells (and CI) see a // distinguishable exit. The runner's `await using` won't get a // chance to run after this, but tearDown above already did all diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 300e0b683..0dc35f0fc 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -396,6 +396,7 @@ async function preamble( } catch (err) { if (err instanceof WizardCancelledError) { captureException(err); + setTag("wizard.outcome", "bailed"); showCancelledFeedback(ui); process.exitCode = 0; return false; @@ -409,6 +410,7 @@ async function preamble( throw err; } if (!confirmed) { + setTag("wizard.outcome", "bailed"); showCancelledFeedback(ui); process.exitCode = 0; return false; @@ -424,6 +426,7 @@ async function preamble( ui, }); if (!gitOk) { + setTag("wizard.outcome", "bailed"); showCancelledFeedback(ui); process.exitCode = 0; return false; @@ -608,6 +611,7 @@ export async function runWizard(initialOptions: WizardOptions): Promise { : initialOptions; const context = await resolveInitContext(effectiveOptions, ui); if (!context) { + setTag("wizard.outcome", "bailed"); return; } diff --git a/test/lib/init/wizard-runner.test.ts b/test/lib/init/wizard-runner.test.ts index 85401b934..343521a15 100644 --- a/test/lib/init/wizard-runner.test.ts +++ b/test/lib/init/wizard-runner.test.ts @@ -453,6 +453,19 @@ describe("runWizard", () => { expect(formatResultSpy).toHaveBeenCalled(); }); + test("aborts cleanly when user declines the experimental prompt", async () => { + const { ui, calls, respond } = createMockUI(); + respond.select("exit"); + useMockUI(ui, calls); + + await forceStdinTty(() => runWizard(makeOptions({ yes: false }))); + + expect(process.exitCode).toBe(0); + expect(lastCancelMessage()).toBe("Setup cancelled."); + expect(lastFeedbackOutcome()).toBe("cancelled"); + expect(getWorkflowSpy).not.toHaveBeenCalled(); + }); + test("stops before workflow creation when preflight returns null", async () => { resolveInitContextSpy.mockResolvedValue(null); diff --git a/test/lib/resolve-target.test.ts b/test/lib/resolve-target.test.ts index b7b2fba7e..49350279d 100644 --- a/test/lib/resolve-target.test.ts +++ b/test/lib/resolve-target.test.ts @@ -390,7 +390,7 @@ describe("fetchProjectId", () => { }) ); - expect(fetchProjectId("test-org", "test-project")).rejects.toThrow( + await expect(fetchProjectId("test-org", "test-project")).rejects.toThrow( ResolutionError ); }); diff --git a/test/lib/sentryclirc-import.property.test.ts b/test/lib/sentryclirc-import.property.test.ts index ebff8fdbf..215ade271 100644 --- a/test/lib/sentryclirc-import.property.test.ts +++ b/test/lib/sentryclirc-import.property.test.ts @@ -132,11 +132,18 @@ describe("property: maskToken", () => { }); test("output never equals the original input", () => { + // Tokens that are entirely asterisks are already fully masked, so + // maskToken cannot meaningfully change them — exclude that edge case. fcAssert( - property(string({ minLength: 1, maxLength: 100 }), (token) => { - const masked = maskToken(token); - expect(masked).not.toBe(token); - }), + property( + string({ minLength: 1, maxLength: 100 }).filter( + (t) => !/^\*+$/.test(t) + ), + (token) => { + const masked = maskToken(token); + expect(masked).not.toBe(token); + } + ), { numRuns: DEFAULT_NUM_RUNS } ); });