Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,680 @@
/**
* Session Domain Entity - Unit Tests
*
* Tests for Session lifecycle, context management, and conversation handling
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createSession, createSessionWithHistory, type Session, type ConversationMessage } from '../../factories';
// Session Entity Types
interface SessionState {
id: string;
tenantId: string;
userId: string;
channelId: string;
threadTs: string;
status: 'active' | 'paused' | 'completed' | 'error';
context: SessionContext;
metadata: SessionMetadata;
}
interface SessionContext {
conversationHistory: ConversationMessage[];
workingDirectory: string;
activeAgents: string[];
variables: Map<string, unknown>;
artifacts: Map<string, Artifact>;
}
interface Artifact {
id: string;
type: 'code' | 'file' | 'image' | 'document';
content: unknown;
createdAt: Date;
}
interface SessionMetadata {
createdAt: Date;
lastActiveAt: Date;
messageCount: number;
tokenUsage: number;
estimatedCost: number;
}
// Mock Session class for testing
class SessionEntity {
private state: SessionState;
private eventLog: Array<{ type: string; payload: unknown; timestamp: Date }> = [];
private readonly maxHistoryLength = 100;
constructor(initialState: Partial<SessionState>) {
this.state = {
id: initialState.id || `session-${Date.now()}`,
tenantId: initialState.tenantId || 'default-tenant',
userId: initialState.userId || 'unknown-user',
channelId: initialState.channelId || 'unknown-channel',
threadTs: initialState.threadTs || `${Date.now()}.000000`,
status: initialState.status || 'active',
context: {
conversationHistory: initialState.context?.conversationHistory || [],
workingDirectory: initialState.context?.workingDirectory || '/workspace',
activeAgents: initialState.context?.activeAgents || [],
variables: new Map(Object.entries(initialState.context?.variables || {})),
artifacts: new Map()
},
metadata: {
createdAt: initialState.metadata?.createdAt || new Date(),
lastActiveAt: initialState.metadata?.lastActiveAt || new Date(),
messageCount: initialState.metadata?.messageCount || 0,
tokenUsage: initialState.metadata?.tokenUsage || 0,
estimatedCost: initialState.metadata?.estimatedCost || 0
}
};
}
getId(): string {
return this.state.id;
}
getTenantId(): string {
return this.state.tenantId;
}
getUserId(): string {
return this.state.userId;
}
getChannelId(): string {
return this.state.channelId;
}
getThreadTs(): string {
return this.state.threadTs;
}
getStatus(): SessionState['status'] {
return this.state.status;
}
isActive(): boolean {
return this.state.status === 'active';
}
getConversationHistory(): ConversationMessage[] {
return [...this.state.context.conversationHistory];
}
getActiveAgents(): string[] {
return [...this.state.context.activeAgents];
}
getWorkingDirectory(): string {
return this.state.context.workingDirectory;
}
getVariable(key: string): unknown {
return this.state.context.variables.get(key);
}
getMetadata(): SessionMetadata {
return { ...this.state.metadata };
}
async addMessage(message: Omit<ConversationMessage, 'timestamp'>): Promise<void> {
if (this.state.status !== 'active') {
throw new Error(`Cannot add message to ${this.state.status} session`);
}
const fullMessage: ConversationMessage = {
...message,
timestamp: new Date()
};
this.state.context.conversationHistory.push(fullMessage);
this.state.metadata.messageCount++;
this.state.metadata.lastActiveAt = new Date();
// Trim history if too long
if (this.state.context.conversationHistory.length > this.maxHistoryLength) {
this.state.context.conversationHistory.shift();
}
this.logEvent('message_added', { role: message.role });
}
async addUserMessage(content: string): Promise<void> {
await this.addMessage({ role: 'user', content });
}
async addAssistantMessage(content: string, agentId?: string): Promise<void> {
await this.addMessage({ role: 'assistant', content, agentId });
}
async addSystemMessage(content: string): Promise<void> {
await this.addMessage({ role: 'system', content });
}
getLastMessage(): ConversationMessage | undefined {
const history = this.state.context.conversationHistory;
return history.length > 0 ? history[history.length - 1] : undefined;
}
getMessageCount(): number {
return this.state.metadata.messageCount;
}
async attachAgent(agentId: string): Promise<void> {
if (!this.state.context.activeAgents.includes(agentId)) {
this.state.context.activeAgents.push(agentId);
this.logEvent('agent_attached', { agentId });
}
}
async detachAgent(agentId: string): Promise<void> {
const index = this.state.context.activeAgents.indexOf(agentId);
if (index !== -1) {
this.state.context.activeAgents.splice(index, 1);
this.logEvent('agent_detached', { agentId });
}
}
setVariable(key: string, value: unknown): void {
this.state.context.variables.set(key, value);
this.logEvent('variable_set', { key });
}
deleteVariable(key: string): boolean {
const deleted = this.state.context.variables.delete(key);
if (deleted) {
this.logEvent('variable_deleted', { key });
}
return deleted;
}
setWorkingDirectory(path: string): void {
this.state.context.workingDirectory = path;
this.logEvent('working_directory_changed', { path });
}
addArtifact(artifact: Omit<Artifact, 'createdAt'>): void {
const fullArtifact: Artifact = {
...artifact,
createdAt: new Date()
};
this.state.context.artifacts.set(artifact.id, fullArtifact);
this.logEvent('artifact_added', { artifactId: artifact.id, type: artifact.type });
}
getArtifact(id: string): Artifact | undefined {
return this.state.context.artifacts.get(id);
}
listArtifacts(): Artifact[] {
return Array.from(this.state.context.artifacts.values());
}
updateTokenUsage(tokens: number, cost: number): void {
this.state.metadata.tokenUsage += tokens;
this.state.metadata.estimatedCost += cost;
}
async pause(): Promise<void> {
if (this.state.status !== 'active') {
throw new Error(`Cannot pause ${this.state.status} session`);
}
this.state.status = 'paused';
this.logEvent('paused', {});
}
async resume(): Promise<void> {
if (this.state.status !== 'paused') {
throw new Error(`Cannot resume ${this.state.status} session`);
}
this.state.status = 'active';
this.state.metadata.lastActiveAt = new Date();
this.logEvent('resumed', {});
}
async complete(): Promise<void> {
if (this.state.status === 'completed') {
return; // Already completed
}
this.state.status = 'completed';
this.state.context.activeAgents = [];
this.logEvent('completed', {});
}
async fail(error: Error): Promise<void> {
this.state.status = 'error';
this.logEvent('failed', { error: error.message });
}
clearHistory(): void {
this.state.context.conversationHistory = [];
this.state.metadata.messageCount = 0;
this.logEvent('history_cleared', {});
}
getEventLog(): Array<{ type: string; payload: unknown; timestamp: Date }> {
return [...this.eventLog];
}
toJSON(): SessionState {
return {
...this.state,
context: {
...this.state.context,
variables: Object.fromEntries(this.state.context.variables) as unknown as Map<string, unknown>,
artifacts: Object.fromEntries(this.state.context.artifacts) as unknown as Map<string, Artifact>
}
};
}
private logEvent(type: string, payload: unknown): void {
this.eventLog.push({ type, payload, timestamp: new Date() });
}
}
// Tests
describe('Session Domain Entity', () => {
describe('Construction', () => {
it('should create session with default values', () => {
const session = new SessionEntity({});
expect(session.getId()).toBeDefined();
expect(session.getStatus()).toBe('active');
expect(session.getConversationHistory()).toEqual([]);
expect(session.getActiveAgents()).toEqual([]);
});
it('should create session with provided values', () => {
const session = new SessionEntity({
id: 'session-001',
tenantId: 'tenant-001',
userId: 'user-001',
channelId: 'C12345',
threadTs: '1234567890.123456'
});
expect(session.getId()).toBe('session-001');
expect(session.getTenantId()).toBe('tenant-001');
expect(session.getUserId()).toBe('user-001');
expect(session.getChannelId()).toBe('C12345');
expect(session.getThreadTs()).toBe('1234567890.123456');
});
it('should initialize metadata correctly', () => {
const session = new SessionEntity({});
const metadata = session.getMetadata();
expect(metadata.messageCount).toBe(0);
expect(metadata.tokenUsage).toBe(0);
expect(metadata.estimatedCost).toBe(0);
expect(metadata.createdAt).toBeInstanceOf(Date);
});
});
describe('Status Management', () => {
it('should be active by default', () => {
const session = new SessionEntity({});
expect(session.isActive()).toBe(true);
});
it('should pause active session', async () => {
const session = new SessionEntity({ status: 'active' });
await session.pause();
expect(session.getStatus()).toBe('paused');
expect(session.isActive()).toBe(false);
});
it('should resume paused session', async () => {
const session = new SessionEntity({ status: 'paused' });
await session.resume();
expect(session.getStatus()).toBe('active');
expect(session.isActive()).toBe(true);
});
it('should complete session', async () => {
const session = new SessionEntity({ status: 'active' });
await session.attachAgent('agent-001');
await session.complete();
expect(session.getStatus()).toBe('completed');
expect(session.getActiveAgents()).toEqual([]);
});
it('should fail session', async () => {
const session = new SessionEntity({ status: 'active' });
await session.fail(new Error('Something went wrong'));
expect(session.getStatus()).toBe('error');
});
it('should throw when pausing non-active session', async () => {
const session = new SessionEntity({ status: 'paused' });
await expect(session.pause()).rejects.toThrow('Cannot pause');
});
it('should throw when resuming non-paused session', async () => {
const session = new SessionEntity({ status: 'active' });
await expect(session.resume()).rejects.toThrow('Cannot resume');
});
});
describe('Conversation History', () => {
it('should add user message', async () => {
const session = new SessionEntity({});
await session.addUserMessage('Hello!');
const history = session.getConversationHistory();
expect(history).toHaveLength(1);
expect(history[0].role).toBe('user');
expect(history[0].content).toBe('Hello!');
});
it('should add assistant message', async () => {
const session = new SessionEntity({});
await session.addAssistantMessage('Hi there!', 'agent-001');
const history = session.getConversationHistory();
expect(history).toHaveLength(1);
expect(history[0].role).toBe('assistant');
expect(history[0].agentId).toBe('agent-001');
});
it('should add system message', async () => {
const session = new SessionEntity({});
await session.addSystemMessage('System initialized');
const history = session.getConversationHistory();
expect(history).toHaveLength(1);
expect(history[0].role).toBe('system');
});
it('should get last message', async () => {
const session = new SessionEntity({});
await session.addUserMessage('First');
await session.addUserMessage('Second');
await session.addAssistantMessage('Third');
const lastMessage = session.getLastMessage();
expect(lastMessage?.content).toBe('Third');
expect(lastMessage?.role).toBe('assistant');
});
it('should return undefined for empty history', () => {
const session = new SessionEntity({});
expect(session.getLastMessage()).toBeUndefined();
});
it('should increment message count', async () => {
const session = new SessionEntity({});
await session.addUserMessage('Message 1');
await session.addAssistantMessage('Message 2');
expect(session.getMessageCount()).toBe(2);
});
it('should update last active time on message', async () => {
const session = new SessionEntity({});
const before = session.getMetadata().lastActiveAt;
await new Promise(resolve => setTimeout(resolve, 10));
await session.addUserMessage('Test');
const after = session.getMetadata().lastActiveAt;
expect(after.getTime()).toBeGreaterThan(before.getTime());
});
it('should throw when adding message to non-active session', async () => {
const session = new SessionEntity({ status: 'completed' });
await expect(session.addUserMessage('Test'))
.rejects.toThrow('Cannot add message');
});
it('should clear history', async () => {
const session = new SessionEntity({});
await session.addUserMessage('Test 1');
await session.addUserMessage('Test 2');
session.clearHistory();
expect(session.getConversationHistory()).toHaveLength(0);
expect(session.getMessageCount()).toBe(0);
});
});
describe('Agent Management', () => {
it('should attach agent', async () => {
const session = new SessionEntity({});
await session.attachAgent('agent-001');
expect(session.getActiveAgents()).toContain('agent-001');
});
it('should not duplicate attached agent', async () => {
const session = new SessionEntity({});
await session.attachAgent('agent-001');
await session.attachAgent('agent-001');
expect(session.getActiveAgents()).toEqual(['agent-001']);
});
it('should detach agent', async () => {
const session = new SessionEntity({});
await session.attachAgent('agent-001');
await session.attachAgent('agent-002');
await session.detachAgent('agent-001');
expect(session.getActiveAgents()).toEqual(['agent-002']);
});
it('should handle detaching non-existent agent gracefully', async () => {
const session = new SessionEntity({});
await expect(session.detachAgent('non-existent')).resolves.not.toThrow();
});
});
describe('Variables', () => {
it('should set and get variable', () => {
const session = new SessionEntity({});
session.setVariable('key', 'value');
expect(session.getVariable('key')).toBe('value');
});
it('should handle complex variable values', () => {
const session = new SessionEntity({});
const complexValue = { nested: { data: [1, 2, 3] } };
session.setVariable('complex', complexValue);
expect(session.getVariable('complex')).toEqual(complexValue);
});
it('should delete variable', () => {
const session = new SessionEntity({});
session.setVariable('toDelete', 'value');
const deleted = session.deleteVariable('toDelete');
expect(deleted).toBe(true);
expect(session.getVariable('toDelete')).toBeUndefined();
});
it('should return false when deleting non-existent variable', () => {
const session = new SessionEntity({});
const deleted = session.deleteVariable('nonExistent');
expect(deleted).toBe(false);
});
});
describe('Working Directory', () => {
it('should get default working directory', () => {
const session = new SessionEntity({});
expect(session.getWorkingDirectory()).toBe('/workspace');
});
it('should set working directory', () => {
const session = new SessionEntity({});
session.setWorkingDirectory('/new/path');
expect(session.getWorkingDirectory()).toBe('/new/path');
});
});
describe('Artifacts', () => {
it('should add artifact', () => {
const session = new SessionEntity({});
session.addArtifact({
id: 'artifact-001',
type: 'code',
content: 'console.log("Hello")'
});
const artifact = session.getArtifact('artifact-001');
expect(artifact).toBeDefined();
expect(artifact?.type).toBe('code');
expect(artifact?.content).toBe('console.log("Hello")');
});
it('should list all artifacts', () => {
const session = new SessionEntity({});
session.addArtifact({ id: 'a1', type: 'code', content: 'code' });
session.addArtifact({ id: 'a2', type: 'file', content: 'file' });
const artifacts = session.listArtifacts();
expect(artifacts).toHaveLength(2);
});
it('should return undefined for non-existent artifact', () => {
const session = new SessionEntity({});
expect(session.getArtifact('non-existent')).toBeUndefined();
});
});
describe('Token Usage', () => {
it('should update token usage', () => {
const session = new SessionEntity({});
session.updateTokenUsage(1000, 0.01);
const metadata = session.getMetadata();
expect(metadata.tokenUsage).toBe(1000);
expect(metadata.estimatedCost).toBe(0.01);
});
it('should accumulate token usage', () => {
const session = new SessionEntity({});
session.updateTokenUsage(500, 0.005);
session.updateTokenUsage(500, 0.005);
const metadata = session.getMetadata();
expect(metadata.tokenUsage).toBe(1000);
expect(metadata.estimatedCost).toBeCloseTo(0.01, 5);
});
});
describe('Event Logging', () => {
it('should log events during lifecycle', async () => {
const session = new SessionEntity({});
await session.addUserMessage('Hello');
await session.attachAgent('agent-001');
await session.pause();
await session.resume();
const events = session.getEventLog();
expect(events.length).toBeGreaterThanOrEqual(4);
expect(events.some(e => e.type === 'message_added')).toBe(true);
expect(events.some(e => e.type === 'agent_attached')).toBe(true);
expect(events.some(e => e.type === 'paused')).toBe(true);
expect(events.some(e => e.type === 'resumed')).toBe(true);
});
});
describe('Serialization', () => {
it('should serialize to JSON', async () => {
const session = new SessionEntity({
id: 'session-001',
tenantId: 'tenant-001'
});
await session.addUserMessage('Test');
session.setVariable('key', 'value');
const json = session.toJSON();
expect(json.id).toBe('session-001');
expect(json.tenantId).toBe('tenant-001');
expect(json.context.conversationHistory).toHaveLength(1);
});
});
});
describe('Session Factory Integration', () => {
it('should create session from factory data', () => {
const factorySession = createSession({
tenantId: 'tenant-factory',
userId: 'user-factory'
});
const session = new SessionEntity({
id: factorySession.id,
tenantId: factorySession.tenantId,
userId: factorySession.userId,
channelId: factorySession.channelId,
threadTs: factorySession.threadTs,
context: {
conversationHistory: factorySession.context.conversationHistory,
workingDirectory: factorySession.context.workingDirectory,
activeAgents: factorySession.context.activeAgents
}
});
expect(session.getTenantId()).toBe('tenant-factory');
expect(session.getUserId()).toBe('user-factory');
});
it('should create session with history from factory', () => {
const factorySession = createSessionWithHistory(5);
const session = new SessionEntity({
id: factorySession.id,
context: {
conversationHistory: factorySession.context.conversationHistory
},
metadata: {
messageCount: factorySession.metadata.messageCount
}
});
expect(session.getConversationHistory()).toHaveLength(5);
expect(session.getMessageCount()).toBe(5);
});
});