mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 05:51:39 +00:00
fix(ollama): retry unreachable setup in place (#116210)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user