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

@@ -36,9 +36,11 @@
"dependencies": {
"@clerk/fastify": "^3.0.0",
"@fastify/cors": "^10.0.0",
"@fastify/static": "^9.0.0",
"@fastify/static": "^10.1.2",
"@fastify/websocket": "^11.0.0",
"@huggingface/transformers": "3.8.1",
"@whiskeysockets/baileys": "^7.0.0-rc13",
"adm-zip": "^0.6.0",
"archiver": "^7.0.1",
"better-sqlite3": "^12.6.2",
"bullmq": "^5.0.0",
@@ -55,6 +57,7 @@
"postgres": "^3.4.0",
"sqlite-vec": "^0.1.7-alpha.2",
"stripe": "^21.0.1",
"undici": "^6.27.0",
"ws": "^8.21.0",
"zod": "^3.24.0"
},

View File

@@ -7,7 +7,7 @@
import { spawn, spawnSync } from 'node:child_process';
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
import fs from 'node:fs';
import net from 'node:net';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { describe, it, expect } from 'vitest';
@@ -94,15 +94,32 @@ function spawnInCwd(
});
}
async function occupyPort(): Promise<{ server: net.Server; port: number }> {
const server = net.createServer();
async function occupyPort(): Promise<{
server: http.Server;
port: number;
requestCount: () => number;
bearerRequestCount: () => number;
}> {
let requests = 0;
let bearerRequests = 0;
const server = http.createServer((request, response) => {
requests += 1;
if (request.headers.authorization?.startsWith('Bearer ')) bearerRequests += 1;
response.writeHead(503, { 'content-type': 'application/json' });
response.end('{}');
});
const port = await new Promise<number>((resolve) => {
server.listen(0, '127.0.0.1', () => {
const addr = server.address();
resolve(typeof addr === 'object' && addr ? addr.port : 0);
});
});
return { server, port };
return {
server,
port,
requestCount: () => requests,
bearerRequestCount: () => bearerRequests,
};
}
async function freePort(): Promise<number> {
@@ -128,6 +145,36 @@ async function waitFor(
throw new Error(`Timed out after ${timeoutMs}ms`);
}
async function waitForReadyFile(
child: ChildProcessWithoutNullStreams,
readyFile: string,
output: () => { stdout: string; stderr: string },
timeoutMs: number,
): Promise<void> {
try {
await waitFor(
() => fs.existsSync(readyFile) || child.exitCode !== null || child.signalCode !== null,
timeoutMs,
);
} catch (error) {
const { stdout, stderr } = output();
throw new Error(
`${error instanceof Error ? error.message : String(error)} waiting for ${readyFile}`
+ `\nexit=${String(child.exitCode)} signal=${String(child.signalCode)}`
+ `\nstdout:\n${stdout}\nstderr:\n${stderr}`,
);
}
if (!fs.existsSync(readyFile)) {
const { stdout, stderr } = output();
throw new Error(
`Launcher exited before writing ${readyFile}`
+ `\nexit=${String(child.exitCode)} signal=${String(child.signalCode)}`
+ `\nstdout:\n${stdout}\nstderr:\n${stderr}`,
);
}
}
async function waitForHealth(url: string, timeoutMs: number): Promise<Record<string, unknown>> {
let lastError = '';
await waitFor(async () => {
@@ -332,7 +379,7 @@ describe('Waggle CLI Launcher', () => {
it('reports occupied ports with a working --port recovery command', async () => {
const home = makeHome();
const { server: blocker, port } = await occupyPort();
const { server: blocker, port, requestCount, bearerRequestCount } = await occupyPort();
try {
const build = await run(bin('npm'), ['run', 'build', '--workspace', '@waggle-ai/waggle'], home);
expect(build.status).toBe(0);
@@ -353,6 +400,8 @@ describe('Waggle CLI Launcher', () => {
expect(result.status).toBe(1);
expect(result.stderr).toContain(`Port ${port} is already in use`);
expect(result.stderr).toContain(`npx waggle --port ${port + 1}`);
expect(bearerRequestCount()).toBe(0);
expect(requestCount()).toBe(0);
} finally {
await new Promise<void>((resolve) => blocker.close(() => resolve()));
fs.rmSync(home, { recursive: true, force: true });
@@ -381,6 +430,7 @@ describe('Waggle CLI Launcher', () => {
const pkg = JSON.parse(
fs.readFileSync(path.join(extractDir, 'package', 'package.json'), 'utf8'),
);
const distFiles = fs.readdirSync(path.join(extractDir, 'package', 'dist'));
const result = await run(
process.execPath,
[path.join(extractDir, 'package', 'dist', 'cli.js'), '--help'],
@@ -388,6 +438,15 @@ describe('Waggle CLI Launcher', () => {
);
expect(pkg.bin.waggle).toBe('./dist/cli.js');
expect({
transformersDependency: pkg.dependencies?.['@huggingface/transformers'],
bundledNativeBindings: distFiles.filter((file) => /^onnxruntime_binding-.*\.node$/.test(file)),
bundledTransformersRuntime: distFiles.filter((file) => /^transformers\.node-.*\.js$/.test(file)),
}).toEqual({
transformersDependency: '3.8.1',
bundledNativeBindings: [],
bundledTransformersRuntime: [],
});
expect(result.status).toBe(0);
expect(result.stdout).toContain('Usage:');
expect(result.stdout).not.toContain('[waggle:service]');
@@ -396,11 +455,11 @@ describe('Waggle CLI Launcher', () => {
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
}, 120_000);
it('installs the packed launcher and runs npx help plus startup recovery', async () => {
const home = makeHome();
const { server: blocker, port } = await occupyPort();
const { server: blocker, port, requestCount, bearerRequestCount } = await occupyPort();
try {
const build = await run(bin('npm'), ['run', 'build', '--workspace', '@waggle-ai/waggle'], home);
expect(build.status).toBe(0);
@@ -434,6 +493,19 @@ describe('Waggle CLI Launcher', () => {
);
expect(install.status).toBe(0);
const nativeEmbeddingRuntime = await runInCwd(
process.execPath,
[
'--input-type=module',
'--eval',
"const runtime = await import('@huggingface/transformers'); if (typeof runtime.pipeline !== 'function') throw new Error('Transformers pipeline API is unavailable');",
],
projectDir,
home,
);
expect(nativeEmbeddingRuntime.status).toBe(0);
expect(nativeEmbeddingRuntime.stderr).toBe('');
const help = await runInCwd(bin('npx'), ['waggle', '--help'], projectDir, home);
expect(help.status).toBe(0);
expect(help.stdout).toContain('Usage:');
@@ -453,14 +525,22 @@ describe('Waggle CLI Launcher', () => {
expect(startup.stderr).toContain(`Port ${port} is already in use`);
expect(startup.stderr).toContain(`npx waggle --port ${port + 1}`);
expect(fs.existsSync(path.join(home, '.waggle'))).toBe(false);
expect(bearerRequestCount()).toBe(0);
expect(requestCount()).toBe(0);
} finally {
await new Promise<void>((resolve) => blocker.close(() => resolve()));
fs.rmSync(home, { recursive: true, force: true });
}
}, 120_000);
}, 240_000);
it('starts the installed launcher long enough to serve health', async () => {
it('starts the installed launcher, isolates concurrent histories, and reloads them after restart', async () => {
const home = makeHome();
const {
server: blocker,
port: preferredPort,
requestCount,
bearerRequestCount,
} = await occupyPort();
let child: ChildProcessWithoutNullStreams | undefined;
try {
const build = await run(bin('npm'), ['run', 'build', '--workspace', '@waggle-ai/waggle'], home);
@@ -495,33 +575,215 @@ describe('Waggle CLI Launcher', () => {
);
expect(install.status).toBe(0);
const port = await freePort();
const dataDir = path.join(home, 'data');
const readyFile = path.join(home, 'desktop-ready.json');
const restartReadyFile = path.join(home, 'desktop-ready-restart.json');
const desktopBootstrapToken = 'launcher-desktop-bootstrap-token-1234567890';
const unreachableOllamaPort = await freePort();
const unreachableVllmPort = await freePort();
const providerFreeEnv = {
EMBEDDING_PROVIDER: 'mock',
ANTHROPIC_API_KEY: '',
OPENAI_API_KEY: '',
GEMINI_API_KEY: '',
GOOGLE_API_KEY: '',
XAI_API_KEY: '',
DEEPSEEK_API_KEY: '',
MISTRAL_API_KEY: '',
DASHSCOPE_API_KEY: '',
MINIMAX_API_KEY: '',
ZHIPU_API_KEY: '',
MOONSHOT_API_KEY: '',
PERPLEXITY_API_KEY: '',
OPENROUTER_API_KEY: '',
OLLAMA_HOST: `http://127.0.0.1:${unreachableOllamaPort}`,
VLLM_HOST: `http://127.0.0.1:${unreachableVllmPort}`,
};
let stdout = '';
let stderr = '';
child = spawnInCwd(
bin('npx'),
['waggle', '--port', String(port), '--skip-litellm', '--no-open'],
['waggle', '--port', String(preferredPort), '--skip-litellm', '--no-open'],
projectDir,
home,
{ WAGGLE_DATA_DIR: dataDir },
{
WAGGLE_DATA_DIR: dataDir,
WAGGLE_DESKTOP_PORT_FALLBACK: '1',
WAGGLE_INSTANCE_ID: 'launcher-fallback-health',
WAGGLE_DESKTOP_BOOTSTRAP_TOKEN: desktopBootstrapToken,
WAGGLE_READY_FILE: readyFile,
...providerFreeEnv,
},
);
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
const health = await waitForHealth(`http://127.0.0.1:${port}/health`, 30_000);
await waitForReadyFile(child, readyFile, () => ({ stdout, stderr }), 90_000);
const ready = JSON.parse(fs.readFileSync(readyFile, 'utf8')) as {
preferredPort: number;
port: number;
};
expect(ready.preferredPort).toBe(preferredPort);
expect(ready.port).not.toBe(preferredPort);
const baseUrl = `http://127.0.0.1:${ready.port}`;
const health = await waitForHealth(`${baseUrl}/health`, 30_000);
await waitFor(() => stdout.includes('Press Ctrl+C to stop'), 10_000);
expect(health.database).toMatchObject({ healthy: true });
expect(stdout).toContain(`Server: http://localhost:${port}`);
expect(stdout).toContain(`Server: http://localhost:${ready.port}`);
expect(stdout).toContain('Browser: not opened (--no-open)');
expect(stdout).toContain(`Open manually: http://localhost:${port}`);
expect(stdout).toContain(`Open manually: http://localhost:${ready.port}`);
expect(stderr).not.toContain('Failed to start Waggle');
expect(fs.existsSync(path.join(dataDir, 'personal.mind'))).toBe(true);
expect(bearerRequestCount()).toBe(0);
expect(requestCount()).toBe(0);
const tokenResponse = await fetch(`${baseUrl}/api/auth/session-token`, {
headers: { 'x-waggle-desktop-bootstrap': desktopBootstrapToken },
});
expect(tokenResponse.status).toBe(200);
const token = (await tokenResponse.json() as { token?: string }).token;
if (!token) throw new Error('Installed launcher returned no session token');
const headers = {
authorization: `Bearer ${token}`,
'content-type': 'application/json',
};
const workspaceResponse = await fetch(`${baseUrl}/api/workspaces`, {
method: 'POST',
headers,
body: JSON.stringify({ name: 'Packed launcher concurrent history', group: 'Test' }),
});
expect(workspaceResponse.status).toBe(201);
const workspaceId = (await workspaceResponse.json() as { id?: string }).id;
if (!workspaceId) throw new Error('Installed launcher returned no workspace id');
const sessionA = 'packed-concurrent-a';
const sessionB = 'packed-concurrent-b';
const markerA = 'PACKED_CONCURRENT_SESSION_A';
const markerB = 'PACKED_CONCURRENT_SESSION_B';
const [chatA, chatB] = await Promise.all([
fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers,
body: JSON.stringify({ message: markerA, workspace: workspaceId, session: sessionA }),
}),
fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers,
body: JSON.stringify({ message: markerB, workspace: workspaceId, session: sessionB }),
}),
]);
const [chatBodyA, chatBodyB] = await Promise.all([chatA.text(), chatB.text()]);
for (const [response, body] of [[chatA, chatBodyA], [chatB, chatBodyB]] as const) {
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toMatch(/^text\/event-stream/);
expect(body.match(/^event:\s*done\s*$/gm)).toHaveLength(1);
expect(body.match(/^event:\s*error\s*$/gm) ?? []).toHaveLength(0);
expect(body).toContain('No AI model is ready');
}
for (const sessionId of [sessionA, sessionB]) {
expect(fs.existsSync(path.join(
dataDir,
'workspaces',
workspaceId,
'sessions',
`${sessionId}.jsonl`,
))).toBe(true);
}
await stopProcess(child);
child = undefined;
let restartStdout = '';
let restartStderr = '';
child = spawnInCwd(
bin('npx'),
['waggle', '--port', String(preferredPort), '--skip-litellm', '--no-open'],
projectDir,
home,
{
WAGGLE_DATA_DIR: dataDir,
WAGGLE_DESKTOP_PORT_FALLBACK: '1',
WAGGLE_INSTANCE_ID: 'launcher-fallback-history-restart',
WAGGLE_DESKTOP_BOOTSTRAP_TOKEN: desktopBootstrapToken,
WAGGLE_READY_FILE: restartReadyFile,
...providerFreeEnv,
},
);
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => { restartStdout += chunk; });
child.stderr.on('data', (chunk) => { restartStderr += chunk; });
await waitForReadyFile(
child,
restartReadyFile,
() => ({ stdout: restartStdout, stderr: restartStderr }),
90_000,
);
const restartReady = JSON.parse(fs.readFileSync(restartReadyFile, 'utf8')) as {
preferredPort: number;
port: number;
};
expect(restartReady.preferredPort).toBe(preferredPort);
expect(restartReady.port).not.toBe(preferredPort);
const restartBaseUrl = `http://127.0.0.1:${restartReady.port}`;
const restartHealth = await waitForHealth(`${restartBaseUrl}/health`, 30_000);
await waitFor(() => restartStdout.includes('Press Ctrl+C to stop'), 10_000);
expect(restartHealth.database).toMatchObject({ healthy: true });
expect(restartStderr).not.toContain('Failed to start Waggle');
const restartTokenResponse = await fetch(`${restartBaseUrl}/api/auth/session-token`, {
headers: { 'x-waggle-desktop-bootstrap': desktopBootstrapToken },
});
expect(restartTokenResponse.status).toBe(200);
const restartToken = (await restartTokenResponse.json() as { token?: string }).token;
if (!restartToken) throw new Error('Restarted launcher returned no session token');
const restartHeaders = { authorization: `Bearer ${restartToken}` };
const [historyResponseA, historyResponseB] = await Promise.all([
fetch(
`${restartBaseUrl}/api/history?workspace=${encodeURIComponent(workspaceId)}&session=${encodeURIComponent(sessionA)}`,
{ headers: restartHeaders },
),
fetch(
`${restartBaseUrl}/api/history?workspace=${encodeURIComponent(workspaceId)}&session=${encodeURIComponent(sessionB)}`,
{ headers: restartHeaders },
),
]);
expect(historyResponseA.status).toBe(200);
expect(historyResponseB.status).toBe(200);
const [historyA, historyB] = await Promise.all([
historyResponseA.json(),
historyResponseB.json(),
]) as Array<{
sessionId: string;
count: number;
messages: Array<{ role: string; content: string }>;
}>;
expect(historyA.sessionId).toBe(sessionA);
expect(historyB.sessionId).toBe(sessionB);
expect(historyA.count).toBe(2);
expect(historyB.count).toBe(2);
expect(historyA.messages).toEqual([
expect.objectContaining({ role: 'user', content: markerA }),
expect.objectContaining({ role: 'assistant', content: expect.stringContaining('No AI model is ready') }),
]);
expect(historyB.messages).toEqual([
expect.objectContaining({ role: 'user', content: markerB }),
expect.objectContaining({ role: 'assistant', content: expect.stringContaining('No AI model is ready') }),
]);
expect(historyA.messages.some(({ content }) => content.includes(markerB))).toBe(false);
expect(historyB.messages.some(({ content }) => content.includes(markerA))).toBe(false);
expect(bearerRequestCount()).toBe(0);
expect(requestCount()).toBe(0);
} finally {
if (child) await stopProcess(child);
await new Promise<void>((resolve) => blocker.close(() => resolve()));
// Windows can release SQLite WAL/SHM handles a few milliseconds after
// taskkill reports success; retry the isolated temp-home cleanup.
fs.rmSync(home, {
@@ -531,6 +793,6 @@ describe('Waggle CLI Launcher', () => {
retryDelay: 100,
});
}
}, 180_000);
}, 360_000);
});
});

View File

@@ -12,8 +12,11 @@ export default defineConfig({
// Inline all @waggle/* workspace packages into the bundle.
// Third-party npm packages stay external (installed as dependencies).
noExternal: [/^@waggle\//],
// Native/optional packages that cannot be bundled.
// Runtime packages that cannot be bundled safely.
external: [
'@huggingface/transformers',
'onnxruntime-node',
'adm-zip',
'playwright-core',
'chromium-bidi',
],