Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions packages/cli/src/lib/init/wizard-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,39 @@ function syncWorkflowStepStatuses(
}
}

type WorkflowFailure = {
message: string;
resultForDisplay: WorkflowRunResult;
workflowCode: number | undefined;
};

function getWorkflowFailure(
result: WorkflowRunResult
): WorkflowFailure | undefined {
const workflowCode = result.result?.exitCode;
if (result.status === "success" && workflowCode === 0) {
return;
}

const missingExitCodeMessage =
result.status === "success" && workflowCode === undefined
? "Workflow reported success without an explicit exit code"
: undefined;
const message =
missingExitCodeMessage ??
result.error ??
result.result?.message ??
"Workflow returned an error";

return {
message,
resultForDisplay: missingExitCodeMessage
? { ...result, error: message }
: result,
workflowCode,
};
}

// biome-ignore lint/nursery/useMaxParams: cwd and sentryProject are optional trailing extensions
export async function handleFinalResult(
result: WorkflowRunResult,
Expand All @@ -1352,26 +1385,22 @@ export async function handleFinalResult(
cwd?: string,
sentryProject?: SentryProjectIdentity
): Promise<void> {
const hasError = result.status !== "success" || result.result?.exitCode;
const failure = getWorkflowFailure(result);

if (hasError) {
if (failure) {
if (spinState.running) {
spin.stop("Failed", 1);
spinState.running = false;
}
formatError(result, ui);
formatError(failure.resultForDisplay, ui);

// Map workflow-internal exit codes to semantic EXIT.* constants
const workflowCode = result.result?.exitCode;
const exitCode = mapWorkflowExitCode(workflowCode);
const exitCode = mapWorkflowExitCode(failure.workflowCode);
setTag("wizard.outcome", "errored");
if (workflowCode !== undefined) {
setTag("wizard.exit_code", workflowCode);
if (failure.workflowCode !== undefined) {
setTag("wizard.exit_code", failure.workflowCode);
}
throw new WizardError(
result.error ?? result.result?.message ?? "Workflow returned an error",
{ exitCode }
);
throw new WizardError(failure.message, { exitCode });
}

// Run verification before printing the final summary so the user
Expand Down
72 changes: 48 additions & 24 deletions packages/cli/test/lib/init/wizard-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ beforeEach(() => {
savedPlainOutput = process.env.SENTRY_PLAIN_OUTPUT;
process.env.SENTRY_PLAIN_OUTPUT = "0";

mockStartResult = { status: "success", result: { platform: "React" } };
mockStartResult = {
status: "success",
result: { exitCode: 0, platform: "React" },
};
mockResumeResults = [];
resumeCallCount = 0;
mockRunByIdResult = new Error("runById not configured");
Expand Down Expand Up @@ -236,6 +239,7 @@ beforeEach(() => {
sharedResumeAsyncMock = vi.fn(() => {
const result = mockResumeResults[resumeCallCount] ?? {
status: "success",
result: { exitCode: 0 },
};
resumeCallCount += 1;
return Promise.resolve(result);
Expand Down Expand Up @@ -610,7 +614,7 @@ describe("runWizard", () => {
"apply-codemods": { suspendPayload: payload },
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -639,7 +643,7 @@ describe("runWizard", () => {
"apply-codemods": { suspendPayload: protocolPayload },
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -672,7 +676,7 @@ describe("runWizard", () => {
},
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -710,7 +714,7 @@ describe("runWizard", () => {
},
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand All @@ -736,7 +740,7 @@ describe("runWizard", () => {
},
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions({ dryRun: true }));

Expand Down Expand Up @@ -907,7 +911,7 @@ describe("runWizard", () => {
},
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -979,7 +983,7 @@ describe("runWizard", () => {
message: "Using existing project",
data: {},
});
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -1013,7 +1017,7 @@ describe("runWizard", () => {
},
};
executeToolSpy.mockResolvedValue({ ok: true, data: identity });
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -1132,7 +1136,9 @@ describe("runWizard — MastraClient lifecycle", () => {
createRun: vi.fn(() =>
Promise.resolve({
startAsync: startAsyncMock,
resumeAsync: vi.fn(() => Promise.resolve({ status: "success" })),
resumeAsync: vi.fn(() =>
Promise.resolve({ status: "success", result: { exitCode: 0 } })
),
})
),
} as any;
Expand All @@ -1149,6 +1155,15 @@ describe("runWizard — MastraClient lifecycle", () => {
// ─── Additional coverage tests ───────────────────────────────────────────────

describe("runWizard — workflow exit codes", () => {
test("rejects workflow success without an explicit exit code", async () => {
mockStartResult = { status: "success", result: { platform: "React" } };

const error = await runWizard(makeOptions()).catch((caught) => caught);

expect(error).toBeInstanceOf(WizardError);
expect((error as WizardError).exitCode).not.toBe(0);
});

// handleFinalResult calls mapWorkflowExitCode when the workflow result
// carries a non-zero exitCode. Each case maps a server-internal code to
// the CLI's semantic EXIT constant.
Expand Down Expand Up @@ -1270,7 +1285,7 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
let capturedResume: Record<string, unknown> | undefined;
makeStaleStepRun((args) => {
capturedResume = args.resumeData as Record<string, unknown>;
return Promise.resolve({ status: "success" });
return Promise.resolve({ status: "success", result: { exitCode: 0 } });
});

await runWizard(makeOptions());
Expand Down Expand Up @@ -1299,7 +1314,10 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
status: "suspended",
suspendPayload: { ...protocolPayload, detail: "new display text" },
})
.mockResolvedValueOnce({ status: "success" });
.mockResolvedValueOnce({
status: "success",
result: { exitCode: 0 },
});
let resumeCount = 0;
makeStaleStepRun(() => {
resumeCount += 1;
Expand All @@ -1322,6 +1340,7 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
};
const currentRunState: WorkflowRunResult = {
status: "success",
result: { exitCode: 0 },
suspended: [],
};
runByIdMock.mockImplementation(
Expand All @@ -1335,7 +1354,7 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
if (resumeCount === 1) {
return Promise.reject(staleStepError(409));
}
return Promise.resolve({ status: "success" });
return Promise.resolve({ status: "success", result: { exitCode: 0 } });
});

await runWizard(makeOptions());
Expand Down Expand Up @@ -1368,6 +1387,7 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
};
runByIdMock.mockResolvedValue({
status: "success",
result: { exitCode: 0 },
suspended: [],
});
let resumeCount = 0;
Expand All @@ -1394,7 +1414,10 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
status: "suspended",
suspendPayload: toolPayload,
})
.mockResolvedValueOnce({ status: "success" });
.mockResolvedValueOnce({
status: "success",
result: { exitCode: 0 },
});

let resumeCount = 0;
makeStaleStepRun(() => {
Expand Down Expand Up @@ -1445,7 +1468,7 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
if (resumeCount === 1) {
return Promise.reject(staleStepError());
}
return Promise.resolve({ status: "success" });
return Promise.resolve({ status: "success", result: { exitCode: 0 } });
});

await runWizard(makeOptions());
Expand Down Expand Up @@ -1518,7 +1541,7 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
suspended: [["tool-step"]],
steps: { "tool-step": { suspendPayload: toolPayload } },
};
mockRunByIdResult = { status: "success" };
mockRunByIdResult = { status: "success", result: { exitCode: 0 } };

let resumeCount = 0;
makeStaleStepRun(() => {
Expand Down Expand Up @@ -1606,7 +1629,7 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => {
steps: { "apply-codemods": { suspendPayload: applyPayload } },
});
}
return Promise.resolve({ status: "success" });
return Promise.resolve({ status: "success", result: { exitCode: 0 } });
});

await runWizard(makeOptions());
Expand Down Expand Up @@ -1793,7 +1816,7 @@ describe("runWizard — additional coverage", () => {
"step-b": { suspendPayload: payload },
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await expect(runWizard(makeOptions())).rejects.toThrow(WizardError);

Expand All @@ -1813,7 +1836,7 @@ describe("runWizard — additional coverage", () => {
"step-b": { suspendPayload: payload },
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -1845,7 +1868,7 @@ describe("runWizard — additional coverage", () => {
suspended: [["detect-platform"]],
steps: { "detect-platform": { suspendPayload: payloadB } },
},
{ status: "success" },
{ status: "success", result: { exitCode: 0 } },
];

await runWizard(makeOptions());
Expand Down Expand Up @@ -1900,6 +1923,7 @@ describe("runWizard — additional coverage", () => {
mockResumeResults = [
{
status: "success",
result: { exitCode: 0 },
steps: {
"discover-context": { status: "success" },
"detect-platform": { status: "success" },
Expand Down Expand Up @@ -1986,7 +2010,7 @@ describe("runWizard — additional coverage", () => {
},
},
};
mockResumeResults = [{ status: "success" }];
mockResumeResults = [{ status: "success", result: { exitCode: 0 } }];

await runWizard(makeOptions());

Expand Down Expand Up @@ -2069,7 +2093,7 @@ describe("runWizard — progress rotation for long-running steps", () => {
).toBe(true);

// Resolve the resume and let the wizard finish
resolveResume({ status: "success" });
resolveResume({ status: "success", result: { exitCode: 0 } });
await vi.advanceTimersByTimeAsync(100);
await runPromise;
});
Expand Down Expand Up @@ -2126,7 +2150,7 @@ describe("runWizard — progress rotation for long-running steps", () => {
// After exhausting messages, should show elapsed time
expect(messages.some((m) => /\(\d+s\)/.test(m))).toBe(true);

resolveResume({ status: "success" });
resolveResume({ status: "success", result: { exitCode: 0 } });
await vi.advanceTimersByTimeAsync(100);
await runPromise;
});
Expand Down Expand Up @@ -2182,7 +2206,7 @@ describe("runWizard — progress rotation for long-running steps", () => {
// No new messages should have been added by the rotation timer
expect(messagesAfter).toBe(messagesBefore);

resolveResume({ status: "success" });
resolveResume({ status: "success", result: { exitCode: 0 } });
await vi.advanceTimersByTimeAsync(100);
await runPromise;
});
Expand Down
Loading
Loading