File size: 6,075 Bytes
f0634fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { McpServer } from '@agentclientprotocol/sdk';
import type { ContentPart } from '@moonshot-ai/agent-core-v2';
import { afterEach, describe, expect, it } from 'vitest';
import {
acpBlocksToContentParts,
acpMcpServersToConfigRecord,
compressPromptImageParts,
} from '../src/convert';
import { solidPng, solidPngBase64 } from './_helpers/png';
describe('acpMcpServersToConfigRecord', () => {
it('returns undefined for an absent or empty list', () => {
expect(acpMcpServersToConfigRecord(undefined)).toBeUndefined();
expect(acpMcpServersToConfigRecord([])).toBeUndefined();
});
it('maps stdio servers (no type field) to local stdio configs', () => {
const servers: McpServer[] = [
{
name: 'fs',
command: '/usr/local/bin/mcp-fs',
args: ['--root', '/tmp'],
env: [
{ name: 'API_KEY', value: 'secret' },
{ name: 'DEBUG', value: '1' },
],
},
];
expect(acpMcpServersToConfigRecord(servers)).toEqual({
fs: {
transport: 'stdio',
command: '/usr/local/bin/mcp-fs',
args: ['--root', '/tmp'],
env: { API_KEY: 'secret', DEBUG: '1' },
runtime_id: 'local',
},
});
});
it('maps http and sse servers with header pairs as a record', () => {
const servers: McpServer[] = [
{
type: 'http',
name: 'web',
url: 'https://example.com/mcp',
headers: [{ name: 'Authorization', value: 'Bearer x' }],
},
{ type: 'sse', name: 'events', url: 'https://example.com/sse', headers: [] },
];
expect(acpMcpServersToConfigRecord(servers)).toEqual({
web: {
transport: 'http',
url: 'https://example.com/mcp',
headers: { Authorization: 'Bearer x' },
},
events: { transport: 'sse', url: 'https://example.com/sse', headers: undefined },
});
});
it('drops the unstable acp transport and returns undefined when nothing survives', () => {
const servers = [{ type: 'acp', name: 'nested', serverId: 'srv-1' } as unknown as McpServer];
expect(acpMcpServersToConfigRecord(servers)).toBeUndefined();
});
});
describe('acpBlocksToContentParts', () => {
it('projects file links and embedded text resources with provenance', () => {
const parts = acpBlocksToContentParts([
{ type: 'resource_link', uri: 'file:///tmp/example.ts#L2-L4', name: 'example.ts' },
{ type: 'resource_link', uri: 'https://example.test/doc', name: 'remote doc' },
{
type: 'resource',
resource: { uri: 'memory://note/1', text: 'remember this' },
},
] as never);
expect(parts).toEqual([
{ type: 'text', text: '/tmp/example.ts:2-4' },
{
type: 'text',
text: '<resource_link uri="https://example.test/doc" name="remote doc" />',
},
{ type: 'text', text: '<resource uri="memory://note/1">remember this</resource>' },
]);
});
});
describe('compressPromptImageParts', () => {
const trash: string[] = [];
afterEach(async () => {
await Promise.all(trash.splice(0).map((dir) => rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })));
});
async function tempOriginalsDir(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'acp-originals-'));
trash.push(dir);
return dir;
}
function imagePart(url: string): ContentPart {
return { type: 'image_url', imageUrl: { url } };
}
it('leaves format judgment to the engine: a format it cannot re-encode passes through', async () => {
// Which formats are acceptable depends on the provider the agent is
// bound to, which this edge does not know; the engine's prompt gate
// decides. So neither a HEIC payload nor a MIME alias is rewritten here.
const heic = `data:image/heic;base64,${Buffer.alloc(32).toString('base64')}`;
const alias = `data:IMAGE/PNG;base64,${solidPngBase64(8, 8)}`;
const out = await compressPromptImageParts([
{ type: 'text', text: 'look' },
imagePart(heic),
imagePart(alias),
]);
expect(out).toEqual([{ type: 'text', text: 'look' }, imagePart(heic), imagePart(alias)]);
});
it('passes an under-limit image through unchanged and persists nothing', async () => {
const originalsDir = await tempOriginalsDir();
const url = `data:image/png;base64,${solidPngBase64(32, 32)}`;
const out = await compressPromptImageParts([imagePart(url)], {
originalsDir,
maxImageEdgePx: 64,
});
expect(out).toHaveLength(1);
expect(out[0]).toEqual(imagePart(url));
// No compression happened, so no original was persisted.
expect(await readdir(originalsDir)).toEqual([]);
});
it('compresses an over-edge image, prefixes a caption, and persists the original', async () => {
const originalsDir = await tempOriginalsDir();
const original = solidPng(128, 128);
const url = `data:image/png;base64,${original.toString('base64')}`;
const out = await compressPromptImageParts([imagePart(url)], {
originalsDir,
maxImageEdgePx: 64,
});
// caption text part immediately precedes the re-encoded image part.
expect(out).toHaveLength(2);
const caption = out[0] as { text: string };
const image = out[1] as { imageUrl: { url: string } };
expect(caption.text).toContain('Image compressed to fit model limits');
expect(caption.text).toContain('128x128');
expect(caption.text).toContain(originalsDir);
expect(image.imageUrl.url).not.toBe(url);
expect(image.imageUrl.url.startsWith('data:image/')).toBe(true);
// The original bytes landed in the session media-originals dir so the
// model can read fine detail back from the captioned path.
const files = await readdir(originalsDir);
expect(files).toHaveLength(1);
expect(caption.text).toContain(files[0]!);
expect(await readFile(join(originalsDir, files[0]!))).toEqual(original);
});
});
|