feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import express, { type RequestHandler } from 'express';
|
||||
import request from 'supertest';
|
||||
import {
|
||||
mountToolsApi,
|
||||
_resetToolCatalogCacheForTests,
|
||||
type McpCatalogDeps,
|
||||
type ToolCatalogEntry,
|
||||
} from './tools-api.js';
|
||||
import { setSshSubsystem, type SshSubsystem } from '../engine/tools/ssh.js';
|
||||
|
||||
/**
|
||||
* Build an express app exposing /api/tools. `user` controls what
|
||||
* (req as any).user is set to before the catalog handler runs.
|
||||
*/
|
||||
function makeApp(opts?: {
|
||||
user?: { id?: string; role?: string } | null;
|
||||
authActive?: boolean;
|
||||
mcp?: McpCatalogDeps | null;
|
||||
}): express.Application {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
if (opts?.user !== null) {
|
||||
const u = opts?.user ?? { id: 'u1', role: 'user' };
|
||||
app.use((req, _res, next) => {
|
||||
(req as unknown as { user: typeof u }).user = u;
|
||||
// supertest doesn't have passport; fake isAuthenticated.
|
||||
(req as unknown as { isAuthenticated: () => boolean }).isAuthenticated = () => true;
|
||||
next();
|
||||
});
|
||||
}
|
||||
const requireAuth: RequestHandler = (req, res, next) => {
|
||||
if ((req as unknown as { user?: { id?: string } }).user?.id) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
};
|
||||
mountToolsApi(app, {
|
||||
authActive: opts?.authActive ?? false,
|
||||
requireAuth,
|
||||
mcp: opts?.mcp ?? null,
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('GET /api/tools (runtime catalog)', () => {
|
||||
beforeEach(() => {
|
||||
_resetToolCatalogCacheForTests();
|
||||
setSshSubsystem(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setSshSubsystem(null);
|
||||
_resetToolCatalogCacheForTests();
|
||||
});
|
||||
|
||||
it('returns ToolCatalogResponse with builtin tools', async () => {
|
||||
const res = await request(makeApp()).get('/api/tools');
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body.tools)).toBe(true);
|
||||
const names = (res.body.tools as ToolCatalogEntry[]).map((t) => t.name);
|
||||
// Core builtin tools must be present.
|
||||
expect(names).toContain('Read');
|
||||
expect(names).toContain('Write');
|
||||
expect(names).toContain('Bash');
|
||||
});
|
||||
|
||||
it('tags core tools with source=builtin and category=core', async () => {
|
||||
const res = await request(makeApp()).get('/api/tools');
|
||||
const read = (res.body.tools as ToolCatalogEntry[]).find((t) => t.name === 'Read');
|
||||
expect(read).toBeDefined();
|
||||
expect(read!.source).toBe('builtin');
|
||||
expect(read!.category).toBe('core');
|
||||
expect(read!.scope).toBe('piece');
|
||||
expect(read!.available).toBe(true);
|
||||
});
|
||||
|
||||
it('includes meta tools tagged source=meta scope=global', async () => {
|
||||
const res = await request(makeApp()).get('/api/tools');
|
||||
const tools = res.body.tools as ToolCatalogEntry[];
|
||||
const readDoc = tools.find((t) => t.name === 'ReadToolDoc');
|
||||
expect(readDoc).toBeDefined();
|
||||
expect(readDoc!.source).toBe('meta');
|
||||
expect(readDoc!.scope).toBe('global');
|
||||
expect(readDoc!.available).toBe(true);
|
||||
|
||||
const brainstorm = tools.find((t) => t.name === 'Brainstorm');
|
||||
expect(brainstorm?.source).toBe('meta');
|
||||
expect(brainstorm?.scope).toBe('global');
|
||||
});
|
||||
|
||||
it('marks ssh tools available=false with reason when SSH subsystem is not initialised', async () => {
|
||||
setSshSubsystem(null);
|
||||
const res = await request(makeApp()).get('/api/tools');
|
||||
const ssh = (res.body.tools as ToolCatalogEntry[]).find((t) => t.name === 'SshExec');
|
||||
// SSH module may not exist in some lean builds; only assert when present.
|
||||
if (ssh) {
|
||||
expect(ssh.category).toBe('ssh');
|
||||
expect(ssh.available).toBe(false);
|
||||
expect(ssh.reason).toMatch(/SSH subsystem not initialised/);
|
||||
expect(ssh.scope).toBe('piece');
|
||||
}
|
||||
});
|
||||
|
||||
it('marks ssh tools available=true when subsystem is initialised', async () => {
|
||||
setSshSubsystem({} as SshSubsystem);
|
||||
const res = await request(makeApp()).get('/api/tools');
|
||||
const ssh = (res.body.tools as ToolCatalogEntry[]).find((t) => t.name === 'SshExec');
|
||||
if (ssh) {
|
||||
expect(ssh.available).toBe(true);
|
||||
expect(ssh.reason).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('includes MCP tools for authenticated user', async () => {
|
||||
const mcp: McpCatalogDeps = {
|
||||
registry: {
|
||||
listEnabledForUser: () => [
|
||||
{ id: 'canva', name: 'Canva', enabled: true },
|
||||
],
|
||||
},
|
||||
tokenManager: {
|
||||
hasToken: () => true,
|
||||
},
|
||||
toolCache: {
|
||||
getAllForServers: () => [
|
||||
{ serverId: 'canva', toolName: 'createDesign' },
|
||||
{ serverId: 'canva', toolName: 'listDesigns' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const res = await request(makeApp({ mcp })).get('/api/tools');
|
||||
const tools = res.body.tools as ToolCatalogEntry[];
|
||||
const create = tools.find((t) => t.name === 'mcp__canva__createDesign');
|
||||
expect(create).toBeDefined();
|
||||
expect(create!.source).toBe('mcp');
|
||||
expect(create!.category).toBe('mcp:canva');
|
||||
expect(create!.serverId).toBe('canva');
|
||||
expect(create!.scope).toBe('user');
|
||||
expect(create!.available).toBe(true);
|
||||
});
|
||||
|
||||
it('marks MCP tools unavailable with reason when user is not connected (offline)', async () => {
|
||||
const mcp: McpCatalogDeps = {
|
||||
registry: {
|
||||
listEnabledForUser: () => [{ id: 'gh', name: 'GitHub', enabled: true }],
|
||||
},
|
||||
tokenManager: {
|
||||
hasToken: () => false, // user not connected
|
||||
},
|
||||
toolCache: {
|
||||
getAllForServers: () => [{ serverId: 'gh', toolName: 'listIssues' }],
|
||||
},
|
||||
};
|
||||
const res = await request(makeApp({ mcp })).get('/api/tools');
|
||||
const tool = (res.body.tools as ToolCatalogEntry[]).find(
|
||||
(t) => t.name === 'mcp__gh__listIssues',
|
||||
);
|
||||
expect(tool).toBeDefined();
|
||||
expect(tool!.available).toBe(false);
|
||||
expect(tool!.reason).toMatch(/offline/);
|
||||
});
|
||||
|
||||
it('omits MCP tools when caller has no user id (unauthenticated)', async () => {
|
||||
const mcp: McpCatalogDeps = {
|
||||
registry: {
|
||||
listEnabledForUser: () => [{ id: 'canva', name: 'Canva', enabled: true }],
|
||||
},
|
||||
tokenManager: { hasToken: () => true },
|
||||
toolCache: {
|
||||
getAllForServers: () => [{ serverId: 'canva', toolName: 'createDesign' }],
|
||||
},
|
||||
};
|
||||
const res = await request(makeApp({ user: null, mcp })).get('/api/tools');
|
||||
expect(res.status).toBe(200);
|
||||
const tools = res.body.tools as ToolCatalogEntry[];
|
||||
expect(tools.some((t) => t.source === 'mcp')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns 401 when authActive=true and caller is not authenticated', async () => {
|
||||
const res = await request(makeApp({ user: null, authActive: true })).get('/api/tools');
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('?legacy=1 returns flat array of tool names', async () => {
|
||||
const res = await request(makeApp()).get('/api/tools?legacy=1');
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body.tools)).toBe(true);
|
||||
// Must be string[], NOT objects.
|
||||
for (const t of res.body.tools as unknown[]) {
|
||||
expect(typeof t).toBe('string');
|
||||
}
|
||||
expect(res.body.tools).toContain('Read');
|
||||
expect(res.body.tools).toContain('ReadToolDoc');
|
||||
});
|
||||
|
||||
it('?legacy=1 includes per-user MCP names when authenticated', async () => {
|
||||
const mcp: McpCatalogDeps = {
|
||||
registry: {
|
||||
listEnabledForUser: () => [{ id: 'canva', name: 'Canva', enabled: true }],
|
||||
},
|
||||
tokenManager: { hasToken: () => true },
|
||||
toolCache: {
|
||||
getAllForServers: () => [{ serverId: 'canva', toolName: 'createDesign' }],
|
||||
},
|
||||
};
|
||||
const res = await request(makeApp({ mcp })).get('/api/tools?legacy=1');
|
||||
expect(res.body.tools).toContain('mcp__canva__createDesign');
|
||||
});
|
||||
|
||||
it('surfaces a placeholder entry when MCP server has no cached tools', async () => {
|
||||
const mcp: McpCatalogDeps = {
|
||||
registry: {
|
||||
listEnabledForUser: () => [{ id: 'fresh', name: 'Fresh', enabled: true }],
|
||||
},
|
||||
tokenManager: { hasToken: () => false },
|
||||
toolCache: { getAllForServers: () => [] },
|
||||
};
|
||||
const res = await request(makeApp({ mcp })).get('/api/tools');
|
||||
const placeholder = (res.body.tools as ToolCatalogEntry[]).find(
|
||||
(t) => t.serverId === 'fresh',
|
||||
);
|
||||
expect(placeholder).toBeDefined();
|
||||
expect(placeholder!.available).toBe(false);
|
||||
expect(placeholder!.reason).toMatch(/offline|no cached tools/);
|
||||
expect(placeholder!.scope).toBe('user');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user