docs: update README for plugin system, add proxyModel tests
- Rewrite README Content Pipeline section as Plugin System section documenting built-in plugins (default, gate, content-pipeline), plugin hooks, and the relationship between gating and proxyModel - Update all README examples to use --proxy-model instead of --gated - Add unit tests: proxyModel normalization in JSON/YAML output (4 tests), Plugin Config section in describe output (2 tests) - Add smoke tests: yaml/json output shows resolved proxyModel without gated field, round-trip compatibility (4 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,44 @@ describe('describe command', () => {
|
||||
expect(text).toContain('user-1');
|
||||
});
|
||||
|
||||
it('shows project Plugin Config with proxyModel', async () => {
|
||||
const deps = makeDeps({
|
||||
id: 'proj-1',
|
||||
name: 'gated-project',
|
||||
description: 'A gated project',
|
||||
ownerId: 'user-1',
|
||||
proxyModel: 'default',
|
||||
proxyMode: 'direct',
|
||||
createdAt: '2025-01-01',
|
||||
});
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'project', 'proj-1']);
|
||||
|
||||
const text = deps.output.join('\n');
|
||||
expect(text).toContain('Plugin Config:');
|
||||
expect(text).toContain('Plugin:');
|
||||
expect(text).toContain('default');
|
||||
expect(text).not.toContain('Gated:');
|
||||
});
|
||||
|
||||
it('shows project Plugin Config defaulting to "default" when proxyModel is empty', async () => {
|
||||
const deps = makeDeps({
|
||||
id: 'proj-1',
|
||||
name: 'old-project',
|
||||
description: '',
|
||||
ownerId: 'user-1',
|
||||
proxyModel: '',
|
||||
gated: true,
|
||||
createdAt: '2025-01-01',
|
||||
});
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'project', 'proj-1']);
|
||||
|
||||
const text = deps.output.join('\n');
|
||||
expect(text).toContain('Plugin Config:');
|
||||
expect(text).toContain('default');
|
||||
});
|
||||
|
||||
it('shows secret detail with masked values', async () => {
|
||||
const deps = makeDeps({
|
||||
id: 'sec-1',
|
||||
|
||||
@@ -335,4 +335,82 @@ describe('get command', () => {
|
||||
await cmd.parseAsync(['node', 'test', 'prompts']);
|
||||
expect(deps.output[0]).toContain('No prompts found');
|
||||
});
|
||||
|
||||
it('lists projects with PLUGIN column showing resolved proxyModel', async () => {
|
||||
const deps = makeDeps([{
|
||||
id: 'proj-1',
|
||||
name: 'home',
|
||||
description: '',
|
||||
proxyMode: 'direct',
|
||||
proxyModel: '',
|
||||
gated: true,
|
||||
ownerId: 'usr-1',
|
||||
servers: [],
|
||||
}]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'projects']);
|
||||
|
||||
const text = deps.output.join('\n');
|
||||
expect(text).toContain('PLUGIN');
|
||||
expect(text).not.toContain('GATED');
|
||||
// proxyModel is empty but gated=true, table shows 'default'
|
||||
expect(text).toContain('default');
|
||||
});
|
||||
|
||||
it('project JSON output resolves proxyModel from gated=true', async () => {
|
||||
const deps = makeDeps([{
|
||||
id: 'proj-1',
|
||||
name: 'home',
|
||||
description: '',
|
||||
proxyMode: 'direct',
|
||||
proxyModel: '',
|
||||
gated: true,
|
||||
ownerId: 'usr-1',
|
||||
servers: [],
|
||||
}]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'projects', '-o', 'json']);
|
||||
|
||||
const parsed = JSON.parse(deps.output[0] ?? '') as Array<Record<string, unknown>>;
|
||||
expect(parsed[0]!.proxyModel).toBe('default');
|
||||
expect(parsed[0]).not.toHaveProperty('gated');
|
||||
});
|
||||
|
||||
it('project JSON output resolves proxyModel from gated=false', async () => {
|
||||
const deps = makeDeps([{
|
||||
id: 'proj-1',
|
||||
name: 'tools',
|
||||
description: '',
|
||||
proxyMode: 'direct',
|
||||
proxyModel: '',
|
||||
gated: false,
|
||||
ownerId: 'usr-1',
|
||||
servers: [],
|
||||
}]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'projects', '-o', 'json']);
|
||||
|
||||
const parsed = JSON.parse(deps.output[0] ?? '') as Array<Record<string, unknown>>;
|
||||
expect(parsed[0]!.proxyModel).toBe('content-pipeline');
|
||||
expect(parsed[0]).not.toHaveProperty('gated');
|
||||
});
|
||||
|
||||
it('project JSON output preserves explicit proxyModel and drops gated', async () => {
|
||||
const deps = makeDeps([{
|
||||
id: 'proj-1',
|
||||
name: 'custom',
|
||||
description: '',
|
||||
proxyMode: 'direct',
|
||||
proxyModel: 'gate',
|
||||
gated: true,
|
||||
ownerId: 'usr-1',
|
||||
servers: [],
|
||||
}]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'projects', '-o', 'json']);
|
||||
|
||||
const parsed = JSON.parse(deps.output[0] ?? '') as Array<Record<string, unknown>>;
|
||||
expect(parsed[0]!.proxyModel).toBe('gate');
|
||||
expect(parsed[0]).not.toHaveProperty('gated');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,5 +142,40 @@ describe('ProxyModel smoke tests', () => {
|
||||
expect(output).toContain('Plugin Config');
|
||||
expect(output).toContain('Plugin:');
|
||||
});
|
||||
|
||||
it('mcpctl get projects -o yaml shows proxyModel and no gated field', async () => {
|
||||
if (!available) return;
|
||||
|
||||
const output = await mcpctl('get projects -o yaml');
|
||||
// proxyModel should be resolved (not empty)
|
||||
expect(output).toContain('proxyModel:');
|
||||
expect(output).not.toContain('gated:');
|
||||
});
|
||||
|
||||
it('mcpctl get projects -o json shows proxyModel and no gated field', async () => {
|
||||
if (!available) return;
|
||||
|
||||
const json = await mcpctl('get projects -o json');
|
||||
const projects = JSON.parse(json) as Array<{ proxyModel?: string; gated?: boolean }>;
|
||||
expect(projects.length).toBeGreaterThan(0);
|
||||
|
||||
for (const project of projects) {
|
||||
expect(project.proxyModel).toBeDefined();
|
||||
expect(project.proxyModel).not.toBe('');
|
||||
expect(project).not.toHaveProperty('gated');
|
||||
}
|
||||
});
|
||||
|
||||
it('mcpctl get projects -o yaml is round-trip compatible with apply', async () => {
|
||||
if (!available) return;
|
||||
|
||||
const yaml = await mcpctl('get projects -o yaml');
|
||||
// Should contain kind and proxyModel (apply-compatible fields)
|
||||
expect(yaml).toContain('kind: project');
|
||||
expect(yaml).toContain('proxyModel:');
|
||||
// Should not contain internal fields
|
||||
expect(yaml).not.toContain('ownerId:');
|
||||
expect(yaml).not.toContain('createdAt:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user