Files
context-paging/tests/RedisCacheTest.php
biondizzle baad4a271a Remove hardcoded Redis URL from tests
- Read REDIS_URL from environment
- Skip tests if not set
2026-03-28 09:43:34 +00:00

262 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
namespace ContextPaging\Tests;
use ContextPaging\CacheInterface;
use ContextPaging\ContextPaging;
use ContextPaging\InMemoryCache;
use ContextPaging\LLMSummarizer;
use ContextPaging\OpenAICompatibleClient;
use ContextPaging\RedisCache;
use ContextPaging\TokenCounter;
use ContextPaging\ToolCallMode;
use GuzzleHttp\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;
/**
* Tests for Redis-backed caching.
*
* Requires REDIS_URL environment variable. Tests will skip if not set.
*/
class RedisCacheTest extends TestCase
{
private CacheInterface $redisCache;
private ?string $redisUrl = null;
protected function setUp(): void
{
$this->redisUrl = getenv('REDIS_URL') ?: null;
if ($this->redisUrl === null) {
$this->markTestSkipped('REDIS_URL not set, skipping Redis tests');
}
$this->redisCache = RedisCache::fromUrl($this->redisUrl, 'test_ctx:');
}
protected function tearDown(): void
{
// Clean up test keys
$this->redisCache->clear();
}
public function testSetAndGet(): void
{
$this->redisCache->set('foo', ['bar' => 'baz']);
$value = $this->redisCache->get('foo');
$this->assertIsArray($value);
$this->assertEquals('baz', $value['bar']);
}
public function testHasReturnsTrueForExistingKey(): void
{
$this->redisCache->set('exists', 'value');
$this->assertTrue($this->redisCache->has('exists'));
$this->assertFalse($this->redisCache->has('nonexistent'));
}
public function testDelete(): void
{
$this->redisCache->set('to_delete', 'value');
$this->assertTrue($this->redisCache->has('to_delete'));
$this->redisCache->delete('to_delete');
$this->assertFalse($this->redisCache->has('to_delete'));
}
public function testGetReturnsNullForMissingKey(): void
{
$this->assertNull($this->redisCache->get('missing_key'));
}
public function testTtl(): void
{
// Set with 1 second TTL
$this->redisCache->set('expires_soon', 'value', 1);
$this->assertTrue($this->redisCache->has('expires_soon'));
$this->assertEquals('value', $this->redisCache->get('expires_soon'));
// Wait for expiry
sleep(2);
$this->assertFalse($this->redisCache->has('expires_soon'));
}
public function testContextPagingWithRedisCache(): void
{
$messageStore = RedisCache::fromUrl($this->redisUrl, 'test_msg:');
$summaryCache = RedisCache::fromUrl($this->redisUrl, 'test_sum:');
$contextPaging = new ContextPaging(
tokenCounter: new TokenCounter(),
messageStore: $messageStore,
summaryCache: $summaryCache
);
$contextPaging->setMaxContextTokens(100)->setResponseReserve(20);
$longContent = str_repeat('This is a long message that will be summarized. ', 30);
$md5 = md5($longContent);
$messages = [
['role' => 'user', 'content' => $longContent],
['role' => 'user', 'content' => 'Short question'],
];
$request = $this->createRequest($messages);
$fitted = $contextPaging->fit($request);
// Verify message was stored in Redis
$storedMessage = $messageStore->get("msg:{$md5}");
$this->assertNotNull($storedMessage, 'Message should be stored in Redis');
$this->assertEquals($longContent, $storedMessage['content']);
// Verify summary was cached
$summaryKey = "summary:{$md5}";
$this->assertTrue($summaryCache->has($summaryKey), 'Summary should be cached');
// Clean up
$messageStore->clear();
$summaryCache->clear();
}
public function testSummaryPersistsBetweenRequests(): void
{
$messageStore = RedisCache::fromUrl($this->redisUrl, 'test_msg2:');
$summaryCache = RedisCache::fromUrl($this->redisUrl, 'test_sum2:');
$longContent = str_repeat('Persist test content. ', 50);
$md5 = md5($longContent);
// First request: create summary
$contextPaging1 = new ContextPaging(
tokenCounter: new TokenCounter(),
messageStore: $messageStore,
summaryCache: $summaryCache
);
$contextPaging1->setMaxContextTokens(100)->setResponseReserve(20);
$request1 = $this->createRequest([
['role' => 'user', 'content' => $longContent],
['role' => 'user', 'content' => 'Short'],
]);
$fitted1 = $contextPaging1->fit($request1);
$fittedMessages1 = $fitted1->getParsedBody()['messages'];
// Get the summary from cache
$cachedSummary = $summaryCache->get("summary:{$md5}");
// Second request: should use cached summary
$contextPaging2 = new ContextPaging(
tokenCounter: new TokenCounter(),
messageStore: $messageStore,
summaryCache: $summaryCache
);
$contextPaging2->setMaxContextTokens(100)->setResponseReserve(20);
$request2 = $this->createRequest([
['role' => 'user', 'content' => $longContent],
['role' => 'user', 'content' => 'Short'],
]);
$fitted2 = $contextPaging2->fit($request2);
$fittedMessages2 = $fitted2->getParsedBody()['messages'];
// Summaries should be identical (from cache)
$this->assertEquals(
$fittedMessages1[0]['content'],
$fittedMessages2[0]['content'],
'Summary should be identical from cache'
);
// Clean up
$messageStore->clear();
$summaryCache->clear();
}
public function testInMemoryVsRedisParity(): void
{
$content = 'Test content for parity check';
$md5 = md5($content);
// In-memory
$inMemory = new InMemoryCache();
$inMemory->set("msg:{$md5}", ['role' => 'user', 'content' => $content]);
// Redis
$redis = RedisCache::fromUrl($this->redisUrl, 'test_parity:');
$redis->set("msg:{$md5}", ['role' => 'user', 'content' => $content]);
// Both should return same data
$this->assertEquals(
$inMemory->get("msg:{$md5}"),
$redis->get("msg:{$md5}"),
'In-memory and Redis should return same data'
);
$redis->clear();
}
public function testMessageStorePersistsAcrossInstances(): void
{
$messageStore = RedisCache::fromUrl($this->redisUrl, 'test_msg3:');
$summaryCache = RedisCache::fromUrl($this->redisUrl, 'test_sum3:');
$longContent = str_repeat('Cross-instance test. ', 40);
$md5 = md5($longContent);
// First instance: fit and store
$instance1 = new ContextPaging(
tokenCounter: new TokenCounter(),
messageStore: $messageStore,
summaryCache: $summaryCache
);
$instance1->setMaxContextTokens(100)->setResponseReserve(20);
$request = $this->createRequest([
['role' => 'user', 'content' => $longContent],
['role' => 'user', 'content' => 'Query'],
]);
$fitted = $instance1->fit($request);
// Second instance: should be able to dereference from shared Redis
$instance2 = new ContextPaging(
tokenCounter: new TokenCounter(),
messageStore: $messageStore,
summaryCache: $summaryCache
);
// Access the message store directly
$retrievedMessage = $instance2->getMessageStore()->get("msg:{$md5}");
$this->assertNotNull($retrievedMessage, 'Second instance should see stored message');
$this->assertEquals($longContent, $retrievedMessage['content']);
// Clean up
$messageStore->clear();
$summaryCache->clear();
}
private function createRequest(array $messages): ServerRequest
{
$body = ['messages' => $messages];
return new ServerRequest(
method: 'POST',
uri: 'test://localhost',
headers: ['Content-Type' => 'application/json'],
body: json_encode($body),
version: '1.1',
serverParams: []
)->withParsedBody($body);
}
}