feat(local-model): the pinned runtime, its supervisor and the runtime client

runtime.ts: the runtime is pinned in a compiled constant (version, URL, size,
sha256). The archive is streamed to a .partial file while it is hashed,
continued with a Range request, retried at most 3 times, and the hash is
compared before anything is unpacked; a mismatch deletes the file. It is
unpacked with /usr/bin/tar into a temporary folder and renamed.

The supervisor runs the runtime as a non-detached child with an environment
built from an allowlist by the pure runtimeEnv(), on a port chosen once by
binding. It restarts the child at most 3 times, stops it with SIGTERM, adopts
or replaces its own orphan on the port and answers port-in-use for any other
listener. parseInferenceComputeTotal() reads the device total the runtime
logs when it starts. Under vitest nothing spawns or fetches.

ollama-client.ts: the pull is parsed incrementally by a pure parser (a 23 GB
pull is about 1.4 million lines), with up to 5 retries on an error line or a
cut stream. verifyModel() loads the model with one tiny chat, checks what the
runtime reports (all on the GPU, the chosen context) and makes one tool
probe; each failure is a code.

developer-tools.ts: only /usr/bin/xcode-select -p, bounded, darwin only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 22:16:38 +01:00
1 parent 492df776d0
commit 2af413aa9b
7 files changed
+4525 -60

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
/**
* @fileoverview Tests for src/local-model/developer-tools.ts: whether this Mac has the
* developer tools a coding agent calls.
*
* Pinned: the ONLY command it ever runs is `/usr/bin/xcode-select -p` (never `git` or
* `python3`, which open a system window on a Mac without the tools), with a timeout; exit 0
* is `present`, any other exit is `missing`, no answer is `unknown`; off macOS nothing is run
* at all; nothing it is given can make it throw; and an answer is reused for a minute, so a
* window that polls once a second does not spawn once a second.
*
* Port: N/A (the runner is injected; nothing is spawned).
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
checkDeveloperTools,
createDeveloperToolsCheck,
DEVELOPER_TOOLS_TIMEOUT_MS,
resetDeveloperToolsCache,
} from '../src/local-model/developer-tools.js';
type Run = (file: string, args: readonly string[], timeoutMs: number) => Promise<number | null>;
/** A runner that answers `code` and records how it was called. */
function runner(code: number | null): { run: Run; calls: [string, readonly string[], number][] } {
const calls: [string, readonly string[], number][] = [];
return {
calls,
run: async (file, args, timeoutMs) => {
calls.push([file, args, timeoutMs]);
return code;
},
};
}
afterEach(() => {
resetDeveloperToolsCache();
vi.restoreAllMocks();
});
describe('checkDeveloperTools', () => {
it('runs exactly one command: /usr/bin/xcode-select -p, bounded', async () => {
const { run, calls } = runner(0);
expect(await checkDeveloperTools({ platform: 'darwin', run })).toBe('present');
expect(calls).toEqual([['/usr/bin/xcode-select', ['-p'], DEVELOPER_TOOLS_TIMEOUT_MS]]);
expect(DEVELOPER_TOOLS_TIMEOUT_MS).toBe(2_000);
});
it('never names git, python or xcrun, whatever the answer is', async () => {
for (const code of [0, 1, 2, null]) {
const { run, calls } = runner(code);
await checkDeveloperTools({ platform: 'darwin', run });
expect(calls).toHaveLength(1);
expect(calls[0][0]).toBe('/usr/bin/xcode-select');
expect(JSON.stringify(calls)).not.toMatch(/git|python|xcrun/);
}
});
it('reads exit 0 as present and any other exit as missing', async () => {
expect(await checkDeveloperTools({ platform: 'darwin', run: runner(0).run })).toBe('present');
// A Mac without the tools: "unable to get active developer directory", exit 2.
expect(await checkDeveloperTools({ platform: 'darwin', run: runner(2).run })).toBe('missing');
expect(await checkDeveloperTools({ platform: 'darwin', run: runner(1).run })).toBe('missing');
expect(await checkDeveloperTools({ platform: 'darwin', run: runner(127).run })).toBe('missing');
});
it('reads no answer as unknown: a timeout, a command that could not be started, a runner that throws', async () => {
expect(await checkDeveloperTools({ platform: 'darwin', run: runner(null).run })).toBe('unknown');
const throwing: Run = async () => {
throw new Error('spawn EACCES');
};
expect(await checkDeveloperTools({ platform: 'darwin', run: throwing })).toBe('unknown');
// An exit code that is no whole number is no answer either.
expect(await checkDeveloperTools({ platform: 'darwin', run: runner(Number.NaN).run })).toBe('unknown');
});
it('runs nothing off macOS and answers unknown', async () => {
for (const platform of ['linux', 'win32', 'freebsd']) {
const { run, calls } = runner(0);
expect(await checkDeveloperTools({ platform, run }), platform).toBe('unknown');
expect(calls).toEqual([]);
}
});
it('spawns nothing with the default runner under vitest', async () => {
// The real `execFile` is never reached here: on a Mac this would otherwise run the command.
expect(await checkDeveloperTools({ platform: 'darwin' })).toBe('unknown');
// And with no argument at all it answers for this machine without throwing.
expect(['present', 'missing', 'unknown']).toContain(await checkDeveloperTools());
});
});
describe('the cached check', () => {
it('reuses an answer for a minute, then asks again', async () => {
const { run, calls } = runner(2);
let clock = 1_000_000;
const check = createDeveloperToolsCheck({ platform: 'darwin', run, now: () => clock });
expect(await check()).toBe('missing');
// A window that polls once a second for a minute.
for (let second = 1; second < 60; second++) {
clock += 1_000;
expect(await check()).toBe('missing');
}
expect(calls).toHaveLength(1);
clock += 1_000;
expect(await check()).toBe('missing');
expect(calls).toHaveLength(2);
});
it('shares one run among callers that ask at the same time', async () => {
let release: (code: number) => void = () => undefined;
let started = 0;
const run: Run = () => {
started += 1;
return new Promise((resolve) => {
release = resolve;
});
};
const check = createDeveloperToolsCheck({ platform: 'darwin', run });
const asked = [check(), check(), check()];
await Promise.resolve();
expect(started).toBe(1);
release(0);
expect(await Promise.all(asked)).toEqual(['present', 'present', 'present']);
});
it('does not remember "unknown": a check that did not work is tried again', async () => {
let calls = 0;
// No answer the first time, exit 0 from then on.
const run: Run = async () => (calls++ === 0 ? null : 0);
const check = createDeveloperToolsCheck({ platform: 'darwin', run, now: () => 5 });
expect(await check()).toBe('unknown');
expect(await check()).toBe('present');
expect(await check()).toBe('present');
expect(calls).toBe(2);
});
it('sees the tools once they were installed, after the minute', async () => {
const answers = [2, 0];
let calls = 0;
let clock = 0;
const check = createDeveloperToolsCheck({
platform: 'darwin',
run: async () => answers[calls++] ?? 0,
now: () => clock,
});
expect(await check()).toBe('missing');
clock = 59_999;
expect(await check()).toBe('missing');
clock = 60_000;
expect(await check()).toBe('present');
});
});
+950
View File
@@ -0,0 +1,950 @@
/**
* @fileoverview Tests for src/local-model/ollama-client.ts: what Codeman asks its own model
* runtime, and the pure functions that read the answers.
*
* Laid out like the module:
*
* 1. The listings (`/api/version`, `/api/tags`, `/api/ps`, `/v1/models`, the delete), against
* the fake runtime, and what each answers when nothing listens.
* 2. The pull parser, pure: the two real line shapes fed in small pieces across line breaks, an
* error line, a resumed pull, and that it keeps no line.
* 3. `pullModel()` against the fake: the total it reports, progress, an error line and a cut
* stream that are retried, the cap of 5 retries, a model the registry does not have (no
* retry), a resumed pull, and Cancel.
* 4. The check after the pull: the two request bodies, the two judges (pure), and
* `verifyModel()` against the fake for every way it can end, the answer without a tool call
* among them.
*
* Port: none fixed (the fake runtime listens on 0).
*/
import { afterEach, describe, expect, it } from 'vitest';
import {
buildChatProbeRequest,
buildToolProbeRequest,
createPullParser,
deleteModel,
getRuntimeVersion,
judgeLoaded,
judgeToolProbe,
listOpenAiModels,
listRunning,
listTags,
PULL_RETRIES,
pullModel,
readModelDigest,
TOOL_PROBE_MAX_TOKENS,
verifyModel,
type OllamaPsModel,
type OllamaTarget,
type PullProgress,
} from '../src/local-model/ollama-client.js';
import {
DEFAULT_FAKE_MODEL,
freeLoopbackPort,
REAL_PULL_ERROR_LINE,
startFakeOllama,
type FakeOllama,
type FakeOllamaOptions,
} from './fixtures/fake-ollama.js';
const MODEL = DEFAULT_FAKE_MODEL.id;
const MODEL_BYTES = DEFAULT_FAKE_MODEL.bytes;
const CONTEXT = 131072;
let fake: FakeOllama | undefined;
afterEach(async () => {
await fake?.close();
fake = undefined;
});
/** Starts the fake and returns the target that points at it. */
async function runtime(options: FakeOllamaOptions = {}): Promise<{ fake: FakeOllama; target: OllamaTarget }> {
fake = await startFakeOllama(options);
return { fake, target: { baseUrl: fake.baseUrl } };
}
/** A target nothing listens on. */
async function deadTarget(): Promise<OllamaTarget> {
return { baseUrl: `http://127.0.0.1:${await freeLoopbackPort()}` };
}
/** No waiting between retries. */
const noSleep = async (): Promise<void> => undefined;
// ---------- 1. Listings ----------
describe('the listings', () => {
it('reads the version of a runtime', async () => {
const { target } = await runtime();
expect(await getRuntimeVersion(target)).toBe('0.40.15');
});
it('answers undefined for a port nothing listens on, and for a listener that is no runtime', async () => {
expect(await getRuntimeVersion(await deadTarget())).toBeUndefined();
// Something answers, but not with a version.
const notARuntime: OllamaTarget = {
baseUrl: 'http://127.0.0.1:1',
fetch: async () => new Response('<html>hello</html>', { status: 200 }),
};
expect(await getRuntimeVersion(notARuntime)).toBeUndefined();
const wrongShape: OllamaTarget = {
baseUrl: 'http://127.0.0.1:1',
fetch: async () => new Response(JSON.stringify({ version: 42 }), { status: 200 }),
};
expect(await getRuntimeVersion(wrongShape)).toBeUndefined();
const refused: OllamaTarget = {
baseUrl: 'http://127.0.0.1:1',
fetch: async () => new Response(JSON.stringify({ version: '0.40.15' }), { status: 503 }),
};
expect(await getRuntimeVersion(refused)).toBeUndefined();
});
it('lists the model on disk with what a Mac reports: size, digest, format, runner', async () => {
const { target } = await runtime({ installed: true });
expect(await listTags(target)).toEqual([
{
name: 'qwen3.6:35b-a3b',
size: 23_105_798_956,
digest: '0ce5ff81f9ffdf89dd8192ca7b5d34cefd8996a875b3aaaaafaf5c4813569eba',
format: 'safetensors',
runner: 'mlx',
capabilities: ['completion', 'vision', 'tools', 'thinking'],
},
]);
expect(await readModelDigest(target, MODEL)).toBe(DEFAULT_FAKE_MODEL.digest);
expect(await readModelDigest(target, 'qwen3.5:9b')).toBeUndefined();
});
it('stores a digest in one form: lowercase hex without the prefix', async () => {
const target: OllamaTarget = {
baseUrl: 'http://127.0.0.1:1',
fetch: async () =>
new Response(JSON.stringify({ models: [{ name: MODEL, size: 1, digest: 'sha256:0CE5FF81' }] }), {
status: 200,
}),
};
expect(await readModelDigest(target, MODEL)).toBe('0ce5ff81');
});
it('lists nothing on an empty store, and reads the "data: null" of the OpenAI route as no model', async () => {
const { target } = await runtime();
expect(await listTags(target)).toEqual([]);
expect(await listRunning(target)).toEqual([]);
expect(await listOpenAiModels(target)).toEqual([]);
expect(await readModelDigest(target, MODEL)).toBeUndefined();
});
it('lists the model ids of the OpenAI route exactly as they are listed', async () => {
const { target } = await runtime({ installed: true });
expect(await listOpenAiModels(target)).toEqual(['qwen3.6:35b-a3b']);
});
it('lists a loaded model with its memory, the part on the GPU and its context', async () => {
const { fake: server, target } = await runtime({ installed: true });
expect(await listRunning(target)).toEqual([]);
server.state.loaded = true;
expect(await listRunning(target)).toEqual([
{
name: 'qwen3.6:35b-a3b',
size: 23_205_798_956,
sizeVram: 23_205_798_956,
contextLength: 131072,
digest: DEFAULT_FAKE_MODEL.digest,
},
]);
});
it('answers an empty list, never an exception, when nothing listens', async () => {
const target = await deadTarget();
expect(await listTags(target)).toEqual([]);
expect(await listRunning(target)).toEqual([]);
expect(await listOpenAiModels(target)).toEqual([]);
expect(await readModelDigest(target, MODEL)).toBeUndefined();
});
it('skips rows that are not models instead of failing on them', async () => {
const target: OllamaTarget = {
baseUrl: 'http://127.0.0.1:1',
fetch: async () =>
new Response(JSON.stringify({ models: [null, 'x', {}, { name: 'a:b' }, { model: 'c:d', size: -1 }] }), {
status: 200,
}),
};
expect(await listTags(target)).toEqual([
{ name: 'a:b', size: 0, digest: '' },
{ name: 'c:d', size: 0, digest: '' },
]);
// A row without a digest has none to compare.
expect(await readModelDigest(target, 'a:b')).toBeUndefined();
});
it('deletes a model, and calls a model that never was there gone too', async () => {
const { fake: server, target } = await runtime({ installed: true });
expect(await deleteModel(target, MODEL)).toBe(true);
expect(server.requests.at(-1)).toMatchObject({ method: 'DELETE', path: '/api/delete', body: { model: MODEL } });
expect(await listTags(target)).toEqual([]);
// The runtime answers 404 for it now.
expect(await deleteModel(target, MODEL)).toBe(true);
// No runtime: nothing was deleted, and it says so.
expect(await deleteModel(await deadTarget(), MODEL)).toBe(false);
});
it('gives up on a listing when the caller aborts', async () => {
const { target } = await runtime({ installed: true });
const controller = new AbortController();
controller.abort();
expect(await listTags(target, controller.signal)).toEqual([]);
expect(await getRuntimeVersion(target, controller.signal)).toBeUndefined();
});
});
// ---------- 2. The pull parser ----------
describe('createPullParser', () => {
const mlxLine = (completed: number): string =>
JSON.stringify({ status: 'pulling model', digest: 'sha256:model', total: MODEL_BYTES, completed });
it('adds up the line shape of an MLX build', () => {
const parser = createPullParser();
parser.push('{"status":"pulling manifest"}\n');
expect(parser.snapshot()).toEqual({
completedBytes: 0,
totalBytes: 0,
status: 'pulling manifest',
succeeded: false,
lines: 1,
});
parser.push(`${mlxLine(0)}\n${mlxLine(6_200_000_000)}\n`);
expect(parser.snapshot()).toMatchObject({ completedBytes: 6_200_000_000, totalBytes: 23_105_798_956, lines: 3 });
parser.push(`${mlxLine(MODEL_BYTES)}\n{"status":"writing manifest"}\n{"status":"success"}\n`);
parser.end();
expect(parser.snapshot()).toEqual({
completedBytes: 23_105_798_956,
totalBytes: 23_105_798_956,
status: 'success',
succeeded: true,
lines: 6,
});
});
it('reads the same stream whatever the size of the pieces, also across line breaks', () => {
const stream =
['{"status":"pulling manifest"}', mlxLine(0), mlxLine(1_000), mlxLine(999_999_999), mlxLine(MODEL_BYTES)].join(
'\n'
) + '\n{"status":"writing manifest"}\n{"status":"success"}\n';
const whole = createPullParser();
whole.push(stream);
whole.end();
const expected = whole.snapshot();
expect(expected).toMatchObject({ succeeded: true, completedBytes: MODEL_BYTES, lines: 7 });
for (const size of [1, 2, 3, 5, 7, 13, 64, 1000]) {
const parser = createPullParser();
for (let at = 0; at < stream.length; at += size) parser.push(stream.slice(at, at + size));
parser.end();
expect(parser.snapshot(), `pieces of ${size}`).toEqual(expected);
}
// And as bytes, one at a time: what a socket may really deliver.
const bytes = new TextEncoder().encode(stream);
const parser = createPullParser();
for (const byte of bytes) parser.push(Uint8Array.of(byte));
parser.end();
expect(parser.snapshot()).toEqual(expected);
});
it('survives a character of several bytes that is split between two pieces', () => {
const bytes = new TextEncoder().encode('{"status":"lädt Manifest ✓"}\n{"status":"success"}\n');
for (let cut = 1; cut < bytes.length; cut++) {
const parser = createPullParser();
parser.push(bytes.subarray(0, cut));
parser.push(bytes.subarray(cut));
parser.end();
expect(parser.snapshot(), `cut at ${cut}`).toMatchObject({ succeeded: true, lines: 2 });
}
const parser = createPullParser();
parser.push(bytes.subarray(0, 14));
parser.push(bytes.subarray(14));
expect(parser.snapshot().lines).toBe(2);
});
it('adds up the layers of a GGUF build, each announced without "completed" first', () => {
const parser = createPullParser();
const big = { status: 'pulling c5396e06af29', digest: 'sha256:c5396e06af29aaaa', total: 397_807_936 };
const small = { status: 'pulling 66b9ea09bd5b', digest: 'sha256:66b9ea09bd5bbbbb', total: 68 };
parser.push(`${JSON.stringify(big)}\n`);
// Known, nothing downloaded yet.
expect(parser.snapshot()).toMatchObject({ totalBytes: 397_807_936, completedBytes: 0 });
parser.push(`${JSON.stringify({ ...big, completed: 100_000_000 })}\n`);
parser.push(`${JSON.stringify(small)}\n`);
// The total grows when a new layer appears; a line without "completed" keeps what was seen.
parser.push(`${JSON.stringify(big)}\n`);
expect(parser.snapshot()).toMatchObject({ totalBytes: 397_808_004, completedBytes: 100_000_000 });
parser.push(`${JSON.stringify({ ...big, completed: 397_807_936 })}\n`);
parser.push(`${JSON.stringify({ ...small, completed: 68 })}\n`);
parser.push('{"status":"verifying sha256 digest"}\n{"status":"writing manifest"}\n{"status":"success"}\n');
expect(parser.snapshot()).toEqual({
completedBytes: 397_808_004,
totalBytes: 397_808_004,
status: 'success',
succeeded: true,
lines: 9,
});
});
it('reads a resumed pull from where it starts, not from zero', () => {
const parser = createPullParser();
parser.push(`{"status":"pulling manifest"}\n${mlxLine(11_000_000_000)}\n`);
expect(parser.snapshot()).toMatchObject({ completedBytes: 11_000_000_000, totalBytes: MODEL_BYTES });
});
it('keeps the text of an error line, the real one included', () => {
const parser = createPullParser();
parser.push(`{"status":"pulling manifest"}\n${mlxLine(5)}\n${REAL_PULL_ERROR_LINE}\n`);
parser.end();
expect(parser.snapshot()).toEqual({
completedBytes: 5,
totalBytes: MODEL_BYTES,
status: 'pulling model',
succeeded: false,
error: 'max retries exceeded: local error: tls: bad record MAC',
lines: 3,
});
// The first error stays.
parser.push('{"error":"another"}\n');
expect(parser.snapshot().error).toBe('max retries exceeded: local error: tls: bad record MAC');
});
it('reads a last line that has no line break only when the body ends', () => {
const parser = createPullParser();
parser.push('{"status":"success"}');
expect(parser.snapshot()).toMatchObject({ succeeded: false, lines: 0 });
parser.end();
expect(parser.snapshot()).toMatchObject({ succeeded: true, lines: 1 });
});
it('skips a line that is not JSON and still counts it; a blank line is no line', () => {
const parser = createPullParser();
parser.push('not json\n\n \n[1,2]\n"text"\nnull\n{"status":"success"}\n');
expect(parser.snapshot()).toEqual({
completedBytes: 0,
totalBytes: 0,
status: 'success',
succeeded: true,
lines: 5,
});
});
it('never lets "completed" pass "total", and ignores sizes that are no byte counts', () => {
const parser = createPullParser();
parser.push('{"status":"pulling a","digest":"sha256:a","total":100,"completed":250}\n');
parser.push('{"status":"pulling b","digest":"sha256:b","total":-5,"completed":1}\n');
parser.push('{"status":"pulling c","digest":"sha256:c","total":"100","completed":1}\n');
expect(parser.snapshot()).toMatchObject({ completedBytes: 100, totalBytes: 100 });
});
it('keeps no line: a million progress lines leave it as small as ten', () => {
// What it holds is one entry per layer and the unfinished line. Proven by its answers: the
// totals are those of the LAST line of each layer, and the count is every line.
const parser = createPullParser();
const lines = 200_000;
let block = '';
for (let i = 1; i <= lines; i++) {
block += `${mlxLine(i * 1000)}\n`;
if (i % 5000 === 0) {
parser.push(block);
block = '';
}
}
parser.end();
expect(parser.snapshot()).toEqual({
completedBytes: lines * 1000,
totalBytes: MODEL_BYTES,
status: 'pulling model',
succeeded: false,
lines,
});
});
it('drops a "line" that never ends instead of buffering it', () => {
const parser = createPullParser();
// Three megabytes without a line break, then the stream goes on normally.
for (let i = 0; i < 3; i++) parser.push('x'.repeat(1024 * 1024));
parser.push('tail of the endless line\n{"status":"success"}\n');
expect(parser.snapshot()).toMatchObject({ succeeded: true, lines: 2 });
});
});
// ---------- 3. pullModel ----------
describe('pullModel', () => {
it('pulls the model and reports the total of the pull itself', async () => {
const { fake: server, target } = await runtime();
const seen: PullProgress[] = [];
const result = await pullModel(target, MODEL, { onProgress: (progress) => seen.push(progress) });
expect(result).toEqual({ ok: true, totalBytes: 23_105_798_956 });
expect(server.state.installed).toBe(true);
expect(server.requests.filter((r) => r.path === '/api/pull')).toEqual([
expect.objectContaining({ method: 'POST', body: { model: MODEL, stream: true } }),
]);
// The last report is the finished pull, and no report runs backwards.
expect(seen.at(-1)).toMatchObject({ completedBytes: MODEL_BYTES, totalBytes: MODEL_BYTES });
for (let i = 1; i < seen.length; i++) {
expect(seen[i].completedBytes).toBeGreaterThanOrEqual(seen[i - 1].completedBytes);
}
for (const progress of seen) expect(progress.totalBytes).toBe(MODEL_BYTES);
});
it('adds up the layers of a GGUF pull to the same total', async () => {
const { target } = await runtime({ pull: { shape: 'gguf', progressLines: 6 } });
expect(await pullModel(target, MODEL)).toEqual({ ok: true, totalBytes: MODEL_BYTES });
});
it('reports a speed from the clock it is given', async () => {
const { target } = await runtime({ pull: { progressLines: 8, lineDelayMs: 5 } });
const seen: PullProgress[] = [];
let clock = 0;
const result = await pullModel(target, MODEL, {
onProgress: (progress) => seen.push(progress),
// One second passes between two looks at the clock.
now: () => (clock += 1000),
});
expect(result.ok).toBe(true);
const speeds = seen.flatMap((progress) => (progress.bytesPerSecond === undefined ? [] : [progress.bytesPerSecond]));
expect(speeds.length).toBeGreaterThan(0);
for (const speed of speeds) {
expect(Number.isInteger(speed)).toBe(true);
expect(speed).toBeGreaterThanOrEqual(0);
}
// The first report has nothing to compare with.
expect(seen[0].bytesPerSecond).toBeUndefined();
expect(Math.max(...speeds)).toBeGreaterThan(0);
});
it('starts again after an error line, and the new pull continues where the last one stopped', async () => {
const { fake: server, target } = await runtime({
pull: { fail: 'error-line', failTimes: 2, failAfterLines: 10, progressLines: 20 },
});
const waits: number[] = [];
const firstOfEachPull: number[] = [];
let last = -1;
const result = await pullModel(target, MODEL, {
sleep: async (ms) => {
waits.push(ms);
last = -1;
},
onProgress: (progress) => {
if (last === -1) firstOfEachPull.push(progress.completedBytes);
last = progress.completedBytes;
},
});
expect(result).toEqual({ ok: true, totalBytes: MODEL_BYTES });
// Two failures, then the one that succeeded.
expect(server.state.pulls).toBe(3);
// It waits before each retry, longer every time.
expect(waits).toEqual([1000, 2000]);
// The second and third pull do not start at zero.
expect(firstOfEachPull).toHaveLength(3);
expect(firstOfEachPull[1]).toBeGreaterThan(0);
expect(firstOfEachPull[2]).toBeGreaterThan(firstOfEachPull[1]);
});
it('starts again after a stream that was cut without a word', async () => {
const { fake: server, target } = await runtime({ pull: { fail: 'cut', failTimes: 1 } });
expect(await pullModel(target, MODEL, { sleep: noSleep })).toEqual({ ok: true, totalBytes: MODEL_BYTES });
expect(server.state.pulls).toBe(2);
});
it('gives up after 5 retries, with the error the runtime gave', async () => {
expect(PULL_RETRIES).toBe(5);
const { fake: server, target } = await runtime({ pull: { fail: 'error-line' } });
const waits: number[] = [];
const result = await pullModel(target, MODEL, { sleep: async (ms) => void waits.push(ms) });
expect(result).toEqual({
ok: false,
code: 'pull-failed',
detail: 'max retries exceeded: local error: tls: bad record MAC',
});
// The first pull and five more.
expect(server.state.pulls).toBe(6);
expect(waits).toEqual([1000, 2000, 4000, 8000, 16000]);
expect(server.state.installed).toBe(false);
});
it('gives up on a stream that is cut every time, and says that it ended early', async () => {
const { fake: server, target } = await runtime({ pull: { fail: 'cut' } });
const result = await pullModel(target, MODEL, { sleep: noSleep, retries: 2 });
expect(result).toMatchObject({ ok: false, code: 'pull-failed' });
expect((result as { detail?: string }).detail).toBeTruthy();
expect(server.state.pulls).toBe(3);
});
it('does not retry when 0 retries are asked for, and falls back to 5 for a nonsense count', async () => {
const { fake: server, target } = await runtime({ pull: { fail: 'error-line' } });
expect(await pullModel(target, MODEL, { sleep: noSleep, retries: 0 })).toMatchObject({ code: 'pull-failed' });
expect(server.state.pulls).toBe(1);
await pullModel(target, MODEL, { sleep: noSleep, retries: -3 });
expect(server.state.pulls).toBe(1 + 6);
});
it('does not ask again for a model the registry does not have', async () => {
const { fake: server, target } = await runtime({ pull: { fail: 'not-found' } });
const result = await pullModel(target, MODEL, { sleep: noSleep });
expect(result).toEqual({
ok: false,
code: 'model-not-found',
detail: 'pull model manifest: file does not exist',
});
expect(server.state.pulls).toBe(1);
});
it('does not ask again when the disk is full', async () => {
const target: OllamaTarget = {
baseUrl: 'http://127.0.0.1:1',
fetch: async () =>
new Response(
'{"status":"pulling manifest"}\n{"error":"write /models/blobs/sha256-abc-partial: no space left on device"}\n',
{ status: 200 }
),
};
let asked = 0;
const counting: OllamaTarget = {
...target,
fetch: (...args) => {
asked += 1;
return (target.fetch as typeof fetch)(...args);
},
};
expect(await pullModel(counting, MODEL, { sleep: noSleep })).toMatchObject({ ok: false, code: 'disk-full' });
expect(asked).toBe(1);
});
it('retries, then fails, when no runtime answers at all', async () => {
const waits: number[] = [];
const result = await pullModel(await deadTarget(), MODEL, { sleep: async (ms) => void waits.push(ms), retries: 2 });
expect(result).toMatchObject({ ok: false, code: 'pull-failed' });
expect(waits).toHaveLength(2);
});
it('continues a pull that an earlier run left unfinished', async () => {
const { target } = await runtime({ pull: { resumeFrom: 11_000_000_000 } });
const seen: number[] = [];
const result = await pullModel(target, MODEL, { onProgress: (progress) => seen.push(progress.completedBytes) });
expect(result).toEqual({ ok: true, totalBytes: MODEL_BYTES });
// The first figure the window shows is where the earlier run stopped.
expect(seen[0]).toBeGreaterThanOrEqual(11_000_000_000);
});
it('reports the size of a model that was complete already, when the pull names no layer', async () => {
// A runtime that answers "success" at once: the size is what it lists for the model.
const { fake: server } = await runtime({ installed: true });
const target: OllamaTarget = {
baseUrl: server.baseUrl,
fetch: async (input, init) =>
String(input).endsWith('/api/pull')
? new Response('{"status":"pulling manifest"}\n{"status":"success"}\n', { status: 200 })
: fetch(input, init),
};
expect(await pullModel(target, MODEL)).toEqual({ ok: true, totalBytes: MODEL_BYTES });
});
it('stops the download when the caller cancels, and does not start again', async () => {
const { fake: server, target } = await runtime({ pull: { progressLines: 200, lineDelayMs: 10 } });
const controller = new AbortController();
const seen: PullProgress[] = [];
const result = await pullModel(target, MODEL, {
signal: controller.signal,
sleep: noSleep,
onProgress: (progress) => {
seen.push(progress);
controller.abort();
},
});
expect(result).toEqual({ ok: false, code: 'cancelled' });
expect(seen).toHaveLength(1);
expect(server.state.pulls).toBe(1);
expect(server.state.installed).toBe(false);
// Pulling again is the resume: it starts where the cancelled one stopped.
server.switches.pull.lineDelayMs = 0;
const again: number[] = [];
expect(await pullModel(target, MODEL, { onProgress: (p) => again.push(p.completedBytes) })).toEqual({
ok: true,
totalBytes: MODEL_BYTES,
});
expect(again[0]).toBeGreaterThan(0);
});
it('answers cancelled at once for a signal that is already aborted', async () => {
const { fake: server, target } = await runtime();
const controller = new AbortController();
controller.abort();
expect(await pullModel(target, MODEL, { signal: controller.signal })).toEqual({ ok: false, code: 'cancelled' });
expect(server.state.pulls).toBe(0);
});
it('calls a cancel a cancel also on its last try, never a failed download', async () => {
// With no retry left, the cut stream of an abort must not read as "the download keeps stopping".
const { fake: server, target } = await runtime({ pull: { progressLines: 200, lineDelayMs: 10 } });
const controller = new AbortController();
const result = await pullModel(target, MODEL, {
signal: controller.signal,
retries: 0,
onProgress: () => controller.abort(),
});
expect(result).toEqual({ ok: false, code: 'cancelled' });
expect(server.state.pulls).toBe(1);
});
it('does not let a progress callback that throws fail the download', async () => {
const { fake: server, target } = await runtime();
let called = 0;
const result = await pullModel(target, MODEL, {
sleep: noSleep,
onProgress: () => {
called += 1;
throw new Error('the window is gone');
},
});
expect(called).toBeGreaterThan(0);
expect(result).toEqual({ ok: true, totalBytes: MODEL_BYTES });
expect(server.state.pulls).toBe(1);
});
it('answers with a result, not an exception, when its own clock throws', async () => {
const { target } = await runtime();
const result = await pullModel(target, MODEL, {
sleep: noSleep,
retries: 1,
onProgress: () => undefined,
now: () => {
throw new Error('no clock');
},
});
expect(result).toEqual({ ok: false, code: 'pull-failed', detail: 'no clock' });
});
});
// ---------- 4. The check after the pull ----------
describe('the probe requests', () => {
it('builds a tiny chat with reasoning off', () => {
expect(buildChatProbeRequest(MODEL)).toEqual({
model: MODEL,
messages: [{ role: 'user', content: 'Reply with the single word: ready' }],
reasoning_effort: 'none',
max_tokens: 16,
temperature: 0,
stream: false,
});
expect(buildChatProbeRequest(MODEL)).not.toHaveProperty('tools');
});
it('builds a tool probe with a tools array, reasoning off and 1024 tokens', () => {
expect(TOOL_PROBE_MAX_TOKENS).toBe(1024);
const request = buildToolProbeRequest(MODEL) as {
model: string;
messages: { role: string; content: string }[];
tools: { type: string; function: { name: string; parameters: { type: string; required: string[] } } }[];
reasoning_effort: string;
max_tokens: number;
stream: boolean;
};
expect(request.model).toBe(MODEL);
expect(request.reasoning_effort).toBe('none');
expect(request.max_tokens).toBe(1024);
expect(request.stream).toBe(false);
expect(request.tools.map((tool) => [tool.type, tool.function.name])).toEqual([
['function', 'read_file'],
['function', 'write_file'],
['function', 'run_command'],
]);
for (const tool of request.tools) {
expect(tool.function.parameters.type).toBe('object');
expect(tool.function.parameters.required.length).toBeGreaterThan(0);
}
// The task can only be done with a tool.
expect(request.messages.at(-1)).toEqual({
role: 'user',
content: 'Read the file README.md and tell me what it says.',
});
// Nothing forces the call: the model has to decide to make it.
expect(request).not.toHaveProperty('tool_choice');
});
});
describe('judgeLoaded', () => {
const loaded = (overrides: Partial<OllamaPsModel> = {}): OllamaPsModel => ({
name: MODEL,
size: 23_205_798_956,
sizeVram: 23_205_798_956,
contextLength: CONTEXT,
...overrides,
});
it('passes a model that is all on the GPU at the chosen context', () => {
expect(judgeLoaded([loaded()], MODEL, CONTEXT)).toEqual({
ok: true,
loaded: { context: 131072, sizeBytes: 23_205_798_956, sizeVramBytes: 23_205_798_956 },
});
// Another loaded model beside it does not matter.
expect(judgeLoaded([loaded({ name: 'other:1', contextLength: 4096 }), loaded()], MODEL, CONTEXT).ok).toBe(true);
});
it('answers load-failed when the model is not listed', () => {
expect(judgeLoaded([], MODEL, CONTEXT)).toMatchObject({ ok: false, code: 'load-failed' });
expect(judgeLoaded([loaded({ name: 'other:1' })], MODEL, CONTEXT)).toMatchObject({
ok: false,
code: 'load-failed',
});
expect(judgeLoaded([loaded({ size: 0, sizeVram: 0 })], MODEL, CONTEXT)).toMatchObject({
ok: false,
code: 'load-failed',
});
});
it('answers does-not-fit when part of the model is not on the GPU', () => {
const judged = judgeLoaded([loaded({ sizeVram: 20_000_000_000 })], MODEL, CONTEXT);
expect(judged).toEqual({
ok: false,
code: 'does-not-fit',
detail: '20000000000 of 23205798956 bytes are on the GPU',
});
// One byte is enough.
expect(judgeLoaded([loaded({ sizeVram: 23_205_798_955 })], MODEL, CONTEXT)).toMatchObject({ code: 'does-not-fit' });
expect(judgeLoaded([loaded({ sizeVram: 0 })], MODEL, CONTEXT)).toMatchObject({ code: 'does-not-fit' });
});
it('answers context-not-applied when the runtime loaded another context', () => {
expect(judgeLoaded([loaded({ contextLength: 32768 })], MODEL, CONTEXT)).toEqual({
ok: false,
code: 'context-not-applied',
detail: 'The runtime reports a context of 32768 tokens, not 131072',
});
// A larger one is not the chosen one either.
expect(judgeLoaded([loaded({ contextLength: 262144 })], MODEL, CONTEXT)).toMatchObject({
code: 'context-not-applied',
});
});
it('looks at the GPU before the context', () => {
expect(judgeLoaded([loaded({ sizeVram: 1, contextLength: 4096 })], MODEL, CONTEXT)).toMatchObject({
code: 'does-not-fit',
});
});
});
describe('judgeToolProbe', () => {
/** An answer of `/v1/chat/completions` with this message and finish reason. */
const answer = (message: Record<string, unknown>, finishReason: string): unknown => ({
id: 'chatcmpl-1',
object: 'chat.completion',
choices: [{ index: 0, message: { role: 'assistant', ...message }, finish_reason: finishReason }],
});
const call = (argumentsValue: unknown, name: unknown = 'read_file'): Record<string, unknown> => ({
id: 'call_1',
index: 0,
type: 'function',
function: { name, arguments: argumentsValue },
});
it('passes on a tool call whose arguments parse as a JSON object', () => {
expect(
judgeToolProbe(200, answer({ content: '', tool_calls: [call('{"path":"README.md"}')] }, 'tool_calls'))
).toEqual({ ok: true });
// Arguments that are an object already, and an empty object, are arguments too.
expect(judgeToolProbe(200, answer({ tool_calls: [call({ path: 'README.md' })] }, 'tool_calls'))).toEqual({
ok: true,
});
expect(judgeToolProbe(200, answer({ tool_calls: [call('{}')] }, 'tool_calls'))).toEqual({ ok: true });
// The finish reason is not what decides: the call is.
expect(judgeToolProbe(200, answer({ tool_calls: [call('{"path":"a"}')] }, 'stop'))).toEqual({ ok: true });
});
it('answers no-tool-call for an answer in plain text', () => {
expect(judgeToolProbe(200, answer({ content: 'I would read the file, but I cannot.' }, 'stop'))).toEqual({
ok: false,
code: 'no-tool-call',
detail: 'The model answered in text: I would read the file, but I cannot.',
});
expect(judgeToolProbe(200, answer({ content: '', tool_calls: [] }, 'stop'))).toMatchObject({
ok: false,
code: 'no-tool-call',
});
});
it('answers no-tool-call for a tool call whose arguments are not a JSON object', () => {
for (const bad of ['{"path": ', 'README.md', '"README.md"', '[1]', 'null', '', 42, null, undefined]) {
expect(judgeToolProbe(200, answer({ tool_calls: [call(bad)] }, 'tool_calls')), JSON.stringify(bad)).toMatchObject(
{
ok: false,
code: 'no-tool-call',
}
);
}
// A call without a name, and one good call beside a broken one.
expect(judgeToolProbe(200, answer({ tool_calls: [call('{}', '')] }, 'tool_calls'))).toMatchObject({
code: 'no-tool-call',
});
expect(
judgeToolProbe(200, answer({ tool_calls: [call('{"path":"a"}'), call('{"path": ')] }, 'tool_calls'))
).toMatchObject({ code: 'no-tool-call' });
expect(judgeToolProbe(200, answer({ tool_calls: ['read_file'] }, 'tool_calls'))).toMatchObject({
code: 'no-tool-call',
});
});
it('answers probe-unfinished when the token limit ended the answer: that proves nothing', () => {
expect(
judgeToolProbe(200, answer({ content: '', reasoning: 'The user wants me to read a file. I should' }, 'length'))
).toEqual({ ok: false, code: 'probe-unfinished', detail: 'The answer ended at 1024 tokens' });
});
it('answers load-failed for any other status, with what the runtime said', () => {
expect(judgeToolProbe(500, { error: { message: 'llm server error: out of memory' } })).toEqual({
ok: false,
code: 'load-failed',
detail: 'llm server error: out of memory',
});
expect(judgeToolProbe(404, { error: "model 'x' not found" })).toMatchObject({
code: 'load-failed',
detail: "model 'x' not found",
});
expect(judgeToolProbe(502, undefined)).toEqual({
ok: false,
code: 'load-failed',
detail: 'The runtime answered 502',
});
});
it('answers no-tool-call for a 200 it cannot read at all', () => {
for (const body of [undefined, null, 'text', [], {}, { choices: [] }, { choices: [null] }, { choices: [{}] }]) {
expect(judgeToolProbe(200, body), JSON.stringify(body)).toEqual({
ok: false,
code: 'no-tool-call',
detail: 'The answer holds no tool call',
});
}
});
});
describe('verifyModel', () => {
it('passes a model that loads, sits on the GPU at the chosen context and calls a tool', async () => {
const { fake: server, target } = await runtime({ installed: true });
expect(await verifyModel(target, MODEL, CONTEXT)).toEqual({
ok: true,
loaded: { context: 131072, sizeBytes: 23_205_798_956, sizeVramBytes: 23_205_798_956 },
});
// In this order: the chat that loads it, the process list, the tool probe.
expect(server.requests.map((r) => `${r.method} ${r.path}`)).toEqual([
'POST /v1/chat/completions',
'GET /api/ps',
'POST /v1/chat/completions',
]);
const [chat, , probe] = server.requests.map((r) => r.body as Record<string, unknown>);
expect(chat).toEqual(buildChatProbeRequest(MODEL));
expect(chat).toMatchObject({ reasoning_effort: 'none' });
expect(chat).not.toHaveProperty('tools');
expect(probe).toEqual(buildToolProbeRequest(MODEL));
expect(probe).toMatchObject({ reasoning_effort: 'none', max_tokens: 1024 });
expect(Array.isArray(probe.tools)).toBe(true);
});
it('answers load-failed when the chat that loads the model fails, and asks nothing more', async () => {
const { fake: server, target } = await runtime({ installed: true, chat: 'error' });
expect(await verifyModel(target, MODEL, CONTEXT)).toEqual({
ok: false,
code: 'load-failed',
detail: 'llm server error: out of memory',
});
expect(server.requests).toHaveLength(1);
});
it('answers load-failed for a model that is not on disk', async () => {
const { target } = await runtime();
expect(await verifyModel(target, MODEL, CONTEXT)).toMatchObject({ ok: false, code: 'load-failed' });
});
it('answers load-failed when no runtime answers', async () => {
expect(await verifyModel(await deadTarget(), MODEL, CONTEXT)).toEqual({
ok: false,
code: 'load-failed',
detail: 'The runtime did not answer the first request',
});
});
it('answers load-failed when the model is not in the process list after the chat', async () => {
const { fake: server, target } = await runtime({ installed: true, ps: { loaded: false } });
expect(await verifyModel(target, MODEL, CONTEXT)).toMatchObject({ ok: false, code: 'load-failed' });
// The tool probe was not sent.
expect(server.requests.filter((r) => r.path === '/v1/chat/completions')).toHaveLength(1);
});
it('answers does-not-fit when part of the model is not on the GPU, before any tool probe', async () => {
const { fake: server, target } = await runtime({ installed: true, ps: { sizeVram: 18_000_000_000 } });
expect(await verifyModel(target, MODEL, CONTEXT)).toEqual({
ok: false,
code: 'does-not-fit',
detail: '18000000000 of 23205798956 bytes are on the GPU',
});
expect(server.requests.filter((r) => r.path === '/v1/chat/completions')).toHaveLength(1);
});
it('answers context-not-applied when the runtime loaded another context than the chosen one', async () => {
const { target } = await runtime({ installed: true, contextLength: 32768 });
expect(await verifyModel(target, MODEL, CONTEXT)).toMatchObject({ ok: false, code: 'context-not-applied' });
// The same runtime passes when 32768 is what was chosen.
expect(await verifyModel(target, MODEL, 32768)).toMatchObject({ ok: true, loaded: { context: 32768 } });
});
it('answers no-tool-call when the model answers the tool probe in text', async () => {
const { target } = await runtime({ installed: true, tool: 'none' });
expect(await verifyModel(target, MODEL, CONTEXT)).toEqual({
ok: false,
code: 'no-tool-call',
detail: 'The model answered in text: I would read the file, but I cannot.',
});
});
it('answers no-tool-call when the arguments of the tool call do not parse', async () => {
const { target } = await runtime({ installed: true, tool: 'bad-arguments' });
expect(await verifyModel(target, MODEL, CONTEXT)).toMatchObject({ ok: false, code: 'no-tool-call' });
});
it('answers probe-unfinished, not no-tool-call, when the token limit ended the answer', async () => {
const { target } = await runtime({ installed: true, tool: 'length' });
expect(await verifyModel(target, MODEL, CONTEXT)).toMatchObject({ ok: false, code: 'probe-unfinished' });
});
it('answers cancelled for an aborted signal, at whatever point it is noticed', async () => {
const { fake: server, target } = await runtime({ installed: true });
const before = new AbortController();
before.abort();
expect(await verifyModel(target, MODEL, CONTEXT, before.signal)).toEqual({ ok: false, code: 'cancelled' });
expect(server.requests).toHaveLength(0);
// Aborted while the first request is on its way.
const during = new AbortController();
const slow: OllamaTarget = {
baseUrl: server.baseUrl,
fetch: async (input, init) => {
during.abort();
return fetch(input, init);
},
};
expect(await verifyModel(slow, MODEL, CONTEXT, during.signal)).toEqual({ ok: false, code: 'cancelled' });
});
it('never throws, whatever the runtime sends back', async () => {
const answers = ['', 'not json', '[]', 'null', '{"choices":"x"}'];
for (const text of answers) {
const target: OllamaTarget = {
baseUrl: 'http://127.0.0.1:1',
fetch: async () => new Response(text, { status: 200 }),
};
const result = await verifyModel(target, MODEL, CONTEXT);
// The chat was "answered", the process list lists nothing.
expect(result, text).toMatchObject({ ok: false, code: 'load-failed' });
}
});
});
+982
View File
@@ -0,0 +1,982 @@
/**
* @fileoverview Tests for src/local-model/runtime.ts, everything but the supervisor (that is
* test/local-model-supervisor.test.ts): the pin, the budget line, the child's environment,
* the download with its hash, the port, and the removal.
*
* Pinned:
* - The pin is the reviewed constant, and a test never reaches it: the default `fetch`
* refuses under vitest.
* - The budget parser reads the REAL log line of both test Macs and no other line.
* - The environment is an allowlist: no `CODEMAN_PASSWORD`, none of the user's own
* `OLLAMA_*`, nothing that is not named.
* - The download: a matching hash unpacks, a wrong hash deletes the file and unpacks
* NOTHING, a cut body is continued with `Range`, the retries are capped, a body longer
* than the pin is refused, Cancel keeps the partial file for the next run.
* - The port is chosen by binding.
* - The removal deletes only inside the one folder and follows no symlink out of it.
*
* Port: none fixed (the fake runtime listens on 0; the port tests bind 0 and read the port).
*/
import { execFile } from 'node:child_process';
import { createHash, randomBytes } from 'node:crypto';
import { existsSync } from 'node:fs';
import { mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises';
import { createServer, type AddressInfo, type Server } from 'node:net';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { localModelPaths, type LocalModelPaths } from '../src/local-model/paths.js';
import {
chooseRuntimePort,
ensureRuntimeInstalled,
isLoopbackPortFree,
isOwnRuntimeCommand,
isRuntimeInstalled,
OLLAMA_RUNTIME_PIN,
parseInferenceComputeTotal,
removeLocalModelFiles,
RUNTIME_DOWNLOAD_RETRIES,
RUNTIME_KEEP_ALIVE,
RUNTIME_MAX_RESTARTS,
RUNTIME_PORT_FIRST,
RUNTIME_PORT_SPAN,
runtimeEnv,
type RuntimeDownloadProgress,
} from '../src/local-model/runtime.js';
import type { LocalModelRuntimePin } from '../src/types.js';
import {
buildFakeArchive,
fakeRuntimeLog,
INFERENCE_COMPUTE_LINE_MACBOOK,
MACBOOK_MEASURED_TOTAL_BYTES,
REAL_INFERENCE_COMPUTE_LINE_MAC_MINI,
REAL_VRAM_DEFAULT_CONTEXT_LINE_MAC_MINI,
startFakeOllama,
type FakeOllama,
} from './fixtures/fake-ollama.js';
const execFileAsync = promisify(execFile);
let fake: FakeOllama | undefined;
let paths: LocalModelPaths;
const cleanup: string[] = [];
beforeEach(() => {
// Under the temp HOME test/setup.ts gives this file.
paths = localModelPaths('0.40.15');
});
afterEach(async () => {
await fake?.close();
fake = undefined;
await rm(paths.root, { recursive: true, force: true });
for (const dir of cleanup.splice(0)) await rm(dir, { recursive: true, force: true });
});
/** A temp folder outside the local model folder, removed after the test. */
async function outsideDir(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'local-model-outside-'));
cleanup.push(dir);
return dir;
}
/** What `ensureRuntimeInstalled()` needs to reach the fake: the global `fetch`, and no waiting. */
function deps(waits: number[] = []): { fetch: typeof fetch; sleep: (ms: number) => Promise<void> } {
return {
fetch,
sleep: async (ms) => {
waits.push(ms);
},
};
}
/** The archive requests the fake saw, as the `Range` header of each (`undefined` without one). */
function archiveRanges(server: FakeOllama): (string | undefined)[] {
return server.requests
.filter((request) => request.path === '/download/ollama-darwin.tgz')
.map((request) => request.headers.range as string | undefined);
}
/** A fake whose archive is about 40 KB: a runtime script plus bytes that do not compress. */
async function fakeWithLargeArchive(archive: FakeOllama['switches']['archive'] | object = {}): Promise<FakeOllama> {
fake = await startFakeOllama({
archive,
archiveFiles: { ollama: '#!/bin/sh\necho "fake runtime"\n', 'lib/blob.bin': randomBytes(30_000).toString('hex') },
});
return fake;
}
// ---------- The pin and the constants ----------
describe('the pin', () => {
it('is the reviewed build: version, address, size and hash', () => {
expect(OLLAMA_RUNTIME_PIN).toEqual({
id: 'ollama',
version: '0.40.15',
url: 'https://github.com/ollama/ollama/releases/download/v0.40.15/ollama-darwin.tgz',
bytes: 185_971_671,
sha256: '607c79d5d6315ef9d896db83adde736a4c0057196329843544e79b95afb260a8',
unpackedBytes: 579_000_000,
minMacosMajor: 14,
});
});
it('cannot be changed by code that holds it', () => {
expect(Object.isFrozen(OLLAMA_RUNTIME_PIN)).toBe(true);
expect(() => {
(OLLAMA_RUNTIME_PIN as { url: string }).url = 'https://example.com/other.tgz';
}).toThrow();
expect(OLLAMA_RUNTIME_PIN.url).toMatch(/^https:\/\/github\.com\/ollama\/ollama\/releases\/download\/v0\.40\.15\//);
});
it('states the limits of the supervisor and the download', () => {
expect(RUNTIME_PORT_FIRST).toBe(11435);
expect(RUNTIME_PORT_SPAN).toBe(20);
expect(RUNTIME_MAX_RESTARTS).toBe(3);
expect(RUNTIME_DOWNLOAD_RETRIES).toBe(3);
expect(RUNTIME_KEEP_ALIVE).toBe('10m');
});
it('is never downloaded by a test: the default fetch refuses under vitest', async () => {
const waits: number[] = [];
const result = await ensureRuntimeInstalled({ deps: { sleep: async (ms) => void waits.push(ms) } });
expect(result).toEqual({
ok: false,
code: 'runtime-download-failed',
detail: 'fetch is not available under vitest: the test must inject its own',
});
// Refused at once, not after the retries.
expect(waits).toEqual([]);
expect(await isRuntimeInstalled()).toBe(false);
});
});
// ---------- The budget line ----------
describe('parseInferenceComputeTotal', () => {
it('reads the device total of the real line of the Mac mini', () => {
// total="11.8 GiB": 11.8 x 1024^3, rounded down.
expect(parseInferenceComputeTotal(REAL_INFERENCE_COMPUTE_LINE_MAC_MINI)).toBe(12_670_153_523);
expect(12_670_153_523).toBe(Math.floor(11.8 * 1024 ** 3));
});
it('reads 37.4 GiB, what the MacBook reported, as 40,157,944,217 bytes', () => {
expect(parseInferenceComputeTotal(INFERENCE_COMPUTE_LINE_MACBOOK)).toBe(40_157_944_217);
expect(parseInferenceComputeTotal(INFERENCE_COMPUTE_LINE_MACBOOK)).toBe(MACBOOK_MEASURED_TOTAL_BYTES);
// The two fields quoted in the measurement notes are enough.
expect(parseInferenceComputeTotal('msg="inference compute" total="37.4 GiB" available="37.4 GiB"')).toBe(
40_157_944_217
);
});
it('does not read the line that follows it, which carries the total under another key', () => {
expect(parseInferenceComputeTotal(REAL_VRAM_DEFAULT_CONTEXT_LINE_MAC_MINI)).toBeUndefined();
// Not even when that key sits on an "inference compute" line.
expect(
parseInferenceComputeTotal('msg="inference compute" total_vram="11.8 GiB" subtotal="1 GiB"')
).toBeUndefined();
});
it('finds exactly one line of a start-up log', () => {
const totals = fakeRuntimeLog('37.4 GiB').map(parseInferenceComputeTotal);
expect(totals).toEqual([undefined, undefined, 40_157_944_217, undefined]);
});
it('reads the binary units the runtime prints', () => {
const line = (total: string): string => `level=INFO msg="inference compute" id=0 total="${total}" available="1 B"`;
expect(parseInferenceComputeTotal(line('512 B'))).toBe(512);
expect(parseInferenceComputeTotal(line('8 KiB'))).toBe(8192);
expect(parseInferenceComputeTotal(line('12713.1 MiB'))).toBe(Math.floor(12713.1 * 1024 ** 2));
expect(parseInferenceComputeTotal(line('96 GiB'))).toBe(96 * 1024 ** 3);
expect(parseInferenceComputeTotal(line('1.5 TiB'))).toBe(1.5 * 1024 ** 4);
// Without the space between the number and the unit.
expect(parseInferenceComputeTotal(line('16GiB'))).toBe(16 * 1024 ** 3);
});
it('answers undefined for anything it does not understand, and never throws', () => {
const line = (total: string): string => `msg="inference compute" total="${total}"`;
for (const text of [
'',
'total="37.4 GiB"',
'msg="inference compute"',
line(''),
line('GiB'),
line('37.4'),
line('37.4 GB'),
line('-1 GiB'),
line('0 GiB'),
line('1e9 GiB'),
line('NaN GiB'),
'msg="inference compute" total=37.4GiB',
'msg="discovering available GPUs..."',
]) {
expect(parseInferenceComputeTotal(text), JSON.stringify(text)).toBeUndefined();
}
});
});
// ---------- The environment ----------
describe('runtimeEnv', () => {
const input = (source: NodeJS.ProcessEnv): Parameters<typeof runtimeEnv>[0] => ({
source,
home: '/data/local-model/home',
modelsDir: '/data/local-model/models',
port: 11435,
contextLength: 131072,
});
/** A server environment with everything a real one may hold. */
const SERVER_ENV: NodeJS.ProcessEnv = {
PATH: '/usr/local/bin:/usr/bin:/bin',
HOME: '/Users/arkon',
TMPDIR: '/var/folders/xy/T/',
LANG: 'de_CH.UTF-8',
HTTP_PROXY: 'http://proxy:3128',
HTTPS_PROXY: 'http://proxy:3128',
NO_PROXY: 'localhost,127.0.0.1',
http_proxy: 'http://proxy-lower:3128',
https_proxy: 'http://proxy-lower:3128',
no_proxy: 'localhost',
// Codeman's own secrets and switches.
CODEMAN_PASSWORD: 'hunter2',
CODEMAN_USERNAME: 'admin',
CODEMAN_MUX: '1',
CODEMAN_API_URL: 'http://127.0.0.1:3000',
// The user's settings for their OWN runtime.
OLLAMA_HOST: '0.0.0.0:11434',
OLLAMA_ORIGINS: '*',
OLLAMA_MODELS: '/Users/arkon/.ollama/models',
OLLAMA_DEBUG: '1',
OLLAMA_DEBUG_LOG_REQUESTS: '1',
OLLAMA_KEEP_ALIVE: '-1',
OLLAMA_NUM_PARALLEL: '8',
OLLAMA_MAX_LOADED_MODELS: '0',
OLLAMA_CONTEXT_LENGTH: '4096',
OLLAMA_NO_CLOUD: '0',
OLLAMA_REMOTES: 'evil.example',
OLLAMA_AUTH: '1',
LLAMA_ARG_CACHE_RAM: '0',
// Other secrets a server environment carries.
ANTHROPIC_API_KEY: 'sk-ant-secret',
OPENAI_API_KEY: 'sk-secret',
GITHUB_TOKEN: 'ghp_secret',
AWS_SECRET_ACCESS_KEY: 'aws-secret',
SSH_AUTH_SOCK: '/tmp/ssh.sock',
USER: 'arkon',
SHELL: '/bin/zsh',
LC_ALL: 'C',
NODE_OPTIONS: '--inspect',
DYLD_INSERT_LIBRARIES: '/tmp/evil.dylib',
};
it('holds exactly the allowlist: what is copied, the private HOME, and seven settings', () => {
expect(runtimeEnv(input(SERVER_ENV))).toEqual({
PATH: '/usr/local/bin:/usr/bin:/bin',
TMPDIR: '/var/folders/xy/T/',
LANG: 'de_CH.UTF-8',
HTTP_PROXY: 'http://proxy:3128',
HTTPS_PROXY: 'http://proxy:3128',
NO_PROXY: 'localhost,127.0.0.1',
http_proxy: 'http://proxy-lower:3128',
https_proxy: 'http://proxy-lower:3128',
no_proxy: 'localhost',
HOME: '/data/local-model/home',
OLLAMA_HOST: '127.0.0.1:11435',
OLLAMA_MODELS: '/data/local-model/models',
OLLAMA_CONTEXT_LENGTH: '131072',
OLLAMA_NO_CLOUD: '1',
OLLAMA_KEEP_ALIVE: '10m',
OLLAMA_NUM_PARALLEL: '1',
OLLAMA_MAX_LOADED_MODELS: '1',
});
});
it('lets no CODEMAN_ variable through, the password least of all', () => {
const env = runtimeEnv(input(SERVER_ENV));
expect(Object.keys(env).filter((name) => name.startsWith('CODEMAN_'))).toEqual([]);
expect(JSON.stringify(env)).not.toContain('hunter2');
});
it("lets none of the user's own OLLAMA_ settings through: every one is ours or absent", () => {
const env = runtimeEnv(input(SERVER_ENV));
expect(
Object.keys(env)
.filter((name) => name.startsWith('OLLAMA_'))
.sort()
).toEqual([
'OLLAMA_CONTEXT_LENGTH',
'OLLAMA_HOST',
'OLLAMA_KEEP_ALIVE',
'OLLAMA_MAX_LOADED_MODELS',
'OLLAMA_MODELS',
'OLLAMA_NO_CLOUD',
'OLLAMA_NUM_PARALLEL',
]);
// The inherited values lost against ours, and the ones we do not set are gone.
expect(env.OLLAMA_HOST).toBe('127.0.0.1:11435');
expect(env.OLLAMA_KEEP_ALIVE).toBe('10m');
expect(env.OLLAMA_MAX_LOADED_MODELS).toBe('1');
for (const name of [
'OLLAMA_ORIGINS',
'OLLAMA_DEBUG',
'OLLAMA_DEBUG_LOG_REQUESTS',
'OLLAMA_REMOTES',
'OLLAMA_AUTH',
]) {
expect(env, name).not.toHaveProperty(name);
}
expect(env).not.toHaveProperty('LLAMA_ARG_CACHE_RAM');
});
it('carries no secret and no value of the server that is not on the list', () => {
const env = runtimeEnv(input(SERVER_ENV));
const text = JSON.stringify(env);
for (const secret of ['sk-ant-secret', 'sk-secret', 'ghp_secret', 'aws-secret', 'evil', '/Users/arkon']) {
expect(text, secret).not.toContain(secret);
}
// Whatever is in the result and came from the server is one of the nine copied names.
const copied = [
'PATH',
'TMPDIR',
'LANG',
'HTTP_PROXY',
'HTTPS_PROXY',
'NO_PROXY',
'http_proxy',
'https_proxy',
'no_proxy',
];
for (const [name, value] of Object.entries(env)) {
if (SERVER_ENV[name] === value) expect(copied, name).toContain(name);
}
});
it('binds the loopback address only, on the port it is given', () => {
for (const port of [11435, 11454, 40123]) {
expect(runtimeEnv({ ...input({}), port }).OLLAMA_HOST).toBe(`127.0.0.1:${port}`);
}
expect(runtimeEnv({ ...input({}), contextLength: 65536 }).OLLAMA_CONTEXT_LENGTH).toBe('65536');
});
it('leaves out what the server does not have, and sets the rest all the same', () => {
expect(Object.keys(runtimeEnv(input({}))).sort()).toEqual([
'HOME',
'OLLAMA_CONTEXT_LENGTH',
'OLLAMA_HOST',
'OLLAMA_KEEP_ALIVE',
'OLLAMA_MAX_LOADED_MODELS',
'OLLAMA_MODELS',
'OLLAMA_NO_CLOUD',
'OLLAMA_NUM_PARALLEL',
]);
// An empty value is not copied either.
expect(runtimeEnv(input({ PATH: '', LANG: undefined }))).not.toHaveProperty('PATH');
});
it('is pure: the server environment is not changed', () => {
const source = { ...SERVER_ENV };
runtimeEnv(input(source));
expect(source).toEqual(SERVER_ENV);
});
});
// ---------- Whose process is it ----------
describe('isOwnRuntimeCommand', () => {
const root = '/Users/arkon/.codeman/local-model/runtime';
it('knows a runtime under our folder by its command line', () => {
expect(isOwnRuntimeCommand(`${root}/ollama-0.40.15/ollama serve`, root)).toBe(true);
expect(isOwnRuntimeCommand(` ${root}/ollama-0.40.15/ollama serve\n`, root)).toBe(true);
// Another version of ours, and a folder name with a space in it.
expect(isOwnRuntimeCommand(`${root}/ollama-0.39.0/ollama serve`, root)).toBe(true);
const spaced = '/Users/Jane Doe/.codeman/local-model/runtime';
expect(isOwnRuntimeCommand(`${spaced}/ollama-0.40.15/ollama serve`, spaced)).toBe(true);
expect(isOwnRuntimeCommand(`${root}/ollama-0.40.15/ollama serve`, `${root}/`)).toBe(true);
});
it('reads /private in front of a path as the same path', () => {
const temp = '/var/folders/xy/T/codeman/local-model/runtime';
expect(isOwnRuntimeCommand(`/private${temp}/ollama-0.40.15/ollama serve`, temp)).toBe(true);
expect(isOwnRuntimeCommand(`${temp}/ollama-0.40.15/ollama serve`, `/private${temp}`)).toBe(true);
});
it('does not take any other process for ours', () => {
for (const command of [
// A runtime the user started.
'/usr/local/bin/ollama serve',
'/Applications/Ollama.app/Contents/Resources/ollama serve',
'ollama serve',
// Our folder is only an argument, or a prefix of another folder.
`/bin/sh ${root}/ollama-0.40.15/ollama serve`,
`/usr/bin/tail -f ${root}/log`,
`${root}-other/ollama-0.40.15/ollama serve`,
`${root}`,
// A way back out of the folder.
`${root}/../../../../usr/local/bin/ollama serve`,
`${root}/ollama-0.40.15/../../../evil serve`,
'',
]) {
expect(isOwnRuntimeCommand(command, root), command).toBe(false);
}
expect(isOwnRuntimeCommand('/anything/ollama serve', '')).toBe(false);
expect(isOwnRuntimeCommand('relative/ollama serve', 'relative')).toBe(false);
});
});
// ---------- The download ----------
describe('ensureRuntimeInstalled', () => {
it('downloads, proves the hash, unpacks, and leaves a runtime that runs', async () => {
fake = await startFakeOllama();
const seen: RuntimeDownloadProgress[] = [];
const result = await ensureRuntimeInstalled({
pin: fake.pin(),
paths,
deps: deps(),
onProgress: (progress) => seen.push(progress),
});
expect(result).toEqual({ ok: true, binaryPath: paths.binary, alreadyInstalled: false });
expect(await isRuntimeInstalled(fake.pin(), paths)).toBe(true);
// Unpacked by the real tar: the file is executable and is the one of the archive.
expect((await stat(paths.binary)).mode & 0o111).not.toBe(0);
expect((await execFileAsync(paths.binary)).stdout).toBe('fake runtime\n');
// The archive and the temporary folder are gone; one request was enough.
expect(existsSync(paths.archivePartial)).toBe(false);
expect(existsSync(paths.unpackDir)).toBe(false);
expect(archiveRanges(fake)).toEqual([undefined]);
// Progress ends at the whole archive, measured against the pin.
expect(seen.at(-1)).toEqual({ completedBytes: fake.archive.size, totalBytes: fake.archive.size });
for (const progress of seen) expect(progress.totalBytes).toBe(fake.pin().bytes);
});
it('does nothing the second time: the runtime is already there', async () => {
fake = await startFakeOllama();
await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() });
const again = await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() });
expect(again).toEqual({ ok: true, binaryPath: paths.binary, alreadyInstalled: true });
expect(fake.state.archiveRequests).toBe(1);
});
it('unpacks NOTHING when the hash is wrong, and deletes the download', async () => {
fake = await startFakeOllama();
const wrongPin: LocalModelRuntimePin = {
...fake.pin(),
sha256: createHash('sha256').update('other').digest('hex'),
};
let unpacked = 0;
const result = await ensureRuntimeInstalled({
pin: wrongPin,
paths,
deps: {
...deps(),
untar: async () => {
unpacked += 1;
},
},
});
expect(result).toMatchObject({ ok: false, code: 'runtime-damaged' });
expect((result as { detail?: string }).detail).toContain(fake.archive.sha256);
expect(unpacked).toBe(0);
expect(existsSync(paths.archivePartial)).toBe(false);
expect(existsSync(paths.unpackDir)).toBe(false);
expect(existsSync(paths.runtimeDir)).toBe(false);
expect(await isRuntimeInstalled(wrongPin, paths)).toBe(false);
// The whole archive was read before the verdict: it is the hash that failed, not the size.
expect(fake.state.archiveRequests).toBe(1);
});
it('compares the hash whatever the case it is written in', async () => {
fake = await startFakeOllama();
const upper: LocalModelRuntimePin = { ...fake.pin(), sha256: fake.archive.sha256.toUpperCase() };
expect(await ensureRuntimeInstalled({ pin: upper, paths, deps: deps() })).toMatchObject({ ok: true });
});
it('continues a download that was cut, with a Range request from where the file ends', async () => {
fake = await fakeWithLargeArchive({ cutAfterBytes: 1000 });
const waits: number[] = [];
const result = await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps(waits) });
expect(result).toMatchObject({ ok: true, alreadyInstalled: false });
// The first request from the start, the second for the rest.
expect(archiveRanges(fake)).toEqual([undefined, 'bytes=1000-']);
expect(waits).toEqual([500]);
expect((await execFileAsync(paths.binary)).stdout).toBe('fake runtime\n');
});
it('gives up after 3 continuations and keeps the partial file for the next run', async () => {
expect(RUNTIME_DOWNLOAD_RETRIES).toBe(3);
fake = await fakeWithLargeArchive({ cutAfterBytes: 1000, cutTimes: Infinity });
const waits: number[] = [];
const result = await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps(waits) });
expect(result).toMatchObject({ ok: false, code: 'runtime-download-failed' });
expect((result as { detail?: string }).detail).toBeTruthy();
// The first request and three more, each from where the last one stopped.
expect(archiveRanges(fake)).toEqual([undefined, 'bytes=1000-', 'bytes=2000-', 'bytes=3000-']);
expect(waits).toEqual([500, 1000, 2000]);
expect((await stat(paths.archivePartial)).size).toBe(4000);
expect(existsSync(paths.runtimeDir)).toBe(false);
// "Try again": the next run continues the same file, it does not start over.
fake.switches.archive.cutAfterBytes = undefined;
expect(await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() })).toMatchObject({ ok: true });
expect(archiveRanges(fake).at(-1)).toBe('bytes=4000-');
expect((await execFileAsync(paths.binary)).stdout).toBe('fake runtime\n');
});
it('fails after the same number of tries when the server keeps answering an error', async () => {
fake = await startFakeOllama({ archive: { status: 503 } });
const waits: number[] = [];
const result = await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps(waits) });
expect(result).toEqual({
ok: false,
code: 'runtime-download-failed',
detail: 'The download server answered HTTP 503',
});
expect(fake.state.archiveRequests).toBe(4);
expect(waits).toHaveLength(3);
});
it('starts over when the server ignores the Range request and sends the whole archive', async () => {
fake = await fakeWithLargeArchive({ ignoreRange: true });
// A partial file of an earlier run: the true first 1500 bytes.
await mkdir(paths.runtimeRoot, { recursive: true });
await writeFile(paths.archivePartial, fake.archive.body.subarray(0, 1500));
const result = await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() });
expect(result).toMatchObject({ ok: true });
expect(archiveRanges(fake)).toEqual(['bytes=1500-']);
// Appending the whole body to the 1500 bytes would not have had the pinned hash.
expect((await execFileAsync(paths.binary)).stdout).toBe('fake runtime\n');
});
it('proves the WHOLE file: a partial file that is not the start of the archive ends as damaged', async () => {
fake = await fakeWithLargeArchive();
await mkdir(paths.runtimeRoot, { recursive: true });
await writeFile(paths.archivePartial, randomBytes(1500));
const first = await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() });
expect(first).toMatchObject({ ok: false, code: 'runtime-damaged' });
expect(existsSync(paths.archivePartial)).toBe(false);
expect(existsSync(paths.runtimeDir)).toBe(false);
// The damaged file is gone, so the next run downloads from the first byte.
expect(await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() })).toMatchObject({ ok: true });
expect(archiveRanges(fake)).toEqual(['bytes=1500-', undefined]);
});
it('throws away a partial file that is larger than the archive', async () => {
fake = await startFakeOllama();
await mkdir(paths.runtimeRoot, { recursive: true });
await writeFile(paths.archivePartial, randomBytes(fake.archive.size + 100));
expect(await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() })).toMatchObject({ ok: true });
expect(archiveRanges(fake)).toEqual([undefined]);
});
it('verifies a file that is already complete without asking the server', async () => {
fake = await startFakeOllama();
await mkdir(paths.runtimeRoot, { recursive: true });
await writeFile(paths.archivePartial, fake.archive.body);
expect(await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() })).toMatchObject({ ok: true });
expect(fake.state.archiveRequests).toBe(0);
});
it('refuses a download whose size is not the pinned one, before it reads it', async () => {
fake = await startFakeOllama();
const shortPin: LocalModelRuntimePin = { ...fake.pin(), bytes: fake.archive.size - 10 };
const result = await ensureRuntimeInstalled({ pin: shortPin, paths, deps: deps() });
expect(result).toEqual({
ok: false,
code: 'runtime-damaged',
detail: `The download is ${fake.archive.size} bytes, the pinned archive has ${fake.archive.size - 10}`,
});
expect(existsSync(paths.archivePartial)).toBe(false);
expect(fake.state.archiveRequests).toBe(1);
});
it('refuses a body that grows past the pinned size, when the server names no size', async () => {
fake = await startFakeOllama();
const pin = fake.pin();
const body = fake.archive.body;
let unpacked = 0;
// The right archive followed by more bytes, as a stream without a content-length.
const endless: typeof fetch = async () =>
new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(body));
controller.enqueue(new Uint8Array(1000));
controller.close();
},
}),
{ status: 200 }
);
const result = await ensureRuntimeInstalled({
pin,
paths,
deps: {
fetch: endless,
sleep: async () => undefined,
untar: async () => {
unpacked += 1;
},
},
});
expect(result).toEqual({
ok: false,
code: 'runtime-damaged',
detail: `The download is longer than the ${pin.bytes} bytes of the pinned archive`,
});
expect(unpacked).toBe(0);
expect(existsSync(paths.archivePartial)).toBe(false);
});
/** A server that sends the first `bytes` of the archive and then nothing more, until it is aborted. */
function stallingFetch(server: FakeOllama, bytes: number, counter: { requests: number }): typeof fetch {
return async (_input, init) => {
counter.requests += 1;
const signal = init?.signal ?? undefined;
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(server.archive.body.subarray(0, bytes)));
signal?.addEventListener('abort', () => controller.error(new DOMException('aborted', 'AbortError')));
},
}),
{ status: 200 }
);
};
}
it('stops when the caller cancels, and the next run continues the same file', async () => {
fake = await fakeWithLargeArchive();
const counter = { requests: 0 };
const controller = new AbortController();
setTimeout(() => controller.abort(), 40);
const cancelled = await ensureRuntimeInstalled({
pin: fake.pin(),
paths,
signal: controller.signal,
deps: { fetch: stallingFetch(fake, 2000, counter), sleep: async () => undefined },
});
expect(cancelled).toEqual({ ok: false, code: 'cancelled' });
// One request, no retry after a cancel, and what arrived is kept.
expect(counter.requests).toBe(1);
expect((await stat(paths.archivePartial)).size).toBe(2000);
expect(existsSync(paths.runtimeDir)).toBe(false);
expect(await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() })).toMatchObject({ ok: true });
expect(archiveRanges(fake)).toEqual(['bytes=2000-']);
});
it('answers cancelled at once for a signal that is already aborted', async () => {
fake = await startFakeOllama();
const controller = new AbortController();
controller.abort();
const result = await ensureRuntimeInstalled({ pin: fake.pin(), paths, signal: controller.signal, deps: deps() });
expect(result).toEqual({ ok: false, code: 'cancelled' });
expect(fake.state.archiveRequests).toBe(0);
});
it('treats a download that delivers nothing as cut, a bounded number of times', async () => {
fake = await fakeWithLargeArchive();
const counter = { requests: 0 };
const result = await ensureRuntimeInstalled({
pin: fake.pin(),
paths,
idleTimeoutMs: 30,
deps: { fetch: stallingFetch(fake, 500, counter), sleep: async () => undefined },
});
expect(result).toEqual({
ok: false,
code: 'runtime-download-failed',
detail: 'The download delivered nothing for 1 s',
});
expect(counter.requests).toBe(4);
});
it('answers runtime-unpack-failed when tar fails, keeps the proven archive, and unpacks it next time', async () => {
fake = await startFakeOllama();
const failed = await ensureRuntimeInstalled({
pin: fake.pin(),
paths,
deps: {
...deps(),
untar: async () => {
throw new Error('tar: Error opening archive: Unrecognized archive format');
},
},
});
expect(failed).toEqual({
ok: false,
code: 'runtime-unpack-failed',
detail: 'tar: Error opening archive: Unrecognized archive format',
});
expect(existsSync(paths.unpackDir)).toBe(false);
expect(existsSync(paths.runtimeDir)).toBe(false);
expect((await stat(paths.archivePartial)).size).toBe(fake.archive.size);
expect(await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() })).toMatchObject({ ok: true });
// Not downloaded a second time.
expect(fake.state.archiveRequests).toBe(1);
});
it('answers disk-full when unpacking runs out of space', async () => {
fake = await startFakeOllama();
const result = await ensureRuntimeInstalled({
pin: fake.pin(),
paths,
deps: {
...deps(),
untar: async () => {
throw new Error("tar: ollama: Can't write: No space left on device");
},
},
});
expect(result).toMatchObject({ ok: false, code: 'disk-full' });
});
it('refuses an archive that holds no runtime binary', async () => {
fake = await startFakeOllama({ archiveFiles: { 'README.txt': 'no binary here\n' } });
const result = await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() });
expect(result).toEqual({
ok: false,
code: 'runtime-unpack-failed',
detail: 'The archive holds no ollama binary',
});
expect(existsSync(paths.runtimeDir)).toBe(false);
expect(existsSync(paths.unpackDir)).toBe(false);
});
it('replaces a runtime folder that an interrupted unpack left without its binary', async () => {
fake = await startFakeOllama();
await mkdir(paths.runtimeDir, { recursive: true });
await writeFile(join(paths.runtimeDir, 'half-written'), 'x');
await mkdir(paths.unpackDir, { recursive: true });
await writeFile(join(paths.unpackDir, 'stale'), 'x');
expect(await isRuntimeInstalled(fake.pin(), paths)).toBe(false);
expect(await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() })).toMatchObject({ ok: true });
expect(existsSync(join(paths.runtimeDir, 'half-written'))).toBe(false);
expect(existsSync(join(paths.runtimeDir, 'stale'))).toBe(false);
expect(await readFile(paths.binary, 'utf8')).toContain('fake runtime');
});
it('writes the download where only the owner can read it, inside the one folder', async () => {
fake = await fakeWithLargeArchive({ cutAfterBytes: 1000, cutTimes: Infinity });
await ensureRuntimeInstalled({ pin: fake.pin(), paths, deps: deps() });
expect((await stat(paths.archivePartial)).mode & 0o077).toBe(0);
expect(paths.archivePartial.startsWith(paths.root)).toBe(true);
});
it('never throws: a version that is no version is an answer', async () => {
const badPin = { ...OLLAMA_RUNTIME_PIN, version: '../../etc' };
expect(await isRuntimeInstalled(badPin)).toBe(false);
expect(await ensureRuntimeInstalled({ pin: badPin, deps: deps() })).toMatchObject({
ok: false,
code: 'runtime-download-failed',
});
});
});
// ---------- The port ----------
describe('chooseRuntimePort', () => {
let holder: Server | undefined;
afterEach(async () => {
await new Promise<void>((resolve) => (holder ? holder.close(() => resolve()) : resolve()));
holder = undefined;
});
/** Binds a loopback port the system picks and keeps it. */
async function holdPort(): Promise<number> {
holder = createServer();
await new Promise<void>((resolve) => holder?.listen(0, '127.0.0.1', () => resolve()));
return (holder.address() as AddressInfo).port;
}
it('takes the first port of the span that is free, in order', async () => {
const asked: number[] = [];
const port = await chooseRuntimePort({
isFree: async (candidate) => {
asked.push(candidate);
return candidate >= 11438;
},
});
expect(port).toBe(11438);
expect(asked).toEqual([11435, 11436, 11437, 11438]);
});
it('starts at 11435 and tries 20 ports, never 11434', async () => {
const asked: number[] = [];
const port = await chooseRuntimePort({
isFree: async (candidate) => {
asked.push(candidate);
return false;
},
});
expect(port).toBeUndefined();
expect(asked).toHaveLength(20);
expect(asked[0]).toBe(11435);
expect(asked.at(-1)).toBe(11454);
expect(asked).not.toContain(11434);
});
it('skips the ports it is told to avoid: "Use another port"', async () => {
const asked: number[] = [];
const port = await chooseRuntimePort({
avoid: [11435, 11436],
isFree: async (candidate) => {
asked.push(candidate);
return true;
},
});
expect(port).toBe(11437);
expect(asked).toEqual([11437]);
});
it('does not take a port whose probe failed for free', async () => {
const port = await chooseRuntimePort({
first: 100,
span: 3,
isFree: async (candidate) => {
if (candidate === 100) throw new Error('EACCES');
return candidate === 102;
},
});
expect(port).toBe(102);
});
it('decides by BINDING: a held port is not free, and is free again once it is let go', async () => {
const port = await holdPort();
expect(await isLoopbackPortFree(port)).toBe(false);
expect(await chooseRuntimePort({ first: port, span: 1 })).toBeUndefined();
await new Promise<void>((resolve) => holder?.close(() => resolve()));
holder = undefined;
expect(await isLoopbackPortFree(port)).toBe(true);
expect(await chooseRuntimePort({ first: port, span: 1 })).toBe(port);
// The probe let go of it again: it can be bound right away.
expect(await isLoopbackPortFree(port)).toBe(true);
});
it('calls a number that is no port not free', async () => {
for (const port of [0, -1, 65536, 1.5, Number.NaN])
expect(await isLoopbackPortFree(port), String(port)).toBe(false);
});
});
// ---------- The removal ----------
describe('removeLocalModelFiles', () => {
/** Everything an install leaves on disk, plus the state file beside the folder. */
async function installTree(): Promise<void> {
await mkdir(join(paths.runtimeDir, 'lib'), { recursive: true });
await writeFile(paths.binary, '#!/bin/sh\n');
await writeFile(join(paths.runtimeDir, 'lib', 'libmlx.dylib'), 'x');
await mkdir(join(paths.runtimeRoot, 'ollama-0.39.0'), { recursive: true });
await writeFile(join(paths.runtimeRoot, 'ollama-0.39.0', 'ollama'), 'old');
await writeFile(paths.archivePartial, 'partial');
await mkdir(join(paths.homeDir, '.ollama'), { recursive: true });
await writeFile(join(paths.homeDir, '.ollama', 'id_ed25519'), 'private key');
await mkdir(join(paths.modelsDir, 'blobs'), { recursive: true });
await writeFile(join(paths.modelsDir, 'blobs', 'sha256-abc'), 'weights');
await writeFile(paths.stateFile, '{"schema":1,"state":"installed"}');
}
it('removes the runtime, every version of it, the archive and the private home, and keeps the models', async () => {
await installTree();
await removeLocalModelFiles({ models: false, paths });
expect(existsSync(paths.runtimeRoot)).toBe(false);
expect(existsSync(paths.homeDir)).toBe(false);
expect(await readFile(join(paths.modelsDir, 'blobs', 'sha256-abc'), 'utf8')).toBe('weights');
// The folder still holds the models, so it stays.
expect(existsSync(paths.root)).toBe(true);
});
it('removes the models too when asked, and then the folder itself', async () => {
await installTree();
await removeLocalModelFiles({ models: true, paths });
expect(existsSync(paths.modelsDir)).toBe(false);
expect(existsSync(paths.root)).toBe(false);
});
it('leaves the state file alone: that is the business of its one writer', async () => {
await installTree();
await removeLocalModelFiles({ models: true, paths });
expect(await readFile(paths.stateFile, 'utf8')).toBe('{"schema":1,"state":"installed"}');
await rm(paths.stateFile, { force: true });
});
it('is fine with nothing to remove, and can be run twice', async () => {
await removeLocalModelFiles({ models: true, paths });
await installTree();
await removeLocalModelFiles({ models: true, paths });
await removeLocalModelFiles({ models: true, paths });
expect(existsSync(paths.root)).toBe(false);
await rm(paths.stateFile, { force: true });
});
it('keeps a folder of the root that is not ours', async () => {
await installTree();
await mkdir(join(paths.root, 'notes'), { recursive: true });
await writeFile(join(paths.root, 'notes', 'mine.txt'), 'keep');
await removeLocalModelFiles({ models: true, paths });
expect(await readFile(join(paths.root, 'notes', 'mine.txt'), 'utf8')).toBe('keep');
await rm(paths.stateFile, { force: true });
});
it('unlinks a symlink in the place of a folder and never deletes what it points at', async () => {
const outside = await outsideDir();
await writeFile(join(outside, 'precious.gguf'), 'the only copy');
await mkdir(paths.root, { recursive: true });
// The model store was moved to another disk and linked back.
await symlink(outside, paths.modelsDir);
await removeLocalModelFiles({ models: true, paths });
expect(existsSync(paths.modelsDir)).toBe(false);
expect(await readFile(join(outside, 'precious.gguf'), 'utf8')).toBe('the only copy');
});
it('removes a symlink it meets inside a folder, not the tree behind it', async () => {
const outside = await outsideDir();
await writeFile(join(outside, 'keep.txt'), 'keep');
await mkdir(paths.homeDir, { recursive: true });
await symlink(outside, join(paths.homeDir, 'link-out'));
await removeLocalModelFiles({ models: false, paths });
expect(existsSync(paths.homeDir)).toBe(false);
expect(await readFile(join(outside, 'keep.txt'), 'utf8')).toBe('keep');
});
it('refuses a path that is not inside the one folder, and deletes nothing', async () => {
const outside = await outsideDir();
await writeFile(join(outside, 'keep.txt'), 'keep');
await installTree();
const cases: Partial<LocalModelPaths>[] = [
{ runtimeRoot: outside },
{ homeDir: outside },
{ modelsDir: outside },
// The folder itself, and a way back out of it.
{ runtimeRoot: paths.root },
{ homeDir: join(paths.root, '..', 'elsewhere') },
];
for (const change of cases) {
await expect(removeLocalModelFiles({ models: true, paths: { ...paths, ...change } })).rejects.toThrow(/Refusing/);
}
expect(await readFile(join(outside, 'keep.txt'), 'utf8')).toBe('keep');
// A refusal deletes NOTHING: every folder is judged before the first one is removed.
expect(await readFile(paths.binary, 'utf8')).toBe('#!/bin/sh\n');
expect(await readFile(join(paths.homeDir, '.ollama', 'id_ed25519'), 'utf8')).toBe('private key');
expect(await readFile(join(paths.modelsDir, 'blobs', 'sha256-abc'), 'utf8')).toBe('weights');
await rm(paths.stateFile, { force: true });
});
it('refuses a folder whose real place is outside the real root', async () => {
// The root is real, but `runtime` inside it is reached through a linked parent.
const outside = await outsideDir();
await mkdir(join(outside, 'runtime'), { recursive: true });
await writeFile(join(outside, 'runtime', 'keep.txt'), 'keep');
await mkdir(paths.root, { recursive: true });
await symlink(outside, join(paths.root, 'nested'));
const twisted: LocalModelPaths = { ...paths, runtimeRoot: join(paths.root, 'nested', 'runtime') };
await expect(removeLocalModelFiles({ models: false, paths: twisted })).rejects.toThrow(/Refusing/);
expect(await readFile(join(outside, 'runtime', 'keep.txt'), 'utf8')).toBe('keep');
});
});
+796
View File
@@ -0,0 +1,796 @@
/**
* @fileoverview Tests for the supervisor of src/local-model/runtime.ts: how Codeman starts,
* watches, restarts and stops its model runtime.
*
* Nothing real runs here. The child process is the fake of test/fixtures/fake-ollama.ts
* (`fake.spawn`): "starting" it makes the fake listen on the port of `OLLAMA_HOST` and write
* the runtime's start-up log, and killing it closes that listener.
*
* Pinned:
* - Start: one non-detached child, `<runtime folder>/ollama serve`, with the allowlisted
* environment; it is ready when `/api/version` answers the pinned version; the device total
* of its log is reported as the measured budget.
* - The port: a listener that is not ours is `port-in-use` and is never signalled; our own
* orphan (the stored pid runs a binary under our runtime folder) is adopted when it holds a
* loaded model at the wanted context, and replaced otherwise.
* - A child that exits by itself is restarted at most 3 times, then given up.
* - Stop: SIGTERM, and nothing is restarted afterwards.
* - Under vitest nothing spawns unless the test injects it.
*
* Port: none fixed (each test reserves a free loopback port).
*/
import { existsSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import { createServer, type Server } from 'node:net';
import type { ChildProcess, SpawnOptions } from 'node:child_process';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { localModelPaths, type LocalModelPaths } from '../src/local-model/paths.js';
import {
createRuntimeSupervisor,
RUNTIME_MAX_RESTARTS,
type RuntimeStatus,
type RuntimeSupervisor,
type RuntimeSupervisorDeps,
} from '../src/local-model/runtime.js';
import {
createFakeOllama,
FakeRuntimeChild,
freeLoopbackPort,
startFakeOllama,
type FakeOllama,
type FakeOllamaOptions,
} from './fixtures/fake-ollama.js';
const CONTEXT = 131072;
/** A pid of an "earlier server's" runtime. Above every real pid, like the fake's own. */
const ORPHAN_PID = 9_800_000;
let paths: LocalModelPaths;
let port: number;
const fakes: FakeOllama[] = [];
const supervisors: RuntimeSupervisor[] = [];
const servers: Server[] = [];
beforeEach(async () => {
paths = localModelPaths('0.40.15');
port = await freeLoopbackPort();
});
afterEach(async () => {
for (const supervisor of supervisors.splice(0)) await supervisor.stop();
for (const fake of fakes.splice(0)) await fake.close();
for (const server of servers.splice(0)) await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(paths.root, { recursive: true, force: true });
});
/** A fake runtime that does not listen until it is "spawned". */
function fakeRuntime(options: FakeOllamaOptions = {}): FakeOllama {
const fake = createFakeOllama(options);
fakes.push(fake);
return fake;
}
/** The children the fake has spawned so far. */
function children(fake: FakeOllama): FakeRuntimeChild[] {
return fake.state.children;
}
interface Harness {
supervisor: RuntimeSupervisor;
/** Every status `onChange` was called with. */
changes: RuntimeStatus[];
/** Every wait the supervisor asked for. */
waits: number[];
/** Every signal sent to a pid that is not our child. */
kills: [number, NodeJS.Signals][];
}
/** A supervisor whose child is `fake`. Waits are instant unless a test passes its own `sleep`. */
function supervise(fake: FakeOllama, overrides: Partial<RuntimeSupervisorDeps> = {}): Harness {
const changes: RuntimeStatus[] = [];
const waits: number[] = [];
const kills: [number, NodeJS.Signals][] = [];
const supervisor = createRuntimeSupervisor({
spawn: fake.spawn,
fetch,
env: { PATH: '/usr/bin:/bin' },
pin: fake.pin(),
paths,
sleep: async (ms) => {
waits.push(ms);
},
commandOfPid: async () => undefined,
kill: (pid, signal) => {
kills.push([pid, signal]);
},
onChange: (status) => changes.push(status),
startTimeoutMs: 3_000,
budgetWaitMs: 500,
...overrides,
});
supervisors.push(supervisor);
return { supervisor, changes, waits, kills };
}
/** Waits until `check()` is true. Fails the test when it never is. */
async function until(check: () => boolean, what: string, timeoutMs = 3_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (!check()) {
if (Date.now() > deadline) throw new Error(`Timed out waiting for: ${what}`);
await new Promise((resolve) => setTimeout(resolve, 5));
}
}
/** The command line `ps` would print for a runtime of ours. */
function ownCommand(): string {
return `${paths.binary} serve`;
}
// ---------- Start ----------
describe('start', () => {
it('starts one child, the runtime binary with "serve", and answers once it is up', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(children(fake)).toHaveLength(1);
const child = children(fake)[0];
expect(child.command).toBe(paths.binary);
expect(child.args).toEqual(['serve']);
expect(result).toEqual({
ok: true,
pid: child.pid,
port,
orphan: 'none',
// 37.4 GiB, read from the child's log line "inference compute".
measuredTotalBytes: 40_157_944_217,
});
expect(supervisor.status()).toEqual({
running: true,
pid: child.pid,
port,
contextLength: CONTEXT,
measuredTotalBytes: 40_157_944_217,
restarts: 0,
gaveUp: false,
});
expect(fake.port).toBe(port);
});
it('starts the child non-detached, with pipes, in the private home', async () => {
const fake = fakeRuntime();
const seen: SpawnOptions[] = [];
const { supervisor } = supervise(fake, {
spawn: (command, args, options) => {
seen.push(options);
return fake.spawn(command, args, options);
},
});
await supervisor.start({ port, contextLength: CONTEXT });
expect(seen).toHaveLength(1);
expect(seen[0].detached).toBe(false);
expect(seen[0].stdio).toEqual(['ignore', 'pipe', 'pipe']);
expect(seen[0].cwd).toBe(paths.homeDir);
expect(seen[0]).not.toHaveProperty('shell');
// The folders it needs exist before it starts.
expect(existsSync(paths.homeDir)).toBe(true);
expect(existsSync(paths.modelsDir)).toBe(true);
});
it('gives the child the allowlisted environment and nothing of the server beyond it', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, {
env: {
PATH: '/usr/bin:/bin',
HOME: '/Users/arkon',
LANG: 'en_US.UTF-8',
CODEMAN_PASSWORD: 'hunter2',
CODEMAN_API_URL: 'http://127.0.0.1:3000',
OLLAMA_ORIGINS: '*',
OLLAMA_HOST: '0.0.0.0:11434',
OLLAMA_KEEP_ALIVE: '-1',
ANTHROPIC_API_KEY: 'sk-ant-secret',
},
});
await supervisor.start({ port, contextLength: 65536 });
expect(children(fake)[0].env).toEqual({
PATH: '/usr/bin:/bin',
LANG: 'en_US.UTF-8',
HOME: paths.homeDir,
OLLAMA_HOST: `127.0.0.1:${port}`,
OLLAMA_MODELS: paths.modelsDir,
OLLAMA_CONTEXT_LENGTH: '65536',
OLLAMA_NO_CLOUD: '1',
OLLAMA_KEEP_ALIVE: '10m',
OLLAMA_NUM_PARALLEL: '1',
OLLAMA_MAX_LOADED_MODELS: '1',
});
});
it('reads the budget the child logs, whatever the Mac', async () => {
const fake = fakeRuntime({ spawn: { total: '11.8 GiB' } });
const { supervisor } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true, measuredTotalBytes: 12_670_153_523 });
expect(supervisor.status().measuredTotalBytes).toBe(12_670_153_523);
});
it('still starts when the child prints no budget line, and reports none', async () => {
const fake = fakeRuntime({ spawn: { logsBudget: false } });
const { supervisor } = supervise(fake, { budgetWaitMs: 60 });
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true, port });
expect(result).not.toHaveProperty('measuredTotalBytes');
expect(supervisor.status()).not.toHaveProperty('measuredTotalBytes');
expect(supervisor.status().running).toBe(true);
});
it('is idempotent: a child that already runs as asked is kept', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
const first = await supervisor.start({ port, contextLength: CONTEXT });
const second = await supervisor.start({ port, contextLength: CONTEXT });
expect(second).toEqual(first);
expect(children(fake)).toHaveLength(1);
expect(children(fake)[0].signals).toEqual([]);
});
it('replaces a child that runs with another context length', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const result = await supervisor.start({ port, contextLength: 65536 });
expect(result).toMatchObject({ ok: true, orphan: 'none' });
expect(children(fake)).toHaveLength(2);
// The first got SIGTERM; the second runs with the new context.
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(children(fake)[1].env.OLLAMA_CONTEXT_LENGTH).toBe('65536');
expect(supervisor.status()).toMatchObject({ running: true, pid: children(fake)[1].pid, contextLength: 65536 });
// Replacing on purpose is not a crash.
expect(supervisor.status().restarts).toBe(0);
});
it('runs two starts one after the other, never two children at once', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
const [a, b] = await Promise.all([
supervisor.start({ port, contextLength: CONTEXT }),
supervisor.start({ port, contextLength: CONTEXT }),
]);
expect(a).toMatchObject({ ok: true });
expect(b).toEqual(a);
expect(children(fake)).toHaveLength(1);
});
it('refuses a port or a context length that is none, without starting anything', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
for (const bad of [0, -1, 70_000, 1.5, Number.NaN]) {
expect(await supervisor.start({ port: bad, contextLength: CONTEXT }), String(bad)).toMatchObject({
ok: false,
code: 'runtime-start-failed',
});
}
for (const bad of [0, -5, 1.5, Number.NaN]) {
expect(await supervisor.start({ port, contextLength: bad }), String(bad)).toMatchObject({
ok: false,
code: 'runtime-start-failed',
});
}
expect(children(fake)).toHaveLength(0);
});
it('tells its caller every change, and survives a caller that throws', async () => {
const fake = fakeRuntime();
const seen: RuntimeStatus[] = [];
const { supervisor } = supervise(fake, {
onChange: (status) => {
seen.push(status);
throw new Error('the state file is read-only');
},
});
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true });
// The caller stores pid and port from this.
expect(seen.at(-1)).toMatchObject({ running: true, pid: children(fake)[0].pid, port, contextLength: CONTEXT });
await supervisor.stop();
expect(seen.at(-1)).toEqual({ running: false, restarts: 0, gaveUp: false });
});
it('hands out a copy of its status', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const status = supervisor.status();
status.running = false;
status.pid = 1;
expect(supervisor.status()).toMatchObject({ running: true, pid: children(fake)[0].pid });
});
it("writes no state file: storing pid and port is the caller's business", async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
await supervisor.stop();
expect(existsSync(paths.stateFile)).toBe(false);
});
});
// ---------- A start that fails ----------
describe('a start that fails', () => {
it('answers runtime-start-failed when the child never answers, and does not leave it running', async () => {
const fake = fakeRuntime({ spawn: { neverListens: true } });
const { supervisor } = supervise(fake, { startTimeoutMs: 150 });
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
const detail = (result as { detail?: string }).detail ?? '';
expect(detail).toContain('did not answer');
// The last lines of its log are the detail.
expect(detail).toContain('Listening on');
expect(children(fake)).toHaveLength(1);
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
});
it('answers runtime-start-failed when the child exits at once, with what it printed', async () => {
const fake = fakeRuntime({ spawn: { exitsAtOnce: 2 } });
const { supervisor } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
const detail = (result as { detail?: string }).detail ?? '';
expect(detail).toContain('exited with code 2');
expect(detail).toContain('address already in use');
// A start that failed is not a crash to recover from: nothing is started again.
await new Promise((resolve) => setTimeout(resolve, 50));
expect(children(fake)).toHaveLength(1);
expect(supervisor.status()).toMatchObject({ running: false, restarts: 0, gaveUp: false });
});
it('answers runtime-start-failed when a runtime of another version answers', async () => {
const fake = fakeRuntime({ version: '0.39.2' });
const { supervisor } = supervise(fake, { pin: { ...fake.pin(), version: '0.40.15' } });
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
expect((result as { detail?: string }).detail).toContain('0.39.2');
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
});
it('answers runtime-start-failed when the binary cannot be started at all', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, {
spawn: () => {
throw new Error('spawn EACCES');
},
});
expect(await supervisor.start({ port, contextLength: CONTEXT })).toEqual({
ok: false,
code: 'runtime-start-failed',
detail: 'spawn EACCES',
});
});
it('spawns nothing under vitest unless the test injects a spawn', async () => {
// No `spawn` and no `fetch` given: the defaults refuse here.
const supervisor = createRuntimeSupervisor({ paths, startTimeoutMs: 200 });
supervisors.push(supervisor);
const result = await supervisor.start({ port, contextLength: CONTEXT });
// The refusal itself, not a failed attempt to run a binary that is not there.
expect(result).toEqual({
ok: false,
code: 'runtime-start-failed',
detail: 'spawn is not available under vitest: the test must inject its own',
});
expect(supervisor.status().running).toBe(false);
});
it('gives up a start at once when stop is called while it waits', async () => {
const fake = fakeRuntime({ spawn: { neverListens: true } });
const { supervisor } = supervise(fake, {
startTimeoutMs: 20_000,
sleep: () => new Promise((resolve) => setTimeout(resolve, 10)),
});
const starting = supervisor.start({ port, contextLength: CONTEXT });
await until(() => children(fake).length === 1, 'the child to be spawned');
const began = Date.now();
await supervisor.stop();
const result = await starting;
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
// Not after the 20 seconds a start may take.
expect(Date.now() - began).toBeLessThan(5_000);
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(supervisor.status().running).toBe(false);
});
});
// ---------- The port ----------
describe('the port', () => {
/** A runtime that listens on `port` already, as an earlier server's would. */
async function listeningRuntime(options: FakeOllamaOptions = {}): Promise<FakeOllama> {
const other = createFakeOllama(options);
fakes.push(other);
await other.listen(port);
return other;
}
it('answers port-in-use for a runtime that is not ours, and does not touch it', async () => {
// The user's own runtime on our port. No pid of ours is stored.
const theirs = await listeningRuntime();
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
// Told apart from any other program on the port: it answers as a runtime, and is not ours.
expect(result).toEqual({
ok: false,
code: 'port-in-use',
detail: `A model runtime that Codeman did not start answers on port ${port} (version 0.40.15)`,
});
expect(children(fake)).toHaveLength(0);
expect(kills).toEqual([]);
expect(theirs.listening).toBe(true);
expect(supervisor.status().running).toBe(false);
});
it('answers port-in-use when the stored pid now runs something else', async () => {
const theirs = await listeningRuntime();
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
// The pid was reused, or it is a runtime outside our folder.
commandOfPid: async () => '/usr/local/bin/ollama serve',
});
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
expect(result).toMatchObject({ ok: false, code: 'port-in-use' });
expect(kills).toEqual([]);
expect(theirs.listening).toBe(true);
expect(children(fake)).toHaveLength(0);
});
it('answers port-in-use for a listener that is no runtime at all', async () => {
// Some other program: it takes the connection and hangs up.
const holder = createServer((socket) => socket.destroy());
servers.push(holder);
await new Promise<void>((resolve) => holder.listen(port, '127.0.0.1', () => resolve()));
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
expect(await supervisor.start({ port, contextLength: CONTEXT })).toEqual({
ok: false,
code: 'port-in-use',
detail: `Another program holds port ${port}`,
});
expect(children(fake)).toHaveLength(0);
});
it('ADOPTS its own orphan when that holds a loaded model at the wanted context', async () => {
const orphan = await listeningRuntime({ installed: true });
orphan.state.loaded = true;
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills, changes } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
});
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
// Kept as it is: nothing spawned, nothing signalled, the loaded model stays loaded.
expect(result).toEqual({ ok: true, pid: ORPHAN_PID, port, orphan: 'adopted' });
expect(children(fake)).toHaveLength(0);
expect(kills).toEqual([]);
expect(orphan.listening).toBe(true);
// Its log is not ours to read, so there is no measured budget.
expect(supervisor.status()).toEqual({
running: true,
pid: ORPHAN_PID,
port,
contextLength: CONTEXT,
restarts: 0,
gaveUp: false,
});
expect(changes.at(-1)).toMatchObject({ running: true, pid: ORPHAN_PID });
// So that the stop after this test finds it gone and does not wait out its grace period.
gone = true;
});
it('stops an adopted orphan by its pid', async () => {
const orphan = await listeningRuntime({ installed: true });
orphan.state.loaded = true;
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
kill: (pid, signal) => {
kills.push([pid, signal]);
gone = true;
},
});
await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
await supervisor.stop();
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
});
/** Starts with an orphan on the port that is ours and goes away when it is signalled. */
async function startOverOrphan(orphanOptions: FakeOllamaOptions, loaded: boolean) {
const orphan = await listeningRuntime(orphanOptions);
orphan.state.loaded = loaded;
let gone = false;
const fake = fakeRuntime();
const harness = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
kill: (pid, signal) => {
harness.kills.push([pid, signal]);
gone = true;
void orphan.close();
},
});
const result = await harness.supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
return { orphan, fake, result, ...harness };
}
it('REPLACES its own orphan when that has no model loaded: nothing is lost, and the budget is read again', async () => {
const { orphan, fake, result, kills, supervisor } = await startOverOrphan({ installed: true }, false);
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(orphan.listening).toBe(false);
expect(children(fake)).toHaveLength(1);
expect(result).toEqual({
ok: true,
pid: children(fake)[0].pid,
port,
orphan: 'replaced',
measuredTotalBytes: 40_157_944_217,
});
expect(supervisor.status()).toMatchObject({ running: true, pid: children(fake)[0].pid });
});
it('replaces its own orphan when the model is loaded at another context', async () => {
const { fake, result, kills } = await startOverOrphan({ installed: true, contextLength: 32768 }, true);
expect(result).toMatchObject({ ok: true, orphan: 'replaced' });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(children(fake)[0].env.OLLAMA_CONTEXT_LENGTH).toBe(String(CONTEXT));
});
it('replaces its own orphan when it is another version than the pinned one', async () => {
const { fake, result, kills } = await startOverOrphan({ installed: true, version: '0.39.2' }, true);
expect(result).toMatchObject({ ok: true, orphan: 'replaced' });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(children(fake)).toHaveLength(1);
});
it('replaces its own orphan that no longer answers, and starts on the free port', async () => {
// The stored pid is a runtime of ours, but nothing listens on the port.
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
kill: (pid, signal) => {
kills.push([pid, signal]);
gone = true;
},
});
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
expect(result).toMatchObject({ ok: true, orphan: 'replaced' });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
});
it('never signals a pid whose command is not under our runtime folder', async () => {
for (const command of [
'/usr/local/bin/ollama serve',
`${paths.runtimeRoot}-other/ollama serve`,
`/bin/sh -c ${paths.binary} serve`,
undefined,
]) {
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, { commandOfPid: async () => command });
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
expect(result, String(command)).toMatchObject({ ok: true, orphan: 'none' });
expect(kills, String(command)).toEqual([]);
await supervisor.stop();
}
});
it('takes over from an adopted orphan that dies: the next runtime is a child of its own', async () => {
const orphan = await listeningRuntime({ installed: true });
orphan.state.loaded = true;
let gone = false;
const fake = fakeRuntime();
const { supervisor } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
orphanPollMs: 15,
});
expect(await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID })).toMatchObject({
orphan: 'adopted',
});
// The orphan dies by itself.
await orphan.close();
gone = true;
await until(() => children(fake).length === 1 && supervisor.status().running, 'a child of our own to run');
expect(supervisor.status()).toMatchObject({
running: true,
pid: children(fake)[0].pid,
port,
restarts: 1,
measuredTotalBytes: 40_157_944_217,
});
});
});
// ---------- Restart ----------
describe('a child that exits by itself', () => {
it('is started again, with the same port and context', async () => {
const fake = fakeRuntime();
const { supervisor, waits, changes } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const first = children(fake)[0];
first.crash(1);
await until(() => children(fake).length === 2 && supervisor.status().running, 'the runtime to run again');
const second = children(fake)[1];
expect(second.env.OLLAMA_HOST).toBe(`127.0.0.1:${port}`);
expect(second.env.OLLAMA_CONTEXT_LENGTH).toBe(String(CONTEXT));
expect(supervisor.status()).toMatchObject({ running: true, pid: second.pid, restarts: 1, gaveUp: false });
// It waited a second before starting it again.
expect(waits).toContain(1000);
// The caller was told that it stopped, and that it runs again.
const running = changes.map((status) => status.running);
expect(running.indexOf(false, running.indexOf(true))).toBeGreaterThan(0);
expect(running.at(-1)).toBe(true);
// Nobody signalled the one that crashed.
expect(first.signals).toEqual([]);
});
it('is started again at most 3 times, then given up', async () => {
expect(RUNTIME_MAX_RESTARTS).toBe(3);
const fake = fakeRuntime();
const { supervisor, changes } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
for (let crash = 1; crash <= 3; crash++) {
children(fake).at(-1)?.crash(1);
await until(() => children(fake).length === crash + 1 && supervisor.status().running, `restart number ${crash}`);
expect(supervisor.status()).toMatchObject({ running: true, restarts: crash, gaveUp: false });
}
// The fourth exit is one too many.
children(fake).at(-1)?.crash(1);
await until(() => supervisor.status().gaveUp, 'the supervisor to give up');
expect(supervisor.status()).toEqual({
running: false,
port,
contextLength: CONTEXT,
measuredTotalBytes: 40_157_944_217,
restarts: 3,
gaveUp: true,
});
// The first child and three restarts, and no fifth.
await new Promise((resolve) => setTimeout(resolve, 60));
expect(children(fake)).toHaveLength(4);
expect(changes.at(-1)).toMatchObject({ running: false, gaveUp: true, restarts: 3 });
});
it('counts a restart that does not come up as one of the three', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, { startTimeoutMs: 100 });
await supervisor.start({ port, contextLength: CONTEXT });
// From now on the runtime exits as soon as it is started.
fake.switches.spawn.exitsAtOnce = 1;
children(fake)[0].crash(1);
await until(() => supervisor.status().gaveUp, 'the supervisor to give up');
expect(children(fake)).toHaveLength(4);
expect(supervisor.status()).toMatchObject({ running: false, restarts: 3, gaveUp: true });
});
it('starts counting again after a start that was asked for', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, { startTimeoutMs: 100 });
await supervisor.start({ port, contextLength: CONTEXT });
fake.switches.spawn.exitsAtOnce = 1;
children(fake)[0].crash(1);
await until(() => supervisor.status().gaveUp, 'the supervisor to give up');
// The person presses Start: it runs again, and the count is back at 0.
fake.switches.spawn.exitsAtOnce = undefined;
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true, orphan: 'none' });
expect(supervisor.status()).toMatchObject({ running: true, restarts: 0, gaveUp: false });
});
it('is not started again once stop was called, also while it waits to restart', async () => {
const fake = fakeRuntime();
let release: () => void = () => undefined;
const { supervisor } = supervise(fake, {
// The wait before a restart does not end by itself; the polls of a start still do.
sleep: (ms) =>
ms === 1000
? new Promise<void>((resolve) => {
release = resolve;
})
: Promise.resolve(),
});
await supervisor.start({ port, contextLength: CONTEXT });
children(fake)[0].crash(1);
await until(() => supervisor.status().restarts === 1, 'the restart to be scheduled');
const stopping = supervisor.stop();
release();
await stopping;
await new Promise((resolve) => setTimeout(resolve, 50));
expect(children(fake)).toHaveLength(1);
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
});
});
// ---------- Stop ----------
describe('stop', () => {
it('sends the child SIGTERM and resolves once it is gone', async () => {
const fake = fakeRuntime();
const { supervisor, changes } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const child = children(fake)[0];
await supervisor.stop();
expect(child.signals).toEqual(['SIGTERM']);
expect(child.signalCode).toBe('SIGTERM');
// Gone, and the port is free again.
expect(fake.listening).toBe(false);
await expect(fetch(`http://127.0.0.1:${port}/api/version`)).rejects.toThrow();
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
expect(changes.at(-1)).toEqual({ running: false, restarts: 0, gaveUp: false });
});
it('does not take the stop for a crash: nothing is started again', async () => {
const fake = fakeRuntime();
const { supervisor, waits } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
await supervisor.stop();
await new Promise((resolve) => setTimeout(resolve, 60));
expect(children(fake)).toHaveLength(1);
expect(waits).not.toContain(1000);
});
it('is safe when nothing runs, and twice in a row', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.stop();
await supervisor.start({ port, contextLength: CONTEXT });
await Promise.all([supervisor.stop(), supervisor.stop()]);
await supervisor.stop();
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(supervisor.status().running).toBe(false);
});
it('can be started again after a stop', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
await supervisor.stop();
const again = await supervisor.start({ port, contextLength: CONTEXT });
expect(again).toMatchObject({ ok: true, pid: children(fake)[1].pid, orphan: 'none' });
expect(children(fake)).toHaveLength(2);
});
it('kills the child when the server process exits without its shutdown', async () => {
// The last resort for a forced exit: a listener on the process, for as long as the child lives.
const fake = fakeRuntime();
const before = process.listenerCount('exit');
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
expect(process.listenerCount('exit')).toBe(before + 1);
const child = children(fake)[0] as unknown as ChildProcess & FakeRuntimeChild;
// What Node does on `process.exit()`: it calls the listeners.
const added = process.listeners('exit').at(-1) as () => void;
const exited = new Promise<void>((resolve) => child.once('exit', () => resolve()));
added();
await exited;
expect(child.signals).toEqual(['SIGTERM']);
// The listener goes with its child. (This process did not really exit, so the supervisor
// took the exit for a crash and started another child, which has a listener of its own.)
expect(process.listeners('exit')).not.toContain(added);
await supervisor.stop();
expect(process.listenerCount('exit')).toBe(before);
});
});