Organized
This commit is contained in:
@@ -54,7 +54,7 @@ try {
|
||||
assertContains('text/event-stream', $sse['headers'], 'SSE POST returns event stream content type', $assertions);
|
||||
assertContains('X-Accel-Buffering: no', $sse['headers'], 'SSE POST disables proxy buffering', $assertions);
|
||||
assertContains("event: message\n", $sse['body'], 'SSE POST emits a message event', $assertions);
|
||||
assertContains('data: {"jsonrpc":"2.0","id":1,"result":[]}', $sse['body'], 'SSE POST emits JSON-RPC response data', $assertions);
|
||||
assertContains('data: {"jsonrpc":"2.0","id":1,"result":{"items":[]}}', $sse['body'], 'SSE POST emits JSON-RPC response data', $assertions);
|
||||
|
||||
$json = httpRequest(
|
||||
'POST',
|
||||
|
||||
@@ -19,6 +19,9 @@ final class RecordingClient implements Client, HttpClient
|
||||
/** @var array<int,array{method:string,path:string,content_type:string,content:string}> */
|
||||
public array $requests = [];
|
||||
|
||||
/** @var array<int,string> */
|
||||
public array $impersonationEvents = [];
|
||||
|
||||
/** @var array<int,Response> */
|
||||
private array $responses = [];
|
||||
|
||||
@@ -54,8 +57,15 @@ final class RecordingClient implements Client, HttpClient
|
||||
throw new RuntimeException('Unexpected vendor API call for ' . $name);
|
||||
}
|
||||
|
||||
public function startImpersonateUser(string $username): void {}
|
||||
public function stopImpersonateUser(): void {}
|
||||
public function startImpersonateUser(string $username): void
|
||||
{
|
||||
$this->impersonationEvents[] = 'start:' . $username;
|
||||
}
|
||||
|
||||
public function stopImpersonateUser(): void
|
||||
{
|
||||
$this->impersonationEvents[] = 'stop';
|
||||
}
|
||||
public function requestGet(string $path): bool { return false; }
|
||||
public function requestPost(string $path, string $body): bool { return false; }
|
||||
public function requestPut(string $path, string $body): bool { return false; }
|
||||
@@ -82,6 +92,8 @@ final class RedmineStructureTest
|
||||
$this->testMcpGetProjectShowsHelpfulErrorForAmbiguousHumanName();
|
||||
$this->testMcpSearchSanitizesNoisyTextFields();
|
||||
$this->testMcpSearchCanDisableTextSanitization();
|
||||
$this->testMcpToolCallSupportsImpersonationAndOmitsArgumentFromPayload();
|
||||
$this->testMcpToolCallStopsImpersonationOnFailure();
|
||||
$this->testCreateRelationDefaultsToRelatesAndRequiresTarget();
|
||||
$this->testAttachmentUploadSupportsPathAndBase64();
|
||||
$this->testAttachmentUploadAcceptsPdfDataUrl();
|
||||
@@ -347,6 +359,49 @@ final class RedmineStructureTest
|
||||
);
|
||||
}
|
||||
|
||||
private function testMcpToolCallSupportsImpersonationAndOmitsArgumentFromPayload(): void
|
||||
{
|
||||
$http = new RecordingClient();
|
||||
$http->queueJson(['issue' => ['id' => 90210]], 201);
|
||||
$dispatcher = new McpDispatcher(new RedmineClient($http));
|
||||
|
||||
$result = $this->callToolJson($dispatcher, 'redmine_create_issue', [
|
||||
'impersonate_user' => 'jason',
|
||||
'project_id' => 'quality-tracker',
|
||||
'subject' => 'Impersonation create smoke',
|
||||
'description' => 'created via MCP test',
|
||||
]);
|
||||
|
||||
$payload = $this->json($http->requests[0]['content']);
|
||||
$this->assertSame(90210, $result['id'], 'impersonated create issue returns created issue payload');
|
||||
$this->assertSame(['start:jason', 'stop'], $http->impersonationEvents, 'impersonation wraps full tool call');
|
||||
$this->assertSame(false, isset($payload['issue']['impersonate_user']), 'impersonate_user is not forwarded as issue field');
|
||||
}
|
||||
|
||||
private function testMcpToolCallStopsImpersonationOnFailure(): void
|
||||
{
|
||||
$http = new RecordingClient();
|
||||
$http->queueJson(['errors' => ['forbidden']], 500);
|
||||
$dispatcher = new McpDispatcher(new RedmineClient($http));
|
||||
|
||||
$response = $dispatcher->handleMessage([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => 1,
|
||||
'method' => 'tools/call',
|
||||
'params' => [
|
||||
'name' => 'redmine_create_issue',
|
||||
'arguments' => [
|
||||
'impersonate_user' => 'jason',
|
||||
'project_id' => 'quality-tracker',
|
||||
'subject' => 'Will fail intentionally',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame(true, is_array($response) && isset($response['error']), 'failed tool call returns MCP error response');
|
||||
$this->assertSame(['start:jason', 'stop'], $http->impersonationEvents, 'impersonation is cleared when tool call fails');
|
||||
}
|
||||
|
||||
private function testAttachmentUploadSupportsPathAndBase64(): void
|
||||
{
|
||||
$path = sys_get_temp_dir() . '/redmcp-upload-test.txt';
|
||||
@@ -507,6 +562,7 @@ final class RedmineStructureTest
|
||||
}
|
||||
$projectDescription = (string) $createIssueTool['inputSchema']['properties']['project_id']['description'];
|
||||
$this->assertStringContains('redmine_find_project', $projectDescription, 'project_id schema points agents to project resolver');
|
||||
$this->assertSame(true, isset($createIssueTool['inputSchema']['properties']['impersonate_user']), 'tool schema advertises impersonate_user');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user