agent/php/tests/Feature/Brain/RememberValidationTest.php
Snider b7bc526d50 test(agent/brain): regression coverage for filter field bounds (closes #1000)
#1000 was stale-fixed: BrainService::recall() validates filter input
via the shared validator at line 489, which already bounds org,
project, type, agent_id. forget() bounds id at line 499.

These tests pin the safety claim explicitly:
- project=129 chars rejected
- agent_id=65 chars rejected
- project="core" accepted (sanity)
- project=128 chars accepted (boundary)

Note: BrainList.php (separate MCP list path) still lacks explicit
max lengths for project + agent_id — file outside this lane's allow-
list. File a follow-up if that surface needs the same bounds.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1000
2026-04-25 19:23:41 +01:00

193 lines
6.8 KiB
PHP

<?php
// SPDX-License-Identifier: EUPL-1.2
declare(strict_types=1);
use Core\Mod\Agentic\Jobs\EmbedMemory;
use Core\Mod\Agentic\Services\BrainService;
use Illuminate\Http\Client\Request as ClientRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
function rememberValidationBrainService(): BrainService
{
return new BrainService(
ollamaUrl: 'https://ollama.test',
qdrantUrl: 'https://qdrant.test',
collection: 'openbrain',
embeddingModel: 'embeddinggemma',
verifySsl: false,
elasticsearchUrl: 'https://elasticsearch.test',
);
}
function rememberValidationAttributes(array $attributes = []): array
{
return array_merge([
'workspace_id' => $attributes['workspace_id'] ?? createWorkspace()->id,
'agent_id' => 'virgil',
'type' => 'observation',
'content' => 'Brain validation test memory.',
'tags' => ['brain', 'validation'],
'confidence' => 0.9,
'org' => 'core',
'project' => 'agent',
], $attributes);
}
test('BrainRememberValidation_remember_Good_accepts_valid_content_and_tags', function (): void {
Queue::fake();
$memory = rememberValidationBrainService()->remember(rememberValidationAttributes([
'content' => 'hello world',
'tags' => ['a', 'b'],
]));
expect($memory->exists)->toBeTrue()
->and($memory->content)->toBe('hello world')
->and($memory->tags)->toBe(['a', 'b']);
Queue::assertPushed(EmbedMemory::class, fn (EmbedMemory $job): bool => $job->memoryId === $memory->id);
});
test('BrainRememberValidation_remember_Bad_rejects_content_longer_than_65536_bytes', function (): void {
Queue::fake();
expect(fn () => rememberValidationBrainService()->remember(rememberValidationAttributes([
'content' => str_repeat('a', 65537),
])))->toThrow(\InvalidArgumentException::class, 'content exceeds maximum length of 65536 bytes');
Queue::assertNothingPushed();
});
test('BrainRememberValidation_remember_Bad_rejects_more_than_100_tags', function (): void {
Queue::fake();
expect(fn () => rememberValidationBrainService()->remember(rememberValidationAttributes([
'tags' => array_fill(0, 101, 'x'),
])))->toThrow(\InvalidArgumentException::class, 'tags array exceeds maximum size of 100');
Queue::assertNothingPushed();
});
test('BrainRememberValidation_remember_Ugly_rejects_tags_longer_than_128_bytes', function (): void {
Queue::fake();
expect(fn () => rememberValidationBrainService()->remember(rememberValidationAttributes([
'tags' => ['valid', str_repeat('x', 129)],
])))->toThrow(\InvalidArgumentException::class, 'tag at index 1 exceeds maximum length of 128');
Queue::assertNothingPushed();
});
test('BrainRememberValidation_remember_Good_accepts_content_at_exactly_65536_bytes', function (): void {
Queue::fake();
$content = str_repeat('a', 65536);
$memory = rememberValidationBrainService()->remember(rememberValidationAttributes([
'content' => $content,
'tags' => ['boundary'],
]));
expect($memory->exists)->toBeTrue()
->and($memory->content)->toBe($content);
Queue::assertPushed(EmbedMemory::class, fn (EmbedMemory $job): bool => $job->memoryId === $memory->id);
});
test('BrainRememberValidation_recall_Bad_rejects_min_confidence_above_one', function (): void {
expect(fn () => rememberValidationBrainService()->recall(
'brain validation query',
5,
['min_confidence' => 1.1],
createWorkspace()->id,
))->toThrow(\InvalidArgumentException::class, 'min_confidence must be between 0.0 and 1.0');
});
test('BrainRememberValidation_recall_Bad_rejects_project_filters_longer_than_128_characters', function (): void {
Http::fake();
expect(fn () => rememberValidationBrainService()->recall(
'brain validation query',
5,
['project' => str_repeat('x', 129)],
createWorkspace()->id,
))->toThrow(\InvalidArgumentException::class, 'project exceeds maximum length of 128');
Http::assertNothingSent();
});
test('BrainRememberValidation_recall_Bad_rejects_agent_id_filters_longer_than_64_characters', function (): void {
Http::fake();
expect(fn () => rememberValidationBrainService()->recall(
'brain validation query',
5,
['agent_id' => str_repeat('x', 65)],
createWorkspace()->id,
))->toThrow(\InvalidArgumentException::class, 'agent_id exceeds maximum length of 64');
Http::assertNothingSent();
});
test('BrainRememberValidation_recall_Good_accepts_project_filters_within_bounds', function (): void {
$workspace = createWorkspace();
Http::fake([
'https://ollama.test/api/embeddings' => Http::response(['embedding' => array_fill(0, 768, 0.125)]),
'https://qdrant.test/collections/openbrain/points/search' => Http::response(['result' => []]),
]);
$result = rememberValidationBrainService()->recall(
'brain validation query',
5,
['project' => 'core'],
$workspace->id,
);
expect($result)->toBe([
'memories' => [],
'scores' => [],
]);
Http::assertSent(fn (ClientRequest $request): bool => $request->url() === 'https://qdrant.test/collections/openbrain/points/search'
&& $request->method() === 'POST'
&& $request['filter']['must'] === [
['key' => 'workspace_id', 'match' => ['value' => $workspace->id]],
['key' => 'project', 'match' => ['value' => 'core']],
]);
});
test('BrainRememberValidation_recall_Ugly_accepts_project_filters_at_the_128_character_boundary', function (): void {
$workspace = createWorkspace();
$project = str_repeat('x', 128);
Http::fake([
'https://ollama.test/api/embeddings' => Http::response(['embedding' => array_fill(0, 768, 0.125)]),
'https://qdrant.test/collections/openbrain/points/search' => Http::response(['result' => []]),
]);
$result = rememberValidationBrainService()->recall(
'brain validation query',
5,
['project' => $project],
$workspace->id,
);
expect($result)->toBe([
'memories' => [],
'scores' => [],
]);
Http::assertSent(fn (ClientRequest $request): bool => $request->url() === 'https://qdrant.test/collections/openbrain/points/search'
&& $request->method() === 'POST'
&& $request['filter']['must'] === [
['key' => 'workspace_id', 'match' => ['value' => $workspace->id]],
['key' => 'project', 'match' => ['value' => $project]],
]);
});
test('BrainRememberValidation_forget_Bad_rejects_ids_longer_than_64_characters', function (): void {
expect(fn () => rememberValidationBrainService()->forget(str_repeat('x', 65)))
->toThrow(\InvalidArgumentException::class, 'id exceeds maximum length of 64');
});