gateway: add local backend update action
This commit is contained in:
@@ -384,7 +384,11 @@ Return status for user-level local LLM backend daemons (for example `ollama.serv
|
|||||||
|
|
||||||
#### `system.localBackendControl`
|
#### `system.localBackendControl`
|
||||||
|
|
||||||
Control a local backend daemon (`start`, `restart`, `stop`).
|
Control a local backend daemon (`start`, `restart`, `stop`, `update`).
|
||||||
|
|
||||||
|
- `update` semantics:
|
||||||
|
- `ollama`: pulls configured Ollama models (tiers/local providers + embedding/audio models) via `ollama pull`.
|
||||||
|
- `llamacpp`: performs a safe service restart (model file refresh remains external to Flynn).
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
"updated_at": "2026-02-23",
|
"updated_at": "2026-02-23",
|
||||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||||
"plans": {
|
"plans": {
|
||||||
|
"dashboard-local-backend-update-actions": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"updated": "2026-02-23",
|
||||||
|
"summary": "Extended local backend controls with explicit `update` action support. `ollama` update now pulls configured local models, while `llamacpp` uses a safe restart fallback with clear operator messaging. Updated gateway validation, dashboard action wiring, and regression coverage.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/gateway/handlers/localBackends.ts",
|
||||||
|
"src/gateway/handlers/system.ts",
|
||||||
|
"src/gateway/handlers/handlers.test.ts",
|
||||||
|
"src/gateway/ui/pages/dashboard.js",
|
||||||
|
"src/gateway/ui/pages/dashboard.test.ts",
|
||||||
|
"docs/api/PROTOCOL.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/gateway/handlers/handlers.test.ts src/gateway/ui/pages/dashboard.test.ts + pnpm typecheck passing"
|
||||||
|
},
|
||||||
"dashboard-local-backend-daemon-controls": {
|
"dashboard-local-backend-daemon-controls": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-23",
|
"date": "2026-02-23",
|
||||||
|
|||||||
@@ -244,6 +244,45 @@ describe('system handlers', () => {
|
|||||||
expect(getPath(result.result, 'status', 'activeState')).toBe('active');
|
expect(getPath(result.result, 'status', 'activeState')).toBe('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('system.localBackendControl accepts update action', async () => {
|
||||||
|
const controlLocalBackend = vi.fn(async (): Promise<LocalBackendControlResult> => ({
|
||||||
|
backend: 'ollama',
|
||||||
|
action: 'update',
|
||||||
|
status: {
|
||||||
|
id: 'ollama',
|
||||||
|
provider: 'ollama',
|
||||||
|
name: 'Ollama',
|
||||||
|
unit: 'ollama.service',
|
||||||
|
configured: true,
|
||||||
|
loadState: 'loaded',
|
||||||
|
activeState: 'active',
|
||||||
|
subState: 'running',
|
||||||
|
unitFileState: 'enabled',
|
||||||
|
description: 'Ollama Service',
|
||||||
|
pid: 321,
|
||||||
|
result: 'success',
|
||||||
|
statusText: 'active (running)',
|
||||||
|
availableActions: ['restart', 'stop', 'update'],
|
||||||
|
},
|
||||||
|
message: 'Updated 2 model(s).',
|
||||||
|
updatedModels: ['llama3.2', 'nomic-embed-text'],
|
||||||
|
}));
|
||||||
|
const handlers = createSystemHandlers({
|
||||||
|
...deps,
|
||||||
|
controlLocalBackend,
|
||||||
|
});
|
||||||
|
|
||||||
|
const req: GatewayRequest = {
|
||||||
|
id: 41,
|
||||||
|
method: 'system.localBackendControl',
|
||||||
|
params: { backend: 'ollama', action: 'update' },
|
||||||
|
};
|
||||||
|
const result = await handlers['system.localBackendControl'](req) as GatewayResponse;
|
||||||
|
expect(controlLocalBackend).toHaveBeenCalledWith('ollama', 'update');
|
||||||
|
expect(getPath(result.result, 'action')).toBe('update');
|
||||||
|
expect(getPath(result.result, 'updatedModels')).toEqual(['llama3.2', 'nomic-embed-text']);
|
||||||
|
});
|
||||||
|
|
||||||
it('system.presence returns empty result when getPresence is not provided', async () => {
|
it('system.presence returns empty result when getPresence is not provided', async () => {
|
||||||
const req: GatewayRequest = { id: 4, method: 'system.presence' };
|
const req: GatewayRequest = { id: 4, method: 'system.presence' };
|
||||||
const result = await handlers['system.presence'](req) as GatewayResponse;
|
const result = await handlers['system.presence'](req) as GatewayResponse;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { Config, ModelConfig } from '../../config/index.js';
|
|||||||
const execFile = promisify(execFileCb);
|
const execFile = promisify(execFileCb);
|
||||||
|
|
||||||
export type LocalBackendId = 'ollama' | 'llamacpp';
|
export type LocalBackendId = 'ollama' | 'llamacpp';
|
||||||
export type LocalBackendAction = 'start' | 'restart' | 'stop';
|
export type LocalBackendAction = 'start' | 'restart' | 'stop' | 'update';
|
||||||
|
|
||||||
export interface LocalBackendStatus {
|
export interface LocalBackendStatus {
|
||||||
id: LocalBackendId;
|
id: LocalBackendId;
|
||||||
@@ -29,6 +29,8 @@ export interface LocalBackendControlResult {
|
|||||||
backend: LocalBackendId;
|
backend: LocalBackendId;
|
||||||
action: LocalBackendAction;
|
action: LocalBackendAction;
|
||||||
status: LocalBackendStatus;
|
status: LocalBackendStatus;
|
||||||
|
message?: string;
|
||||||
|
updatedModels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemctlResult = { stdout: string; stderr: string };
|
type SystemctlResult = { stdout: string; stderr: string };
|
||||||
@@ -135,20 +137,26 @@ function isUnitMissing(errorText: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeAvailableActions(activeState: string, loadState: string): LocalBackendAction[] {
|
function computeAvailableActions(activeState: string, loadState: string, configured: boolean): LocalBackendAction[] {
|
||||||
if (loadState === 'not-found') {
|
if (loadState === 'not-found') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const withUpdate = (actions: LocalBackendAction[]): LocalBackendAction[] => {
|
||||||
|
if (configured && !actions.includes('update')) {
|
||||||
|
return [...actions, 'update'];
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
if (activeState === 'active') {
|
if (activeState === 'active') {
|
||||||
return ['restart', 'stop'];
|
return withUpdate(['restart', 'stop']);
|
||||||
}
|
}
|
||||||
if (activeState === 'inactive' || activeState === 'failed') {
|
if (activeState === 'inactive' || activeState === 'failed') {
|
||||||
return ['start', 'restart'];
|
return withUpdate(['start', 'restart']);
|
||||||
}
|
}
|
||||||
if (activeState === 'activating' || activeState === 'deactivating') {
|
if (activeState === 'activating' || activeState === 'deactivating') {
|
||||||
return ['restart', 'stop'];
|
return withUpdate(['restart', 'stop']);
|
||||||
}
|
}
|
||||||
return ['start', 'restart', 'stop'];
|
return withUpdate(['start', 'restart', 'stop']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildStatusText(activeState: string, subState: string): string {
|
function buildStatusText(activeState: string, subState: string): string {
|
||||||
@@ -206,7 +214,7 @@ async function fetchUnitStatus(
|
|||||||
statusText: '',
|
statusText: '',
|
||||||
availableActions: [],
|
availableActions: [],
|
||||||
};
|
};
|
||||||
status.availableActions = computeAvailableActions(status.activeState, status.loadState);
|
status.availableActions = computeAvailableActions(status.activeState, status.loadState, configured);
|
||||||
status.statusText = buildStatusText(status.activeState, status.subState);
|
status.statusText = buildStatusText(status.activeState, status.subState);
|
||||||
return status;
|
return status;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -228,14 +236,14 @@ async function fetchUnitStatus(
|
|||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
statusText: 'unknown',
|
statusText: 'unknown',
|
||||||
availableActions: ['start', 'restart', 'stop'],
|
availableActions: computeAvailableActions(base.activeState, base.loadState, configured),
|
||||||
error: detail,
|
error: detail,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidAction(action: string): asserts action is LocalBackendAction {
|
function ensureValidAction(action: string): asserts action is LocalBackendAction {
|
||||||
if (action !== 'start' && action !== 'restart' && action !== 'stop') {
|
if (action !== 'start' && action !== 'restart' && action !== 'stop' && action !== 'update') {
|
||||||
throw new Error(`Unsupported action: ${action}`);
|
throw new Error(`Unsupported action: ${action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,6 +254,89 @@ function ensureValidBackend(id: string): asserts id is LocalBackendId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectBackendModels(config: Config, backend: LocalBackendId): string[] {
|
||||||
|
const models = new Set<string>();
|
||||||
|
|
||||||
|
for (const model of collectModelConfigs(config)) {
|
||||||
|
if (model.provider === backend && typeof model.model === 'string' && model.model.trim().length > 0) {
|
||||||
|
models.add(model.model.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.memory.embedding.provider === backend) {
|
||||||
|
const embeddingModel = config.memory.embedding.model?.trim();
|
||||||
|
if (embeddingModel) {
|
||||||
|
models.add(embeddingModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioProvider = config.audio.provider;
|
||||||
|
if (config.audio.enabled && audioProvider?.type === backend) {
|
||||||
|
const audioModel = audioProvider.model?.trim();
|
||||||
|
if (audioModel) {
|
||||||
|
models.add(audioModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(models);
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateOllama(config: Config, runner: SystemctlRunner): Promise<{
|
||||||
|
message: string;
|
||||||
|
updatedModels?: string[];
|
||||||
|
}> {
|
||||||
|
const models = collectBackendModels(config, 'ollama');
|
||||||
|
if (models.length === 0) {
|
||||||
|
return { message: 'No configured Ollama models were found to update.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await fetchUnitStatus('ollama', true, runner);
|
||||||
|
if (status.loadState !== 'not-found' && status.activeState !== 'active') {
|
||||||
|
await runner(['--user', 'start', LOCAL_BACKEND_UNITS.ollama.unit, '--no-pager']);
|
||||||
|
// Briefly wait for the daemon to accept pull requests.
|
||||||
|
await delay(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedModels: string[] = [];
|
||||||
|
const failures: string[] = [];
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
try {
|
||||||
|
await execFile('ollama', ['pull', model], { timeout: 15 * 60_000, maxBuffer: 4 * 1024 * 1024 });
|
||||||
|
updatedModels.push(model);
|
||||||
|
} catch (error) {
|
||||||
|
failures.push(`${model} (${normalizeError(error)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedModels.length === 0 && failures.length > 0) {
|
||||||
|
throw new Error(`Failed to update configured Ollama models: ${failures.join('; ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
return {
|
||||||
|
message: `Updated ${updatedModels.length} model(s); ${failures.length} failed.`,
|
||||||
|
updatedModels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Updated ${updatedModels.length} model(s).`,
|
||||||
|
updatedModels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateLlamaCpp(runner: SystemctlRunner): Promise<{ message: string }> {
|
||||||
|
await runner(['--user', 'restart', LOCAL_BACKEND_UNITS.llamacpp.unit, '--no-pager']);
|
||||||
|
return {
|
||||||
|
message: 'Restarted llama.cpp service (model file updates are managed outside Flynn).',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function listLocalBackendStatuses(
|
export async function listLocalBackendStatuses(
|
||||||
config: Config,
|
config: Config,
|
||||||
runner: SystemctlRunner = defaultRunner,
|
runner: SystemctlRunner = defaultRunner,
|
||||||
@@ -265,8 +356,22 @@ export async function controlLocalBackend(
|
|||||||
ensureValidBackend(backend);
|
ensureValidBackend(backend);
|
||||||
ensureValidAction(action);
|
ensureValidAction(action);
|
||||||
|
|
||||||
const unitDef = LOCAL_BACKEND_UNITS[backend];
|
let message: string | undefined;
|
||||||
await runner(['--user', action, unitDef.unit, '--no-pager']);
|
let updatedModels: string[] | undefined;
|
||||||
|
|
||||||
|
if (action === 'update') {
|
||||||
|
if (backend === 'ollama') {
|
||||||
|
const result = await updateOllama(config, runner);
|
||||||
|
message = result.message;
|
||||||
|
updatedModels = result.updatedModels;
|
||||||
|
} else {
|
||||||
|
const result = await updateLlamaCpp(runner);
|
||||||
|
message = result.message;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const unitDef = LOCAL_BACKEND_UNITS[backend];
|
||||||
|
await runner(['--user', action, unitDef.unit, '--no-pager']);
|
||||||
|
}
|
||||||
|
|
||||||
const configured = collectConfiguredLocalBackends(config);
|
const configured = collectConfiguredLocalBackends(config);
|
||||||
const status = await fetchUnitStatus(backend, configured.has(backend), runner);
|
const status = await fetchUnitStatus(backend, configured.has(backend), runner);
|
||||||
@@ -274,5 +379,7 @@ export async function controlLocalBackend(
|
|||||||
backend,
|
backend,
|
||||||
action,
|
action,
|
||||||
status,
|
status,
|
||||||
|
message,
|
||||||
|
updatedModels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,8 +324,8 @@ export function createSystemHandlers(deps: SystemHandlerDeps) {
|
|||||||
if (!params?.action || typeof params.action !== 'string') {
|
if (!params?.action || typeof params.action !== 'string') {
|
||||||
return makeError(request.id, ErrorCode.InvalidRequest, 'action is required');
|
return makeError(request.id, ErrorCode.InvalidRequest, 'action is required');
|
||||||
}
|
}
|
||||||
if (!['start', 'restart', 'stop'].includes(params.action)) {
|
if (!['start', 'restart', 'stop', 'update'].includes(params.action)) {
|
||||||
return makeError(request.id, ErrorCode.InvalidRequest, 'action must be one of: start, restart, stop');
|
return makeError(request.id, ErrorCode.InvalidRequest, 'action must be one of: start, restart, stop, update');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const LOCAL_BACKEND_ACTION_LABELS = {
|
|||||||
start: 'Start',
|
start: 'Start',
|
||||||
restart: 'Restart',
|
restart: 'Restart',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
|
update: 'Update',
|
||||||
};
|
};
|
||||||
const SERVICE_TOGGLE_PATCH_PATHS = {
|
const SERVICE_TOGGLE_PATCH_PATHS = {
|
||||||
heartbeat: 'automation.heartbeat.enabled',
|
heartbeat: 'automation.heartbeat.enabled',
|
||||||
@@ -1569,7 +1570,7 @@ function updateLocalBackends(localBackendsData) {
|
|||||||
const loadText = backend.loadState || 'unknown';
|
const loadText = backend.loadState || 'unknown';
|
||||||
const resultText = backend.result || 'unknown';
|
const resultText = backend.result || 'unknown';
|
||||||
const availableActions = Array.isArray(backend.availableActions)
|
const availableActions = Array.isArray(backend.availableActions)
|
||||||
? backend.availableActions.filter((value) => ['start', 'restart', 'stop'].includes(String(value)))
|
? backend.availableActions.filter((value) => ['start', 'restart', 'stop', 'update'].includes(String(value)))
|
||||||
: [];
|
: [];
|
||||||
const actionMessage = actionState?.message
|
const actionMessage = actionState?.message
|
||||||
? `<div class="text-xs ${actionState.tone === 'error' ? 'text-red-400' : actionState.tone === 'success' ? 'text-green-400' : 'text-zinc-400'}">${escapeHtml(String(actionState.message))}</div>`
|
? `<div class="text-xs ${actionState.tone === 'error' ? 'text-red-400' : actionState.tone === 'success' ? 'text-green-400' : 'text-zinc-400'}">${escapeHtml(String(actionState.message))}</div>`
|
||||||
@@ -1623,6 +1624,7 @@ async function handleLocalBackendAction(backendId, action) {
|
|||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
const status = result?.status;
|
const status = result?.status;
|
||||||
|
const resultMessage = typeof result?.message === 'string' ? result.message : null;
|
||||||
if (status && typeof status === 'object') {
|
if (status && typeof status === 'object') {
|
||||||
_lastLocalBackends = _lastLocalBackends.map((backend) =>
|
_lastLocalBackends = _lastLocalBackends.map((backend) =>
|
||||||
backend.id === backendId ? status : backend);
|
backend.id === backendId ? status : backend);
|
||||||
@@ -1630,7 +1632,7 @@ async function handleLocalBackendAction(backendId, action) {
|
|||||||
_localBackendActionState.set(backendId, {
|
_localBackendActionState.set(backendId, {
|
||||||
pending: false,
|
pending: false,
|
||||||
tone: 'success',
|
tone: 'success',
|
||||||
message: `${actionLabel} completed`,
|
message: resultMessage ? `${actionLabel} completed: ${resultMessage}` : `${actionLabel} completed`,
|
||||||
});
|
});
|
||||||
updateLocalBackends({ backends: _lastLocalBackends });
|
updateLocalBackends({ backends: _lastLocalBackends });
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ function createMockClient() {
|
|||||||
pid: 111,
|
pid: 111,
|
||||||
result: 'success',
|
result: 'success',
|
||||||
statusText: 'active (running)',
|
statusText: 'active (running)',
|
||||||
availableActions: ['restart', 'stop'],
|
availableActions: ['restart', 'stop', 'update'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'llamacpp',
|
id: 'llamacpp',
|
||||||
@@ -122,7 +122,7 @@ function createMockClient() {
|
|||||||
pid: null,
|
pid: null,
|
||||||
result: 'success',
|
result: 'success',
|
||||||
statusText: 'inactive/dead',
|
statusText: 'inactive/dead',
|
||||||
availableActions: ['start', 'restart'],
|
availableActions: ['start', 'restart', 'update'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
calls: [] as Array<{ method: string; params?: Record<string, unknown> }>,
|
calls: [] as Array<{ method: string; params?: Record<string, unknown> }>,
|
||||||
@@ -227,14 +227,16 @@ function createMockClient() {
|
|||||||
backend.subState = 'running';
|
backend.subState = 'running';
|
||||||
backend.statusText = 'active (running)';
|
backend.statusText = 'active (running)';
|
||||||
backend.pid = backend.id === 'ollama' ? 222 : 333;
|
backend.pid = backend.id === 'ollama' ? 222 : 333;
|
||||||
backend.availableActions = ['restart', 'stop'];
|
backend.availableActions = ['restart', 'stop', 'update'];
|
||||||
backend.result = 'success';
|
backend.result = 'success';
|
||||||
} else if (action === 'stop') {
|
} else if (action === 'stop') {
|
||||||
backend.activeState = 'inactive';
|
backend.activeState = 'inactive';
|
||||||
backend.subState = 'dead';
|
backend.subState = 'dead';
|
||||||
backend.statusText = 'inactive/dead';
|
backend.statusText = 'inactive/dead';
|
||||||
backend.pid = null;
|
backend.pid = null;
|
||||||
backend.availableActions = ['start', 'restart'];
|
backend.availableActions = ['start', 'restart', 'update'];
|
||||||
|
backend.result = 'success';
|
||||||
|
} else if (action === 'update') {
|
||||||
backend.result = 'success';
|
backend.result = 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +244,7 @@ function createMockClient() {
|
|||||||
backend: backendId,
|
backend: backendId,
|
||||||
action,
|
action,
|
||||||
status: deepClone(backend),
|
status: deepClone(backend),
|
||||||
|
message: action === 'update' ? 'Updated backend assets' : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -445,7 +448,7 @@ describe('DashboardPage assistant controls', () => {
|
|||||||
expect(toolCalls.some((entry) => entry.params?.tool === 'cron.trigger')).toBe(true);
|
expect(toolCalls.some((entry) => entry.params?.tool === 'cron.trigger')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders local backend controls and triggers system.localBackendControl', async () => {
|
it('renders local backend controls and triggers system.localBackendControl actions', async () => {
|
||||||
const { state, client } = createMockClient();
|
const { state, client } = createMockClient();
|
||||||
|
|
||||||
await DashboardPage.render(container, client);
|
await DashboardPage.render(container, client);
|
||||||
@@ -460,10 +463,16 @@ describe('DashboardPage assistant controls', () => {
|
|||||||
startBtn.dispatchEvent(new windowObj.Event('click', { bubbles: true }));
|
startBtn.dispatchEvent(new windowObj.Event('click', { bubbles: true }));
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
|
const updateBtn = container.querySelector('#ops-local-backends .local-backend-action-btn[data-backend-id="ollama"][data-action="update"]');
|
||||||
|
expect(updateBtn).toBeTruthy();
|
||||||
|
updateBtn.dispatchEvent(new windowObj.Event('click', { bubbles: true }));
|
||||||
|
await flush();
|
||||||
|
|
||||||
const backendCalls = state.calls.filter((entry) => entry.method === 'system.localBackendControl');
|
const backendCalls = state.calls.filter((entry) => entry.method === 'system.localBackendControl');
|
||||||
expect(backendCalls).toHaveLength(2);
|
expect(backendCalls).toHaveLength(3);
|
||||||
expect(backendCalls[0].params).toEqual({ backend: 'ollama', action: 'restart' });
|
expect(backendCalls[0].params).toEqual({ backend: 'ollama', action: 'restart' });
|
||||||
expect(backendCalls[1].params).toEqual({ backend: 'llamacpp', action: 'start' });
|
expect(backendCalls[1].params).toEqual({ backend: 'llamacpp', action: 'start' });
|
||||||
|
expect(backendCalls[2].params).toEqual({ backend: 'ollama', action: 'update' });
|
||||||
expect(state.localBackends.find((entry) => entry.id === 'llamacpp')?.activeState).toBe('active');
|
expect(state.localBackends.find((entry) => entry.id === 'llamacpp')?.activeState).toBe('active');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user