Add friendly redMCP query options
This commit is contained in:
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use RedMCP\ListQueryNormalizer;
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
final class QueryNormalizerTest
|
||||
{
|
||||
private int $assertions = 0;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->testDefaultPagingAndRawOverride();
|
||||
$this->testIssueFriendlyFilters();
|
||||
$this->testSortShortcutsAndStructuredSort();
|
||||
$this->testDatePresetsAndRanges();
|
||||
$this->testUserParams();
|
||||
$this->testSearchParams();
|
||||
|
||||
fwrite(STDOUT, "OK {$this->assertions} assertions\n");
|
||||
}
|
||||
|
||||
private function testDefaultPagingAndRawOverride(): void
|
||||
{
|
||||
$params = ListQueryNormalizer::listParams([
|
||||
'limit' => 500,
|
||||
'page' => 3,
|
||||
'params' => ['limit' => 75],
|
||||
]);
|
||||
|
||||
$this->assertSame(75, $params['limit'], 'raw params override normalized limit');
|
||||
$this->assertSame(200, $params['offset'], 'page offset uses clamped normalized limit before raw override');
|
||||
}
|
||||
|
||||
private function testIssueFriendlyFilters(): void
|
||||
{
|
||||
$filters = ListQueryNormalizer::issueFilters([
|
||||
'project_id' => 'customer-service',
|
||||
'status' => 'open',
|
||||
'assigned_to_id' => 25,
|
||||
'updated' => 'since 2026-04-01',
|
||||
'filters' => ['assigned_to_id' => 'me'],
|
||||
]);
|
||||
|
||||
$this->assertSame('customer-service', $filters['project_id'], 'project_id is copied');
|
||||
$this->assertSame('open', $filters['status_id'], 'open status alias maps to Redmine open status');
|
||||
$this->assertSame('me', $filters['assigned_to_id'], 'raw filters override normalized filters');
|
||||
$this->assertSame('>=2026-04-01', $filters['updated_on'], 'since date maps to Redmine lower bound');
|
||||
}
|
||||
|
||||
private function testSortShortcutsAndStructuredSort(): void
|
||||
{
|
||||
$newest = ListQueryNormalizer::listParams(['sort' => 'newest']);
|
||||
$this->assertSame('updated_on:desc', $newest['sort'], 'newest shortcut sorts by updated_on descending');
|
||||
|
||||
$priority = ListQueryNormalizer::listParams(['sort' => 'priority']);
|
||||
$this->assertSame('priority:desc,updated_on:desc', $priority['sort'], 'priority shortcut includes updated_on tie-breaker');
|
||||
|
||||
$structured = ListQueryNormalizer::listParams([
|
||||
'sort' => [
|
||||
['field' => 'created_on', 'direction' => 'desc'],
|
||||
['field' => 'id', 'direction' => 'asc'],
|
||||
],
|
||||
]);
|
||||
$this->assertSame('created_on:desc,id:asc', $structured['sort'], 'structured sort converts to Redmine sort string');
|
||||
}
|
||||
|
||||
private function testDatePresetsAndRanges(): void
|
||||
{
|
||||
$clock = new DateTimeImmutable('2026-04-25 12:00:00', new DateTimeZone('UTC'));
|
||||
|
||||
$today = ListQueryNormalizer::issueFilters(['created' => 'today'], $clock);
|
||||
$this->assertSame('2026-04-25', $today['created_on'], 'today maps to exact date');
|
||||
|
||||
$range = ListQueryNormalizer::issueFilters(['created' => '2026-04-01..2026-04-25'], $clock);
|
||||
$this->assertSame('><2026-04-01|2026-04-25', $range['created_on'], 'range string maps to Redmine between syntax');
|
||||
|
||||
$objectRange = ListQueryNormalizer::issueFilters(['due' => ['from' => '2026-05-01', 'to' => '2026-05-31']], $clock);
|
||||
$this->assertSame('><2026-05-01|2026-05-31', $objectRange['due_date'], 'object range maps to due_date');
|
||||
|
||||
$lastSeven = ListQueryNormalizer::issueFilters(['updated' => 'last 7 days'], $clock);
|
||||
$this->assertSame('><2026-04-19|2026-04-25', $lastSeven['updated_on'], 'last N days includes today');
|
||||
|
||||
$freeText = ListQueryNormalizer::issueFilters(['created' => 'April 2 2026'], $clock);
|
||||
$this->assertSame('2026-04-02', $freeText['created_on'], 'simple free text date parses');
|
||||
}
|
||||
|
||||
private function testSearchParams(): void
|
||||
{
|
||||
$params = ListQueryNormalizer::searchParams([
|
||||
'project_id' => 'customer-service',
|
||||
'all_words' => true,
|
||||
'titles_only' => false,
|
||||
'open_issues' => true,
|
||||
'sort' => 'oldest',
|
||||
'page' => 2,
|
||||
'params' => ['offset' => 5],
|
||||
]);
|
||||
|
||||
$this->assertSame('customer-service', $params['project_id'], 'search project_id is copied');
|
||||
$this->assertSame('1', $params['all_words'], 'true search flags map to Redmine 1');
|
||||
$this->assertSame('0', $params['titles_only'], 'false search flags map to Redmine 0');
|
||||
$this->assertSame('1', $params['open_issues'], 'open_issues flag maps to Redmine 1');
|
||||
$this->assertSame('created_on:asc', $params['sort'], 'oldest shortcut maps to created_on ascending');
|
||||
$this->assertSame(5, $params['offset'], 'raw search params override normalized paging');
|
||||
}
|
||||
|
||||
private function testUserParams(): void
|
||||
{
|
||||
$params = ListQueryNormalizer::userParams([
|
||||
'status' => 'active',
|
||||
'name' => 'danny',
|
||||
'group_id' => 4,
|
||||
'sort' => 'oldest',
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $params['status'], 'active user status maps to Redmine active status id');
|
||||
$this->assertSame('danny', $params['name'], 'user name filter is copied');
|
||||
$this->assertSame(4, $params['group_id'], 'user group filter is copied');
|
||||
$this->assertSame('created_on:asc', $params['sort'], 'user list accepts sort shortcuts');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $expected
|
||||
* @param mixed $actual
|
||||
*/
|
||||
private function assertSame($expected, $actual, string $message): void
|
||||
{
|
||||
$this->assertions++;
|
||||
if ($expected === $actual) {
|
||||
return;
|
||||
}
|
||||
|
||||
fwrite(STDERR, "FAIL: {$message}\nExpected: " . var_export($expected, true) . "\nActual: " . var_export($actual, true) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
(new QueryNormalizerTest())->run();
|
||||
Reference in New Issue
Block a user