coder(providers): v2.3 provider-lifecycle phase 1 — config-backed registry
Adds a config layer merged over the hardcoded built-ins (tasks 1.1-1.6): CODER_PROVIDERS_PATH env (default /data/coder-providers.json); provider-config.ts (Zod schema + never-throw loader — missing/invalid file falls back to built-ins only — + save); provider-config-registry.ts (ResolvedProviderDef + buildResolvedRegistry merge: override built-ins, add custom extends:'acp' entries, boocode always enabled + singleton); agent-probe now iterates the resolved registry, probes custom-ACP command[0] via execFile (no shell), skips disabled providers (keeps the row), reads enabled from memory only (no DB column). No snapshot/dispatch/route/UI changes (Phase 2+). 6 new unit tests; empty config provably yields exactly the built-ins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { buildResolvedRegistry } from '../provider-config-registry.js';
|
||||
import { PROVIDERS } from '../provider-registry.js';
|
||||
import type { CoderProvidersFile } from '../provider-config.js';
|
||||
|
||||
describe('buildResolvedRegistry', () => {
|
||||
it('applies a built-in override (goose label)', () => {
|
||||
const config: CoderProvidersFile = { providers: { goose: { label: 'Goosey' } } };
|
||||
const reg = buildResolvedRegistry(PROVIDERS, config);
|
||||
const goose = reg.get('goose');
|
||||
expect(goose).toBeDefined();
|
||||
expect(goose!.label).toBe('Goosey');
|
||||
expect(goose!.configLabel).toBe('Goosey');
|
||||
expect(goose!.enabled).toBe(true);
|
||||
expect(goose!.isBuiltin).toBe(true);
|
||||
expect(goose!.isCustomAcp).toBe(false);
|
||||
});
|
||||
|
||||
it('adds a custom ACP entry (extends:acp + label + command)', () => {
|
||||
const config: CoderProvidersFile = {
|
||||
providers: {
|
||||
'amp-acp': { extends: 'acp', label: 'Amp', description: 'ACP wrapper', command: ['amp-acp', '--acp'], env: { AMP: '1' } },
|
||||
},
|
||||
};
|
||||
const reg = buildResolvedRegistry(PROVIDERS, config);
|
||||
const amp = reg.get('amp-acp');
|
||||
expect(amp).toBeDefined();
|
||||
expect(amp!.isCustomAcp).toBe(true);
|
||||
expect(amp!.isBuiltin).toBe(false);
|
||||
expect(amp!.transport).toBe('acp');
|
||||
expect(amp!.modelSource).toBe('probe');
|
||||
expect(amp!.launchCommand).toEqual(['amp-acp', '--acp']);
|
||||
expect(amp!.env).toEqual({ AMP: '1' });
|
||||
expect(amp!.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps a disabled built-in in the registry flagged disabled (goose)', () => {
|
||||
const config: CoderProvidersFile = { providers: { goose: { enabled: false } } };
|
||||
const reg = buildResolvedRegistry(PROVIDERS, config);
|
||||
expect(reg.has('goose')).toBe(true);
|
||||
expect(reg.get('goose')!.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('skips a custom id without extends (no throw)', () => {
|
||||
const config: CoderProvidersFile = { providers: { weird: { label: 'Weird', command: ['weird'] } } };
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const reg = buildResolvedRegistry(PROVIDERS, config);
|
||||
expect(reg.has('weird')).toBe(false);
|
||||
// built-ins untouched
|
||||
expect(reg.size).toBe(PROVIDERS.length);
|
||||
expect(warn).toHaveBeenCalled();
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it('ignores enabled:false on boocode and warns', () => {
|
||||
const config: CoderProvidersFile = { providers: { boocode: { enabled: false } } };
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const reg = buildResolvedRegistry(PROVIDERS, config);
|
||||
expect(reg.get('boocode')!.enabled).toBe(true);
|
||||
expect(warn).toHaveBeenCalled();
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it('REGRESSION: empty config returns exactly the built-ins, all enabled', () => {
|
||||
const reg = buildResolvedRegistry(PROVIDERS, { providers: {} });
|
||||
expect(reg.size).toBe(PROVIDERS.length);
|
||||
expect([...reg.keys()]).toEqual(PROVIDERS.map((p) => p.name));
|
||||
for (const def of PROVIDERS) {
|
||||
const r = reg.get(def.name)!;
|
||||
expect(r.enabled).toBe(true);
|
||||
expect(r.isBuiltin).toBe(true);
|
||||
expect(r.isCustomAcp).toBe(false);
|
||||
expect(r.launchCommand).toBeNull();
|
||||
expect(r.label).toBe(def.label);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user