Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
169 lines
4.7 KiB
TypeScript
169 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import {
|
|
mergeModels,
|
|
prefixLlamaSwapModels,
|
|
clearProviderSnapshotCache,
|
|
getProviderSnapshot,
|
|
} from '../provider-snapshot.js';
|
|
|
|
vi.mock('../acp-probe.js', () => ({
|
|
probeAcpProvider: vi.fn(),
|
|
}));
|
|
|
|
import { probeAcpProvider } from '../acp-probe.js';
|
|
|
|
const mockProbe = vi.mocked(probeAcpProvider);
|
|
|
|
function mockSql(agents: Array<{
|
|
name: string;
|
|
install_path: string | null;
|
|
supports_acp: boolean;
|
|
models: Array<{ id: string; label: string }> | null;
|
|
label: string | null;
|
|
transport: string | null;
|
|
}>) {
|
|
return vi.fn((strings: TemplateStringsArray) => {
|
|
const query = strings.join('');
|
|
if (query.includes('FROM available_agents')) {
|
|
return Promise.resolve(agents);
|
|
}
|
|
if (query.includes('UPDATE available_agents')) {
|
|
return Promise.resolve([]);
|
|
}
|
|
return Promise.resolve([]);
|
|
}) as unknown as import('../db.js').Sql;
|
|
}
|
|
|
|
const config = {
|
|
LLAMA_SWAP_URL: 'http://llama-swap.test',
|
|
} as import('../config.js').Config;
|
|
|
|
describe('prefixLlamaSwapModels', () => {
|
|
it('prefixes bare ids', () => {
|
|
expect(prefixLlamaSwapModels([{ id: 'qwen3', label: 'qwen3' }])).toEqual([
|
|
{ id: 'llama-swap/qwen3', label: 'qwen3' },
|
|
]);
|
|
});
|
|
|
|
it('leaves already-prefixed ids unchanged', () => {
|
|
expect(prefixLlamaSwapModels([{ id: 'llama-swap/qwen3', label: 'qwen3' }])).toEqual([
|
|
{ id: 'llama-swap/qwen3', label: 'qwen3' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('mergeModels', () => {
|
|
it('dedupes by id preserving first occurrence', () => {
|
|
const merged = mergeModels(
|
|
[{ id: 'a', label: 'A' }],
|
|
[{ id: 'a', label: 'A2' }, { id: 'b', label: 'B' }],
|
|
);
|
|
expect(merged).toEqual([
|
|
{ id: 'a', label: 'A' },
|
|
{ id: 'b', label: 'B' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('getProviderSnapshot', () => {
|
|
beforeEach(() => {
|
|
clearProviderSnapshotCache();
|
|
vi.restoreAllMocks();
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: [{ id: 'local-model' }, { id: 'llama-swap/existing' }],
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('merges opencode ACP models with prefixed llama-swap models', async () => {
|
|
mockProbe.mockResolvedValue({
|
|
ok: true,
|
|
models: [{ id: 'opencode/big-pickle', label: 'Big Pickle', isDefault: true }],
|
|
modes: [{ id: 'build', label: 'Build' }],
|
|
defaultModeId: 'build',
|
|
commands: [{ name: 'custom', description: 'From ACP probe' }],
|
|
});
|
|
|
|
const sql = mockSql([
|
|
{
|
|
name: 'opencode',
|
|
install_path: '/usr/bin/opencode',
|
|
supports_acp: true,
|
|
models: null,
|
|
label: 'OpenCode',
|
|
transport: 'acp',
|
|
},
|
|
]);
|
|
|
|
const entries = await getProviderSnapshot(sql, config, '/tmp/project', true);
|
|
const opencode = entries.find((e) => e.name === 'opencode');
|
|
|
|
expect(opencode?.models.map((m) => m.id)).toEqual([
|
|
'opencode/big-pickle',
|
|
'llama-swap/local-model',
|
|
'llama-swap/existing',
|
|
]);
|
|
expect(opencode?.commands.some((c) => c.name === 'help')).toBe(true);
|
|
expect(opencode?.commands.some((c) => c.name === 'custom')).toBe(true);
|
|
});
|
|
|
|
it('combines qwen-shaped probe and settings model lists via mergeModels', () => {
|
|
const merged = mergeModels(
|
|
[{ id: 'qwen-probed', label: 'Qwen Probed' }],
|
|
[{ id: 'from-settings', label: 'from-settings' }],
|
|
);
|
|
expect(merged.map((m) => m.id)).toEqual(['qwen-probed', 'from-settings']);
|
|
});
|
|
|
|
it('returns cached entries on second call within TTL', async () => {
|
|
mockProbe.mockResolvedValue({
|
|
ok: true,
|
|
models: [{ id: 'm1', label: 'M1' }],
|
|
modes: [],
|
|
defaultModeId: null,
|
|
commands: [],
|
|
});
|
|
|
|
const sql = mockSql([
|
|
{
|
|
name: 'goose',
|
|
install_path: '/usr/bin/goose',
|
|
supports_acp: true,
|
|
models: null,
|
|
label: 'Goose',
|
|
transport: 'acp',
|
|
},
|
|
]);
|
|
|
|
await getProviderSnapshot(sql, config, '/tmp/cwd', true);
|
|
await getProviderSnapshot(sql, config, '/tmp/cwd', false);
|
|
|
|
expect(mockProbe).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('attaches claude thinking options', async () => {
|
|
const sql = mockSql([
|
|
{
|
|
name: 'claude',
|
|
install_path: '/usr/bin/claude',
|
|
supports_acp: false,
|
|
models: [{ id: 'claude-sonnet', label: 'Sonnet' }],
|
|
label: 'Claude Code',
|
|
transport: 'pty',
|
|
},
|
|
]);
|
|
|
|
const entries = await getProviderSnapshot(sql, config, '/tmp/project', true);
|
|
const claude = entries.find((e) => e.name === 'claude');
|
|
|
|
expect(claude?.models[0]?.thinkingOptions?.length).toBeGreaterThan(0);
|
|
expect(claude?.modes.length).toBeGreaterThan(0);
|
|
expect(claude?.commands.some((c) => c.name === 'help')).toBe(true);
|
|
});
|
|
});
|