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,490 @@
/**
* Agent Domain Entity - Unit Tests
*
* Tests for Agent lifecycle, state management, and behavior
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createAgent, createAgents, type Agent, type AgentConfig } from '../../factories';
// Agent Entity Types (would be imported from src/domain/agent.ts)
interface AgentState {
id: string;
name: string;
type: Agent['type'];
status: Agent['status'];
capabilities: string[];
config: AgentConfig;
currentTask?: string;
metrics: AgentMetrics;
}
interface AgentMetrics {
tasksCompleted: number;
averageLatency: number;
errorCount: number;
lastActiveAt: Date | null;
}
// Mock Agent class for testing
class AgentEntity {
private state: AgentState;
private eventLog: Array<{ type: string; payload: unknown; timestamp: Date }> = [];
constructor(initialState: Partial<AgentState>) {
this.state = {
id: initialState.id || `agent-${Date.now()}`,
name: initialState.name || 'Unnamed Agent',
type: initialState.type || 'coder',
status: initialState.status || 'idle',
capabilities: initialState.capabilities || [],
config: initialState.config || {
model: 'claude-sonnet-4',
temperature: 0.7,
maxTokens: 4096
},
currentTask: initialState.currentTask,
metrics: initialState.metrics || {
tasksCompleted: 0,
averageLatency: 0,
errorCount: 0,
lastActiveAt: null
}
};
}
getId(): string {
return this.state.id;
}
getName(): string {
return this.state.name;
}
getType(): Agent['type'] {
return this.state.type;
}
getStatus(): Agent['status'] {
return this.state.status;
}
getCapabilities(): string[] {
return [...this.state.capabilities];
}
getConfig(): AgentConfig {
return { ...this.state.config };
}
getMetrics(): AgentMetrics {
return { ...this.state.metrics };
}
getCurrentTask(): string | undefined {
return this.state.currentTask;
}
isAvailable(): boolean {
return this.state.status === 'idle';
}
hasCapability(capability: string): boolean {
return this.state.capabilities.includes(capability);
}
async assignTask(taskId: string): Promise<void> {
if (this.state.status !== 'idle') {
throw new Error(`Agent ${this.state.id} is not available (status: ${this.state.status})`);
}
this.state.status = 'busy';
this.state.currentTask = taskId;
this.state.metrics.lastActiveAt = new Date();
this.logEvent('task_assigned', { taskId });
}
async completeTask(result: { success: boolean; latency: number }): Promise<void> {
if (this.state.status !== 'busy') {
throw new Error(`Agent ${this.state.id} has no active task`);
}
const taskId = this.state.currentTask;
this.state.status = 'idle';
this.state.currentTask = undefined;
this.state.metrics.tasksCompleted++;
// Update average latency
const totalLatency = this.state.metrics.averageLatency * (this.state.metrics.tasksCompleted - 1);
this.state.metrics.averageLatency = (totalLatency + result.latency) / this.state.metrics.tasksCompleted;
if (!result.success) {
this.state.metrics.errorCount++;
}
this.logEvent('task_completed', { taskId, result });
}
async failTask(error: Error): Promise<void> {
if (this.state.status !== 'busy') {
throw new Error(`Agent ${this.state.id} has no active task`);
}
const taskId = this.state.currentTask;
this.state.status = 'error';
this.state.currentTask = undefined;
this.state.metrics.errorCount++;
this.logEvent('task_failed', { taskId, error: error.message });
}
async recover(): Promise<void> {
if (this.state.status !== 'error') {
throw new Error(`Agent ${this.state.id} is not in error state`);
}
this.state.status = 'idle';
this.logEvent('recovered', {});
}
async terminate(): Promise<void> {
this.state.status = 'terminated';
this.state.currentTask = undefined;
this.logEvent('terminated', {});
}
updateConfig(config: Partial<AgentConfig>): void {
this.state.config = { ...this.state.config, ...config };
this.logEvent('config_updated', { config });
}
addCapability(capability: string): void {
if (!this.state.capabilities.includes(capability)) {
this.state.capabilities.push(capability);
this.logEvent('capability_added', { capability });
}
}
removeCapability(capability: string): void {
const index = this.state.capabilities.indexOf(capability);
if (index !== -1) {
this.state.capabilities.splice(index, 1);
this.logEvent('capability_removed', { capability });
}
}
getEventLog(): Array<{ type: string; payload: unknown; timestamp: Date }> {
return [...this.eventLog];
}
toJSON(): AgentState {
return { ...this.state };
}
private logEvent(type: string, payload: unknown): void {
this.eventLog.push({ type, payload, timestamp: new Date() });
}
}
// Tests
describe('Agent Domain Entity', () => {
describe('Construction', () => {
it('should create agent with default values', () => {
const agent = new AgentEntity({});
expect(agent.getId()).toBeDefined();
expect(agent.getName()).toBe('Unnamed Agent');
expect(agent.getType()).toBe('coder');
expect(agent.getStatus()).toBe('idle');
expect(agent.getCapabilities()).toEqual([]);
});
it('should create agent with provided values', () => {
const agent = new AgentEntity({
id: 'test-agent',
name: 'Test Agent',
type: 'researcher',
capabilities: ['web-search', 'analysis']
});
expect(agent.getId()).toBe('test-agent');
expect(agent.getName()).toBe('Test Agent');
expect(agent.getType()).toBe('researcher');
expect(agent.getCapabilities()).toEqual(['web-search', 'analysis']);
});
it('should initialize metrics correctly', () => {
const agent = new AgentEntity({});
const metrics = agent.getMetrics();
expect(metrics.tasksCompleted).toBe(0);
expect(metrics.averageLatency).toBe(0);
expect(metrics.errorCount).toBe(0);
expect(metrics.lastActiveAt).toBeNull();
});
});
describe('Availability', () => {
it('should be available when idle', () => {
const agent = new AgentEntity({ status: 'idle' });
expect(agent.isAvailable()).toBe(true);
});
it('should not be available when busy', () => {
const agent = new AgentEntity({ status: 'busy' });
expect(agent.isAvailable()).toBe(false);
});
it('should not be available when in error state', () => {
const agent = new AgentEntity({ status: 'error' });
expect(agent.isAvailable()).toBe(false);
});
it('should not be available when terminated', () => {
const agent = new AgentEntity({ status: 'terminated' });
expect(agent.isAvailable()).toBe(false);
});
});
describe('Capabilities', () => {
it('should check for capability correctly', () => {
const agent = new AgentEntity({
capabilities: ['code-generation', 'code-review']
});
expect(agent.hasCapability('code-generation')).toBe(true);
expect(agent.hasCapability('code-review')).toBe(true);
expect(agent.hasCapability('unknown')).toBe(false);
});
it('should add capability', () => {
const agent = new AgentEntity({ capabilities: [] });
agent.addCapability('new-capability');
expect(agent.hasCapability('new-capability')).toBe(true);
});
it('should not duplicate capability', () => {
const agent = new AgentEntity({ capabilities: ['existing'] });
agent.addCapability('existing');
expect(agent.getCapabilities()).toEqual(['existing']);
});
it('should remove capability', () => {
const agent = new AgentEntity({ capabilities: ['to-remove', 'to-keep'] });
agent.removeCapability('to-remove');
expect(agent.hasCapability('to-remove')).toBe(false);
expect(agent.hasCapability('to-keep')).toBe(true);
});
});
describe('Task Lifecycle', () => {
it('should assign task to idle agent', async () => {
const agent = new AgentEntity({ status: 'idle' });
await agent.assignTask('task-001');
expect(agent.getStatus()).toBe('busy');
expect(agent.getCurrentTask()).toBe('task-001');
expect(agent.getMetrics().lastActiveAt).not.toBeNull();
});
it('should throw error when assigning task to busy agent', async () => {
const agent = new AgentEntity({ status: 'busy', currentTask: 'existing-task' });
await expect(agent.assignTask('new-task')).rejects.toThrow('not available');
});
it('should complete task successfully', async () => {
const agent = new AgentEntity({ status: 'busy', currentTask: 'task-001' });
await agent.completeTask({ success: true, latency: 100 });
expect(agent.getStatus()).toBe('idle');
expect(agent.getCurrentTask()).toBeUndefined();
expect(agent.getMetrics().tasksCompleted).toBe(1);
expect(agent.getMetrics().averageLatency).toBe(100);
});
it('should track error count on failed completion', async () => {
const agent = new AgentEntity({ status: 'busy', currentTask: 'task-001' });
await agent.completeTask({ success: false, latency: 50 });
expect(agent.getMetrics().errorCount).toBe(1);
expect(agent.getMetrics().tasksCompleted).toBe(1);
});
it('should calculate average latency correctly', async () => {
const agent = new AgentEntity({ status: 'idle' });
// First task
await agent.assignTask('task-1');
await agent.completeTask({ success: true, latency: 100 });
// Second task
await agent.assignTask('task-2');
await agent.completeTask({ success: true, latency: 200 });
expect(agent.getMetrics().averageLatency).toBe(150);
});
it('should fail task and enter error state', async () => {
const agent = new AgentEntity({ status: 'busy', currentTask: 'task-001' });
await agent.failTask(new Error('Task execution failed'));
expect(agent.getStatus()).toBe('error');
expect(agent.getCurrentTask()).toBeUndefined();
expect(agent.getMetrics().errorCount).toBe(1);
});
it('should throw error when completing non-existent task', async () => {
const agent = new AgentEntity({ status: 'idle' });
await expect(agent.completeTask({ success: true, latency: 100 }))
.rejects.toThrow('no active task');
});
});
describe('Recovery', () => {
it('should recover from error state', async () => {
const agent = new AgentEntity({ status: 'error' });
await agent.recover();
expect(agent.getStatus()).toBe('idle');
expect(agent.isAvailable()).toBe(true);
});
it('should throw error when recovering from non-error state', async () => {
const agent = new AgentEntity({ status: 'idle' });
await expect(agent.recover()).rejects.toThrow('not in error state');
});
});
describe('Termination', () => {
it('should terminate agent', async () => {
const agent = new AgentEntity({ status: 'idle' });
await agent.terminate();
expect(agent.getStatus()).toBe('terminated');
expect(agent.isAvailable()).toBe(false);
});
it('should terminate busy agent and clear task', async () => {
const agent = new AgentEntity({ status: 'busy', currentTask: 'task-001' });
await agent.terminate();
expect(agent.getStatus()).toBe('terminated');
expect(agent.getCurrentTask()).toBeUndefined();
});
});
describe('Configuration', () => {
it('should update config partially', () => {
const agent = new AgentEntity({
config: {
model: 'claude-sonnet-4',
temperature: 0.7,
maxTokens: 4096
}
});
agent.updateConfig({ temperature: 0.5 });
const config = agent.getConfig();
expect(config.temperature).toBe(0.5);
expect(config.model).toBe('claude-sonnet-4');
expect(config.maxTokens).toBe(4096);
});
});
describe('Event Logging', () => {
it('should log events during lifecycle', async () => {
const agent = new AgentEntity({ status: 'idle' });
await agent.assignTask('task-001');
await agent.completeTask({ success: true, latency: 100 });
const events = agent.getEventLog();
expect(events).toHaveLength(2);
expect(events[0].type).toBe('task_assigned');
expect(events[1].type).toBe('task_completed');
});
it('should log configuration changes', () => {
const agent = new AgentEntity({});
agent.updateConfig({ temperature: 0.5 });
agent.addCapability('new-cap');
const events = agent.getEventLog();
expect(events.some(e => e.type === 'config_updated')).toBe(true);
expect(events.some(e => e.type === 'capability_added')).toBe(true);
});
});
describe('Serialization', () => {
it('should serialize to JSON', () => {
const agent = new AgentEntity({
id: 'test-agent',
name: 'Test Agent',
type: 'coder',
capabilities: ['code-generation']
});
const json = agent.toJSON();
expect(json.id).toBe('test-agent');
expect(json.name).toBe('Test Agent');
expect(json.type).toBe('coder');
expect(json.capabilities).toEqual(['code-generation']);
});
});
});
describe('Agent Factory Integration', () => {
it('should create agent from factory data', () => {
const factoryAgent = createAgent({
name: 'Factory Agent',
type: 'tester',
capabilities: ['test-generation']
});
const agent = new AgentEntity({
id: factoryAgent.id,
name: factoryAgent.name,
type: factoryAgent.type,
capabilities: factoryAgent.capabilities,
config: factoryAgent.config
});
expect(agent.getId()).toBe(factoryAgent.id);
expect(agent.getName()).toBe('Factory Agent');
expect(agent.getType()).toBe('tester');
});
it('should create multiple agents from factory', () => {
const agents = createAgents(5);
const agentEntities = agents.map(a => new AgentEntity({
id: a.id,
name: a.name,
type: a.type
}));
expect(agentEntities).toHaveLength(5);
agentEntities.forEach((agent, i) => {
expect(agent.getName()).toBe(`Agent ${i + 1}`);
});
});
});

View File

@@ -0,0 +1,710 @@
/**
* Memory Domain Entity - Unit Tests
*
* Tests for Memory storage, retrieval, and vector operations
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createMemory, createVectorMemory, type Memory, type MemoryMetadata } from '../../factories';
// Memory Entity Types
interface MemoryEntry {
id: string;
tenantId: string;
sessionId: string | null;
type: 'short-term' | 'long-term' | 'vector' | 'episodic';
key: string;
value: unknown;
embedding: Float32Array | null;
metadata: MemoryEntryMetadata;
}
interface MemoryEntryMetadata {
createdAt: Date;
updatedAt: Date;
expiresAt: Date | null;
accessCount: number;
importance: number;
tags: string[];
}
interface VectorSearchResult {
entry: MemoryEntry;
score: number;
distance: number;
}
// Mock Memory Store class for testing
class MemoryStore {
private entries: Map<string, MemoryEntry> = new Map();
private indexByKey: Map<string, Set<string>> = new Map();
private indexByTenant: Map<string, Set<string>> = new Map();
private indexBySession: Map<string, Set<string>> = new Map();
private readonly dimension: number;
constructor(dimension: number = 384) {
this.dimension = dimension;
}
async set(entry: Omit<MemoryEntry, 'metadata'> & { metadata?: Partial<MemoryEntryMetadata> }): Promise<MemoryEntry> {
const fullEntry: MemoryEntry = {
...entry,
metadata: {
createdAt: entry.metadata?.createdAt || new Date(),
updatedAt: new Date(),
expiresAt: entry.metadata?.expiresAt || null,
accessCount: entry.metadata?.accessCount || 0,
importance: entry.metadata?.importance || 0.5,
tags: entry.metadata?.tags || []
}
};
// Validate embedding dimension
if (fullEntry.embedding && fullEntry.embedding.length !== this.dimension) {
throw new Error(`Embedding dimension mismatch: expected ${this.dimension}, got ${fullEntry.embedding.length}`);
}
this.entries.set(entry.id, fullEntry);
this.updateIndexes(fullEntry);
return fullEntry;
}
async get(id: string): Promise<MemoryEntry | null> {
const entry = this.entries.get(id);
if (entry) {
entry.metadata.accessCount++;
entry.metadata.updatedAt = new Date();
}
return entry || null;
}
async getByKey(key: string, tenantId: string): Promise<MemoryEntry | null> {
const ids = this.indexByKey.get(key);
if (!ids) return null;
for (const id of ids) {
const entry = this.entries.get(id);
if (entry && entry.tenantId === tenantId) {
entry.metadata.accessCount++;
return entry;
}
}
return null;
}
async delete(id: string): Promise<boolean> {
const entry = this.entries.get(id);
if (!entry) return false;
this.removeFromIndexes(entry);
return this.entries.delete(id);
}
async deleteByKey(key: string, tenantId: string): Promise<boolean> {
const entry = await this.getByKey(key, tenantId);
if (!entry) return false;
return this.delete(entry.id);
}
async listByTenant(tenantId: string, limit: number = 100): Promise<MemoryEntry[]> {
const ids = this.indexByTenant.get(tenantId);
if (!ids) return [];
const entries: MemoryEntry[] = [];
for (const id of ids) {
const entry = this.entries.get(id);
if (entry) entries.push(entry);
if (entries.length >= limit) break;
}
return entries;
}
async listBySession(sessionId: string, limit: number = 100): Promise<MemoryEntry[]> {
const ids = this.indexBySession.get(sessionId);
if (!ids) return [];
const entries: MemoryEntry[] = [];
for (const id of ids) {
const entry = this.entries.get(id);
if (entry) entries.push(entry);
if (entries.length >= limit) break;
}
return entries;
}
async search(query: Float32Array, tenantId: string, topK: number = 10): Promise<VectorSearchResult[]> {
if (query.length !== this.dimension) {
throw new Error(`Query dimension mismatch: expected ${this.dimension}, got ${query.length}`);
}
const results: VectorSearchResult[] = [];
const tenantIds = this.indexByTenant.get(tenantId);
if (!tenantIds) return [];
for (const id of tenantIds) {
const entry = this.entries.get(id);
if (entry?.embedding) {
const score = this.cosineSimilarity(query, entry.embedding);
results.push({
entry,
score,
distance: 1 - score
});
}
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, topK);
}
async expire(): Promise<number> {
const now = new Date();
let expiredCount = 0;
for (const [id, entry] of this.entries) {
if (entry.metadata.expiresAt && entry.metadata.expiresAt < now) {
this.delete(id);
expiredCount++;
}
}
return expiredCount;
}
async clear(tenantId?: string): Promise<number> {
if (tenantId) {
const ids = this.indexByTenant.get(tenantId);
if (!ids) return 0;
let deletedCount = 0;
for (const id of Array.from(ids)) {
if (this.delete(id)) deletedCount++;
}
return deletedCount;
}
const count = this.entries.size;
this.entries.clear();
this.indexByKey.clear();
this.indexByTenant.clear();
this.indexBySession.clear();
return count;
}
size(): number {
return this.entries.size;
}
sizeByTenant(tenantId: string): number {
return this.indexByTenant.get(tenantId)?.size || 0;
}
private updateIndexes(entry: MemoryEntry): void {
// Key index
let keySet = this.indexByKey.get(entry.key);
if (!keySet) {
keySet = new Set();
this.indexByKey.set(entry.key, keySet);
}
keySet.add(entry.id);
// Tenant index
let tenantSet = this.indexByTenant.get(entry.tenantId);
if (!tenantSet) {
tenantSet = new Set();
this.indexByTenant.set(entry.tenantId, tenantSet);
}
tenantSet.add(entry.id);
// Session index
if (entry.sessionId) {
let sessionSet = this.indexBySession.get(entry.sessionId);
if (!sessionSet) {
sessionSet = new Set();
this.indexBySession.set(entry.sessionId, sessionSet);
}
sessionSet.add(entry.id);
}
}
private removeFromIndexes(entry: MemoryEntry): void {
this.indexByKey.get(entry.key)?.delete(entry.id);
this.indexByTenant.get(entry.tenantId)?.delete(entry.id);
if (entry.sessionId) {
this.indexBySession.get(entry.sessionId)?.delete(entry.id);
}
}
private cosineSimilarity(a: Float32Array, b: Float32Array): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dotProduct / denominator;
}
}
// Tests
describe('Memory Store', () => {
let store: MemoryStore;
beforeEach(() => {
store = new MemoryStore(384);
});
describe('Basic Operations', () => {
it('should set and get memory entry', async () => {
const entry = await store.set({
id: 'mem-001',
tenantId: 'tenant-001',
sessionId: null,
type: 'long-term',
key: 'test-key',
value: { data: 'test' },
embedding: null
});
const retrieved = await store.get('mem-001');
expect(retrieved).not.toBeNull();
expect(retrieved?.id).toBe('mem-001');
expect(retrieved?.value).toEqual({ data: 'test' });
});
it('should return null for non-existent entry', async () => {
const entry = await store.get('non-existent');
expect(entry).toBeNull();
});
it('should increment access count on get', async () => {
await store.set({
id: 'mem-001',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'test',
value: 'test',
embedding: null
});
await store.get('mem-001');
await store.get('mem-001');
const entry = await store.get('mem-001');
expect(entry?.metadata.accessCount).toBe(3);
});
it('should delete entry', async () => {
await store.set({
id: 'mem-001',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'test',
value: 'test',
embedding: null
});
const deleted = await store.delete('mem-001');
const entry = await store.get('mem-001');
expect(deleted).toBe(true);
expect(entry).toBeNull();
});
it('should return false when deleting non-existent entry', async () => {
const deleted = await store.delete('non-existent');
expect(deleted).toBe(false);
});
});
describe('Key-based Operations', () => {
it('should get entry by key and tenant', async () => {
await store.set({
id: 'mem-001',
tenantId: 'tenant-001',
sessionId: null,
type: 'long-term',
key: 'unique-key',
value: 'value1',
embedding: null
});
await store.set({
id: 'mem-002',
tenantId: 'tenant-002',
sessionId: null,
type: 'long-term',
key: 'unique-key',
value: 'value2',
embedding: null
});
const entry1 = await store.getByKey('unique-key', 'tenant-001');
const entry2 = await store.getByKey('unique-key', 'tenant-002');
expect(entry1?.value).toBe('value1');
expect(entry2?.value).toBe('value2');
});
it('should delete by key', async () => {
await store.set({
id: 'mem-001',
tenantId: 'tenant-001',
sessionId: null,
type: 'long-term',
key: 'to-delete',
value: 'test',
embedding: null
});
const deleted = await store.deleteByKey('to-delete', 'tenant-001');
const entry = await store.getByKey('to-delete', 'tenant-001');
expect(deleted).toBe(true);
expect(entry).toBeNull();
});
});
describe('Listing Operations', () => {
beforeEach(async () => {
for (let i = 0; i < 5; i++) {
await store.set({
id: `mem-${i}`,
tenantId: 'tenant-001',
sessionId: 'session-001',
type: 'short-term',
key: `key-${i}`,
value: `value-${i}`,
embedding: null
});
}
for (let i = 5; i < 8; i++) {
await store.set({
id: `mem-${i}`,
tenantId: 'tenant-002',
sessionId: 'session-002',
type: 'short-term',
key: `key-${i}`,
value: `value-${i}`,
embedding: null
});
}
});
it('should list entries by tenant', async () => {
const entries = await store.listByTenant('tenant-001');
expect(entries).toHaveLength(5);
entries.forEach(e => expect(e.tenantId).toBe('tenant-001'));
});
it('should list entries by session', async () => {
const entries = await store.listBySession('session-001');
expect(entries).toHaveLength(5);
entries.forEach(e => expect(e.sessionId).toBe('session-001'));
});
it('should respect limit parameter', async () => {
const entries = await store.listByTenant('tenant-001', 3);
expect(entries).toHaveLength(3);
});
it('should return empty array for unknown tenant', async () => {
const entries = await store.listByTenant('unknown');
expect(entries).toEqual([]);
});
});
describe('Vector Operations', () => {
const createRandomEmbedding = (dim: number): Float32Array => {
const arr = new Float32Array(dim);
let norm = 0;
for (let i = 0; i < dim; i++) {
arr[i] = Math.random() - 0.5;
norm += arr[i] * arr[i];
}
norm = Math.sqrt(norm);
for (let i = 0; i < dim; i++) {
arr[i] /= norm;
}
return arr;
};
it('should search by vector similarity', async () => {
const embedding1 = createRandomEmbedding(384);
const embedding2 = createRandomEmbedding(384);
const embedding3 = createRandomEmbedding(384);
await store.set({
id: 'vec-1',
tenantId: 'tenant-001',
sessionId: null,
type: 'vector',
key: 'doc-1',
value: { text: 'Document 1' },
embedding: embedding1
});
await store.set({
id: 'vec-2',
tenantId: 'tenant-001',
sessionId: null,
type: 'vector',
key: 'doc-2',
value: { text: 'Document 2' },
embedding: embedding2
});
await store.set({
id: 'vec-3',
tenantId: 'tenant-001',
sessionId: null,
type: 'vector',
key: 'doc-3',
value: { text: 'Document 3' },
embedding: embedding3
});
const results = await store.search(embedding1, 'tenant-001', 2);
expect(results).toHaveLength(2);
expect(results[0].entry.id).toBe('vec-1'); // Most similar to itself
expect(results[0].score).toBeCloseTo(1, 5);
});
it('should throw error for dimension mismatch on set', async () => {
const wrongDimensionEmbedding = new Float32Array(256);
await expect(store.set({
id: 'vec-wrong',
tenantId: 'tenant-001',
sessionId: null,
type: 'vector',
key: 'wrong',
value: {},
embedding: wrongDimensionEmbedding
})).rejects.toThrow('dimension mismatch');
});
it('should throw error for dimension mismatch on search', async () => {
const wrongDimensionQuery = new Float32Array(256);
await expect(store.search(wrongDimensionQuery, 'tenant-001'))
.rejects.toThrow('dimension mismatch');
});
it('should only search within tenant', async () => {
const embedding = createRandomEmbedding(384);
await store.set({
id: 'vec-1',
tenantId: 'tenant-001',
sessionId: null,
type: 'vector',
key: 'doc-1',
value: {},
embedding
});
await store.set({
id: 'vec-2',
tenantId: 'tenant-002',
sessionId: null,
type: 'vector',
key: 'doc-2',
value: {},
embedding
});
const results = await store.search(embedding, 'tenant-001');
expect(results).toHaveLength(1);
expect(results[0].entry.tenantId).toBe('tenant-001');
});
});
describe('Expiration', () => {
it('should expire entries', async () => {
const pastDate = new Date(Date.now() - 1000);
const futureDate = new Date(Date.now() + 100000);
await store.set({
id: 'expired',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'expired',
value: 'test',
embedding: null,
metadata: { expiresAt: pastDate }
});
await store.set({
id: 'not-expired',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'not-expired',
value: 'test',
embedding: null,
metadata: { expiresAt: futureDate }
});
const expiredCount = await store.expire();
expect(expiredCount).toBe(1);
expect(await store.get('expired')).toBeNull();
expect(await store.get('not-expired')).not.toBeNull();
});
});
describe('Clear Operations', () => {
beforeEach(async () => {
await store.set({
id: 'mem-1',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'key-1',
value: 'test',
embedding: null
});
await store.set({
id: 'mem-2',
tenantId: 'tenant-002',
sessionId: null,
type: 'short-term',
key: 'key-2',
value: 'test',
embedding: null
});
});
it('should clear all entries', async () => {
const cleared = await store.clear();
expect(cleared).toBe(2);
expect(store.size()).toBe(0);
});
it('should clear entries by tenant', async () => {
const cleared = await store.clear('tenant-001');
expect(cleared).toBe(1);
expect(store.sizeByTenant('tenant-001')).toBe(0);
expect(store.sizeByTenant('tenant-002')).toBe(1);
});
});
describe('Size Operations', () => {
it('should return total size', async () => {
await store.set({
id: 'mem-1',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'k1',
value: 'v1',
embedding: null
});
await store.set({
id: 'mem-2',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'k2',
value: 'v2',
embedding: null
});
expect(store.size()).toBe(2);
});
it('should return size by tenant', async () => {
await store.set({
id: 'mem-1',
tenantId: 'tenant-001',
sessionId: null,
type: 'short-term',
key: 'k1',
value: 'v1',
embedding: null
});
await store.set({
id: 'mem-2',
tenantId: 'tenant-002',
sessionId: null,
type: 'short-term',
key: 'k2',
value: 'v2',
embedding: null
});
expect(store.sizeByTenant('tenant-001')).toBe(1);
expect(store.sizeByTenant('tenant-002')).toBe(1);
});
});
});
describe('Memory Factory Integration', () => {
let store: MemoryStore;
beforeEach(() => {
store = new MemoryStore(384);
});
it('should create memory from factory data', async () => {
const factoryMemory = createMemory({
key: 'factory-key',
value: { factory: 'data' },
type: 'long-term'
});
const entry = await store.set({
id: factoryMemory.id,
tenantId: factoryMemory.tenantId,
sessionId: factoryMemory.sessionId,
type: factoryMemory.type,
key: factoryMemory.key,
value: factoryMemory.value,
embedding: factoryMemory.embedding
});
expect(entry.key).toBe('factory-key');
expect(entry.type).toBe('long-term');
});
it('should create vector memory from factory data', async () => {
const factoryMemory = createVectorMemory(384, {
key: 'vector-key',
value: { text: 'Test document' }
});
const entry = await store.set({
id: factoryMemory.id,
tenantId: factoryMemory.tenantId,
sessionId: factoryMemory.sessionId,
type: factoryMemory.type,
key: factoryMemory.key,
value: factoryMemory.value,
embedding: factoryMemory.embedding
});
expect(entry.embedding).not.toBeNull();
expect(entry.embedding?.length).toBe(384);
expect(entry.type).toBe('vector');
});
});

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);
});
});

View File

@@ -0,0 +1,762 @@
/**
* Skill Domain Entity - Unit Tests
*
* Tests for Skill registration, execution, and validation
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createSkill, type Skill } from '../../factories';
// Skill Types
interface SkillDefinition {
id: string;
name: string;
version: string;
description: string;
inputSchema: JSONSchema;
outputSchema: JSONSchema;
executor: string;
timeout: number;
retries: number;
metadata: SkillMetadata;
}
interface JSONSchema {
type: string;
properties?: Record<string, unknown>;
required?: string[];
additionalProperties?: boolean;
}
interface SkillMetadata {
author: string;
createdAt: Date;
updatedAt: Date;
usageCount: number;
averageLatency: number;
successRate: number;
tags: string[];
}
interface SkillExecutionContext {
tenantId: string;
sessionId: string;
agentId: string;
timeout?: number;
}
interface SkillExecutionResult {
success: boolean;
output: unknown;
error?: string;
latency: number;
tokensUsed?: number;
}
// Mock Skill Registry class for testing
class SkillRegistry {
private skills: Map<string, SkillDefinition> = new Map();
private executors: Map<string, (input: unknown, context: SkillExecutionContext) => Promise<unknown>> = new Map();
async register(skill: Omit<SkillDefinition, 'metadata'> & { metadata?: Partial<SkillMetadata> }): Promise<SkillDefinition> {
if (this.skills.has(skill.id)) {
throw new Error(`Skill ${skill.id} is already registered`);
}
this.validateSchema(skill.inputSchema);
this.validateSchema(skill.outputSchema);
const fullSkill: SkillDefinition = {
...skill,
metadata: {
author: skill.metadata?.author || 'unknown',
createdAt: skill.metadata?.createdAt || new Date(),
updatedAt: new Date(),
usageCount: skill.metadata?.usageCount || 0,
averageLatency: skill.metadata?.averageLatency || 0,
successRate: skill.metadata?.successRate || 1,
tags: skill.metadata?.tags || []
}
};
this.skills.set(skill.id, fullSkill);
return fullSkill;
}
async unregister(skillId: string): Promise<boolean> {
return this.skills.delete(skillId);
}
async get(skillId: string): Promise<SkillDefinition | null> {
return this.skills.get(skillId) || null;
}
async getByName(name: string, version?: string): Promise<SkillDefinition | null> {
for (const skill of this.skills.values()) {
if (skill.name === name) {
if (!version || skill.version === version) {
return skill;
}
}
}
return null;
}
async list(tags?: string[]): Promise<SkillDefinition[]> {
let skills = Array.from(this.skills.values());
if (tags && tags.length > 0) {
skills = skills.filter(s =>
tags.some(tag => s.metadata.tags.includes(tag))
);
}
return skills;
}
async listByExecutorType(type: string): Promise<SkillDefinition[]> {
return Array.from(this.skills.values()).filter(s =>
s.executor.startsWith(type)
);
}
registerExecutor(
pattern: string,
executor: (input: unknown, context: SkillExecutionContext) => Promise<unknown>
): void {
this.executors.set(pattern, executor);
}
async execute(
skillId: string,
input: unknown,
context: SkillExecutionContext
): Promise<SkillExecutionResult> {
const skill = await this.get(skillId);
if (!skill) {
return {
success: false,
output: null,
error: `Skill ${skillId} not found`,
latency: 0
};
}
// Validate input
const validationError = this.validateInput(input, skill.inputSchema);
if (validationError) {
return {
success: false,
output: null,
error: validationError,
latency: 0
};
}
// Find executor
const executor = this.findExecutor(skill.executor);
if (!executor) {
return {
success: false,
output: null,
error: `No executor found for ${skill.executor}`,
latency: 0
};
}
// Execute with timeout
const startTime = performance.now();
const timeout = context.timeout || skill.timeout;
try {
const result = await Promise.race([
executor(input, context),
this.createTimeout(timeout)
]);
// Use performance.now() for sub-millisecond precision, ensure minimum 0.001ms
const latency = Math.max(performance.now() - startTime, 0.001);
// Update metrics
this.updateMetrics(skill, true, latency);
return {
success: true,
output: result,
latency
};
} catch (error) {
const latency = Math.max(performance.now() - startTime, 0.001);
// Update metrics
this.updateMetrics(skill, false, latency);
return {
success: false,
output: null,
error: error instanceof Error ? error.message : 'Unknown error',
latency
};
}
}
async executeWithRetry(
skillId: string,
input: unknown,
context: SkillExecutionContext,
maxRetries?: number
): Promise<SkillExecutionResult> {
const skill = await this.get(skillId);
const retries = maxRetries ?? skill?.retries ?? 0;
let lastResult: SkillExecutionResult | null = null;
for (let attempt = 0; attempt <= retries; attempt++) {
const result = await this.execute(skillId, input, context);
if (result.success) {
return result;
}
lastResult = result;
// Exponential backoff
if (attempt < retries) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100));
}
}
return lastResult!;
}
size(): number {
return this.skills.size;
}
clear(): void {
this.skills.clear();
}
private validateSchema(schema: JSONSchema): void {
if (!schema.type) {
throw new Error('Schema must have a type');
}
const validTypes = ['object', 'array', 'string', 'number', 'boolean', 'null'];
if (!validTypes.includes(schema.type)) {
throw new Error(`Invalid schema type: ${schema.type}`);
}
}
private validateInput(input: unknown, schema: JSONSchema): string | null {
if (schema.type === 'object') {
if (typeof input !== 'object' || input === null) {
return 'Input must be an object';
}
const inputObj = input as Record<string, unknown>;
// Check required fields
if (schema.required) {
for (const field of schema.required) {
if (!(field in inputObj)) {
return `Missing required field: ${field}`;
}
}
}
// Validate property types if defined
if (schema.properties) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (key in inputObj) {
const propError = this.validateProperty(inputObj[key], propSchema as JSONSchema);
if (propError) {
return `Invalid ${key}: ${propError}`;
}
}
}
}
}
return null;
}
private validateProperty(value: unknown, schema: JSONSchema): string | null {
const type = schema.type;
switch (type) {
case 'string':
if (typeof value !== 'string') return 'must be a string';
break;
case 'number':
if (typeof value !== 'number') return 'must be a number';
break;
case 'boolean':
if (typeof value !== 'boolean') return 'must be a boolean';
break;
case 'array':
if (!Array.isArray(value)) return 'must be an array';
break;
case 'object':
if (typeof value !== 'object' || value === null) return 'must be an object';
break;
}
return null;
}
private findExecutor(
executorUri: string
): ((input: unknown, context: SkillExecutionContext) => Promise<unknown>) | null {
for (const [pattern, executor] of this.executors) {
if (executorUri.startsWith(pattern)) {
return executor;
}
}
return null;
}
private async createTimeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Skill execution timed out')), ms);
});
}
private updateMetrics(skill: SkillDefinition, success: boolean, latency: number): void {
const previousCount = skill.metadata.usageCount;
const totalExecutions = previousCount + 1;
const totalLatency = skill.metadata.averageLatency * previousCount + latency;
// Calculate success count from previous executions
const previousSuccessCount = skill.metadata.successRate * previousCount;
const newSuccessCount = success ? previousSuccessCount + 1 : previousSuccessCount;
skill.metadata.usageCount = totalExecutions;
skill.metadata.averageLatency = totalLatency / totalExecutions;
skill.metadata.successRate = newSuccessCount / totalExecutions;
skill.metadata.updatedAt = new Date();
}
}
// Tests
describe('Skill Registry', () => {
let registry: SkillRegistry;
beforeEach(() => {
registry = new SkillRegistry();
});
describe('Registration', () => {
it('should register a skill', async () => {
const skill = await registry.register({
id: 'skill-001',
name: 'test-skill',
version: '1.0.0',
description: 'A test skill',
inputSchema: { type: 'object', properties: { input: { type: 'string' } } },
outputSchema: { type: 'object', properties: { output: { type: 'string' } } },
executor: 'native://test',
timeout: 30000,
retries: 3
});
expect(skill.id).toBe('skill-001');
expect(skill.name).toBe('test-skill');
expect(skill.metadata.usageCount).toBe(0);
});
it('should throw error when registering duplicate skill', async () => {
await registry.register({
id: 'skill-001',
name: 'test',
version: '1.0.0',
description: 'Test',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://test',
timeout: 30000,
retries: 0
});
await expect(registry.register({
id: 'skill-001',
name: 'duplicate',
version: '1.0.0',
description: 'Duplicate',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://test',
timeout: 30000,
retries: 0
})).rejects.toThrow('already registered');
});
it('should throw error for invalid schema type', async () => {
await expect(registry.register({
id: 'skill-001',
name: 'test',
version: '1.0.0',
description: 'Test',
inputSchema: { type: 'invalid' as any },
outputSchema: { type: 'object' },
executor: 'native://test',
timeout: 30000,
retries: 0
})).rejects.toThrow('Invalid schema type');
});
it('should unregister skill', async () => {
await registry.register({
id: 'skill-001',
name: 'test',
version: '1.0.0',
description: 'Test',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://test',
timeout: 30000,
retries: 0
});
const result = await registry.unregister('skill-001');
const skill = await registry.get('skill-001');
expect(result).toBe(true);
expect(skill).toBeNull();
});
});
describe('Retrieval', () => {
beforeEach(async () => {
await registry.register({
id: 'skill-001',
name: 'code-gen',
version: '1.0.0',
description: 'Generate code',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'wasm://code-gen',
timeout: 30000,
retries: 0,
metadata: { tags: ['code', 'generation'] }
});
await registry.register({
id: 'skill-002',
name: 'code-gen',
version: '2.0.0',
description: 'Generate code v2',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'wasm://code-gen-v2',
timeout: 30000,
retries: 0,
metadata: { tags: ['code', 'generation', 'v2'] }
});
await registry.register({
id: 'skill-003',
name: 'test-gen',
version: '1.0.0',
description: 'Generate tests',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://test-gen',
timeout: 60000,
retries: 2,
metadata: { tags: ['testing', 'generation'] }
});
});
it('should get skill by ID', async () => {
const skill = await registry.get('skill-001');
expect(skill?.name).toBe('code-gen');
});
it('should get skill by name', async () => {
const skill = await registry.getByName('code-gen');
expect(skill).not.toBeNull();
expect(skill?.name).toBe('code-gen');
});
it('should get skill by name and version', async () => {
const skill = await registry.getByName('code-gen', '2.0.0');
expect(skill?.id).toBe('skill-002');
});
it('should list all skills', async () => {
const skills = await registry.list();
expect(skills).toHaveLength(3);
});
it('should list skills by tag', async () => {
const skills = await registry.list(['testing']);
expect(skills).toHaveLength(1);
expect(skills[0].name).toBe('test-gen');
});
it('should list skills by executor type', async () => {
const wasmSkills = await registry.listByExecutorType('wasm://');
const nativeSkills = await registry.listByExecutorType('native://');
expect(wasmSkills).toHaveLength(2);
expect(nativeSkills).toHaveLength(1);
});
});
describe('Execution', () => {
const context: SkillExecutionContext = {
tenantId: 'tenant-001',
sessionId: 'session-001',
agentId: 'agent-001'
};
beforeEach(async () => {
await registry.register({
id: 'skill-001',
name: 'echo',
version: '1.0.0',
description: 'Echo input',
inputSchema: {
type: 'object',
properties: { message: { type: 'string' } },
required: ['message']
},
outputSchema: { type: 'object' },
executor: 'native://echo',
timeout: 5000,
retries: 2
});
registry.registerExecutor('native://echo', async (input) => {
return { echoed: (input as any).message };
});
});
it('should execute skill successfully', async () => {
const result = await registry.execute(
'skill-001',
{ message: 'Hello' },
context
);
expect(result.success).toBe(true);
expect(result.output).toEqual({ echoed: 'Hello' });
expect(result.latency).toBeGreaterThan(0);
});
it('should fail for non-existent skill', async () => {
const result = await registry.execute(
'non-existent',
{},
context
);
expect(result.success).toBe(false);
expect(result.error).toContain('not found');
});
it('should validate required input fields', async () => {
const result = await registry.execute(
'skill-001',
{},
context
);
expect(result.success).toBe(false);
expect(result.error).toContain('Missing required field');
});
it('should fail without executor', async () => {
await registry.register({
id: 'skill-no-executor',
name: 'no-executor',
version: '1.0.0',
description: 'No executor',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'unknown://test',
timeout: 5000,
retries: 0
});
const result = await registry.execute(
'skill-no-executor',
{},
context
);
expect(result.success).toBe(false);
expect(result.error).toContain('No executor found');
});
it('should handle execution errors', async () => {
await registry.register({
id: 'skill-error',
name: 'error',
version: '1.0.0',
description: 'Throws error',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://error',
timeout: 5000,
retries: 0
});
registry.registerExecutor('native://error', async () => {
throw new Error('Execution failed');
});
const result = await registry.execute(
'skill-error',
{},
context
);
expect(result.success).toBe(false);
expect(result.error).toBe('Execution failed');
});
it('should update metrics after execution', async () => {
await registry.execute(
'skill-001',
{ message: 'test' },
context
);
const skill = await registry.get('skill-001');
expect(skill?.metadata.usageCount).toBe(1);
expect(skill?.metadata.averageLatency).toBeGreaterThan(0);
expect(skill?.metadata.successRate).toBe(1);
});
it('should update success rate on failure', async () => {
await registry.register({
id: 'skill-flaky',
name: 'flaky',
version: '1.0.0',
description: 'Flaky skill',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://flaky',
timeout: 5000,
retries: 0
});
let callCount = 0;
registry.registerExecutor('native://flaky', async () => {
callCount++;
if (callCount === 1) {
throw new Error('First call fails');
}
return { success: true };
});
// First call fails
await registry.execute('skill-flaky', {}, context);
// Second call succeeds
await registry.execute('skill-flaky', {}, context);
const skill = await registry.get('skill-flaky');
expect(skill?.metadata.usageCount).toBe(2);
expect(skill?.metadata.successRate).toBe(0.5);
});
});
describe('Retry Mechanism', () => {
const context: SkillExecutionContext = {
tenantId: 'tenant-001',
sessionId: 'session-001',
agentId: 'agent-001'
};
it('should retry failed executions', async () => {
await registry.register({
id: 'skill-retry',
name: 'retry',
version: '1.0.0',
description: 'Retry skill',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://retry',
timeout: 5000,
retries: 2
});
let attempts = 0;
registry.registerExecutor('native://retry', async () => {
attempts++;
if (attempts < 3) {
throw new Error(`Attempt ${attempts} failed`);
}
return { success: true };
});
const result = await registry.executeWithRetry(
'skill-retry',
{},
context
);
expect(result.success).toBe(true);
expect(attempts).toBe(3);
});
it('should fail after max retries', async () => {
await registry.register({
id: 'skill-always-fail',
name: 'always-fail',
version: '1.0.0',
description: 'Always fails',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
executor: 'native://always-fail',
timeout: 5000,
retries: 2
});
registry.registerExecutor('native://always-fail', async () => {
throw new Error('Always fails');
});
const result = await registry.executeWithRetry(
'skill-always-fail',
{},
context
);
expect(result.success).toBe(false);
});
});
});
describe('Skill Factory Integration', () => {
let registry: SkillRegistry;
beforeEach(() => {
registry = new SkillRegistry();
});
it('should register skill from factory data', async () => {
const factorySkill = createSkill({
name: 'factory-skill',
description: 'Created from factory'
});
const skill = await registry.register({
id: factorySkill.id,
name: factorySkill.name,
version: factorySkill.version,
description: factorySkill.description,
inputSchema: factorySkill.inputSchema as any,
outputSchema: factorySkill.outputSchema as any,
executor: factorySkill.executor,
timeout: factorySkill.timeout,
retries: 0
});
expect(skill.name).toBe('factory-skill');
expect(skill.description).toBe('Created from factory');
});
});