This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -1,10 +1,12 @@
import { describe, it, expect, afterEach, vi } from 'vitest';
import fs from 'node:fs';
import net from 'node:net';
import path from 'node:path';
import os from 'node:os';
import { MindDB } from '@waggle/core';
import { startService } from '../src/local/service.js';
import { getLiteLLMStatus } from '../src/local/lifecycle.js';
import { getLiteLLMStatus, selectLiteLLMPython } from '../src/local/lifecycle.js';
import { PROVIDER_ENV_NAMES } from '../src/local/provider-env.js';
import type { FastifyInstance } from 'fastify';
function makeTmpDir(): string {
@@ -15,6 +17,26 @@ function randomPort(): number {
return 3333 + Math.floor(Math.random() * 1000);
}
function occupyLoopbackPort(server: net.Server): Promise<number> {
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
reject(new Error('Unable to resolve occupied test port'));
return;
}
resolve(address.port);
});
});
}
function clearProviderEnv(): void {
for (const envName of new Set(Object.values(PROVIDER_ENV_NAMES).flat())) {
vi.stubEnv(envName, '');
}
}
describe('Agent Service', () => {
const cleanups: Array<() => Promise<void>> = [];
const tmpDirs: string[] = [];
@@ -107,7 +129,7 @@ describe('Agent Service', () => {
const port = randomPort();
const litellmPort = randomPort();
vi.stubEnv('ANTHROPIC_API_KEY', '');
clearProviderEnv();
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = String(input);
if (url.endsWith('/health/liveliness')) {
@@ -139,6 +161,242 @@ describe('Agent Service', () => {
expect(body.defaultModel).toBe('ollama/llama3.2:latest');
});
it('does not let a pre-provider readiness probe overwrite fresh Ollama health', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
const litellmPort = randomPort();
let releaseStaleProbe!: () => void;
const staleProbeGate = new Promise<void>((resolve) => { releaseStaleProbe = resolve; });
let markInitialProbeConsumed!: () => void;
const initialProbeConsumed = new Promise<void>((resolve) => { markInitialProbeConsumed = resolve; });
let ollamaTagsCalls = 0;
clearProviderEnv();
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = String(input);
if (url.endsWith('/health/liveliness')) {
return { ok: false, status: 503 } as Response;
}
if (url.endsWith('/health/readiness')) {
await staleProbeGate;
return {
get ok() {
markInitialProbeConsumed();
return false;
},
status: 404,
} as Response;
}
if (url.endsWith('/api/tags')) {
ollamaTagsCalls += 1;
if (ollamaTagsCalls === 1) setImmediate(releaseStaleProbe);
return {
ok: true,
json: async () => {
if (ollamaTagsCalls > 1) markInitialProbeConsumed();
return { models: [{ name: 'llama3.2:latest' }] };
},
} as Response;
}
return { ok: false, status: 404 } as Response;
});
const { server } = await startService({ dataDir, port, litellmPort, skipLiteLLM: true });
cleanups.push(async () => { await server.close(); });
await initialProbeConsumed;
await new Promise<void>((resolve) => { setImmediate(resolve); });
const res = await server.inject({ method: 'GET', url: '/health' });
expect(res.statusCode).toBe(200);
expect(res.json()).toMatchObject({
status: 'ok',
llm: {
provider: 'ollama',
health: 'healthy',
reachable: true,
},
defaultModel: 'ollama/llama3.2:latest',
});
});
it('reports the built-in provider proxy degraded until a configured key is verified', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
const litellmPort = randomPort();
clearProviderEnv();
vi.stubEnv('OPENAI_API_KEY', 'openai-solo-test-key');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = String(input);
if (url.endsWith('/health/liveliness') || url.endsWith('/api/tags')) {
return { ok: false, status: 503 } as Response;
}
return { ok: false, status: 404 } as Response;
});
const { server } = await startService({ dataDir, port, litellmPort, skipLiteLLM: true });
cleanups.push(async () => { await server.close(); });
const res = await server.inject({ method: 'GET', url: '/health' });
expect(res.statusCode).toBe(200);
expect(res.json()).toMatchObject({
status: 'degraded',
llm: {
provider: 'anthropic-proxy',
health: 'degraded',
},
});
expect(server.agentState.llmProvider.detail).toContain('provider proxy');
expect(server.agentState.llmProvider.detail).toContain('verification pending');
expect(server.localConfig.manageLiteLLM).toBe(false);
});
it('does not probe or adopt an unrelated LiteLLM when explicitly skipped', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
const litellmPort = randomPort();
clearProviderEnv();
vi.stubEnv('OPENAI_API_KEY', 'openai-solo-test-key');
const workerRequests: Array<{ url: string; authorization: string | null }> = [];
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
const url = String(input);
if (url === `http://127.0.0.1:${port}/v1/chat/completions`) {
workerRequests.push({
url,
authorization: new Headers(init?.headers).get('authorization'),
});
return new Response(JSON.stringify({
choices: [{
message: { role: 'assistant', content: 'Isolated sub-agent response.' },
finish_reason: 'stop',
}],
usage: { prompt_tokens: 7, completion_tokens: 4 },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
return new Response('{}', { status: 503 });
});
const { server, litellm } = await startService({ dataDir, port, litellmPort, skipLiteLLM: true });
cleanups.push(async () => { await server.close(); });
expect(litellm).toEqual({ status: 'error', port: litellmPort, error: 'Skipped' });
expect(server.agentState.llmProvider).toMatchObject({
provider: 'anthropic-proxy',
health: 'degraded',
});
expect(server.localConfig.manageLiteLLM).toBe(false);
expect(server.localConfig.useBuiltInProxy).toBe(true);
expect(server.localConfig.litellmUrl).toBe(`http://127.0.0.1:${port}/v1`);
expect(fetchSpy.mock.calls.some(([input]) => (
String(input) === `http://localhost:${litellmPort}/health/liveliness`
))).toBe(false);
const spawn = server.agentState.allTools.find(tool => tool.name === 'spawn_agent');
expect(spawn).toBeDefined();
const output = await spawn!.execute({
name: 'Isolation verifier',
role: 'custom',
task: 'Confirm the active provider route.',
tools: [],
model: 'openrouter/openai/gpt-5.3-codex',
max_turns: 1,
});
expect(output).toContain('Isolated sub-agent response.');
expect(workerRequests).toEqual([{
url: `http://127.0.0.1:${port}/v1/chat/completions`,
authorization: `Bearer ${server.agentState.wsSessionToken}`,
}]);
});
it('atomically falls back from an occupied desktop port and routes spawned agents to it', async () => {
const base = makeTmpDir();
tmpDirs.push(base);
const dataDir = path.join(base, 'data');
const readyFile = path.join(base, 'desktop-ready.json');
const blocker = net.createServer();
const preferredPort = await occupyLoopbackPort(blocker);
cleanups.push(() => new Promise<void>((resolve) => blocker.close(() => resolve())));
vi.stubEnv('WAGGLE_DESKTOP_PORT_FALLBACK', '1');
vi.stubEnv('WAGGLE_INSTANCE_ID', 'desktop-fallback-test');
vi.stubEnv('WAGGLE_READY_FILE', readyFile);
vi.stubEnv('WAGGLE_BIND_ALL', '1');
clearProviderEnv();
vi.stubEnv('OPENAI_API_KEY', 'openai-solo-test-key');
const workerRequests: Array<{ url: string; authorization: string | null }> = [];
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
const url = String(input);
if (url.endsWith('/health/liveliness') || url.endsWith('/api/tags')) {
return new Response('{}', { status: 503 });
}
if (url.endsWith('/v1/chat/completions')) {
workerRequests.push({
url,
authorization: new Headers(init?.headers).get('authorization'),
});
return new Response(JSON.stringify({
choices: [{
message: { role: 'assistant', content: 'Fallback sub-agent response.' },
finish_reason: 'stop',
}],
usage: { prompt_tokens: 7, completion_tokens: 4 },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
return new Response('{}', { status: 503 });
});
const { server } = await startService({ dataDir, port: preferredPort, skipLiteLLM: true });
const address = server.server.address();
expect(address && typeof address === 'object').toBe(true);
const actualPort = address && typeof address === 'object' ? address.port : 0;
expect(address && typeof address === 'object' ? address.address : '').toBe('127.0.0.1');
expect(actualPort).toBeGreaterThan(0);
expect(actualPort).not.toBe(preferredPort);
expect(server.localConfig.port).toBe(actualPort);
expect(server.localConfig.litellmUrl).toBe(`http://127.0.0.1:${actualPort}/v1`);
expect(JSON.parse(fs.readFileSync(readyFile, 'utf8'))).toMatchObject({
schemaVersion: 1,
instanceId: 'desktop-fallback-test',
pid: process.pid,
preferredPort,
port: actualPort,
});
expect((await server.inject({ method: 'GET', url: '/health' })).json()).toMatchObject({
instanceId: 'desktop-fallback-test',
port: actualPort,
});
const spawn = server.agentState.allTools.find(tool => tool.name === 'spawn_agent');
expect(spawn).toBeDefined();
expect(await spawn!.execute({
name: 'Fallback verifier',
role: 'custom',
task: 'Confirm the fallback provider route.',
tools: [],
model: 'openrouter/openai/gpt-5.3-codex',
max_turns: 1,
})).toContain('Fallback sub-agent response.');
expect(workerRequests).toEqual([{
url: `http://127.0.0.1:${actualPort}/v1/chat/completions`,
authorization: `Bearer ${server.agentState.wsSessionToken}`,
}]);
await server.close();
expect(fs.existsSync(readyFile)).toBe(false);
});
it('server gracefully shuts down on close', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
@@ -156,6 +414,27 @@ describe('Agent Service', () => {
});
describe('LiteLLM Lifecycle', () => {
it('skips a Hermes venv without LiteLLM and selects the next working interpreter', () => {
const hermesPython = 'C:\\Users\\test\\hermes\\venv\\Scripts\\python.exe';
const systemPython = 'C:\\Python311\\python.exe';
const probed: string[] = [];
const selected = selectLiteLLMPython(
[hermesPython, systemPython],
(candidate) => {
probed.push(candidate);
return candidate === systemPython;
},
);
expect(selected).toBe(systemPython);
expect(probed).toEqual([hermesPython, systemPython]);
});
it('returns null when no discovered interpreter can import LiteLLM', () => {
expect(selectLiteLLMPython(['python-a', 'python-b'], () => false)).toBeNull();
});
it('getLiteLLMStatus returns error when nothing is running', async () => {
// Use a very unlikely port
const status = await getLiteLLMStatus(59999);