maestro/src/mcp/raw-logger.test.ts
2026-06-03 05:08:00 +00:00

204 lines
6.4 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { saveMcpRaw } from './raw-logger.js';
describe('saveMcpRaw', () => {
let workspace: string;
beforeEach(async () => {
workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-raw-'));
});
afterEach(async () => {
await fs.rm(workspace, { recursive: true, force: true });
});
async function listMcpDir(serverId: string): Promise<string[]> {
try {
return await fs.readdir(path.join(workspace, 'logs', 'mcp', serverId));
} catch {
return [];
}
}
it('writes a JSON file under logs/mcp/{serverId}/ with the expected structure', async () => {
saveMcpRaw({
workspacePath: workspace,
serverId: 'canva',
toolName: 'generate_designs',
args: { prompt: 'cat' },
content: [{ type: 'text', text: 'hello' }],
isError: false,
output: 'hello',
savedPaths: [],
});
const files = await listMcpDir('canva');
expect(files.length).toBe(1);
expect(files[0]).toMatch(/^generate_designs-.*\.json$/);
const body = JSON.parse(
await fs.readFile(path.join(workspace, 'logs', 'mcp', 'canva', files[0]), 'utf-8'),
);
expect(body.serverId).toBe('canva');
expect(body.toolName).toBe('generate_designs');
expect(body.arguments).toEqual({ prompt: 'cat' });
expect(body.isError).toBe(false);
expect(body.content).toEqual([{ type: 'text', text: 'hello' }]);
expect(body.output).toBe('hello');
expect(body.savedBinaries).toEqual([]);
expect(typeof body.timestamp).toBe('string');
});
it('appends one line per call to logs/mcp-history.jsonl', async () => {
saveMcpRaw({
workspacePath: workspace,
serverId: 'canva',
toolName: 'tool_a',
args: {},
content: [{ type: 'text', text: 'A' }],
isError: false,
output: 'A',
savedPaths: [],
});
saveMcpRaw({
workspacePath: workspace,
serverId: 'canva',
toolName: 'tool_b',
args: {},
content: [{ type: 'text', text: 'B' }],
isError: true,
output: 'B',
savedPaths: [],
});
const lines = (
await fs.readFile(path.join(workspace, 'logs', 'mcp-history.jsonl'), 'utf-8')
)
.trim()
.split('\n');
expect(lines.length).toBe(2);
const e0 = JSON.parse(lines[0]);
const e1 = JSON.parse(lines[1]);
expect(e0.toolName).toBe('tool_a');
expect(e0.isError).toBe(false);
expect(e0.serverId).toBe('canva');
expect(e0.filename).toMatch(/^logs\/mcp\/canva\/tool_a-/);
expect(e0.bytes).toBeGreaterThan(0);
expect(e1.toolName).toBe('tool_b');
expect(e1.isError).toBe(true);
});
it('redacts secrets in arguments', async () => {
saveMcpRaw({
workspacePath: workspace,
serverId: 'oauth',
toolName: 'tok',
args: {
prompt: 'hi',
access_token: 'super-secret',
nested: { refresh_token: 'r' },
},
content: [],
isError: false,
output: '',
savedPaths: [],
});
const files = await listMcpDir('oauth');
const body = JSON.parse(
await fs.readFile(path.join(workspace, 'logs', 'mcp', 'oauth', files[0]), 'utf-8'),
);
expect(body.arguments.prompt).toBe('hi');
expect(body.arguments.access_token).toBe('***');
expect(body.arguments.nested.refresh_token).toBe('***');
});
it('strips base64 from image and resource blocks (replaces with reference)', async () => {
saveMcpRaw({
workspacePath: workspace,
serverId: 'canva',
toolName: 'render',
args: {},
content: [
{ type: 'image', data: 'AAAA'.repeat(10000), mimeType: 'image/png' },
{ type: 'resource', resource: { blob: 'BBBB'.repeat(10000), mimeType: 'application/pdf' } },
{ type: 'text', text: 'caption' },
],
isError: false,
output: 'Saved: output/mcp/canva/render-x.png',
savedPaths: ['output/mcp/canva/render-x.png', 'output/mcp/canva/render-y.pdf'],
});
const files = await listMcpDir('canva');
const body = JSON.parse(
await fs.readFile(path.join(workspace, 'logs', 'mcp', 'canva', files[0]), 'utf-8'),
);
expect(body.content[0].type).toBe('image');
expect(body.content[0].data).toMatch(/^<base64 omitted/);
expect(body.content[0].mimeType).toBe('image/png');
expect(body.content[1].resource.blob).toMatch(/^<base64 omitted/);
expect(body.content[1].resource.mimeType).toBe('application/pdf');
expect(body.content[2]).toEqual({ type: 'text', text: 'caption' });
expect(body.savedBinaries).toEqual([
'output/mcp/canva/render-x.png',
'output/mcp/canva/render-y.pdf',
]);
});
it('saves failure responses as well (isError true with synthetic content)', async () => {
saveMcpRaw({
workspacePath: workspace,
serverId: 'canva',
toolName: 'fail',
args: { x: 1 },
content: [],
isError: true,
output: 'MCP call failed: boom',
savedPaths: [],
});
const files = await listMcpDir('canva');
expect(files.length).toBe(1);
const body = JSON.parse(
await fs.readFile(path.join(workspace, 'logs', 'mcp', 'canva', files[0]), 'utf-8'),
);
expect(body.isError).toBe(true);
expect(body.output).toBe('MCP call failed: boom');
});
it('sanitizes unsafe serverId / toolName for filesystem paths', async () => {
saveMcpRaw({
workspacePath: workspace,
serverId: '../escape',
toolName: 'weird/tool name',
args: {},
content: [],
isError: false,
output: '',
savedPaths: [],
});
// Must not create files outside workspace
const outside = await fs.readdir(path.dirname(workspace));
expect(outside.some((f) => f === 'escape')).toBe(false);
// Should still write under logs/mcp/ with sanitized names
const root = await fs.readdir(path.join(workspace, 'logs', 'mcp'));
expect(root.length).toBe(1);
expect(root[0]).not.toContain('/');
expect(root[0]).not.toContain('..');
});
it('does not throw when workspacePath is invalid (best-effort)', () => {
expect(() =>
saveMcpRaw({
workspacePath: '/nonexistent/path/that/should/fail/\0invalid',
serverId: 'x',
toolName: 'y',
args: {},
content: [],
isError: false,
output: '',
savedPaths: [],
}),
).not.toThrow();
});
});