128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { executeMcpCall } from './tool-executor.js';
|
|
|
|
describe('executeMcpCall', () => {
|
|
let workspace: string;
|
|
beforeEach(async () => {
|
|
workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-exec-'));
|
|
});
|
|
afterEach(async () => {
|
|
await fs.rm(workspace, { recursive: true, force: true });
|
|
});
|
|
|
|
function baseCtx() {
|
|
return {
|
|
workspacePath: workspace,
|
|
ownerId: 'u1',
|
|
jobId: 'j1',
|
|
config: {
|
|
maxBinarySizeMb: 1,
|
|
maxOutputFilesPerJob: 5,
|
|
maxOutputSizeMbPerJob: 10,
|
|
callTimeoutSeconds: 30,
|
|
},
|
|
quotaState: { files: 0, bytes: 0 },
|
|
};
|
|
}
|
|
|
|
it('concatenates text content into output', async () => {
|
|
const fakeClient = {
|
|
callTool: vi.fn().mockResolvedValue({
|
|
content: [
|
|
{ type: 'text', text: 'hello ' },
|
|
{ type: 'text', text: 'world' },
|
|
],
|
|
}),
|
|
};
|
|
const res = await executeMcpCall({
|
|
client: fakeClient as never,
|
|
serverId: 'canva',
|
|
toolName: 'ping',
|
|
input: {},
|
|
ctx: baseCtx(),
|
|
});
|
|
expect(res.isError).toBeFalsy();
|
|
expect(res.output).toContain('hello world');
|
|
});
|
|
|
|
it('saves image content to output/mcp', async () => {
|
|
const PNG = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.alloc(4, 0)]);
|
|
const fakeClient = {
|
|
callTool: vi.fn().mockResolvedValue({
|
|
content: [{ type: 'image', data: PNG.toString('base64'), mimeType: 'image/png' }],
|
|
}),
|
|
};
|
|
const res = await executeMcpCall({
|
|
client: fakeClient as never,
|
|
serverId: 'canva',
|
|
toolName: 'render',
|
|
input: {},
|
|
ctx: baseCtx(),
|
|
});
|
|
expect(res.isError).toBeFalsy();
|
|
expect(res.output).toMatch(/Saved: output\/mcp\/canva\/render-/);
|
|
// Image must NOT be pushed to images[] (context-bloat guard)
|
|
expect((res as { images?: unknown }).images).toBeUndefined();
|
|
});
|
|
|
|
it('returns isError when callTool throws', async () => {
|
|
const fakeClient = {
|
|
callTool: vi.fn().mockRejectedValue(new Error('boom')),
|
|
};
|
|
const res = await executeMcpCall({
|
|
client: fakeClient as never,
|
|
serverId: 'canva',
|
|
toolName: 'x',
|
|
input: {},
|
|
ctx: baseCtx(),
|
|
});
|
|
expect(res.isError).toBe(true);
|
|
expect(res.output).toMatch(/boom/);
|
|
});
|
|
|
|
it('writes a raw JSON log under logs/mcp/{serverId}/ on success', async () => {
|
|
const fakeClient = {
|
|
callTool: vi.fn().mockResolvedValue({
|
|
content: [{ type: 'text', text: 'pong' }],
|
|
}),
|
|
};
|
|
await executeMcpCall({
|
|
client: fakeClient as never,
|
|
serverId: 'canva',
|
|
toolName: 'ping',
|
|
input: { q: 'hi' },
|
|
ctx: baseCtx(),
|
|
});
|
|
const files = await fs.readdir(path.join(workspace, 'logs', 'mcp', 'canva'));
|
|
expect(files.some((f) => f.startsWith('ping-') && f.endsWith('.json'))).toBe(true);
|
|
const history = await fs.readFile(path.join(workspace, 'logs', 'mcp-history.jsonl'), 'utf-8');
|
|
expect(history).toMatch(/"toolName":"ping"/);
|
|
});
|
|
|
|
it('writes a raw JSON log under logs/mcp/{serverId}/ on failure', async () => {
|
|
const fakeClient = {
|
|
callTool: vi.fn().mockRejectedValue(new Error('boom')),
|
|
};
|
|
await executeMcpCall({
|
|
client: fakeClient as never,
|
|
serverId: 'canva',
|
|
toolName: 'fail',
|
|
input: {},
|
|
ctx: baseCtx(),
|
|
});
|
|
const files = await fs.readdir(path.join(workspace, 'logs', 'mcp', 'canva'));
|
|
expect(files.some((f) => f.startsWith('fail-') && f.endsWith('.json'))).toBe(true);
|
|
const body = JSON.parse(
|
|
await fs.readFile(
|
|
path.join(workspace, 'logs', 'mcp', 'canva', files.find((f) => f.startsWith('fail-'))!),
|
|
'utf-8',
|
|
),
|
|
);
|
|
expect(body.isError).toBe(true);
|
|
expect(body.output).toMatch(/boom/);
|
|
});
|
|
});
|