This commit is contained in:
147
packages/server/tests/local/executor-registry.test.ts
Normal file
147
packages/server/tests/local/executor-registry.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { AgentPersona } from '@waggle/agent';
|
||||
import type { DetectedTool } from '@waggle/shared';
|
||||
import { ExecutorRegistry } from '../../src/local/executor-registry.js';
|
||||
|
||||
function persona(id: string, overrides: Partial<AgentPersona> = {}): AgentPersona {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
description: '',
|
||||
icon: '',
|
||||
systemPrompt: '',
|
||||
modelPreference: '',
|
||||
tools: [],
|
||||
workspaceAffinity: [],
|
||||
suggestedCommands: [],
|
||||
defaultWorkflow: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function detectedTool(id: string, installed = true): DetectedTool {
|
||||
return {
|
||||
id,
|
||||
displayName: id,
|
||||
installed,
|
||||
installedPath: installed ? `/tools/${id}` : null,
|
||||
version: installed ? '1.0.0' : null,
|
||||
hooksInstalled: false,
|
||||
hookPointerPath: null,
|
||||
};
|
||||
}
|
||||
|
||||
const PERSONAS = [
|
||||
persona('writer'),
|
||||
persona('coordinator'),
|
||||
persona('analyst'),
|
||||
persona('general-purpose'),
|
||||
persona('researcher'),
|
||||
persona('coder'),
|
||||
persona('planner', { isReadOnly: true }),
|
||||
];
|
||||
|
||||
const DETECTED_TOOLS = [
|
||||
detectedTool('claude-code'),
|
||||
detectedTool('codex', false),
|
||||
detectedTool('hermes'),
|
||||
detectedTool('openclaw'),
|
||||
detectedTool('cursor'),
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('ExecutorRegistry', () => {
|
||||
it('composes the five v1 personas and four headless external executors', async () => {
|
||||
const registry = new ExecutorRegistry({
|
||||
detectTools: vi.fn(async () => DETECTED_TOOLS),
|
||||
personas: () => PERSONAS,
|
||||
});
|
||||
|
||||
const candidates = await registry.snapshot(1_000);
|
||||
|
||||
expect(candidates.map((candidate) => candidate.id)).toEqual([
|
||||
'persona:general-purpose',
|
||||
'persona:coder',
|
||||
'persona:writer',
|
||||
'persona:researcher',
|
||||
'persona:analyst',
|
||||
'external:claude-code',
|
||||
'external:codex',
|
||||
'external:hermes',
|
||||
'external:openclaw',
|
||||
]);
|
||||
expect(candidates.find((candidate) => candidate.id === 'persona:coder')).toMatchObject({
|
||||
kind: 'persona',
|
||||
authClass: 'api-key',
|
||||
installed: true,
|
||||
healthy: true,
|
||||
rateLimit: { state: 'unknown' },
|
||||
supportsHeadless: false,
|
||||
egressDestination: 'configured model provider',
|
||||
taskFit: { coding: 0.85, writing: 0.3 },
|
||||
});
|
||||
expect(candidates.find((candidate) => candidate.id === 'external:claude-code')).toMatchObject({
|
||||
kind: 'external',
|
||||
displayName: 'Claude Code',
|
||||
authClass: 'subscription-cli',
|
||||
installed: true,
|
||||
healthy: true,
|
||||
rateLimit: { state: 'unknown' },
|
||||
supportsHeadless: true,
|
||||
egressDestination: 'Anthropic',
|
||||
});
|
||||
expect(candidates.find((candidate) => candidate.id === 'external:codex')).toMatchObject({
|
||||
installed: false,
|
||||
healthy: false,
|
||||
egressDestination: 'OpenAI',
|
||||
});
|
||||
expect(candidates.find((candidate) => candidate.id === 'external:hermes')?.egressDestination).toBe('Nous');
|
||||
expect(candidates.find((candidate) => candidate.id === 'external:openclaw')?.egressDestination)
|
||||
.toBe('configured provider');
|
||||
});
|
||||
|
||||
it('normalizes tool ids and expires rate-limit observations', async () => {
|
||||
vi.spyOn(Date, 'now').mockReturnValue(10_000);
|
||||
const registry = new ExecutorRegistry({
|
||||
detectTools: vi.fn(async () => DETECTED_TOOLS),
|
||||
personas: () => PERSONAS,
|
||||
});
|
||||
|
||||
registry.noteRateLimit('codex', null);
|
||||
const exhausted = (await registry.snapshot(10_000))
|
||||
.find((candidate) => candidate.id === 'external:codex');
|
||||
expect(exhausted?.rateLimit).toEqual({ state: 'observed_exhausted' });
|
||||
|
||||
const recovered = (await registry.snapshot(10_000 + 15 * 60_000))
|
||||
.find((candidate) => candidate.id === 'external:codex');
|
||||
expect(recovered?.rateLimit).toEqual({ state: 'available' });
|
||||
|
||||
registry.noteRateLimit('external:codex', 2_000_000);
|
||||
const withReset = (await registry.snapshot(1_999_999))
|
||||
.find((candidate) => candidate.id === 'external:codex');
|
||||
expect(withReset?.rateLimit).toEqual({
|
||||
state: 'observed_exhausted',
|
||||
resumeAtMs: 2_000_000,
|
||||
});
|
||||
|
||||
registry.noteHealthy('codex');
|
||||
const healthy = (await registry.snapshot(1_999_999))
|
||||
.find((candidate) => candidate.id === 'external:codex');
|
||||
expect(healthy?.rateLimit).toEqual({ state: 'available' });
|
||||
});
|
||||
|
||||
it('caches tool detection for thirty seconds', async () => {
|
||||
const detectTools = vi.fn(async () => DETECTED_TOOLS);
|
||||
const registry = new ExecutorRegistry({ detectTools, personas: () => PERSONAS });
|
||||
|
||||
await registry.snapshot(1_000);
|
||||
await registry.snapshot(30_999);
|
||||
expect(detectTools).toHaveBeenCalledTimes(1);
|
||||
|
||||
await registry.snapshot(31_000);
|
||||
expect(detectTools).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user