Organized

This commit is contained in:
Jason Thistlethwaite
2026-10-17 20:50:35 -04:00
parent bd26c8894f
commit 9ee9eb3ec6
130 changed files with 24172 additions and 140 deletions
+1 -1
View File
@@ -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',
+58 -2
View File
@@ -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');
}
/**