Files
context-paging/tests/ContextPagingTest.php
2026-03-28 09:01:07 +00:00

227 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
namespace ContextPaging\Tests;
use ContextPaging\ContextPaging;
use ContextPaging\TokenCounter;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;
class ContextPagingTest extends TestCase
{
private ContextPaging $contextPaging;
protected function setUp(): void
{
$this->contextPaging = new ContextPaging();
}
/**
* Test basic fit with a small payload that doesn't need summarization.
*/
public function testFitWithSmallPayload(): void
{
$messages = [
['role' => 'user', 'content' => 'Hello, how are you?'],
];
$request = $this->createRequest($messages);
$fitted = $this->contextPaging->fit($request);
$this->assertTrue($fitted->getAttribute('context_fitted'));
$fittedMessages = $fitted->getParsedBody()['messages'];
$this->assertCount(1, $fittedMessages);
$this->assertEquals('Hello, how are you?', $fittedMessages[0]['content']);
}
/**
* Test fit with a larger payload that exceeds context limit.
*/
public function testFitWithLargePayloadTriggersSummarization(): void
{
// Set a limit low enough to force summarization but high enough for last message
$this->contextPaging->setMaxContextTokens(100)->setResponseReserve(20);
$messages = [
['role' => 'user', 'content' => str_repeat('This is a long message that should be summarized. ', 50)],
['role' => 'assistant', 'content' => 'I understand your message.'],
['role' => 'user', 'content' => 'Short question'],
];
$request = $this->createRequest($messages);
$fitted = $this->contextPaging->fit($request);
$fittedMessages = $fitted->getParsedBody()['messages'];
// First message should be summarized
$this->assertTrue($fittedMessages[0]['_summarized'] ?? false);
$this->assertStringContainsString('[md5:', $fittedMessages[0]['content']);
// Last message should NOT be summarized
$lastIndex = count($fittedMessages) - 1;
$this->assertFalse($fittedMessages[$lastIndex]['_summarized'] ?? false);
// Should be under budget
$tokens = $fitted->getAttribute('context_tokens');
$this->assertLessThanOrEqual(80, $tokens);
}
/**
* Test execute with no tool calls returns response as-is.
*/
public function testExecuteWithNoToolCalls(): void
{
$messages = [
['role' => 'user', 'content' => 'Hello!'],
];
$request = $this->createRequest($messages);
$response = $this->contextPaging->execute($request, function (array $msgs, $req) {
return new Response(200, [], json_encode(['choices' => [[
'message' => ['role' => 'assistant', 'content' => 'Hi there!'],
]]]));
});
$body = json_decode($response->getBody()->getContents(), true);
$this->assertEquals('Hi there!', $body['choices'][0]['message']['content']);
}
/**
* Test that original messages are stored for dereferencing.
*/
public function testOriginalMessagesStoredForDereferencing(): void
{
$longContent = str_repeat('This is a long message. ', 100);
$messages = [
['role' => 'user', 'content' => $longContent],
['role' => 'user', 'content' => 'Short question'],
];
$this->contextPaging->setMaxContextTokens(100)->setResponseReserve(20);
$request = $this->createRequest($messages);
$fitted = $this->contextPaging->fit($request);
// The MD5 hash in the summarized message should reference the original
$fittedMessages = $fitted->getParsedBody()['messages'];
$this->assertMatchesRegularExpression('/\[md5:([a-f0-9]{32})\]/', $fittedMessages[0]['content']);
// Extract MD5
preg_match('/\[md5:([a-f0-9]{32})\]/', $fittedMessages[0]['content'], $matches);
$md5 = $matches[1];
// Verify the message store has the original
// (We'd need to expose this or use reflection in a real test)
$this->assertNotEmpty($md5);
}
/**
* Test that last message is never summarized.
*/
public function testLastMessageNeverSummarized(): void
{
$this->contextPaging->setMaxContextTokens(80)->setResponseReserve(15);
$messages = [
['role' => 'user', 'content' => 'First message that is quite long and should be summarized'],
['role' => 'user', 'content' => 'Second message that is also quite long'],
['role' => 'user', 'content' => 'Short'],
];
$request = $this->createRequest($messages);
$fitted = $this->contextPaging->fit($request);
$fittedMessages = $fitted->getParsedBody()['messages'];
$lastIndex = count($fittedMessages) - 1;
// Last message must not be summarized
$this->assertFalse($fittedMessages[$lastIndex]['_summarized'] ?? false);
$this->assertEquals('Short', $fittedMessages[$lastIndex]['content']);
}
/**
* Test error when last message itself exceeds context.
*/
public function testErrorWhenLastMessageTooLarge(): void
{
$this->contextPaging->setMaxContextTokens(20)->setResponseReserve(5);
// Single message that's way too big
$messages = [
['role' => 'user', 'content' => str_repeat('This is a massive message that will never fit. ', 100)],
];
$request = $this->createRequest($messages);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('too large');
$this->contextPaging->fit($request);
}
/**
* Test that max_tokens in request is used for budget calculation.
*/
public function testMaxTokensUsedForBudgetCalculation(): void
{
$this->contextPaging->setMaxContextTokens(65536);
$messages = [
['role' => 'user', 'content' => 'Hello'],
];
// Request with max_tokens: 8000
$request = $this->createRequest($messages, ['max_tokens' => 8000]);
$fitted = $this->contextPaging->fit($request);
// Budget should be 65536 - 8000 = 57536
$this->assertEquals(57536, $fitted->getAttribute('context_budget'));
}
/**
* Test fallback to responseReserve when max_tokens not provided.
*/
public function testFallbackToResponseReserveWhenNoMaxTokens(): void
{
$this->contextPaging->setMaxContextTokens(65536)->setResponseReserve(4096);
$messages = [
['role' => 'user', 'content' => 'Hello'],
];
// Request WITHOUT max_tokens
$request = $this->createRequest($messages);
$fitted = $this->contextPaging->fit($request);
// Budget should be 65536 - 4096 = 61440
$this->assertEquals(61440, $fitted->getAttribute('context_budget'));
}
// -----------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------
private function createRequest(array $messages, array $extraBody = []): ServerRequest
{
$body = array_merge(['messages' => $messages], $extraBody);
return new ServerRequest(
method: 'POST',
uri: 'test://localhost',
headers: ['Content-Type' => 'application/json'],
body: json_encode($body),
version: '1.1',
serverParams: []
)->withParsedBody($body);
}
}