Codex 5.5 batch lane processed 8 brain Mantis tickets. 4 implemented, 1 stale, 3 deferred. Tickets implemented: - #313 — MCP schemas (BrainRemember/Recall/List): org field maxLength=128 with runtime validation; recall filter.org also bounded; pest test coverage added - #314 — BrainList: removed withCircuitBreaker('brain') from DB-only handler; CircuitBreakerTest updated to assert no breaker call - #315 — BrainService.retryableHttp(): now retries 408 (request-timeout), 429 (rate-limit), and 5xx; honours Retry-After header; focused retry tests added - #326 — BrainService.forget(): dispatches DeleteFromIndex only when row has indexed_at (was unconditional); SupersedeForgetIndexCleanupTest covers never-indexed case Tickets stale-fixed: #316 (RememberKnowledge already rejects missing/deleted supersedes target before dangling retry) Tickets deferred: #121 (cross-surface audit), #311 (retry-inside-breaker architectural redesign), #312 (no authoritative org claim in MCP request context yet) Co-authored-by: Codex <noreply@openai.com> Closes tasks.lthn.sh/view.php?id=313 Closes tasks.lthn.sh/view.php?id=314 Closes tasks.lthn.sh/view.php?id=315 Closes tasks.lthn.sh/view.php?id=326
122 lines
3.8 KiB
PHP
122 lines
3.8 KiB
PHP
<?php
|
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Core\Mcp\Services\CircuitBreaker;
|
|
use Core\Mod\Agentic\Mcp\Tools\Agent\Brain\BrainList;
|
|
use Core\Mod\Agentic\Models\BrainMemory;
|
|
use Core\Mod\Agentic\Services\BrainService;
|
|
use Illuminate\Http\Client\Request;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
function retryableBrainService(): BrainService
|
|
{
|
|
return new class extends BrainService
|
|
{
|
|
public array $sleepCalls = [];
|
|
|
|
protected function sleepMilliseconds(int $milliseconds): void
|
|
{
|
|
$this->sleepCalls[] = $milliseconds;
|
|
}
|
|
};
|
|
}
|
|
|
|
test('CircuitBreaker_brain_list_Good_executes_without_circuit_breaker_for_db_only_query', function (): void {
|
|
$workspace = createWorkspace();
|
|
$breaker = Mockery::mock(CircuitBreaker::class);
|
|
BrainMemory::create([
|
|
'workspace_id' => $workspace->id,
|
|
'agent_id' => 'virgil',
|
|
'type' => 'fact',
|
|
'content' => 'DB-backed list result.',
|
|
'confidence' => 0.8,
|
|
]);
|
|
|
|
$breaker->shouldNotReceive('call');
|
|
|
|
$this->app->instance(CircuitBreaker::class, $breaker);
|
|
|
|
$result = (new BrainList)->handle([], [
|
|
'workspace_id' => $workspace->id,
|
|
]);
|
|
|
|
expect($result['success'])->toBeTrue()
|
|
->and($result['count'])->toBe(1)
|
|
->and($result['memories'][0]['content'])->toBe('DB-backed list result.');
|
|
});
|
|
|
|
test('CircuitBreaker_retryable_http_Bad_retries_qdrant_requests_on_503', function (): void {
|
|
$brain = retryableBrainService();
|
|
|
|
Http::fake([
|
|
'http://localhost:6334/collections/openbrain/points' => Http::sequence()
|
|
->push(['error' => 'unavailable'], 503)
|
|
->push(['result' => ['status' => 'ok']], 200),
|
|
]);
|
|
|
|
$brain->qdrantUpsert([
|
|
['id' => 'memory-1', 'vector' => [0.1, 0.2], 'payload' => ['type' => 'fact']],
|
|
]);
|
|
|
|
expect($brain->sleepCalls)->toBe([100]);
|
|
|
|
Http::assertSentCount(2);
|
|
Http::assertSent(fn (Request $request): bool => $request->url() === 'http://localhost:6334/collections/openbrain/points'
|
|
&& $request->method() === 'PUT');
|
|
});
|
|
|
|
test('CircuitBreaker_retryable_http_Ugly_does_not_retry_qdrant_requests_on_401', function (): void {
|
|
$brain = retryableBrainService();
|
|
|
|
Http::fake([
|
|
'http://localhost:6334/collections/openbrain/points' => Http::sequence()
|
|
->push(['error' => 'unauthorised'], 401)
|
|
->push(['result' => ['status' => 'ok']], 200),
|
|
]);
|
|
|
|
expect(fn () => $brain->qdrantUpsert([
|
|
['id' => 'memory-2', 'vector' => [0.3, 0.4], 'payload' => ['type' => 'fact']],
|
|
]))->toThrow(RuntimeException::class, 'Qdrant upsert failed: 401');
|
|
|
|
expect($brain->sleepCalls)->toBe([]);
|
|
Http::assertSentCount(1);
|
|
});
|
|
|
|
test('CircuitBreaker_retryable_http_retries_qdrant_requests_on_429_using_retry_after_header', function (): void {
|
|
$brain = retryableBrainService();
|
|
|
|
Http::fake([
|
|
'http://localhost:6334/collections/openbrain/points' => Http::sequence()
|
|
->push(['error' => 'rate limited'], 429, ['Retry-After' => '2'])
|
|
->push(['result' => ['status' => 'ok']], 200),
|
|
]);
|
|
|
|
$brain->qdrantUpsert([
|
|
['id' => 'memory-3', 'vector' => [0.5, 0.6], 'payload' => ['type' => 'fact']],
|
|
]);
|
|
|
|
expect($brain->sleepCalls)->toBe([2000]);
|
|
|
|
Http::assertSentCount(2);
|
|
});
|
|
|
|
test('CircuitBreaker_retryable_http_retries_qdrant_requests_on_408', function (): void {
|
|
$brain = retryableBrainService();
|
|
|
|
Http::fake([
|
|
'http://localhost:6334/collections/openbrain/points' => Http::sequence()
|
|
->push(['error' => 'timeout'], 408)
|
|
->push(['result' => ['status' => 'ok']], 200),
|
|
]);
|
|
|
|
$brain->qdrantUpsert([
|
|
['id' => 'memory-4', 'vector' => [0.7, 0.8], 'payload' => ['type' => 'fact']],
|
|
]);
|
|
|
|
expect($brain->sleepCalls)->toBe([100]);
|
|
|
|
Http::assertSentCount(2);
|
|
});
|