moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
/**
* First-run detection and startup progress event tests.
*/
import { describe, it, expect, afterEach } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { MindDB } from '@waggle/core';
import { startService, isFirstRun } from '../src/local/service.js';
import type { StartupEvent, StartupPhase } from '../src/local/service.js';
function makeTmpDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-firstrun-test-'));
}
// Pick an OS-assigned ephemeral port by briefly binding to port 0.
// Falls back to random high port if probing fails. This avoids the
// collisions we saw when multiple test files ran in parallel with a
// narrow random range.
import net from 'node:net';
function randomPort(): number {
try {
const server = net.createServer();
server.listen(0);
const addr = server.address();
const port = typeof addr === 'object' && addr ? addr.port : 0;
server.close();
if (port > 0) return port;
} catch { /* fall through */ }
return 20_000 + Math.floor(Math.random() * 40_000);
}
// ── isFirstRun ─────────────────────────────────────────────────────
describe('isFirstRun', () => {
const tmpDirs: string[] = [];
afterEach(() => {
for (const dir of tmpDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tmpDirs.length = 0;
});
it('returns true for empty directory (fresh install)', () => {
const dir = makeTmpDir();
tmpDirs.push(dir);
expect(isFirstRun(dir)).toBe(true);
});
it('returns false when personal.mind exists', () => {
const dir = makeTmpDir();
tmpDirs.push(dir);
const mind = new MindDB(path.join(dir, 'personal.mind'));
mind.close();
expect(isFirstRun(dir)).toBe(false);
});
it('returns false when default.mind exists (needs migration)', () => {
const dir = makeTmpDir();
tmpDirs.push(dir);
const mind = new MindDB(path.join(dir, 'default.mind'));
mind.close();
expect(isFirstRun(dir)).toBe(false);
});
it('returns true when directory does not exist yet', () => {
const dir = path.join(os.tmpdir(), 'waggle-nonexistent-' + Date.now());
// Don't create it
expect(isFirstRun(dir)).toBe(true);
});
});
// ── onProgress callback ────────────────────────────────────────────
describe('startService onProgress', () => {
const cleanups: Array<() => Promise<void>> = [];
const tmpDirs: string[] = [];
afterEach(async () => {
for (const cleanup of cleanups) {
await cleanup();
}
cleanups.length = 0;
for (const dir of tmpDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tmpDirs.length = 0;
});
it('emits progress events during startup (fresh install)', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
const events: StartupEvent[] = [];
const { server } = await startService({
dataDir,
port,
skipLiteLLM: true,
onProgress: (event) => events.push(event),
});
cleanups.push(async () => { await server.close(); });
// Should have emitted at least init and ready
const phases = events.map(e => e.phase);
expect(phases).toContain('init');
expect(phases).toContain('ready');
// Should NOT have migration phase (fresh install, no default.mind)
expect(phases).not.toContain('migration');
// Should have creating-mind phase (fresh install)
expect(phases).toContain('creating-mind');
// Progress should go from 0 to 1
expect(events[0].progress).toBeLessThanOrEqual(0.2);
expect(events[events.length - 1].progress).toBe(1);
// All events should have messages
for (const event of events) {
expect(event.message).toBeTruthy();
expect(typeof event.progress).toBe('number');
}
});
it('emits migration phase when default.mind exists', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
// Create default.mind to trigger migration
const mind = new MindDB(path.join(dataDir, 'default.mind'));
mind.close();
const port = randomPort();
const events: StartupEvent[] = [];
const { server } = await startService({
dataDir,
port,
skipLiteLLM: true,
onProgress: (event) => events.push(event),
});
cleanups.push(async () => { await server.close(); });
const phases = events.map(e => e.phase);
expect(phases).toContain('migration');
expect(phases).toContain('ready');
});
it('emits litellm phase when not skipped (but still skips in test)', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
const events: StartupEvent[] = [];
const { server } = await startService({
dataDir,
port,
skipLiteLLM: true,
onProgress: (event) => events.push(event),
});
cleanups.push(async () => { await server.close(); });
const phases = events.map(e => e.phase);
// Even when skipped, the litellm phase should be emitted
expect(phases).toContain('litellm');
});
it('emits server phase', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
const events: StartupEvent[] = [];
const { server } = await startService({
dataDir,
port,
skipLiteLLM: true,
onProgress: (event) => events.push(event),
});
cleanups.push(async () => { await server.close(); });
const phases = events.map(e => e.phase);
expect(phases).toContain('server');
});
it('works without onProgress callback (backward compat)', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
// Should not throw when no callback
const { server } = await startService({ dataDir, port, skipLiteLLM: true });
cleanups.push(async () => { await server.close(); });
expect(server.server.listening).toBe(true);
});
it('progress values are monotonically increasing', async () => {
const dataDir = makeTmpDir();
tmpDirs.push(dataDir);
const port = randomPort();
const events: StartupEvent[] = [];
const { server } = await startService({
dataDir,
port,
skipLiteLLM: true,
onProgress: (event) => events.push(event),
});
cleanups.push(async () => { await server.close(); });
for (let i = 1; i < events.length; i++) {
expect(events[i].progress).toBeGreaterThanOrEqual(events[i - 1].progress);
}
});
});