mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 10:15:10 +00:00
fix(image-analysis): harden runtime and provisioning
This commit is contained in:
1 parent
6d1c895721
commit
77b488f8b1
21 files changed
+811
-169
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import { spawn } from 'child_process';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
@@ -11,6 +11,11 @@ function encodeMessage(message: unknown): string {
|
||||
return `${JSON.stringify(message)}\n`;
|
||||
}
|
||||
|
||||
function encodeLegacyMessage(message: unknown): string {
|
||||
const body = JSON.stringify(message);
|
||||
return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
|
||||
}
|
||||
|
||||
function collectResponses(
|
||||
child: ReturnType<typeof spawn>,
|
||||
expectedCount: number
|
||||
@@ -133,6 +138,7 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
});
|
||||
|
||||
const child = spawn('node', [serverPath], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
||||
@@ -189,7 +195,7 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Absolute or workspace-relative path to a local image or PDF file to analyze.',
|
||||
'Workspace-relative path, or an absolute path inside the current workspace, to a local image or PDF file to analyze.',
|
||||
},
|
||||
focus: {
|
||||
type: 'string',
|
||||
@@ -228,7 +234,10 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
});
|
||||
|
||||
it('returns a structured tool error when the file does not exist', async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-mcp-server-missing-'));
|
||||
const missingPath = join(tempDir, 'missing-image.png');
|
||||
const child = spawn('node', [serverPath], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
||||
@@ -260,7 +269,7 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'ImageAnalysis',
|
||||
arguments: { filePath: join(tmpdir(), 'missing-image.png') },
|
||||
arguments: { filePath: missingPath },
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -272,7 +281,7 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `ImageAnalysis could not find file: ${join(tmpdir(), 'missing-image.png')}`,
|
||||
text: `ImageAnalysis could not find file: ${missingPath}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
@@ -282,6 +291,62 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects paths outside the current workspace', async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-mcp-server-scope-'));
|
||||
const workspaceDir = join(tempDir, 'workspace');
|
||||
mkdirSync(workspaceDir, { recursive: true });
|
||||
const outsidePath = join(tempDir, 'outside.png');
|
||||
createTestPng(outsidePath);
|
||||
|
||||
const child = spawn('node', [serverPath], {
|
||||
cwd: workspaceDir,
|
||||
env: {
|
||||
...process.env,
|
||||
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
||||
CCS_IMAGE_ANALYSIS_SKIP: '0',
|
||||
CCS_CURRENT_PROVIDER: 'agy',
|
||||
CCS_IMAGE_ANALYSIS_MODEL: 'gemini-3-1-flash-preview',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
try {
|
||||
const responsesPromise = collectResponses(child, 2);
|
||||
child.stdin.write(
|
||||
encodeMessage({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'bun-test', version: '1.0.0' },
|
||||
},
|
||||
})
|
||||
);
|
||||
child.stdin.write(
|
||||
encodeMessage({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'ImageAnalysis',
|
||||
arguments: { filePath: '../outside.png' },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const responses = await responsesPromise;
|
||||
const toolCall = responses.find((message) => message.id === 2);
|
||||
expect(toolCall?.error).toEqual({
|
||||
code: -32602,
|
||||
message: 'ImageAnalysis only allows files inside the current workspace.',
|
||||
});
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
});
|
||||
|
||||
it('sends PDF files as document blocks to the provider-scoped route', async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-mcp-server-pdf-'));
|
||||
const pdfPath = join(tempDir, 'manual.pdf');
|
||||
@@ -322,6 +387,7 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
});
|
||||
|
||||
const child = spawn('node', [serverPath], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
||||
@@ -377,4 +443,39 @@ describe('ccs-image-analysis MCP server', () => {
|
||||
child.kill();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts legacy Content-Length framed requests for compatibility', async () => {
|
||||
const child = spawn('node', [serverPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
CCS_IMAGE_ANALYSIS_ENABLED: '1',
|
||||
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
||||
CCS_CURRENT_PROVIDER: 'agy',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
try {
|
||||
const responsesPromise = collectResponses(child, 2);
|
||||
child.stdin.write(
|
||||
encodeLegacyMessage({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'bun-test', version: '1.0.0' },
|
||||
},
|
||||
})
|
||||
);
|
||||
child.stdin.write(encodeLegacyMessage({ jsonrpc: '2.0', id: 2, method: 'tools/list' }));
|
||||
|
||||
const responses = await responsesPromise;
|
||||
const toolsList = responses.find((message) => message.id === 2);
|
||||
expect(toolsList?.result).toEqual({ tools: [] });
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user