debshots/test/controllers/packages_controller_test.rb
Christoph Haas 4948fe50e3 Add semantic package search via pgvector embeddings
Package text search now combines several strategies: exact name match,
name prefix promotion, compound word splitting ("sqlite browser" finds
"sqlitebrowser") and semantic nearest neighbor search over description
embeddings computed by an external embedding service (all-MiniLM-L6-v2,
384 dims) stored with the pgvector extension. Classic PostgreSQL
full-text search remains as fallback when the vector service is
unreachable. Gibberish queries without lexical overlap with the package
data return empty results instead of random matches.

Embeddings can be backfilled with bin/rails debshots:compute_vectors.

Also drops the unused lograge gem from the Gemfile.
2026-08-22 22:07:56 +02:00

161 lines
5.9 KiB
Ruby

# frozen_string_literal: true
require 'test_helper'
class PackagesControllerTest < ActionController::TestCase
test 'should get index and contain dummy package-4' do
get :grid
assert_response :success
assert_select 'div', 'package-4'
end
test 'should get list view with_screenshots and find Firefox' do
# package = Package.create!(name: 'firefox', description: 'A web browser.')
# Screenshot.create!(package: package, approved: true, hidden: false)
# get :list, params: { show: 'with' }
# assert_response :success
# assert_select 'div', 'A web browser.'
get :list, params: { show: 'with' }
assert_response :success
assert_select 'div', 'A program to browse web sites.'
end
test 'exact package name search wins without calling the vector service' do
with_stubbed_method(Package, :nearest_to_text,
->(_text) { flunk 'should not call vector search' }) do
get :grid, params: { search: 'vim' }
assert_response :success
assert_select 'div', 'vim'
end
end
test 'gibberish search shows no results and never calls the vector service' do
with_stubbed_method(Package, :nearest_to_text,
->(_text) { flunk 'should not call vector search' }) do
get :grid, params: { search: 'asdfgh zzzz' }
assert_response :success
# the grid renders every package in the database when no search
# filter applies - so an empty result must not fall back to that
assert_select 'div', text: /package-\d+/, count: 0
end
end
test 'name prefix matches rank above semantic results' do
with_stubbed_method(Package, :nearest_to_text,
->(_text, **_opts) { Package.where(name: packages(:vim).name) }) do
get :grid, params: { search: 'package' }
assert_response :success
# fixtures create package-0..package-99 - prefix hits must render
assert_select 'div', 'package-0'
# ...and before any semantically found package
assert response.body.index('package-0') < response.body.index('vim')
end
end
test 'prefix matches and semantic results are deduplicated' do
with_stubbed_method(Package, :nearest_to_text,
->(_text, **_opts) { Package.where(name: packages(:firefox).name) }) do
get :grid, params: { search: 'fire' }
assert_response :success
assert_select 'div', text: 'firefox', count: 1
end
end
test 'compound words in search match packages with joined names' do
# "fire fox" should find "firefox" via name substring matching
with_stubbed_method(Package, :nearest_to_text,
->(_text, **_opts) { Package.none }) do
get :grid, params: { search: 'fire fox' }
assert_response :success
assert_select 'div', text: 'firefox', count: 1
end
end
test 'search uses semantic (vector) results' do
relation = Package.where(name: packages(:vim).name)
with_stubbed_method(Package, :nearest_to_text, ->(_text, **_opts) { relation }) do
get :grid, params: { search: 'editor' }
assert_response :success
assert_select 'div', 'vim'
end
end
# There is deliberately no fallback for an empty vector search
# result: KNN always returns something as long as packages have
# embeddings. Full-text search is only used when the vector service
# cannot be reached.
test 'search falls back to full-text search when vector service fails' do
with_stubbed_method(Vectorizer, :embed,
->(_text) { raise Vectorizer::Error, 'service down' }) do
get :grid, params: { search: 'editor' }
assert_response :success
assert_select 'div', 'vim'
assert flash[:error].present?
end
end
# test 'should get thumbnail for a package and a desired version' do
# # get '/thumbnail-with-version/package-5/0.1'
# get thumbnail_with_version_url('package-5', '0.1')
# end
# test 'should get without_screenshots' do
# get :without_screenshots
# assert_response :success
# end
# test "should get moderate" do
# get :moderate
# assert_response :success
# end
# test "upload screenshot anonymously then login and get screenshots assigned" do
# newest_screenshot1 = Package.find_by_name(:firefox).screenshots.order(id: :desc).first
# # Load the upload page
# get :upload, params: { name: 'firefox' }
# assert_response :success
# # Upload a screenshot and expect to be redirected afterwards
# post :upload_receive, params: { name: 'firefox', file: [ fixture_file_upload('test/fixtures/files/large1.png','image/png') ] }
# assert_redirected_to package_path(name: 'firefox')
# # Check that the newest screenshot is not yet approved
# newest_screenshot2 = Package.find_by_name(:firefox).screenshots.order(id: :desc).first
# assert_not_equal newest_screenshot1, newest_screenshot2
# assert_equal newest_screenshot2.approved, false
# # Login
# sign_in users(:normal)
# # Get the information about the newest screenshot again
# newest_screenshot2 = Package.find_by_name(:firefox).screenshots.order(id: :desc).first
# # Expect the uploaded screenshot to be assigned to this user
# byebug
# assert_equal newest_screenshot2.user, users(:normal)
# # (name: 'firefox')
# # img_count_before = page.find_all('img').count
# # attach_file 'file[]', Rails.root.join('test/fixtures/files/large1.png')
# # click_on 'Start upload'
# # # The cookie session must remember this anonymous upload
# # newest_screenshot = Package.find_by_name(:firefox).screenshots.order(id: :desc).first
# # assert_equal session[:uploaded_screenshots].length, [newest_screenshot]
# # #
# # # sign_in users(:debian)
# # # # Screenshot is instantly public
# # # newest_screenshot = Package.find_by_name(:firefox).screenshots.order(id: :desc).first
# # # assert_equal newest_screenshot.approved, true
# # # # Clean up
# # newest_screenshot.destroy
# # sign_out users(:normal)
# end
end