fix(ollama): retry unreachable setup in place (#116210)

This commit is contained in:
Vincent Koc
2026-07-30 12:23:56 +08:00
committed by GitHub
parent 4e5bf66fb1
commit 69f10e72f2
3 changed files with 251 additions and 28 deletions

View File

@@ -361,8 +361,43 @@ describe("ollama setup", () => {
expect(events).toEqual(["select", "text"]);
});
it("shows cloud-mode unreachable guidance when the host is down", async () => {
it("retries the configured host after showing unreachable guidance", async () => {
const prompter = createLocalPrompter();
prompter.confirm = vi.fn().mockResolvedValueOnce(true);
const reachableFetch = createOllamaFetchMock({ tags: ["qwen3:0.6b"] });
const fetchMock = vi
.fn()
.mockRejectedValueOnce(new Error("down"))
.mockImplementation(reachableFetch);
vi.stubGlobal("fetch", fetchMock);
const result = await promptAndConfigureOllama({
cfg: {},
prompter,
});
expect(prompter.note).toHaveBeenCalledWith(
[
"Ollama could not be reached at http://127.0.0.1:11434.",
"Start or restart the Ollama server for this address.",
"If Ollama is not installed on that machine, download it at https://ollama.com/download",
"",
"Continue when it is running. OpenClaw will retry this address.",
].join("\n"),
"Ollama",
);
expect(prompter.confirm).toHaveBeenCalledWith({
message: "Retry this Ollama address now?",
initialValue: true,
});
expect(result.config.models?.providers?.ollama?.models).toEqual(
expect.arrayContaining([expect.objectContaining({ id: "qwen3:0.6b" })]),
);
});
it("reports the configured host when the retry is still unreachable", async () => {
const prompter = createLocalPrompter();
prompter.confirm = vi.fn().mockResolvedValueOnce(true);
const fetchMock = createOllamaFetchMock({ tagsError: new Error("down") });
vi.stubGlobal("fetch", fetchMock);
@@ -371,17 +406,9 @@ describe("ollama setup", () => {
cfg: {},
prompter,
}),
).rejects.toThrow("Ollama not reachable");
).rejects.toThrow("Ollama is still not reachable at http://127.0.0.1:11434");
expect(prompter.note).toHaveBeenCalledWith(
[
"Ollama could not be reached at http://127.0.0.1:11434.",
"Download it at https://ollama.com/download",
"",
"Start Ollama and re-run setup.",
].join("\n"),
"Ollama",
);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it("cloud + local mode falls back to local models when ollama signin is missing", async () => {
@@ -1048,7 +1075,8 @@ describe("ollama setup", () => {
expect(runtime.error).toHaveBeenCalledWith(
[
"Ollama could not be reached at http://127.0.0.1:11435.",
"Download it at https://ollama.com/download",
"Start or restart the Ollama server for this address.",
"If Ollama is not installed on that machine, download it at https://ollama.com/download",
].join("\n"),
);
expect(runtime.exit).toHaveBeenCalledWith(1);

View File

@@ -96,12 +96,12 @@ const HOST_BACKED_OLLAMA_MODE_CONFIG: Record<
},
};
function buildOllamaUnreachableLines(baseUrl: string): string[] {
function buildOllamaUnreachableLines(baseUrl: string, retry: boolean): string[] {
return [
`Ollama could not be reached at ${baseUrl}.`,
"Download it at https://ollama.com/download",
"",
"Start Ollama and re-run setup.",
"Start or restart the Ollama server for this address.",
"If Ollama is not installed on that machine, download it at https://ollama.com/download",
...(retry ? ["", "Continue when it is running. OpenClaw will retry this address."] : []),
];
}
@@ -283,24 +283,41 @@ async function promptAndConfigureHostBackedOllama(params: {
signal?: AbortSignal;
}): Promise<OllamaSetupResult> {
const baseUrl = await promptForOllamaBaseUrl(params.prompter, params.env);
const {
reachable,
models,
inspectedModels,
discoveredModelsByName,
inspectionFailures,
hasToolsCapableModel,
} = await discoverOllamaModelsForSetup({
let discovery = await discoverOllamaModelsForSetup({
baseUrl,
inspectTools: true,
...(params.signal ? { signal: params.signal } : {}),
});
if (!reachable) {
await params.prompter.note(buildOllamaUnreachableLines(baseUrl).join("\n"), "Ollama");
throw new WizardCancelledError("Ollama not reachable");
if (!discovery.reachable) {
await params.prompter.note(buildOllamaUnreachableLines(baseUrl, true).join("\n"), "Ollama");
const shouldRetry = await params.prompter.confirm({
message: "Retry this Ollama address now?",
initialValue: true,
});
if (!shouldRetry) {
throw new WizardCancelledError("Ollama setup cancelled");
}
params.signal?.throwIfAborted();
discovery = await discoverOllamaModelsForSetup({
baseUrl,
inspectTools: true,
...(params.signal ? { signal: params.signal } : {}),
});
}
if (!discovery.reachable) {
throw new WizardCancelledError(`Ollama is still not reachable at ${baseUrl}`);
}
const {
models,
inspectedModels,
discoveredModelsByName,
inspectionFailures,
hasToolsCapableModel,
} = discovery;
if (inspectionFailures.length > 0) {
await params.prompter.note(
[
@@ -449,7 +466,7 @@ export async function configureOllamaNonInteractive(params: {
const explicitModel = normalizeOllamaModelName(params.opts.customModelId);
if (!reachable) {
params.runtime.error(buildOllamaUnreachableLines(baseUrl).slice(0, 2).join("\n"));
params.runtime.error(buildOllamaUnreachableLines(baseUrl, false).join("\n"));
params.runtime.exit(1);
return params.nextConfig;
}

View File

@@ -235,6 +235,184 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => {
}
});
it("recovers Ollama setup in place after the local server starts", async () => {
const context = await browser.newContext({
colorScheme: "dark",
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
const initialDetection = {
candidates: [],
manualProviders: [],
recommendedInstalls: [
{
id: "ollama",
brandId: "ollama",
label: "Ollama",
hint: "Run open models locally",
website: "https://ollama.com/download",
},
],
workspace: "/tmp/openclaw-e2e",
setupComplete: false,
};
const gateway = await installMockGateway(page, {
featureMethods: [
"chat.metadata",
"chat.startup",
"openclaw.setup.detect",
"openclaw.setup.prepare.start",
"wizard.next",
],
methodResponses: {
"openclaw.setup.detect": initialDetection,
"openclaw.setup.prepare.start": {
sessionId: "ollama-prepare-session",
done: false,
status: "running",
},
"wizard.next": {
sequence: [
{
done: false,
status: "running",
step: {
id: "ollama-mode",
type: "select",
message: "Ollama mode",
options: [
{
value: "cloud-local",
label: "Cloud + Local",
hint: "Route cloud and local models through your Ollama host",
},
{ value: "cloud-only", label: "Cloud only", hint: "Hosted Ollama models" },
{ value: "local-only", label: "Local only", hint: "Local models only" },
],
},
},
{
done: false,
status: "running",
step: {
id: "ollama-base-url",
type: "text",
message: "Ollama base URL",
initialValue: "http://127.0.0.1:11434",
},
},
{
done: false,
status: "running",
step: {
id: "ollama-retry",
type: "note",
title: "Ollama",
message: [
"Ollama could not be reached at http://127.0.0.1:11434.",
"Start or restart the Ollama server for this address.",
"If Ollama is not installed on that machine, download it at https://ollama.com/download",
"",
"Continue when it is running. OpenClaw will retry this address.",
].join("\n"),
},
},
{
done: false,
status: "running",
step: {
id: "ollama-retry-confirm",
type: "confirm",
message: "Retry this Ollama address now?",
initialValue: true,
},
},
{ done: true, status: "done" },
],
},
},
});
try {
const response = await page.goto(`${server.baseUrl}settings/model-setup`);
expect(response?.status()).toBe(200);
await page
.locator('[data-prepare-choice="ollama"]')
.getByRole("button", { name: "Check & set up" })
.click();
const start = await gateway.waitForRequest("openclaw.setup.prepare.start");
expect(start.params).toMatchObject({ authChoice: "ollama" });
await page.getByRole("radio", { name: /Local only/u }).check();
await page.getByRole("button", { name: "Continue" }).click();
const baseUrl = page.getByLabel("Ollama base URL");
await expect.poll(() => baseUrl.inputValue()).toBe("http://127.0.0.1:11434");
await page.getByRole("button", { name: "Submit" }).click();
await page.getByText("Ollama could not be reached at http://127.0.0.1:11434.").waitFor();
await page
.getByText("Continue when it is running. OpenClaw will retry this address.")
.waitFor();
if (artifactDir) {
await mkdir(artifactDir, { recursive: true });
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "ollama-recovery-desktop.png"),
});
}
await page.getByRole("button", { name: "Continue" }).click();
await page.getByText("Retry this Ollama address now?").waitFor();
await gateway.setMethodResponse("openclaw.setup.detect", {
...initialDetection,
candidates: [
{
kind: "provider-auto:ollama",
brandId: "ollama",
label: "Ollama",
detail: "qwen3:0.6b at http://127.0.0.1:11434",
modelRef: "ollama/qwen3:0.6b",
recommended: true,
credentials: true,
},
],
recommendedInstalls: [],
});
await page.getByRole("button", { name: "Yes" }).click();
await page.getByText("qwen3:0.6b at http://127.0.0.1:11434").waitFor();
await expect.poll(() => page.locator('[data-prepare-choice="ollama"]').count()).toBe(0);
if (artifactDir) {
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(artifactDir, "ollama-ready-desktop.png"),
});
}
const wizardRequests = await gateway.getRequests("wizard.next");
expect(wizardRequests).toHaveLength(5);
expect(wizardRequests[2]?.params).toMatchObject({
answer: {
stepId: "ollama-base-url",
value: "http://127.0.0.1:11434",
},
});
expect(wizardRequests[3]?.params).toMatchObject({
answer: { stepId: "ollama-retry" },
});
expect(wizardRequests[4]?.params).toMatchObject({
answer: { stepId: "ollama-retry-confirm", value: true },
});
} finally {
await context.close();
}
});
it("turns an unverifiable Gemini CLI login into direct recovery actions", async () => {
const context = await browser.newContext({
colorScheme: "dark",