Files
waggle-os/packages/server/tests/local/workspace-sessions.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

250 lines
9.4 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { WorkspaceSessionManager, type WorkspaceSession } from '../../src/local/workspace-sessions.js';
import type { MindDB } from '@waggle/core';
import type { Orchestrator } from '@waggle/agent';
function createMockMind(): MindDB {
return {
close: vi.fn(),
} as unknown as MindDB;
}
function createMockTools() {
return [{ name: 'mock_tool', description: 'mock', parameters: {}, execute: async () => 'ok' }];
}
function createMockOrchestrator(): Orchestrator {
return {
setWorkspaceMind: vi.fn(),
} as unknown as Orchestrator;
}
describe('WorkspaceSessionManager', () => {
it('creates a session for a workspace', () => {
const manager = new WorkspaceSessionManager(3);
const mind = createMockMind();
const session = manager.create('ws-1', mind, createMockOrchestrator(), createMockTools());
expect(session.workspaceId).toBe('ws-1');
expect(session.mind).toBe(mind);
expect(session.status).toBe('active');
expect(session.personaId).toBeNull();
expect(manager.size).toBe(1);
});
it('returns existing session on getOrCreate', () => {
const manager = new WorkspaceSessionManager(3);
const mind = createMockMind();
const session1 = manager.create('ws-1', mind, createMockOrchestrator(), createMockTools());
const mindFactory = vi.fn(() => createMockMind());
const orchestratorFactory = vi.fn(() => createMockOrchestrator());
const toolsFactory = vi.fn(() => createMockTools());
const session2 = manager.getOrCreate('ws-1', mindFactory, orchestratorFactory, toolsFactory);
expect(session2).toBe(session1);
// Factories should NOT have been called (existing session returned)
expect(mindFactory).not.toHaveBeenCalled();
expect(orchestratorFactory).not.toHaveBeenCalled();
expect(toolsFactory).not.toHaveBeenCalled();
});
it('limits to maxSessions (default 3)', () => {
const manager = new WorkspaceSessionManager(2);
manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
manager.create('ws-2', createMockMind(), createMockOrchestrator(), createMockTools());
expect(() => manager.create('ws-3', createMockMind(), createMockOrchestrator(), createMockTools()))
.toThrow('Max concurrent sessions reached (2)');
});
it('closes session and releases resources', () => {
const manager = new WorkspaceSessionManager(3);
const mind = createMockMind();
manager.create('ws-1', mind, createMockOrchestrator(), createMockTools());
expect(manager.has('ws-1')).toBe(true);
const closed = manager.close('ws-1');
expect(closed).toBe(true);
expect(manager.has('ws-1')).toBe(false);
expect(manager.size).toBe(0);
expect(mind.close).toHaveBeenCalled();
});
it('releases the cache pin instead of closing a borrowed mind', () => {
const manager = new WorkspaceSessionManager(3);
const mind = createMockMind();
const release = vi.fn();
// Session borrows a cache-owned handle: close() must drop the pin, NOT
// close the shared MindDB (closing it would poison other borrows).
manager.create('ws-1', mind, createMockOrchestrator(), createMockTools(), undefined, release);
expect(manager.close('ws-1')).toBe(true);
expect(release).toHaveBeenCalledTimes(1);
expect(mind.close).not.toHaveBeenCalled();
});
it('rolls back the pin when getOrCreate fails after the mindFactory pins', () => {
const manager = new WorkspaceSessionManager(1);
manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
// ws-1 fills the single slot; creating ws-2 hits the max-sessions cap in
// create() AFTER the mindFactory ran, so getOrCreate must release the pin.
const release = vi.fn();
expect(() => manager.getOrCreate(
'ws-2',
() => createMockMind(),
createMockOrchestrator,
createMockTools,
undefined,
release,
)).toThrow('Max concurrent sessions reached (1)');
expect(release).toHaveBeenCalledTimes(1);
});
it('closes idle sessions past threshold', () => {
const manager = new WorkspaceSessionManager(3);
const s1 = manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
const s2 = manager.create('ws-2', createMockMind(), createMockOrchestrator(), createMockTools());
// Make ws-1 idle by backdating its activity
s1.lastActivity = Date.now() - 60000; // 60s ago
s2.lastActivity = Date.now(); // just now
const closed = manager.closeIdleSessions(30000); // 30s threshold
expect(closed).toBe(1);
expect(manager.has('ws-1')).toBe(false);
expect(manager.has('ws-2')).toBe(true);
});
it('each session has independent abortController', () => {
const manager = new WorkspaceSessionManager(3);
const s1 = manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
const s2 = manager.create('ws-2', createMockMind(), createMockOrchestrator(), createMockTools());
expect(s1.abortController).not.toBe(s2.abortController);
// Aborting one should not affect the other
s1.abortController.abort();
expect(s1.abortController.signal.aborted).toBe(true);
expect(s2.abortController.signal.aborted).toBe(false);
});
it('getActive() returns only open sessions', () => {
const manager = new WorkspaceSessionManager(3);
manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
manager.create('ws-2', createMockMind(), createMockOrchestrator(), createMockTools());
const active = manager.getActive();
expect(active).toHaveLength(2);
expect(active.map(s => s.workspaceId)).toEqual(['ws-1', 'ws-2']);
});
it('pause/resume cycle works', () => {
const manager = new WorkspaceSessionManager(3);
const session = manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
expect(session.status).toBe('active');
manager.pause('ws-1');
expect(session.status).toBe('paused');
// AbortController was replaced (old one aborted)
expect(session.abortController.signal.aborted).toBe(false); // New controller
manager.resume('ws-1');
expect(session.status).toBe('active');
});
it('closeAll() cleans up everything', () => {
const manager = new WorkspaceSessionManager(3);
const m1 = createMockMind();
const m2 = createMockMind();
manager.create('ws-1', m1, createMockOrchestrator(), createMockTools());
manager.create('ws-2', m2, createMockOrchestrator(), createMockTools());
manager.closeAll();
expect(manager.size).toBe(0);
expect(m1.close).toHaveBeenCalled();
expect(m2.close).toHaveBeenCalled();
});
it('supports persona assignment', () => {
const manager = new WorkspaceSessionManager(3);
const session = manager.create(
'ws-1', createMockMind(), createMockOrchestrator(), createMockTools(), 'researcher',
);
expect(session.personaId).toBe('researcher');
});
it('close returns false for non-existent session', () => {
const manager = new WorkspaceSessionManager(3);
expect(manager.close('nonexistent')).toBe(false);
});
describe('addTokens (L-17 C3)', () => {
it('initializes new sessions with tokensUsed = 0', () => {
const manager = new WorkspaceSessionManager(3);
const s = manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
expect(s.tokensUsed).toBe(0);
});
it('accumulates tokens across multiple calls', () => {
const manager = new WorkspaceSessionManager(3);
manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
manager.addTokens('ws-1', 120);
manager.addTokens('ws-1', 80);
manager.addTokens('ws-1', 50);
expect(manager.get('ws-1')!.tokensUsed).toBe(250);
});
it('tracks tokens per workspace independently', () => {
const manager = new WorkspaceSessionManager(3);
manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
manager.create('ws-2', createMockMind(), createMockOrchestrator(), createMockTools());
manager.addTokens('ws-1', 100);
manager.addTokens('ws-2', 200);
manager.addTokens('ws-1', 50);
expect(manager.get('ws-1')!.tokensUsed).toBe(150);
expect(manager.get('ws-2')!.tokensUsed).toBe(200);
});
it('no-ops silently when the session does not exist', () => {
const manager = new WorkspaceSessionManager(3);
expect(() => manager.addTokens('nonexistent', 500)).not.toThrow();
});
it('ignores zero, negative, NaN, and Infinity deltas', () => {
const manager = new WorkspaceSessionManager(3);
manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
manager.addTokens('ws-1', 0);
manager.addTokens('ws-1', -50);
manager.addTokens('ws-1', NaN);
manager.addTokens('ws-1', Infinity);
manager.addTokens('ws-1', -Infinity);
expect(manager.get('ws-1')!.tokensUsed).toBe(0);
});
it('persists accumulated tokens across pause/resume', () => {
const manager = new WorkspaceSessionManager(3);
manager.create('ws-1', createMockMind(), createMockOrchestrator(), createMockTools());
manager.addTokens('ws-1', 300);
manager.pause('ws-1');
expect(manager.get('ws-1')!.tokensUsed).toBe(300);
manager.resume('ws-1');
manager.addTokens('ws-1', 200);
expect(manager.get('ws-1')!.tokensUsed).toBe(500);
});
});
});