Files
sure/test/models/vector_store/embeddable_test.rb
T
Juan José Mata fd6f4ff078 Add live AI checks to system health (#3155)
* Add live AI checks to system health

Give super admins a dedicated AI status view with bounded liveness probes for LLMs, vector stores, pgvector, and embedding endpoints. Record sanitized failures in both the system debug log and Rails logger, and document the recommended local configuration.\n\nCloses #3145

* Fix AI health CI checks

* Address AI health review feedback

* Correct Ollama model preload guidance

* Distinguish OpenAI-compatible providers

* Make Ollama startup readiness explicit

* Recognize Cloudflare AI endpoints
2026-08-24 22:41:08 +02:00

219 lines
6.8 KiB
Ruby

require "test_helper"
class VectorStore::EmbeddableTest < ActiveSupport::TestCase
class EmbeddableHost
include VectorStore::Embeddable
# Expose private methods for testing
public :extract_text, :chunk_text, :embed, :embed_batch,
:embedding_model, :embedding_dimensions, :embedding_uri_base,
:embedding_access_token
end
setup do
@host = EmbeddableHost.new
end
# --- extract_text ---
test "extract_text returns plain text for .txt files" do
result = @host.extract_text("Hello world", "notes.txt")
assert_equal "Hello world", result
end
test "extract_text returns content for markdown files" do
result = @host.extract_text("# Heading\n\nBody", "readme.md")
assert_equal "# Heading\n\nBody", result
end
test "extract_text returns content for code files" do
result = @host.extract_text("def foo; end", "app.rb")
assert_equal "def foo; end", result
end
test "extract_text returns nil for unsupported binary formats" do
assert_nil @host.extract_text("\x00\x01binary", "photo.png")
assert_nil @host.extract_text("\x00\x01binary", "archive.zip")
end
test "extract_text handles PDF files" do
pdf_content = "fake pdf bytes"
mock_page = mock("page")
mock_page.stubs(:text).returns("Page 1 content")
mock_reader = mock("reader")
mock_reader.stubs(:pages).returns([ mock_page ])
PDF::Reader.expects(:new).with(instance_of(StringIO)).returns(mock_reader)
result = @host.extract_text(pdf_content, "document.pdf")
assert_equal "Page 1 content", result
end
test "extract_text returns nil when PDF extraction fails" do
PDF::Reader.expects(:new).raises(StandardError, "corrupt pdf")
result = @host.extract_text("bad data", "broken.pdf")
assert_nil result
end
# --- chunk_text ---
test "chunk_text returns empty array for blank text" do
assert_equal [], @host.chunk_text("")
assert_equal [], @host.chunk_text(nil)
end
test "chunk_text returns single chunk for short text" do
text = "Short paragraph."
chunks = @host.chunk_text(text)
assert_equal 1, chunks.size
assert_equal "Short paragraph.", chunks.first
end
test "chunk_text splits on paragraph boundaries" do
# Create text that exceeds CHUNK_SIZE when combined
para1 = "A" * 1200
para2 = "B" * 1200
text = "#{para1}\n\n#{para2}"
chunks = @host.chunk_text(text)
assert_equal 2, chunks.size
assert_includes chunks.first, "A" * 1200
assert_includes chunks.last, "B" * 1200
end
test "chunk_text includes overlap between chunks" do
para1 = "A" * 1500
para2 = "B" * 1500
text = "#{para1}\n\n#{para2}"
chunks = @host.chunk_text(text)
assert_equal 2, chunks.size
# Second chunk should start with overlap from end of first chunk
overlap = para1.last(VectorStore::Embeddable::CHUNK_OVERLAP)
assert chunks.last.start_with?(overlap)
end
test "chunk_text keeps small paragraphs together" do
paragraphs = Array.new(5) { |i| "Paragraph #{i} content." }
text = paragraphs.join("\n\n")
chunks = @host.chunk_text(text)
assert_equal 1, chunks.size
end
test "chunk_text hard-splits oversized paragraphs" do
# A single paragraph longer than CHUNK_SIZE with no paragraph breaks
long_para = "X" * 5000
chunks = @host.chunk_text(long_para)
assert chunks.size > 1
chunks.each do |chunk|
assert chunk.length <= VectorStore::Embeddable::CHUNK_SIZE + VectorStore::Embeddable::CHUNK_OVERLAP + 2,
"Chunk too large: #{chunk.length} chars"
end
end
# --- embed ---
test "embed calls embedding endpoint and returns vector" do
expected_vector = [ 0.1, 0.2, 0.3 ]
stub_response = { "data" => [ { "embedding" => expected_vector, "index" => 0 } ] }
mock_client = mock("faraday")
mock_client.expects(:post).with("embeddings").yields(mock_request).returns(
OpenStruct.new(body: stub_response)
)
@host.instance_variable_set(:@embedding_client, mock_client)
result = @host.embed("test text")
assert_equal expected_vector, result
end
test "embed raises on failed response" do
mock_client = mock("faraday")
mock_client.expects(:post).with("embeddings").yields(mock_request).returns(
OpenStruct.new(body: { "error" => "bad request" })
)
@host.instance_variable_set(:@embedding_client, mock_client)
assert_raises(VectorStore::Error) { @host.embed("test text") }
end
test "embedding configuration delegates to the shared runtime configuration" do
VectorStore.expects(:embedding_model).returns("model")
VectorStore.expects(:embedding_dimensions).returns(3)
VectorStore.expects(:embedding_uri_base).returns("https://embeddings.example.test/v1")
VectorStore.expects(:embedding_access_token).returns("token")
assert_equal "model", @host.embedding_model
assert_equal 3, @host.embedding_dimensions
assert_equal "https://embeddings.example.test/v1", @host.embedding_uri_base
assert_equal "token", @host.embedding_access_token
end
# --- embed_batch ---
test "embed_batch processes texts and returns ordered vectors" do
texts = [ "first", "second", "third" ]
vectors = [ [ 0.1 ], [ 0.2 ], [ 0.3 ] ]
stub_response = {
"data" => [
{ "embedding" => vectors[0], "index" => 0 },
{ "embedding" => vectors[1], "index" => 1 },
{ "embedding" => vectors[2], "index" => 2 }
]
}
mock_client = mock("faraday")
mock_client.expects(:post).with("embeddings").yields(mock_request).returns(
OpenStruct.new(body: stub_response)
)
@host.instance_variable_set(:@embedding_client, mock_client)
result = @host.embed_batch(texts)
assert_equal vectors, result
end
test "embed_batch handles multiple batches" do
# Override batch size constant for testing
original = VectorStore::Embeddable::EMBED_BATCH_SIZE
VectorStore::Embeddable.send(:remove_const, :EMBED_BATCH_SIZE)
VectorStore::Embeddable.const_set(:EMBED_BATCH_SIZE, 2)
texts = [ "a", "b", "c" ]
batch1_response = {
"data" => [
{ "embedding" => [ 0.1 ], "index" => 0 },
{ "embedding" => [ 0.2 ], "index" => 1 }
]
}
batch2_response = {
"data" => [
{ "embedding" => [ 0.3 ], "index" => 0 }
]
}
mock_client = mock("faraday")
mock_client.expects(:post).with("embeddings").twice
.yields(mock_request)
.returns(OpenStruct.new(body: batch1_response))
.then.returns(OpenStruct.new(body: batch2_response))
@host.instance_variable_set(:@embedding_client, mock_client)
result = @host.embed_batch(texts)
assert_equal [ [ 0.1 ], [ 0.2 ], [ 0.3 ] ], result
ensure
VectorStore::Embeddable.send(:remove_const, :EMBED_BATCH_SIZE)
VectorStore::Embeddable.const_set(:EMBED_BATCH_SIZE, original)
end
private
def mock_request
request = OpenStruct.new(body: nil)
request
end
end