feat: per-project LLM models, ACP session pool, smart pagination tests
- ACP session pool with per-model subprocesses and 8h idle eviction - Per-project LLM config: local override → mcpd recommendation → global default - Model override support in ResponsePaginator - /llm/models endpoint + available models in mcpctl status - Remove --llm-provider/--llm-model from create project (use edit/apply) - 8 new smart pagination integration tests (e2e flow) - 260 mcplocal tests, 330 CLI tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ describe('GeminiAcpProvider', () => {
|
||||
expect(result.content).toBe('padded response');
|
||||
});
|
||||
|
||||
it('serializes concurrent calls', async () => {
|
||||
it('serializes concurrent calls to same model', async () => {
|
||||
const callOrder: number[] = [];
|
||||
let callCount = 0;
|
||||
|
||||
@@ -110,6 +110,70 @@ describe('GeminiAcpProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('session pool', () => {
|
||||
it('creates separate pool entries for different models', async () => {
|
||||
mockPrompt.mockResolvedValue('ok');
|
||||
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'a' }], model: 'gemini-2.5-flash' });
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'b' }], model: 'gemini-2.5-pro' });
|
||||
|
||||
expect(provider.poolSize).toBe(2);
|
||||
});
|
||||
|
||||
it('reuses existing pool entry for same model', async () => {
|
||||
mockPrompt.mockResolvedValue('ok');
|
||||
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'a' }], model: 'gemini-2.5-flash' });
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'b' }], model: 'gemini-2.5-flash' });
|
||||
|
||||
expect(provider.poolSize).toBe(1);
|
||||
});
|
||||
|
||||
it('uses defaultModel when no model specified', async () => {
|
||||
mockPrompt.mockResolvedValue('ok');
|
||||
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'a' }] });
|
||||
|
||||
expect(provider.poolSize).toBe(1);
|
||||
});
|
||||
|
||||
it('evicts idle sessions', async () => {
|
||||
// Use a very short TTL for testing
|
||||
const shortTtl = new GeminiAcpProvider({
|
||||
binaryPath: '/usr/bin/gemini',
|
||||
defaultModel: 'gemini-2.5-flash',
|
||||
idleTtlMs: 1, // 1ms TTL
|
||||
});
|
||||
|
||||
mockPrompt.mockResolvedValue('ok');
|
||||
await shortTtl.complete({ messages: [{ role: 'user', content: 'a' }], model: 'model-a' });
|
||||
expect(shortTtl.poolSize).toBe(1);
|
||||
|
||||
// Wait for TTL to expire
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
// Next complete call triggers eviction of old entry and creates new one
|
||||
await shortTtl.complete({ messages: [{ role: 'user', content: 'b' }], model: 'model-b' });
|
||||
// model-a should have been evicted, only model-b remains
|
||||
expect(shortTtl.poolSize).toBe(1);
|
||||
expect(mockDispose).toHaveBeenCalled();
|
||||
|
||||
shortTtl.dispose();
|
||||
});
|
||||
|
||||
it('dispose kills all pooled clients', async () => {
|
||||
mockPrompt.mockResolvedValue('ok');
|
||||
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'a' }], model: 'model-a' });
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'b' }], model: 'model-b' });
|
||||
expect(provider.poolSize).toBe(2);
|
||||
|
||||
provider.dispose();
|
||||
expect(provider.poolSize).toBe(0);
|
||||
expect(mockDispose).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listModels', () => {
|
||||
it('returns static model list', async () => {
|
||||
const models = await provider.listModels();
|
||||
@@ -120,7 +184,9 @@ describe('GeminiAcpProvider', () => {
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('delegates to AcpClient', () => {
|
||||
it('delegates to all pooled AcpClients', async () => {
|
||||
mockPrompt.mockResolvedValue('ok');
|
||||
await provider.complete({ messages: [{ role: 'user', content: 'test' }] });
|
||||
provider.dispose();
|
||||
expect(mockDispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user