require "test_helper" class VectorStore::PgvectorTest < ActiveSupport::TestCase setup do @adapter = VectorStore::Pgvector.new end test "create_store returns a UUID" do response = @adapter.create_store(name: "Test Store") assert response.success? assert_match(/\A[0-9a-f-]{36}\z/, response.data[:id]) end test "delete_store executes delete query" do mock_conn = mock("connection") mock_conn.expects(:exec_delete).with( "DELETE FROM vector_store_chunks WHERE store_id = $1", "VectorStore::Pgvector DeleteStore", anything ).returns(0) @adapter.stubs(:connection).returns(mock_conn) response = @adapter.delete_store(store_id: "store-123") assert response.success? end test "upload_file extracts text, chunks, embeds, and inserts" do file_content = "Hello world" filename = "test.txt" store_id = "store-123" @adapter.expects(:extract_text).with(file_content, filename).returns("Hello world") @adapter.expects(:chunk_text).with("Hello world").returns([ "Hello world" ]) @adapter.expects(:embed_batch).with([ "Hello world" ]).returns([ [ 0.1, 0.2, 0.3 ] ]) mock_conn = mock("connection") mock_conn.expects(:transaction).yields mock_conn.expects(:exec_insert).once @adapter.stubs(:connection).returns(mock_conn) response = @adapter.upload_file(store_id: store_id, file_content: file_content, filename: filename) assert response.success? assert_match(/\A[0-9a-f-]{36}\z/, response.data[:file_id]) end test "upload_file fails when text extraction returns nil" do @adapter.expects(:extract_text).returns(nil) response = @adapter.upload_file(store_id: "store-123", file_content: "\x00binary", filename: "photo.png") assert_not response.success? assert_match(/Could not extract text/, response.error.message) end test "upload_file fails when no chunks produced" do @adapter.expects(:extract_text).returns("some text") @adapter.expects(:chunk_text).returns([]) response = @adapter.upload_file(store_id: "store-123", file_content: "some text", filename: "empty.txt") assert_not response.success? assert_match(/No chunks produced/, response.error.message) end test "upload_file inserts multiple chunks in a transaction" do @adapter.expects(:extract_text).returns("chunk1\n\nchunk2") @adapter.expects(:chunk_text).returns([ "chunk1", "chunk2" ]) @adapter.expects(:embed_batch).returns([ [ 0.1 ], [ 0.2 ] ]) mock_conn = mock("connection") mock_conn.expects(:transaction).yields mock_conn.expects(:exec_insert).twice @adapter.stubs(:connection).returns(mock_conn) response = @adapter.upload_file(store_id: "store-123", file_content: "chunk1\n\nchunk2", filename: "doc.txt") assert response.success? end test "remove_file executes delete with store_id and file_id" do mock_conn = mock("connection") mock_conn.expects(:exec_delete).with( "DELETE FROM vector_store_chunks WHERE store_id = $1 AND file_id = $2", "VectorStore::Pgvector RemoveFile", anything ).returns(1) @adapter.stubs(:connection).returns(mock_conn) response = @adapter.remove_file(store_id: "store-123", file_id: "file-456") assert response.success? end test "search embeds query and returns scored results" do query_vector = [ 0.1, 0.2, 0.3 ] @adapter.expects(:embed).with("income").returns(query_vector) mock_result = [ { "content" => "Total income: $85,000", "filename" => "tax_return.pdf", "file_id" => "file-xyz", "score" => 0.95 } ] mock_conn = mock("connection") mock_conn.expects(:exec_query).returns(mock_result) @adapter.stubs(:connection).returns(mock_conn) response = @adapter.search(store_id: "store-123", query: "income", max_results: 5) assert response.success? assert_equal 1, response.data.size assert_equal "Total income: $85,000", response.data.first[:content] assert_equal "tax_return.pdf", response.data.first[:filename] assert_equal 0.95, response.data.first[:score] assert_equal "file-xyz", response.data.first[:file_id] end test "search returns empty array when no results" do @adapter.expects(:embed).returns([ 0.1 ]) mock_conn = mock("connection") mock_conn.expects(:exec_query).returns([]) @adapter.stubs(:connection).returns(mock_conn) response = @adapter.search(store_id: "store-123", query: "nothing") assert response.success? assert_empty response.data end test "wraps errors in failure response" do @adapter.expects(:extract_text).raises(StandardError, "unexpected error") response = @adapter.upload_file(store_id: "store-123", file_content: "data", filename: "test.txt") assert_not response.success? assert_equal "unexpected error", response.error.message end test "supported_extensions matches extractable formats only" do assert_includes @adapter.supported_extensions, ".pdf" assert_includes @adapter.supported_extensions, ".txt" assert_includes @adapter.supported_extensions, ".csv" assert_not_includes @adapter.supported_extensions, ".png" assert_not_includes @adapter.supported_extensions, ".zip" assert_not_includes @adapter.supported_extensions, ".docx" end end