Skip to content

Commit 588bb6d

Browse files
fix(onboard): make the Ollama upgrade reach the required version (#9284)
<!-- markdownlint-disable MD041 --> ## Summary <!-- 1-3 plain sentences: what changes and why. Describe before-and-after behavior when it applies. Follow the NemoClaw Writing Guide: https://github.com/NVIDIA/NemoClaw/blob/main/WRITING.md. Do not add unrelated prose cleanup. --> A Linux Ollama upgrade ran the official installer without naming a version, so on a host whose latest release is below the required minimum the install reported success, the version never moved, and non-interactive onboarding exited 1 while advising a daemon restart that could not help. The upgrade now asks the installer for the minimum version by name, reads the binary's own version separately from the daemon's, restarts the service even when the current listener is already loopback-only, and names which side is stale in the failure. When the installed binary is already current and only the daemon is stale, recovery restarts the daemon without replacing or downgrading that binary. ## Related Issue <!-- Fixes #NNN or Closes #NNN. Remove this section if none. --> Fixes #9276 ## Changes <!-- List concrete changes. If this adds an abstraction, configuration, fallback, migration, or compatibility path, name its current requirement and consumer, explain why a direct change is insufficient, and identify the test that protects it. --> - `runOfficialInstallScript` in `src/lib/onboard/install-ollama-linux.ts` prefixes the installer with `OLLAMA_VERSION=<minimum>` when upgrade recovery must install or replace a stale binary. A fresh install stays unpinned and takes the latest version. When an installed binary already meets the floor and only the daemon is stale, Linux recovery skips the installer and restarts that daemon, preventing a downgrade. - `getInstalledOllamaVersion` in `src/lib/inference/ollama-version.ts` prefers the `client version is X` line. `ollama --version` reports the version of the daemon it can reach and prints the client's own version only when the two differ, so the previous first-match read returned the daemon's version for both probes and collapsed the stale-binary and stale-daemon cases the install menu distinguishes. - `ensureOllamaLoopbackSystemdOverride` in `src/lib/onboard/ollama-systemd.ts` takes an `isUpgrade` flag, passed by `installOllamaSystem`. The existing shortcut that skips the drop-in rewrite when the active listener is already loopback-only also skips the service restart, which an upgrade needs to move the daemon onto the new binary, so the flag refuses that shortcut and reports the missing sudo instead. - `assertOllamaUpgradeApplied` in `src/lib/onboard/ollama-install-menu.ts` now requires both the daemon and installed binary to be readable and at or above the minimum. It selects remediation from both probes: restart when the binary is current but the daemon is stale, run the pinned installer when the binary is stale, check the binary and `PATH` when only its version is unreadable, and check installation and daemon state when neither version is readable. - `docs/inference/set-up-ollama.mdx` records the pinned upgrade, the daemon-only restart that preserves a current binary, the `ollama --version` reporting behavior, the restart requirement, and the three failure outcomes. ## Type of Change - [ ] Code change (feature, bug fix, or refactor) - [x] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Quality Gates <!-- Check one tests line and one docs line. Check other lines when applicable. Add every requested justification or approval reference. --> - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [x] Docs updated for user-facing behavior changes - [ ] Docs not applicable — justification: - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: security review and requested corrections recorded in #9284 (review) - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: ## Documentation Writer Review <!-- Required for code and documentation changes after the changes and applicable validation are complete. Keep one review checkbox and one instance of each visible or hidden field. For Evidence, list changed documentation paths. For documentation-only changes, also state that the writing rules and documentation style were reviewed. For other results, explain why no documentation change is needed or why the review is blocked. For Agent, use a consistent product and surface name, such as Codex Desktop, Codex CLI, Claude Code, or Cursor. After committing all review changes, put `git rev-parse --short HEAD` and `git rev-parse --short HEAD:AGENTS.md` in the hidden metadata below. Rerun the review and refresh that metadata after any new commit. This receipt is advisory during the data-collection pilot. --> - [x] Documentation writer subagent reviewed the completed changes - Result: `docs-updated` - Evidence: `docs/inference/set-up-ollama.mdx`; reviewed terminology, structure, voice, Linux installer and restart behavior claims, user-visible remediation, behavior test titles, and the OpenClaw and Hermes generated variants against the implementation and tests. - Agent: Codex Desktop <!-- docs-review-head-sha: 0834862 --> <!-- docs-review-agents-blob-sha: b9fb6a9 --> ## DGX Station Hardware Evidence <!-- Required only when scripts/prepare-dgx-station-host.sh changes. Maintainers must review the linked evidence before approving or merging. This is human-reviewed evidence, not authenticated hardware provenance. Exceptional bypasses use existing repository governance and must be documented on the PR. --> - [ ] Tested on DGX Station - Tested commit: - Station profile/scenario: - Result: - Supporting evidence: ## Verification <!-- Check each applicable item only when supported by the requested evidence. Run targeted tests once per relevant change set and rerun after later edits or hook autofixes that can affect the tested behavior. Do not rerun hook-covered checks. --> - [x] PR description includes a `Signed-off-by:` line and every commit appears as `Verified` in GitHub - [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or `npm run validate:pr` passed after refreshing `origin/main` when hooks were skipped or unavailable - [x] Targeted behavior tests pass for the current change set, or tests are marked not applicable above — command/result or justification: `npx vitest run src/lib/inference/ollama-version.test.ts src/lib/onboard/ollama-install-menu.test.ts src/lib/onboard/install-ollama-linux-upgrade.test.ts src/lib/onboard/install-ollama-linux.test.ts src/lib/onboard/ollama-systemd.test.ts src/lib/onboard/setup-nim-ollama.test.ts test/onboard-ollama-upgrade-version-floor.test.ts test/onboard-selection.test.ts` — 8 files, 174 tests passed. Also `npm run typecheck`, `npm run typecheck:cli`, `npm run lint`, `npm run docs`, `npm run test-size:check`, and `npm run validate:pr` passed. - [ ] Applicable broad gate passed — `npm test` for broad runtime/test-harness changes; `npm run check` for repo-wide validation/coverage changes — command/result: - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [x] `npm run docs` builds without warnings (doc changes only) - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) --- <!-- DCO sign-off is required in this PR description, and every commit must appear as Verified in GitHub. Run: git config user.name && git config user.email --> Signed-off-by: Tinson Lai <tinsonl@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Ollama upgrades now install the required minimum version, while fresh installations continue to use the latest release. * Existing newer binaries can recover stale services without reinstalling. * Upgrade validation checks both the running service and installed binary, with clearer guidance when either is outdated or unavailable. * Upgrades fail safely when the service cannot restart onto the newly installed version. * Version detection correctly prioritizes the installed client version. * **Documentation** * Updated Linux Ollama setup guidance to explain version-pinned upgrades, required permissions, and validation behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Tinson Lai <tinsonl@nvidia.com> Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> Co-authored-by: Prekshi Vyas <prekshiv@nvidia.com>
1 parent f10b259 commit 588bb6d

13 files changed

Lines changed: 565 additions & 40 deletions

docs/inference/set-up-ollama.mdx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,21 @@ The wizard checks `ollama --version` and `/api/version` on port `11434` independ
3131
If NemoClaw detects an installed CLI or local running daemon but cannot read its version, onboarding uses the upgrade path instead of reusing it.
3232

3333
On macOS, the wizard uses `brew upgrade ollama` for the platform upgrade path.
34-
On Linux, the wizard uses the official `https://ollama.com/install.sh` path.
34+
On Linux, the wizard uses the official `https://ollama.com/install.sh` path and asks it for `0.32.9` by name when the installed binary is stale, because the version the installer calls latest is below the minimum on some hosts.
35+
If the installed binary is already at or above the minimum and only the daemon is stale, the wizard restarts the daemon without running the installer or replacing the newer binary.
3536
Linux upgrades use the sudo-driven system path because a user-local fallback would leave an existing system daemon serving the stale binary.
3637
If sudo is unavailable in a non-interactive run, rerun interactively or upgrade Ollama manually.
37-
38-
After an upgrade, NemoClaw probes the running daemon again.
39-
If the version remains below the minimum or cannot be read, interactive onboarding returns to provider selection, and non-interactive onboarding exits.
38+
An upgrade also needs sudo to restart the service onto the new binary, so it does not accept an already-loopback-only daemon as a reason to skip that step.
39+
A fresh install takes the latest version.
40+
41+
After an upgrade, NemoClaw probes the running daemon and the installed binary again.
42+
`ollama --version` reports the version of the daemon it can reach, so NemoClaw reads the binary's own version from the client-version line that the command prints when the two differ.
43+
Both versions must be readable and at or above `0.32.9` before onboarding accepts the upgrade.
44+
If either version is below the minimum or cannot be read, interactive onboarding returns to provider selection, and non-interactive onboarding exits.
45+
The failure identifies each stale or unreadable version.
46+
A binary at or above the minimum means the service still serves the old one and needs a restart, while a binary below it means the installer did not deliver the required version on that host.
47+
When only the binary cannot be read, the failure asks you to verify the installed Ollama binary before you retry.
48+
When neither side can be read, the failure asks you to check that Ollama is installed and running before you retry.
4049
Fresh installs skip this second probe because the bundled installers provide a daemon at or above the minimum.
4150

4251
The version gate does not apply to Windows-host Ollama reached from Docker Desktop through `host.docker.internal`.

src/lib/inference/ollama-version.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ describe("Ollama version detection", () => {
1616
expect(getInstalledOllamaVersion(capture)).toBe("0.6.2");
1717
});
1818

19+
it("prefers the client version line over the daemon version the CLI reports (#9276)", () => {
20+
const capture = () => "ollama version is 0.23.4\nWarning: client version is 0.32.9";
21+
expect(getInstalledOllamaVersion(capture)).toBe("0.32.9");
22+
});
23+
1924
it("returns null when ollama --version produces no output", () => {
2025
const capture = () => "";
2126
expect(getInstalledOllamaVersion(capture)).toBeNull();

src/lib/inference/ollama-version.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ export type OllamaVersionRunCapture = (
2424
*/
2525
export const MIN_OLLAMA_VERSION = "0.32.9";
2626

27+
const OLLAMA_CLIENT_VERSION_LINE = /client version is\s+(\d+\.\d+\.\d+)/i;
28+
2729
export function getInstalledOllamaVersion(runCaptureImpl?: OllamaVersionRunCapture): string | null {
2830
const capture = runCaptureImpl ?? runCapture;
2931
const out = capture(["ollama", "--version"], { ignoreError: true });
3032
if (!out) return null;
33+
const clientVersion = out.match(OLLAMA_CLIENT_VERSION_LINE);
34+
if (clientVersion) return clientVersion[1];
3135
const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
3236
return match ? match[0] : null;
3337
}

src/lib/onboard/__test-helpers__/setup-nim-flow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function makeHostState(
7171
vllmProfile: null,
7272
hasVllmImage: false,
7373
vllmEntries: [],
74-
ollamaInstallMenu: { entry: null, hasUpgradableOllama: false },
74+
ollamaInstallMenu: { entry: null, hasUpgradableOllama: false, binaryNeedsUpgrade: false },
7575
gpuNimCapable: false,
7676
...overrides,
7777
};

src/lib/onboard/install-ollama-linux-upgrade.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
55

6+
import { MIN_OLLAMA_VERSION } from "../inference/ollama-version";
67
import {
78
decideInstallOllamaLinuxMode,
89
type InstallOllamaLinuxOptions,
@@ -213,6 +214,68 @@ describe("installOllamaOnLinux (upgrade recovery)", () => {
213214
expect(sleepSecondsImpl).toHaveBeenCalled();
214215
});
215216

217+
it("asks the official installer for the required version on an upgrade (#9276)", () => {
218+
const runShellImpl = vi
219+
.fn()
220+
.mockReturnValue({ status: 0, stdout: "", stderr: "", error: null });
221+
const opts = makeOpts({
222+
modeOverride: "system",
223+
runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"),
224+
runShellImpl,
225+
isUpgrade: true,
226+
});
227+
expect(installOllamaOnLinux(opts).ok).toBe(true);
228+
const installer = findRunShellCall(runShellImpl, "ollama.com/install.sh");
229+
expect(installer).toContain(`OLLAMA_VERSION=${MIN_OLLAMA_VERSION} sh`);
230+
});
231+
232+
it("restarts the daemon for an already-current binary without running the pinned installer", () => {
233+
const runShellImpl = vi
234+
.fn()
235+
.mockReturnValue({ status: 0, stdout: "", stderr: "", error: null });
236+
const ensureOverride = vi.fn().mockReturnValue("ready");
237+
const log = vi.fn();
238+
const opts = makeOpts({
239+
modeOverride: "system",
240+
runShellImpl,
241+
ensureManagedOllamaLoopbackSystemdOverrideImpl: ensureOverride,
242+
isUpgrade: true,
243+
restartOnly: true,
244+
log,
245+
});
246+
expect(installOllamaOnLinux(opts).ok).toBe(true);
247+
expect(findRunShellCall(runShellImpl, "ollama.com/install.sh")).toBeUndefined();
248+
expect(ensureOverride).toHaveBeenCalledWith(expect.objectContaining({ isUpgrade: true }));
249+
expect(log).toHaveBeenCalledWith(expect.stringContaining("without replacing the binary"));
250+
});
251+
252+
it("leaves a fresh install unpinned so it takes the latest Ollama", () => {
253+
const runShellImpl = vi
254+
.fn()
255+
.mockReturnValue({ status: 0, stdout: "", stderr: "", error: null });
256+
const opts = makeOpts({
257+
modeOverride: "system",
258+
runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"),
259+
runShellImpl,
260+
});
261+
expect(installOllamaOnLinux(opts).ok).toBe(true);
262+
const installer = findRunShellCall(runShellImpl, "ollama.com/install.sh");
263+
expect(installer).toContain("| sh");
264+
expect(installer).not.toContain("OLLAMA_VERSION=");
265+
});
266+
267+
it("tells the systemd override that this run replaced the binary (#9276)", () => {
268+
const ensureOverride = vi.fn().mockReturnValue("ready");
269+
const opts = makeOpts({
270+
modeOverride: "system",
271+
runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"),
272+
ensureManagedOllamaLoopbackSystemdOverrideImpl: ensureOverride,
273+
isUpgrade: true,
274+
});
275+
expect(installOllamaOnLinux(opts).ok).toBe(true);
276+
expect(ensureOverride).toHaveBeenCalledWith(expect.objectContaining({ isUpgrade: true }));
277+
});
278+
216279
it("re-probes loopback fresh instead of trusting the cached findReachableOllamaHost result", () => {
217280
const runShellImpl = vi
218281
.fn()

src/lib/onboard/install-ollama-linux.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW,
1111
resolveOllamaContextWindowFloor,
1212
} from "../inference/ollama-runtime-context";
13+
import { MIN_OLLAMA_VERSION } from "../inference/ollama-version";
1314
import { cliName } from "./branding";
1415
import {
1516
OLLAMA_PORT,
@@ -55,6 +56,8 @@ export type InstallOllamaLinuxResult = {
5556
};
5657

5758
export type InstallOllamaLinuxOptions = InstallOllamaLinuxModeOptions & {
59+
/** Restart the daemon for an already-current binary without running the pinned installer. */
60+
restartOnly?: boolean;
5861
/** Test seam: override `os.homedir()`. */
5962
homedir?: () => string;
6063
/** Test seam: override `process.arch`. */
@@ -115,7 +118,9 @@ function detectJetpackVariant(opts: InstallOllamaLinuxOptions): "jetpack5" | "je
115118
/**
116119
* Run the official `https://ollama.com/install.sh`. Sudo-bound. Configures
117120
* the systemd `ollama.service`, creates the `ollama` system user, and
118-
* installs CUDA drivers when applicable.
121+
* installs CUDA drivers when applicable. An upgrade that replaces a stale
122+
* binary asks the installer for `MIN_OLLAMA_VERSION` by name; a fresh install
123+
* has no floor to satisfy and takes latest.
119124
*/
120125
function runOfficialInstallScript(opts: InstallOllamaLinuxOptions): void {
121126
const log = opts.log ?? ((m: string) => console.log(m));
@@ -125,7 +130,11 @@ function runOfficialInstallScript(opts: InstallOllamaLinuxOptions): void {
125130
" The Ollama installer creates a system user, a systemd service, and writes to /usr/local. " +
126131
"It uses sudo, may ask for your password, and can take a few minutes; installer output will stream below.",
127132
);
128-
runShellImpl("set -o pipefail; curl -fsSL https://ollama.com/install.sh | sh", {
133+
const versionPin = opts.isUpgrade ? `OLLAMA_VERSION=${MIN_OLLAMA_VERSION} ` : "";
134+
if (versionPin) {
135+
log(` Requesting Ollama ${MIN_OLLAMA_VERSION} from the installer.`);
136+
}
137+
runShellImpl(`set -o pipefail; curl -fsSL https://ollama.com/install.sh | ${versionPin}sh`, {
129138
stdio: "inherit",
130139
});
131140
}
@@ -316,12 +325,19 @@ function installOllamaSystem(opts: InstallOllamaLinuxOptions): InstallOllamaLinu
316325
opts.ensureManagedOllamaLoopbackSystemdOverrideImpl ??
317326
ensureManagedOllamaLoopbackSystemdOverride;
318327

319-
runOfficialInstallScript(opts);
320-
sleepSecondsImpl(2);
328+
if (opts.restartOnly) {
329+
log(
330+
` Installed Ollama already meets ${MIN_OLLAMA_VERSION}; restarting its daemon without replacing the binary.`,
331+
);
332+
} else {
333+
runOfficialInstallScript(opts);
334+
sleepSecondsImpl(2);
335+
}
321336

322337
const overrideState: OllamaLoopbackSystemdOverrideState = ensureOverrideImpl({
323338
isNonInteractive: opts.isNonInteractive,
324339
contextWindowFloor: opts.contextWindowFloor,
340+
isUpgrade: opts.isUpgrade,
325341
});
326342
if (overrideState === "failed") {
327343
errorLog(" Ollama systemd restart did not recover after applying the loopback override.");

src/lib/onboard/ollama-install-menu.test.ts

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@ import {
1111

1212
const LINUX_NON_WSL = { platform: "linux" as const, isWsl: false };
1313

14+
function captureOllamaVersions(
15+
daemonVersion: string | null,
16+
binaryVersion: string | null,
17+
): (command: readonly string[]) => string {
18+
const responses = new Map<string, string>([
19+
[
20+
JSON.stringify([
21+
"curl",
22+
"-sf",
23+
"--connect-timeout",
24+
"2",
25+
"--max-time",
26+
"5",
27+
"http://127.0.0.1:11434/api/version",
28+
]),
29+
daemonVersion ? JSON.stringify({ version: daemonVersion }) : "",
30+
],
31+
[
32+
JSON.stringify(["ollama", "--version"]),
33+
binaryVersion ? `ollama version is ${binaryVersion}` : "",
34+
],
35+
]);
36+
return (command) => responses.get(JSON.stringify(command)) ?? "";
37+
}
38+
1439
describe("resolveRunningOllamaMenuEntry", () => {
1540
it("labels unsupported Windows-host Ollama without suggesting it", () => {
1641
const entry = resolveRunningOllamaMenuEntry({
@@ -80,6 +105,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
80105
...LINUX_NON_WSL,
81106
});
82107
expect(result.hasUpgradableOllama).toBe(true);
108+
expect(result.binaryNeedsUpgrade).toBe(true);
83109
expect(result.entry?.key).toBe("install-ollama");
84110
expect(result.entry?.label).toBe(
85111
`Upgrade Ollama (Linux) — upgrade installed binary 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
@@ -112,12 +138,31 @@ describe("resolveOllamaInstallMenuEntry", () => {
112138
...LINUX_NON_WSL,
113139
});
114140
expect(result.hasUpgradableOllama).toBe(true);
141+
expect(result.binaryNeedsUpgrade).toBe(false);
115142
// Stale source is the daemon; suffix names "running daemon" with that version.
116143
expect(result.entry?.label).toBe(
117144
`Upgrade Ollama (Linux) — upgrade running daemon 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
118145
);
119146
});
120147

148+
it("requires a binary install when a stale daemon is reachable without a local binary", () => {
149+
const result = resolveOllamaInstallMenuEntry({
150+
hasOllama: false,
151+
ollamaRunning: true,
152+
hasWindowsOllama: false,
153+
windowsHostOllamaSupported: true,
154+
ollamaHost: "127.0.0.1",
155+
installedOllamaVersion: null,
156+
runningOllamaVersion: "0.6.2",
157+
...LINUX_NON_WSL,
158+
});
159+
expect(result.hasUpgradableOllama).toBe(true);
160+
expect(result.binaryNeedsUpgrade).toBe(true);
161+
expect(result.entry?.label).toBe(
162+
`Upgrade Ollama (Linux) — upgrade running daemon 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
163+
);
164+
});
165+
121166
it("offers an upgrade entry when the binary is stale even though the daemon is fresh", () => {
122167
const result = resolveOllamaInstallMenuEntry({
123168
hasOllama: true,
@@ -130,6 +175,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
130175
...LINUX_NON_WSL,
131176
});
132177
expect(result.hasUpgradableOllama).toBe(true);
178+
expect(result.binaryNeedsUpgrade).toBe(true);
133179
// Stale source is the binary; suffix names "installed binary" with that version.
134180
expect(result.entry?.label).toBe(
135181
`Upgrade Ollama (Linux) — upgrade installed binary 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
@@ -243,32 +289,45 @@ describe("resolveOllamaInstallMenuEntry", () => {
243289
expect(result.ok).toBe(true);
244290
});
245291

246-
it("accepts the upgrade when the running daemon reports a fresh version", () => {
247-
const capture = (cmd: readonly string[]) => {
248-
const joined = cmd.join(" ");
249-
if (joined.includes("/api/version")) return '{"version":"0.32.9"}';
250-
if (joined.includes("ollama --version")) return "ollama version is 0.32.9";
251-
return "";
252-
};
292+
it("accepts the upgrade when the daemon and installed binary meet the minimum", () => {
293+
const capture = captureOllamaVersions("0.32.9", "0.32.9");
253294
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
254295
expect(result.ok).toBe(true);
255296
expect(result.detectedDaemonVersion).toBe("0.32.9");
256297
expect(result.detectedBinaryVersion).toBe("0.32.9");
257298
});
258299

259300
it("rejects the upgrade when the daemon still serves the stale version even though the binary is fresh", () => {
260-
const capture = (cmd: readonly string[]) => {
261-
const joined = cmd.join(" ");
262-
if (joined.includes("/api/version")) return '{"version":"0.6.2"}';
263-
if (joined.includes("ollama --version")) return "ollama version is 0.32.9";
264-
return "";
265-
};
301+
const capture = captureOllamaVersions("0.6.2", "0.32.9");
266302
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
267303
expect(result.ok).toBe(false);
268304
expect(result.detectedDaemonVersion).toBe("0.6.2");
269305
expect(result.detectedBinaryVersion).toBe("0.32.9");
270306
expect(result.message).toContain("0.6.2");
271307
expect(result.message).toContain(MIN_OLLAMA_VERSION);
308+
expect(result.message).toContain("systemctl restart ollama");
309+
});
310+
311+
it("rejects a stale binary when the running daemon meets the minimum (#9276)", () => {
312+
const capture = captureOllamaVersions("0.32.9", "0.23.4");
313+
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
314+
expect(result.ok).toBe(false);
315+
expect(result.detectedDaemonVersion).toBe("0.32.9");
316+
expect(result.detectedBinaryVersion).toBe("0.23.4");
317+
expect(result.message).toContain(`did not deliver ${MIN_OLLAMA_VERSION} on this host`);
318+
expect(result.message).toContain(`OLLAMA_VERSION=${MIN_OLLAMA_VERSION} sh`);
319+
expect(result.message).not.toContain("systemctl restart ollama");
320+
});
321+
322+
it("reports a known daemon when the installed binary version is unavailable (#9276)", () => {
323+
const capture = captureOllamaVersions("0.23.4", null);
324+
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
325+
expect(result.ok).toBe(false);
326+
expect(result.detectedDaemonVersion).toBe("0.23.4");
327+
expect(result.detectedBinaryVersion).toBeNull();
328+
expect(result.message).toContain("running daemon reports 0.23.4 (binary: unknown)");
329+
expect(result.message).toContain("installed binary version could not be read");
330+
expect(result.message).not.toContain("Neither the daemon nor the binary could be read");
272331
});
273332

274333
it("rejects the upgrade when the daemon is unreachable", () => {
@@ -277,6 +336,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
277336
expect(result.ok).toBe(false);
278337
expect(result.detectedDaemonVersion).toBeNull();
279338
expect(result.message).toContain("unreachable");
339+
expect(result.message).toContain("Neither the daemon nor the binary could be read");
280340
});
281341

282342
it("does not return an entry on unsupported platforms", () => {

0 commit comments

Comments
 (0)