Files
openclaw/src/wizard/setup.model-auth.test.ts
Peter Steinberger 21d919deb8 fix(onboard): keep the wizard alive through provider auth failures and polish standalone install UX (#100632)
Fixes rough edges in the standalone install flow (install.sh -> openclaw onboard), found and verified by running the flow in a clean container and on a clean macOS Tahoe VM:

- Provider auth setup failures (e.g. the preselected "Anthropic Claude CLI" option on a host without a Claude CLI login) no longer kill the whole wizard. The interactive wizard notes the error and returns to the provider picker; explicit --auth-choice automation still fails fast.
- Onboarding config now persists before the channel/search/skills steps, so a crash or cancel during channel pairing no longer loses auth + gateway decisions.
- With model auth skipped, finalize no longer auto-sends the "Wake up, my friend!" message (which always failed with a provider auth error). The hatch seed is gated on usable model credentials and a "Model auth missing" note explains the next step.
- Search provider picker no longer labels non-key credentials (e.g. SearXNG base URL) as "API key required".
- install.sh no longer warns "PATH missing npm global bin dir" with manual fix steps after it already persisted the export line; it reports the PATH was updated and how to reload the current shell.
- Removed the dead interactive hooks onboarding step (setupInternalHooks); quickstart enables default hooks silently.

Verified live per fix in a clean Debian/Node 24 container and on a clean macOS 26.5 Parallels VM (wizard re-prompt, SearXNG label), plus wizard/onboard test suites and tsgo:core.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-07 04:11:23 +01:00

113 lines
3.6 KiB
TypeScript

// Regression tests: provider auth failures re-prompt instead of killing the wizard.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import { WizardCancelledError, type WizardPrompter } from "./prompts.js";
import { runSetupModelAuthStep } from "./setup.model-auth.js";
const applyAuthChoice = vi.hoisted(() => vi.fn());
const warnIfModelConfigLooksOff = vi.hoisted(() => vi.fn());
const resolvePreferredProviderForAuthChoice = vi.hoisted(() => vi.fn());
const promptDefaultModel = vi.hoisted(() => vi.fn());
const applyPrimaryModel = vi.hoisted(() => vi.fn((config: unknown) => config));
const promptAuthChoiceGrouped = vi.hoisted(() => vi.fn());
vi.mock("../commands/auth-choice.js", () => ({
applyAuthChoice,
warnIfModelConfigLooksOff,
resolvePreferredProviderForAuthChoice,
}));
vi.mock("../commands/model-picker.js", () => ({
applyPrimaryModel,
promptDefaultModel,
}));
vi.mock("../commands/auth-choice-prompt.js", () => ({
KEEP_CURRENT_AUTH_CHOICE: "__keep_current__",
promptAuthChoiceGrouped,
}));
vi.mock("../agents/auth-profiles.runtime.js", () => ({
ensureAuthProfileStore: vi.fn(() => ({ profiles: {} })),
}));
function createPrompter(): WizardPrompter {
return {
intro: vi.fn(),
outro: vi.fn(),
note: vi.fn(),
select: vi.fn(),
multiselect: vi.fn(),
text: vi.fn(),
confirm: vi.fn(),
progress: vi.fn(() => ({ stop: vi.fn(), update: vi.fn() })),
disableBackNavigation: vi.fn(),
} as unknown as WizardPrompter;
}
function createRuntime(): RuntimeEnv {
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv;
}
describe("runSetupModelAuthStep provider failures", () => {
beforeEach(() => {
vi.clearAllMocks();
promptDefaultModel.mockResolvedValue({});
warnIfModelConfigLooksOff.mockResolvedValue(undefined);
});
it("re-prompts after a provider setup error instead of aborting", async () => {
promptAuthChoiceGrouped.mockResolvedValueOnce("anthropic-cli").mockResolvedValueOnce("skip");
applyAuthChoice.mockRejectedValueOnce(
new Error("Claude CLI is not authenticated on this host."),
);
const prompter = createPrompter();
const result = await runSetupModelAuthStep({
config: {},
opts: {},
prompter,
runtime: createRuntime(),
workspaceDir: "/tmp/workspace",
});
expect(result).toEqual({});
expect(promptAuthChoiceGrouped).toHaveBeenCalledTimes(2);
expect(prompter.note).toHaveBeenCalledWith(
expect.stringContaining("Claude CLI is not authenticated on this host."),
"Provider setup failed",
);
});
it("still fails loudly when the auth choice came from a flag", async () => {
applyAuthChoice.mockRejectedValueOnce(
new Error("Claude CLI is not authenticated on this host."),
);
await expect(
runSetupModelAuthStep({
config: {},
opts: { authChoice: "anthropic-cli" },
prompter: createPrompter(),
runtime: createRuntime(),
workspaceDir: "/tmp/workspace",
}),
).rejects.toThrow("Claude CLI is not authenticated");
});
it("propagates wizard cancellation from provider setup", async () => {
promptAuthChoiceGrouped.mockResolvedValueOnce("anthropic-cli");
applyAuthChoice.mockRejectedValueOnce(new WizardCancelledError());
await expect(
runSetupModelAuthStep({
config: {},
opts: {},
prompter: createPrompter(),
runtime: createRuntime(),
workspaceDir: "/tmp/workspace",
}),
).rejects.toThrow(WizardCancelledError);
});
});