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,455 @@
/**
* Cross-Platform Verification Tests
*
* Verifies that the Waggle server works correctly without Tauri-specific
* APIs and that all critical subsystems function on any OS.
*
* Coverage:
* 1. Server starts without window.__TAURI__
* 2. All critical endpoints respond
* 3. SPA fallback (index.html for unknown paths)
* 4. WebSocket endpoint exists
* 5. SSE notification stream works
* 6. Mind DB creates and reads on any OS
* 7. Vault encryption works on any OS
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { MindDB, FrameStore, SessionStore, VaultStore } from '@waggle/core';
import { buildLocalServer } from '../src/local/index.js';
import type { FastifyInstance } from 'fastify';
import { injectWithAuth } from './test-utils.js';
// ── Helpers ─────────────────────────────────────────────────────────────
function createTmpDir(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), `waggle-xplat-${prefix}-`));
}
function cleanupDir(dir: string): void {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch { /* best-effort cleanup */ }
}
// ── 1. Server Starts Without Tauri ──────────────────────────────────────
describe('Cross-Platform Verification', () => {
let server: FastifyInstance;
let tmpDir: string;
beforeAll(async () => {
tmpDir = createTmpDir('xplat');
// Create personal.mind so the server can boot
const personalPath = path.join(tmpDir, 'personal.mind');
const mind = new MindDB(personalPath);
const sessions = new SessionStore(mind);
const frames = new FrameStore(mind);
const s = sessions.create('xplat-test');
frames.createIFrame(s.gop_id, 'Cross-platform test memory frame', 'normal');
mind.close();
// Ensure no Tauri globals exist
// (globalThis.__TAURI__ should be undefined in Node.js)
expect((globalThis as { __TAURI__?: unknown }).__TAURI__).toBeUndefined();
server = await buildLocalServer({ dataDir: tmpDir });
}, 30_000);
afterAll(async () => {
await server.close();
cleanupDir(tmpDir);
});
// ── 1. No Tauri dependency ────────────────────────────────────────
it('server starts without window.__TAURI__', () => {
// If we reached here, the server booted successfully without Tauri
expect(server).toBeDefined();
expect((globalThis as { __TAURI__?: unknown }).__TAURI__).toBeUndefined();
expect((globalThis as { window?: { __TAURI__?: unknown } }).window?.__TAURI__).toBeUndefined();
});
// ── 2. All Critical Endpoints Respond ─────────────────────────────
describe('critical endpoints', () => {
it('GET /health returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/health' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.mode).toBe('local');
expect(body.timestamp).toBeDefined();
});
it('GET /api/settings returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/settings' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.defaultModel).toBeDefined();
});
it('GET /api/workspaces returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/workspaces' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(Array.isArray(body)).toBe(true);
});
it('GET /api/vault returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/vault' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.secrets).toBeDefined();
});
it('GET /api/memory/search?q=test returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/memory/search?q=test' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.results).toBeDefined();
});
it('GET /api/memory/frames returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/memory/frames?limit=5' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.results).toBeDefined();
});
it('GET /api/cron returns 200 with schedules', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/cron' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.schedules).toBeDefined();
expect(Array.isArray(body.schedules)).toBe(true);
expect(typeof body.count).toBe('number');
});
it('GET /api/skills returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/skills' });
expect(res.statusCode).toBe(200);
});
it('GET /api/capabilities/status returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/capabilities/status' });
expect(res.statusCode).toBe(200);
});
it('GET /api/connectors returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/connectors' });
expect(res.statusCode).toBe(200);
});
it('GET /api/personas returns 200', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/personas' });
expect(res.statusCode).toBe(200);
});
it('POST /api/chat without message returns 400', async () => {
const res = await injectWithAuth(server, {
method: 'POST',
url: '/api/chat',
payload: {},
});
expect(res.statusCode).toBe(400);
});
});
// ── 3. SPA Fallback ──────────────────────────────────────────────
describe('SPA fallback', () => {
it('unknown GET paths do not crash the server', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/some/unknown/path' });
// Server should respond (404 or SPA fallback), not crash
expect([200, 404]).toContain(res.statusCode);
});
it('unknown API paths return 404', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/nonexistent' });
expect(res.statusCode).toBe(404);
});
});
// ── 4. WebSocket Endpoint ────────────────────────────────────────
describe('WebSocket endpoint', () => {
it('WebSocket route is registered at /ws', async () => {
// Fastify's inject doesn't support real WebSocket upgrades,
// but we can verify the route exists by checking the response
// to a non-upgrade request (Fastify will respond with an error
// or handle it, but shouldn't 404).
const res = await injectWithAuth(server, { method: 'GET', url: '/ws' });
// The WebSocket route is registered — a non-upgrade GET request
// will get a 404 or a connection error (not a route-not-found).
// In Fastify with @fastify/websocket, a plain GET to a websocket
// route returns 404 (no upgrade header) — what matters is the
// route is registered and doesn't crash.
expect(res.statusCode).toBeDefined();
});
});
// ── 5. SSE Notification Stream ───────────────────────────────────
describe('SSE notification stream', () => {
it('notification route module is registered and exports are valid', async () => {
// The SSE endpoint uses reply.raw.writeHead which means Fastify's
// inject() will hang indefinitely (the stream never closes).
// Instead, verify the route module exports are valid and that
// the notification route was registered by checking OPTIONS works.
const mod = await import('../src/local/routes/notifications.js');
expect(mod.notificationRoutes).toBeDefined();
expect(typeof mod.notificationRoutes).toBe('function');
expect(mod.emitNotification).toBeDefined();
expect(typeof mod.emitNotification).toBe('function');
});
it('eventBus is available for notification dispatch', () => {
// Verify the eventBus is decorated on the server (used by SSE stream)
expect(server.eventBus).toBeDefined();
expect(typeof server.eventBus.on).toBe('function');
expect(typeof server.eventBus.emit).toBe('function');
});
});
});
// ── 6. Mind DB Cross-Platform ───────────────────────────────────────────
describe('Mind DB cross-platform', () => {
let tmpDir: string;
let mind: MindDB;
beforeEach(() => {
tmpDir = createTmpDir('mind-xplat');
const dbPath = path.join(tmpDir, 'test.mind');
mind = new MindDB(dbPath);
});
afterEach(() => {
mind.close();
cleanupDir(tmpDir);
});
it('creates a .mind file on any OS', () => {
const dbPath = path.join(tmpDir, 'test.mind');
expect(fs.existsSync(dbPath)).toBe(true);
});
it('writes and reads frames correctly', () => {
const sessions = new SessionStore(mind);
const frames = new FrameStore(mind);
const session = sessions.create('xplat-project');
const frame = frames.createIFrame(session.gop_id, 'Cross-platform frame content', 'normal');
expect(frame.id).toBeGreaterThan(0);
expect(frame.content).toBe('Cross-platform frame content');
expect(frame.frame_type).toBe('I');
// Read back
const retrieved = frames.getById(frame.id);
expect(retrieved).toBeDefined();
expect(retrieved!.content).toBe('Cross-platform frame content');
});
it('FTS5 search works on any OS', () => {
const sessions = new SessionStore(mind);
const frames = new FrameStore(mind);
const session = sessions.create('fts-xplat');
frames.createIFrame(session.gop_id, 'SQLite full text search verification', 'normal');
frames.createIFrame(session.gop_id, 'Another unrelated frame about cooking', 'normal');
// Search using FTS5
const raw = mind.getDatabase();
const results = raw.prepare(`
SELECT mf.* FROM memory_frames mf
INNER JOIN memory_frames_fts fts ON mf.id = fts.rowid
WHERE memory_frames_fts MATCH ?
`).all('SQLite');
expect(results.length).toBe(1);
expect((results[0] as { content: string }).content).toContain('SQLite');
});
it('WAL mode is enabled', () => {
const raw = mind.getDatabase();
const mode = raw.pragma('journal_mode', { simple: true });
expect(mode).toBe('wal');
});
it('foreign keys are enforced', () => {
const raw = mind.getDatabase();
const fk = raw.pragma('foreign_keys', { simple: true });
expect(fk).toBe(1);
});
it('sessions CRUD works on any OS', () => {
const sessions = new SessionStore(mind);
// Create
const session = sessions.create('xplat-crud');
expect(session.gop_id).toBeDefined();
expect(session.status).toBe('active');
// Close
const closed = sessions.close(session.gop_id, 'Test complete');
expect(closed.status).toBe('closed');
expect(closed.summary).toBe('Test complete');
// Archive
const archived = sessions.archive(session.gop_id);
expect(archived.status).toBe('archived');
});
it('handles Unicode content correctly', () => {
const sessions = new SessionStore(mind);
const frames = new FrameStore(mind);
const session = sessions.create('unicode-test');
const unicodeContent = 'Tschuss! Bonjour! Konnichiwa! Emoji test: special chars: <>&"\'';
const frame = frames.createIFrame(session.gop_id, unicodeContent, 'normal');
const retrieved = frames.getById(frame.id);
expect(retrieved!.content).toBe(unicodeContent);
});
it('handles large content blocks', () => {
const sessions = new SessionStore(mind);
const frames = new FrameStore(mind);
const session = sessions.create('large-content');
// Create a 100KB content block
const largeContent = 'A'.repeat(100_000);
const frame = frames.createIFrame(session.gop_id, largeContent, 'normal');
const retrieved = frames.getById(frame.id);
expect(retrieved!.content.length).toBe(100_000);
});
});
// ── 7. Vault Encryption Cross-Platform ──────────────────────────────────
describe('Vault encryption cross-platform', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = createTmpDir('vault-xplat');
});
afterEach(() => {
cleanupDir(tmpDir);
});
it('creates vault files on any OS', () => {
const vault = new VaultStore(tmpDir);
vault.set('TEST_KEY', 'test-value');
expect(fs.existsSync(path.join(tmpDir, 'vault.json'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, '.vault-key'))).toBe(true);
});
it('encrypts and decrypts correctly', () => {
const vault = new VaultStore(tmpDir);
const secretValue = 'sk-ant-very-secret-api-key-123456789';
vault.set('ANTHROPIC_API_KEY', secretValue, { credentialType: 'api_key' });
const result = vault.get('ANTHROPIC_API_KEY');
expect(result).not.toBeNull();
expect(result!.value).toBe(secretValue);
expect(result!.metadata?.credentialType).toBe('api_key');
});
it('stored data is actually encrypted (not plaintext)', () => {
const vault = new VaultStore(tmpDir);
vault.set('PLAIN_CHECK', 'this-should-not-appear-in-file');
// Read the raw vault.json
const raw = fs.readFileSync(path.join(tmpDir, 'vault.json'), 'utf-8');
expect(raw).not.toContain('this-should-not-appear-in-file');
// The encrypted field should contain hex-encoded data with colons
const parsed = JSON.parse(raw);
expect(parsed.PLAIN_CHECK.encrypted).toMatch(/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/);
});
it('vault key file is regenerated and works after initial creation', () => {
// Create vault and store a secret
const vault1 = new VaultStore(tmpDir);
vault1.set('PERSIST_KEY', 'persist-value');
// Open a new VaultStore instance (simulates restart)
const vault2 = new VaultStore(tmpDir);
const result = vault2.get('PERSIST_KEY');
expect(result).not.toBeNull();
expect(result!.value).toBe('persist-value');
});
it('handles special characters in secret values', () => {
const vault = new VaultStore(tmpDir);
const specialValue = 'p@$$w0rd!#%^&*(){}[]|\\:";\'<>,.?/~`+=';
vault.set('SPECIAL_KEY', specialValue);
const result = vault.get('SPECIAL_KEY');
expect(result!.value).toBe(specialValue);
});
it('handles empty string values', () => {
const vault = new VaultStore(tmpDir);
vault.set('EMPTY_KEY', '');
const result = vault.get('EMPTY_KEY');
expect(result).not.toBeNull();
expect(result!.value).toBe('');
});
it('delete removes secrets correctly', () => {
const vault = new VaultStore(tmpDir);
vault.set('DELETE_ME', 'temporary');
expect(vault.has('DELETE_ME')).toBe(true);
const deleted = vault.delete('DELETE_ME');
expect(deleted).toBe(true);
expect(vault.has('DELETE_ME')).toBe(false);
expect(vault.get('DELETE_ME')).toBeNull();
});
it('list returns all secret names without values', () => {
const vault = new VaultStore(tmpDir);
vault.set('KEY_A', 'value-a');
vault.set('KEY_B', 'value-b');
vault.set('KEY_C', 'value-c');
const list = vault.list();
expect(list.length).toBe(3);
const names = list.map(s => s.name);
expect(names).toContain('KEY_A');
expect(names).toContain('KEY_B');
expect(names).toContain('KEY_C');
// Values should not be in the list output
for (const entry of list) {
expect((entry as { value?: unknown }).value).toBeUndefined();
}
});
it('connector credential helpers work', () => {
const vault = new VaultStore(tmpDir);
vault.setConnectorCredential('github', {
type: 'bearer',
value: 'ghp_test123',
scopes: ['repo', 'read:org'],
});
const cred = vault.getConnectorCredential('github');
expect(cred).not.toBeNull();
expect(cred!.value).toBe('ghp_test123');
expect(cred!.type).toBe('bearer');
expect(cred!.scopes).toEqual(['repo', 'read:org']);
expect(cred!.isExpired).toBe(false);
});
});