moving
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import { localInferenceRoutes } from '../../src/local/routes/local-inference.js';
|
||||
import { ManagedRuntimeRollbackError } from '../../src/local/managed-ollama-runtime.js';
|
||||
|
||||
describe('local-inference route — TS engine wiring', () => {
|
||||
let server: ReturnType<typeof Fastify>;
|
||||
@@ -34,3 +35,793 @@ describe('local-inference route — TS engine wiring', () => {
|
||||
expect(m.runtime).toBe('Ollama');
|
||||
});
|
||||
});
|
||||
|
||||
const unavailableOllama = {
|
||||
type: 'ollama' as const,
|
||||
available: false,
|
||||
url: 'http://127.0.0.1:11434',
|
||||
models: [],
|
||||
cloudModels: [],
|
||||
};
|
||||
|
||||
const unavailableVllm = {
|
||||
type: 'vllm' as const,
|
||||
available: false,
|
||||
url: 'http://localhost:8000',
|
||||
models: [],
|
||||
cloudModels: [],
|
||||
};
|
||||
|
||||
function managedStatus(supported = true) {
|
||||
return {
|
||||
source: 'waggle-managed' as const,
|
||||
supported,
|
||||
installed: false,
|
||||
running: false,
|
||||
targetVersion: 'test-1.0.0',
|
||||
version: 'test-1.0.0',
|
||||
targetInstalled: false,
|
||||
activeVersion: null,
|
||||
previousVersion: null,
|
||||
fallbackActive: false,
|
||||
rollback: {
|
||||
available: false,
|
||||
active: false,
|
||||
lastAttempt: null,
|
||||
},
|
||||
artifactSizeBytes: 123,
|
||||
downloadRequired: true,
|
||||
dockerRequired: false as const,
|
||||
...(!supported ? { reason: 'unsupported fixture' } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const rollbackAttempt = {
|
||||
failedVersion: 'test-2.0.0',
|
||||
restoredVersion: 'test-1.0.0',
|
||||
occurredAt: '2026-07-21T09:00:00.000Z',
|
||||
reason: 'target failed its readiness probe',
|
||||
};
|
||||
|
||||
function managedRollbackStatus(running = true) {
|
||||
return {
|
||||
...managedStatus(),
|
||||
installed: true,
|
||||
running,
|
||||
targetVersion: rollbackAttempt.failedVersion,
|
||||
version: rollbackAttempt.restoredVersion,
|
||||
targetInstalled: true,
|
||||
activeVersion: rollbackAttempt.restoredVersion,
|
||||
previousVersion: null,
|
||||
fallbackActive: true,
|
||||
rollback: {
|
||||
available: true,
|
||||
active: true,
|
||||
lastAttempt: rollbackAttempt,
|
||||
},
|
||||
downloadRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe('local-inference route — Waggle-managed runtime', () => {
|
||||
it('forwards target, active, previous, and rollback state without flattening it', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const rollbackStatus = managedRollbackStatus();
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => rollbackStatus,
|
||||
ensureReady: vi.fn(),
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'GET', url: '/api/local-inference/status' });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().managedRuntime).toEqual(rollbackStatus);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('advertises a Docker-free managed bootstrap instead of requiring a system Ollama install', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const stop = vi.fn(async () => undefined);
|
||||
const ensureReady = vi.fn();
|
||||
const startInstalled = vi.fn();
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => managedStatus(),
|
||||
ensureReady,
|
||||
startInstalled,
|
||||
stop,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'GET', url: '/api/local-inference/status' });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
ollamaInstalled: false,
|
||||
ollamaRunning: false,
|
||||
offlineReady: false,
|
||||
dockerRequired: false,
|
||||
managedRuntime: { supported: true, downloadRequired: true, dockerRequired: false },
|
||||
});
|
||||
expect(response.json().setupMessage).toMatch(/system Ollama install (?:is|are) not required/i);
|
||||
expect(ensureReady).not.toHaveBeenCalled();
|
||||
expect(startInstalled).not.toHaveBeenCalled();
|
||||
await server.close();
|
||||
expect(stop).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('restarts an active fallback runtime on sidecar start without retrying the upgrade', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const installedStatus = managedRollbackStatus(false);
|
||||
const runningStatus = { ...installedStatus, running: true };
|
||||
const ensureReady = vi.fn();
|
||||
const startInstalled = vi.fn(async () => ({
|
||||
installedNow: false,
|
||||
startedNow: true,
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
status: runningStatus,
|
||||
}));
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => installedStatus,
|
||||
ensureReady,
|
||||
startInstalled,
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
await server.ready();
|
||||
expect(startInstalled).toHaveBeenCalledOnce();
|
||||
await expect(startInstalled.mock.results[0]?.value).resolves.toMatchObject({
|
||||
installedNow: false,
|
||||
startedNow: true,
|
||||
});
|
||||
expect(ensureReady).not.toHaveBeenCalled();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('does not auto-start an older active runtime before an explicit target upgrade', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const pendingUpgradeStatus = {
|
||||
...managedStatus(),
|
||||
installed: true,
|
||||
running: false,
|
||||
targetVersion: 'test-2.0.0',
|
||||
version: 'test-1.0.0',
|
||||
targetInstalled: true,
|
||||
activeVersion: 'test-1.0.0',
|
||||
previousVersion: null,
|
||||
fallbackActive: false,
|
||||
rollback: {
|
||||
available: true,
|
||||
active: false,
|
||||
lastAttempt: null,
|
||||
},
|
||||
downloadRequired: false,
|
||||
};
|
||||
const ensureReady = vi.fn();
|
||||
const startInstalled = vi.fn();
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => pendingUpgradeStatus,
|
||||
ensureReady,
|
||||
startInstalled,
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
await server.ready();
|
||||
expect(startInstalled).not.toHaveBeenCalled();
|
||||
expect(ensureReady).not.toHaveBeenCalled();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('keeps the sidecar available when an installed runtime cannot restart', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const installedStatus = {
|
||||
...managedStatus(),
|
||||
installed: true,
|
||||
running: false,
|
||||
targetInstalled: true,
|
||||
downloadRequired: false,
|
||||
};
|
||||
const ensureReady = vi.fn();
|
||||
const startInstalled = vi.fn(async () => {
|
||||
throw new Error('runtime blocked by quarantine');
|
||||
});
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => installedStatus,
|
||||
ensureReady,
|
||||
startInstalled,
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'GET', url: '/api/local-inference/status' });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
ollamaInstalled: true,
|
||||
ollamaRunning: false,
|
||||
offlineReady: false,
|
||||
});
|
||||
expect(startInstalled).toHaveBeenCalledOnce();
|
||||
expect(ensureReady).not.toHaveBeenCalled();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('does not delay sidecar readiness and stops again after a close during restart', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const installedStatus = {
|
||||
...managedStatus(),
|
||||
installed: true,
|
||||
running: false,
|
||||
targetInstalled: true,
|
||||
downloadRequired: false,
|
||||
};
|
||||
let finishRestart!: () => void;
|
||||
const restartReleased = new Promise<void>((resolve) => { finishRestart = resolve; });
|
||||
const startInstalled = vi.fn(async () => {
|
||||
await restartReleased;
|
||||
return {
|
||||
installedNow: false,
|
||||
startedNow: true,
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
status: { ...installedStatus, running: true },
|
||||
};
|
||||
});
|
||||
const stop = vi.fn(async () => undefined);
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => installedStatus,
|
||||
ensureReady: vi.fn(),
|
||||
startInstalled,
|
||||
stop,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const readyOutcome = await Promise.race([
|
||||
server.ready().then(() => 'ready' as const),
|
||||
new Promise<'blocked'>((resolve) => setTimeout(() => resolve('blocked'), 500)),
|
||||
]);
|
||||
expect(readyOutcome).toBe('ready');
|
||||
expect(startInstalled).toHaveBeenCalledOnce();
|
||||
|
||||
await server.close();
|
||||
expect(stop).toHaveBeenCalledOnce();
|
||||
finishRestart();
|
||||
await vi.waitFor(() => expect(stop).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it('installs, starts, and health-checks the managed runtime through one bootstrap route', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const readyStatus = {
|
||||
...managedStatus(),
|
||||
installed: true,
|
||||
running: true,
|
||||
targetInstalled: true,
|
||||
activeVersion: 'test-1.0.0',
|
||||
downloadRequired: false,
|
||||
};
|
||||
const ensureReady = vi.fn(async () => ({
|
||||
installedNow: true,
|
||||
startedNow: true,
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
status: readyStatus,
|
||||
}));
|
||||
const ollamaProbe = vi.fn()
|
||||
.mockResolvedValueOnce(unavailableOllama)
|
||||
.mockResolvedValueOnce({
|
||||
...unavailableOllama,
|
||||
available: true,
|
||||
models: [],
|
||||
version: 'test-1.0.0',
|
||||
});
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => managedStatus(),
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
ok: true,
|
||||
installedNow: true,
|
||||
startedNow: true,
|
||||
dockerRequired: false,
|
||||
server: { available: true, version: 'test-1.0.0' },
|
||||
status: {
|
||||
activeVersion: 'test-1.0.0',
|
||||
fallbackActive: false,
|
||||
rollback: {
|
||||
available: false,
|
||||
active: false,
|
||||
lastAttempt: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(ensureReady).toHaveBeenCalledOnce();
|
||||
expect(ollamaProbe).toHaveBeenCalledTimes(2);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('reports a failed target upgrade after the verified prior runtime is restored', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const rollbackStatus = managedRollbackStatus();
|
||||
const ensureReady = vi.fn(async () => {
|
||||
throw new ManagedRuntimeRollbackError(rollbackAttempt, new Error(rollbackAttempt.reason));
|
||||
});
|
||||
const ollamaProbe = vi.fn()
|
||||
.mockResolvedValueOnce(unavailableOllama)
|
||||
.mockResolvedValueOnce({
|
||||
...unavailableOllama,
|
||||
available: true,
|
||||
version: 'test-1.0.0',
|
||||
});
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => rollbackStatus,
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(502);
|
||||
const body = response.json();
|
||||
expect(body).toMatchObject({
|
||||
ok: false,
|
||||
code: 'MANAGED_RUNTIME_ROLLED_BACK',
|
||||
server: { available: true, version: 'test-1.0.0' },
|
||||
});
|
||||
expect(body.managedRuntime).toEqual(rollbackStatus);
|
||||
expect(body).not.toHaveProperty('installedNow');
|
||||
expect(body).not.toHaveProperty('startedNow');
|
||||
expect(ensureReady).toHaveBeenCalledOnce();
|
||||
expect(ollamaProbe).toHaveBeenCalledTimes(2);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('does not claim bootstrap success when the already-running endpoint is a restored fallback', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const rollbackStatus = managedRollbackStatus();
|
||||
const ensureReady = vi.fn(async () => ({
|
||||
installedNow: false,
|
||||
startedNow: false,
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
status: rollbackStatus,
|
||||
}));
|
||||
const restoredServer = {
|
||||
...unavailableOllama,
|
||||
available: true,
|
||||
version: 'test-1.0.0',
|
||||
};
|
||||
const ollamaProbe = vi.fn(async () => restoredServer);
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => rollbackStatus,
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toEqual(expect.objectContaining({
|
||||
ok: false,
|
||||
code: 'MANAGED_RUNTIME_ROLLED_BACK',
|
||||
server: restoredServer,
|
||||
managedRuntime: rollbackStatus,
|
||||
}));
|
||||
expect(ensureReady).toHaveBeenCalledOnce();
|
||||
expect(ollamaProbe).toHaveBeenCalledTimes(2);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('waits for managed activation to commit when the runtime already answers loopback', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const activatingStatus = {
|
||||
...managedStatus(),
|
||||
installed: true,
|
||||
running: true,
|
||||
targetInstalled: true,
|
||||
downloadRequired: false,
|
||||
};
|
||||
const readyStatus = {
|
||||
...activatingStatus,
|
||||
activeVersion: 'test-1.0.0',
|
||||
};
|
||||
const ensureReady = vi.fn(async () => ({
|
||||
installedNow: false,
|
||||
startedNow: false,
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
status: readyStatus,
|
||||
}));
|
||||
const existingServer = {
|
||||
...unavailableOllama,
|
||||
available: true,
|
||||
version: 'test-1.0.0',
|
||||
};
|
||||
const ollamaProbe = vi.fn(async () => existingServer);
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => activatingStatus,
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
ok: true,
|
||||
installedNow: false,
|
||||
startedNow: false,
|
||||
server: existingServer,
|
||||
status: { activeVersion: 'test-1.0.0', fallbackActive: false },
|
||||
});
|
||||
expect(ensureReady).toHaveBeenCalledOnce();
|
||||
expect(ollamaProbe).toHaveBeenCalledTimes(2);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('preserves bootstrap success for an already-running endpoint that is not a fallback', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const status = managedStatus();
|
||||
const ensureReady = vi.fn(async () => ({
|
||||
installedNow: false,
|
||||
startedNow: false,
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
status,
|
||||
}));
|
||||
const existingServer = {
|
||||
...unavailableOllama,
|
||||
available: true,
|
||||
version: 'system-1.0.0',
|
||||
};
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => status,
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => existingServer,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
ok: true,
|
||||
installedNow: false,
|
||||
startedNow: false,
|
||||
server: existingServer,
|
||||
status,
|
||||
});
|
||||
expect(ensureReady).toHaveBeenCalledOnce();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('does not turn a fallback result into bootstrap success after a probe race', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const rollbackStatus = managedRollbackStatus();
|
||||
const ensureReady = vi.fn(async () => ({
|
||||
installedNow: false,
|
||||
startedNow: false,
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
status: rollbackStatus,
|
||||
}));
|
||||
const restoredServer = {
|
||||
...unavailableOllama,
|
||||
available: true,
|
||||
version: 'test-1.0.0',
|
||||
};
|
||||
const ollamaProbe = vi.fn()
|
||||
.mockResolvedValueOnce(unavailableOllama)
|
||||
.mockResolvedValueOnce(restoredServer);
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => rollbackStatus,
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toEqual(expect.objectContaining({
|
||||
ok: false,
|
||||
code: 'MANAGED_RUNTIME_ROLLED_BACK',
|
||||
server: restoredServer,
|
||||
managedRuntime: rollbackStatus,
|
||||
}));
|
||||
expect(response.json()).not.toHaveProperty('installedNow');
|
||||
expect(response.json()).not.toHaveProperty('startedNow');
|
||||
expect(ensureReady).toHaveBeenCalledOnce();
|
||||
expect(ollamaProbe).toHaveBeenCalledTimes(2);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('keeps the generic bootstrap failure contract for failures without a restored runtime', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const ensureReady = vi.fn(async () => {
|
||||
throw new Error('runtime download blocked');
|
||||
});
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => managedStatus(),
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toEqual({
|
||||
error: 'runtime download blocked',
|
||||
code: 'MANAGED_RUNTIME_BOOTSTRAP_FAILED',
|
||||
managedRuntime: managedStatus(),
|
||||
});
|
||||
expect(ensureReady).toHaveBeenCalledOnce();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('returns a truthful unsupported-platform contract without attempting a download', async () => {
|
||||
const server = Fastify({ logger: false });
|
||||
const ensureReady = vi.fn();
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => managedStatus(false),
|
||||
ensureReady,
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => unavailableOllama,
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
|
||||
const response = await server.inject({ method: 'POST', url: '/api/local-inference/bootstrap' });
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(response.json()).toMatchObject({
|
||||
code: 'MANAGED_RUNTIME_UNSUPPORTED',
|
||||
managedRuntime: { supported: false },
|
||||
});
|
||||
expect(ensureReady).not.toHaveBeenCalled();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('local-inference route — verified model installation', () => {
|
||||
async function buildPullServer(
|
||||
models: string[],
|
||||
modelDigests: Record<string, string> = Object.fromEntries(
|
||||
models.map((model) => [model, `sha256:${'a'.repeat(64)}`]),
|
||||
),
|
||||
) {
|
||||
const server = Fastify({ logger: false });
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => ({ ...managedStatus(), installed: true, running: true, downloadRequired: false }),
|
||||
ensureReady: vi.fn(),
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
ollamaProbe: async () => ({
|
||||
type: 'ollama',
|
||||
available: true,
|
||||
url: 'http://127.0.0.1:11434',
|
||||
models,
|
||||
modelDigests,
|
||||
cloudModels: [],
|
||||
version: 'test-1.0.0',
|
||||
}),
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
return server;
|
||||
}
|
||||
|
||||
it('rejects cloud aliases and malformed refs before any download', async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const server = await buildPullServer([]);
|
||||
try {
|
||||
const missing = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/local-inference/pull',
|
||||
});
|
||||
expect(missing.statusCode).toBe(400);
|
||||
expect(missing.json().code).toBe('MODEL_REQUIRED');
|
||||
|
||||
const cloud = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/local-inference/pull',
|
||||
payload: { model: 'minimax-m2.7:cloud' },
|
||||
});
|
||||
expect(cloud.statusCode).toBe(400);
|
||||
expect(cloud.json().code).toBe('REMOTE_MODEL_NOT_LOCAL');
|
||||
|
||||
const malformed = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/local-inference/pull',
|
||||
payload: { model: 'trusted/../../escape' },
|
||||
});
|
||||
expect(malformed.statusCode).toBe(400);
|
||||
expect(malformed.json().code).toBe('INVALID_MODEL_REF');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await server.close();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it('reports ready only after the pulled local model produces a real token', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ response: 'OK', done: true }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const server = await buildPullServer(['qwen3:1.7b']);
|
||||
try {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/local-inference/pull',
|
||||
payload: { model: 'qwen3:1.7b' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
ok: true,
|
||||
model: 'qwen3:1.7b',
|
||||
digest: `sha256:${'a'.repeat(64)}`,
|
||||
verifiedGeneration: true,
|
||||
sample: 'OK',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2,
|
||||
'http://127.0.0.1:11434/api/generate',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
);
|
||||
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toMatchObject({
|
||||
model: 'qwen3:1.7b',
|
||||
stream: false,
|
||||
think: false,
|
||||
options: { temperature: 0, num_predict: 8 },
|
||||
});
|
||||
} finally {
|
||||
await server.close();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it('canonicalizes the bare digest advertised by the live Ollama tag API', async () => {
|
||||
const wireDigest = 'B'.repeat(64);
|
||||
const digest = `sha256:${'b'.repeat(64)}`;
|
||||
const fetchMock = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith('/api/pull')) {
|
||||
return new Response(JSON.stringify({ status: 'success' }), { status: 200 });
|
||||
}
|
||||
if (url.endsWith('/api/tags')) {
|
||||
return new Response(JSON.stringify({
|
||||
models: [{ name: 'qwen3:1.7b', digest: wireDigest }],
|
||||
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
||||
}
|
||||
if (url.endsWith('/api/version')) {
|
||||
return new Response(JSON.stringify({ version: 'test-1.0.0' }), { status: 200 });
|
||||
}
|
||||
if (url.endsWith('/api/generate')) {
|
||||
return new Response(JSON.stringify({ response: 'OK', done: true }), { status: 200 });
|
||||
}
|
||||
return new Response('', { status: 503 });
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const server = Fastify({ logger: false });
|
||||
await server.register(localInferenceRoutes, {
|
||||
runtimeFactory: () => ({
|
||||
getStatus: () => ({ ...managedStatus(), installed: true, running: true }),
|
||||
ensureReady: vi.fn(),
|
||||
startInstalled: vi.fn(),
|
||||
stop: async () => undefined,
|
||||
}),
|
||||
vllmProbe: async () => unavailableVllm,
|
||||
});
|
||||
try {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/local-inference/pull',
|
||||
payload: { model: 'qwen3:1.7b' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({ model: 'qwen3:1.7b', digest });
|
||||
} finally {
|
||||
await server.close();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it('fails closed when Ollama does not advertise an immutable model digest', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'success' }), { status: 200 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const server = await buildPullServer(['qwen3:1.7b'], {});
|
||||
try {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/local-inference/pull',
|
||||
payload: { model: 'qwen3:1.7b' },
|
||||
});
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toMatchObject({
|
||||
code: 'MODEL_DIGEST_UNAVAILABLE',
|
||||
model: 'qwen3:1.7b',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
await server.close();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not claim readiness when an installed model fails generation', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'success' }), { status: 200 }))
|
||||
.mockResolvedValueOnce(new Response('model failed to load', { status: 500 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const server = await buildPullServer(['qwen3:1.7b']);
|
||||
try {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/local-inference/pull',
|
||||
payload: { model: 'qwen3:1.7b' },
|
||||
});
|
||||
expect(response.statusCode).toBe(502);
|
||||
expect(response.json()).toMatchObject({
|
||||
code: 'MODEL_GENERATION_PROBE_FAILED',
|
||||
installed: true,
|
||||
model: 'qwen3:1.7b',
|
||||
});
|
||||
} finally {
|
||||
await server.close();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user