diff --git a/app/Services/QdrantService.php b/app/Services/QdrantService.php index cdd86d7..66958c2 100644 --- a/app/Services/QdrantService.php +++ b/app/Services/QdrantService.php @@ -787,7 +787,7 @@ private function mapResultToEntry(array $result): array 'score' => $result['score'] ?? 0.0, 'title' => $payload['title'] ?? '', 'content' => $payload['content'] ?? '', - 'tags' => $payload['tags'] ?? [], + 'tags' => $this->normalizeTags($payload['tags'] ?? []), 'category' => $payload['category'] ?? null, 'module' => $payload['module'] ?? null, 'priority' => $payload['priority'] ?? null, @@ -818,7 +818,7 @@ private function mapPointToEntry(array $point): array 'id' => $point['id'], 'title' => $payload['title'] ?? '', 'content' => $payload['content'] ?? '', - 'tags' => $payload['tags'] ?? [], + 'tags' => $this->normalizeTags($payload['tags'] ?? []), 'category' => $payload['category'] ?? null, 'module' => $payload['module'] ?? null, 'priority' => $payload['priority'] ?? null, @@ -973,6 +973,27 @@ private function findByTitleAndCommit(string $title, string $commit, string $pro /** * Get collection name for project namespace. */ + /** + * Normalize tags from Qdrant payload — handles JSON-encoded strings. + * + * @return array + */ + private function normalizeTags(mixed $tags): array + { + if (is_array($tags)) { + return $tags; + } + + if (is_string($tags) && str_starts_with($tags, '[')) { + $decoded = json_decode($tags, true); + if (is_array($decoded)) { + return $decoded; + } + } + + return []; + } + public function getCollectionName(string $project): string { return 'knowledge_'.str_replace(['/', '\\', ' '], '_', $project); diff --git a/tests/Unit/Services/QdrantServiceTest.php b/tests/Unit/Services/QdrantServiceTest.php index 0a1bdc9..637ab18 100644 --- a/tests/Unit/Services/QdrantServiceTest.php +++ b/tests/Unit/Services/QdrantServiceTest.php @@ -1784,3 +1784,32 @@ function mockCollectionExists(Mockery\MockInterface $connector, int $times = 1): expect($results)->toBeEmpty(); }); }); + +describe('normalizeTags', function (): void { + it('passes through normal arrays', function (): void { + $method = new ReflectionMethod($this->service, 'normalizeTags'); + $method->setAccessible(true); + + $result = $method->invoke($this->service, ['rock', 'punk']); + + expect($result)->toBe(['rock', 'punk']); + }); + + it('decodes JSON-encoded string arrays', function (): void { + $method = new ReflectionMethod($this->service, 'normalizeTags'); + $method->setAccessible(true); + + $result = $method->invoke($this->service, '["rock","punk","metal"]'); + + expect($result)->toBe(['rock', 'punk', 'metal']); + }); + + it('returns empty array for non-array non-JSON strings', function (): void { + $method = new ReflectionMethod($this->service, 'normalizeTags'); + $method->setAccessible(true); + + expect($method->invoke($this->service, 'just a string'))->toBe([]) + ->and($method->invoke($this->service, null))->toBe([]) + ->and($method->invoke($this->service, '[not valid json'))->toBe([]); + }); +});