glancer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +96 -0
- data/.rubocop.yml +54 -0
- data/CHANGELOG.md +88 -0
- data/CLAUDE.md +115 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +354 -0
- data/app/assets/config/glancer_manifest.js +1 -0
- data/app/assets/javascripts/glancer/application.js +15 -0
- data/app/assets/javascripts/glancer/controllers/chat_controller.js +101 -0
- data/app/assets/javascripts/glancer/controllers/message_controller.js +1052 -0
- data/app/assets/javascripts/glancer/controllers/toast_controller.js +63 -0
- data/app/assets/stylesheets/glancer/application.css +350 -0
- data/app/assets/stylesheets/glancer/code-blocks.css +6 -0
- data/app/assets/stylesheets/glancer/list.css +31 -0
- data/app/assets/stylesheets/glancer/scrollbar.css +16 -0
- data/app/assets/stylesheets/glancer/table.css +97 -0
- data/app/controllers/glancer/application_controller.rb +33 -0
- data/app/controllers/glancer/chats_controller.rb +49 -0
- data/app/controllers/glancer/messages_controller.rb +144 -0
- data/app/controllers/glancer/schema_controller.rb +29 -0
- data/app/controllers/glancer/settings_controller.rb +23 -0
- data/app/helpers/glancer/application_helper.rb +17 -0
- data/app/jobs/glancer/application_job.rb +6 -0
- data/app/jobs/glancer/process_message_job.rb +38 -0
- data/app/models/glancer/audit.rb +12 -0
- data/app/models/glancer/chat.rb +8 -0
- data/app/models/glancer/code_version.rb +12 -0
- data/app/models/glancer/embedding.rb +6 -0
- data/app/models/glancer/message.rb +25 -0
- data/app/models/glancer/setting.rb +23 -0
- data/app/models/glancer/sql_version.rb +6 -0
- data/app/views/glancer/_data/_importmap.json.erb +7 -0
- data/app/views/glancer/chats/_chat_sidebar.html.erb +2 -0
- data/app/views/glancer/chats/_show.html.erb +52 -0
- data/app/views/glancer/chats/_sidebar_chat_list.html.erb +30 -0
- data/app/views/glancer/chats/index.html.erb +10 -0
- data/app/views/glancer/chats/show.html.erb +1 -0
- data/app/views/glancer/messages/_data_table.html.erb +268 -0
- data/app/views/glancer/messages/_execution_error.html.erb +26 -0
- data/app/views/glancer/messages/_form.html.erb +93 -0
- data/app/views/glancer/messages/_message.html.erb +206 -0
- data/app/views/glancer/messages/_message_info.html.erb +176 -0
- data/app/views/glancer/messages/_temp_form.html.erb +100 -0
- data/app/views/glancer/messages/create.turbo_stream.erb +25 -0
- data/app/views/glancer/schema/show.html.erb +123 -0
- data/app/views/glancer/settings/show.html.erb +306 -0
- data/app/views/glancer/shared/_icons.html.erb +126 -0
- data/app/views/layouts/glancer/application.html.erb +234 -0
- data/config/locales/glancer.en.yml +90 -0
- data/config/locales/glancer.es.yml +90 -0
- data/config/locales/glancer.pt-BR.yml +90 -0
- data/config/routes.rb +20 -0
- data/db/migrate/20250629212642_create_glancer_audits.rb +19 -0
- data/db/migrate/20250629212643_create_glancer_chats.rb +10 -0
- data/db/migrate/20250629212645_create_glancer_embeddings.rb +17 -0
- data/db/migrate/20250629212647_create_glancer_messages.rb +29 -0
- data/db/migrate/20260513204129_add_user_edited_sql_to_glancer_messages.rb +11 -0
- data/db/migrate/20260513210647_create_glancer_sql_versions.rb +18 -0
- data/db/migrate/20260513210648_add_message_id_to_glancer_audits.rb +8 -0
- data/db/migrate/20260513220000_create_glancer_settings.rb +12 -0
- data/db/migrate/20260514083509_add_llm_model_to_glancer_messages.rb +9 -0
- data/db/migrate/20260523120000_rename_code_columns_in_glancer_messages.rb +8 -0
- data/db/migrate/20260523120001_rename_code_column_in_glancer_audits.rb +7 -0
- data/db/migrate/20260523120002_add_code_type_to_glancer_tables.rb +10 -0
- data/db/migrate/20260523120003_rename_glancer_sql_versions_to_code_versions.rb +8 -0
- data/db/migrate/20260523130000_add_enriched_question_to_glancer_messages.rb +7 -0
- data/db/migrate/20260524100000_add_status_to_glancer_messages.rb +9 -0
- data/lib/generators/glancer/install/install_generator.rb +74 -0
- data/lib/generators/glancer/install/templates/glancer.rb +227 -0
- data/lib/generators/glancer/install/templates/llm_context.glancer.md +51 -0
- data/lib/glancer/async_runner.rb +50 -0
- data/lib/glancer/chart_analyzer.rb +230 -0
- data/lib/glancer/configuration.rb +372 -0
- data/lib/glancer/engine.rb +90 -0
- data/lib/glancer/indexer/context_indexer.rb +58 -0
- data/lib/glancer/indexer/model_indexer.rb +64 -0
- data/lib/glancer/indexer/schema_indexer.rb +171 -0
- data/lib/glancer/indexer.rb +50 -0
- data/lib/glancer/retriever.rb +114 -0
- data/lib/glancer/utils/logger.rb +83 -0
- data/lib/glancer/utils/markdown_helper.rb +56 -0
- data/lib/glancer/utils/result_formatter.rb +25 -0
- data/lib/glancer/utils/table_stats.rb +18 -0
- data/lib/glancer/utils/transaction.rb +59 -0
- data/lib/glancer/version.rb +5 -0
- data/lib/glancer/workflow/ar_executor.rb +104 -0
- data/lib/glancer/workflow/ar_extractor.rb +25 -0
- data/lib/glancer/workflow/ar_prompt_builder.rb +64 -0
- data/lib/glancer/workflow/ar_sanitizer.rb +88 -0
- data/lib/glancer/workflow/builder.rb +129 -0
- data/lib/glancer/workflow/cache.rb +55 -0
- data/lib/glancer/workflow/executor.rb +72 -0
- data/lib/glancer/workflow/llm.rb +123 -0
- data/lib/glancer/workflow/prompt_builder.rb +143 -0
- data/lib/glancer/workflow/query_enricher.rb +117 -0
- data/lib/glancer/workflow/sql_extractor.rb +42 -0
- data/lib/glancer/workflow/sql_sanitizer.rb +42 -0
- data/lib/glancer/workflow/sql_validator.rb +67 -0
- data/lib/glancer/workflow.rb +158 -0
- data/lib/glancer.rb +50 -0
- data/lib/tasks/glancer/tailwind.rake +8 -0
- data/lib/tasks/glancer.rake +99 -0
- data/spec/glancer_spec.rb +62 -0
- data/spec/lib/glancer/async_runner_spec.rb +133 -0
- data/spec/lib/glancer/chart_analyzer_spec.rb +296 -0
- data/spec/lib/glancer/configuration_spec.rb +858 -0
- data/spec/lib/glancer/engine_spec.rb +209 -0
- data/spec/lib/glancer/indexer/context_indexer_spec.rb +96 -0
- data/spec/lib/glancer/indexer/model_indexer_spec.rb +103 -0
- data/spec/lib/glancer/indexer/schema_indexer_spec.rb +382 -0
- data/spec/lib/glancer/indexer_spec.rb +95 -0
- data/spec/lib/glancer/retriever_spec.rb +179 -0
- data/spec/lib/glancer/utils/logger_spec.rb +85 -0
- data/spec/lib/glancer/utils/markdown_helper_spec.rb +92 -0
- data/spec/lib/glancer/utils/result_formatter_spec.rb +73 -0
- data/spec/lib/glancer/utils/table_stats_spec.rb +34 -0
- data/spec/lib/glancer/utils/transaction_spec.rb +73 -0
- data/spec/lib/glancer/workflow/ar_executor_spec.rb +155 -0
- data/spec/lib/glancer/workflow/ar_extractor_spec.rb +50 -0
- data/spec/lib/glancer/workflow/ar_prompt_builder_spec.rb +79 -0
- data/spec/lib/glancer/workflow/ar_sanitizer_spec.rb +175 -0
- data/spec/lib/glancer/workflow/builder_spec.rb +204 -0
- data/spec/lib/glancer/workflow/cache_spec.rb +142 -0
- data/spec/lib/glancer/workflow/executor_spec.rb +149 -0
- data/spec/lib/glancer/workflow/llm_spec.rb +124 -0
- data/spec/lib/glancer/workflow/prompt_builder_spec.rb +196 -0
- data/spec/lib/glancer/workflow/query_enricher_spec.rb +184 -0
- data/spec/lib/glancer/workflow/sql_extractor_spec.rb +82 -0
- data/spec/lib/glancer/workflow/sql_sanitizer_spec.rb +98 -0
- data/spec/lib/glancer/workflow/sql_validator_spec.rb +166 -0
- data/spec/lib/glancer/workflow_spec.rb +308 -0
- data/spec/models/glancer/audit_spec.rb +82 -0
- data/spec/models/glancer/chat_spec.rb +60 -0
- data/spec/models/glancer/code_version_spec.rb +71 -0
- data/spec/models/glancer/embedding_spec.rb +73 -0
- data/spec/models/glancer/message_spec.rb +144 -0
- data/spec/models/glancer/setting_spec.rb +88 -0
- data/spec/models/glancer/sql_version_spec.rb +4 -0
- data/spec/spec_helper.rb +128 -0
- data/spec/support/schema.rb +55 -0
- metadata +255 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::Configuration do
|
|
6
|
+
subject(:config) { described_class.new }
|
|
7
|
+
|
|
8
|
+
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
describe "constants" do
|
|
11
|
+
it "defines ADAPTERS_SUPPORTED" do
|
|
12
|
+
expect(described_class::ADAPTERS_SUPPORTED).to contain_exactly(:postgres, :mysql, :mysql2, :sqlite)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "defines LLM_PROVIDERS" do
|
|
16
|
+
expect(described_class::LLM_PROVIDERS).to contain_exactly(:gemini, :openai, :openrouter)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "defines LOG_VERBOSITY_LEVELS" do
|
|
20
|
+
expect(described_class::LOG_VERBOSITY_LEVELS).to contain_exactly(:silent, :none, :info, :debug)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "defines EMBEDDING_DEFAULTS for each LLM provider" do
|
|
24
|
+
expect(described_class::EMBEDDING_DEFAULTS.keys).to contain_exactly(:gemini, :openai, :openrouter)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "defines QUERY_MODES" do
|
|
28
|
+
expect(described_class::QUERY_MODES).to contain_exactly(:sql, :activerecord)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ── Default values ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
describe "default values" do
|
|
35
|
+
it "sets llm_provider to :gemini" do
|
|
36
|
+
expect(config.llm_provider).to eq(:gemini)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "sets llm_model to gemini-2.0-flash" do
|
|
40
|
+
expect(config.llm_model).to eq("gemini-2.0-flash")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "sets schema_permission to false" do
|
|
44
|
+
expect(config.schema_permission).to be(false)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "sets models_permission to false" do
|
|
48
|
+
expect(config.models_permission).to be(false)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "sets log_verbosity to :info" do
|
|
52
|
+
expect(config.log_verbosity).to eq(:info)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "sets k to 5" do
|
|
56
|
+
expect(config.k).to eq(5)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "sets min_score to 0.6" do
|
|
60
|
+
expect(config.min_score).to eq(0.6)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "sets schema_documents_weight to 1.3" do
|
|
64
|
+
expect(config.schema_documents_weight).to eq(1.3)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "sets context_documents_weight to 1.2" do
|
|
68
|
+
expect(config.context_documents_weight).to eq(1.2)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "sets models_documents_weight to 1.1" do
|
|
72
|
+
expect(config.models_documents_weight).to eq(1.1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "sets chunk_size to 1000" do
|
|
76
|
+
expect(config.chunk_size).to eq(1000)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "sets chunk_overlap to 150" do
|
|
80
|
+
expect(config.chunk_overlap).to eq(150)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "sets history_limit to 6" do
|
|
84
|
+
expect(config.history_limit).to eq(6)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "sets embedding_provider to nil" do
|
|
88
|
+
expect(config.embedding_provider).to be_nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "sets embedding_model to nil" do
|
|
92
|
+
expect(config.embedding_model).to be_nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "sets code_provider to nil" do
|
|
96
|
+
expect(config.code_provider).to be_nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "sets code_model to nil" do
|
|
100
|
+
expect(config.code_model).to be_nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "sets chat_provider to nil" do
|
|
104
|
+
expect(config.chat_provider).to be_nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "sets chat_model to nil" do
|
|
108
|
+
expect(config.chat_model).to be_nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "sets blazer_path to nil" do
|
|
112
|
+
expect(config.blazer_path).to be_nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "sets query_mode to :sql" do
|
|
116
|
+
expect(config.query_mode).to eq(:sql)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "sets context_file_path to a string" do
|
|
120
|
+
expect(config.context_file_path).to be_a(String)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "sets api_key to nil" do
|
|
124
|
+
expect(config.api_key).to be_nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "sets gemini_api_key to nil" do
|
|
128
|
+
expect(config.gemini_api_key).to be_nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "sets openai_api_key to nil" do
|
|
132
|
+
expect(config.openai_api_key).to be_nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "sets openrouter_api_key to nil" do
|
|
136
|
+
expect(config.openrouter_api_key).to be_nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "sets log_output_path to nil" do
|
|
140
|
+
expect(config.log_output_path).to be_nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "sets read_only_db to nil" do
|
|
144
|
+
expect(config.read_only_db).to be_nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ── adapter= ─────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe "#adapter=" do
|
|
151
|
+
it "accepts :sqlite" do
|
|
152
|
+
config.adapter = :sqlite
|
|
153
|
+
expect(config.adapter).to eq(:sqlite)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "accepts :postgres" do
|
|
157
|
+
config.adapter = :postgres
|
|
158
|
+
expect(config.adapter).to eq(:postgres)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "accepts :mysql" do
|
|
162
|
+
config.adapter = :mysql
|
|
163
|
+
expect(config.adapter).to eq(:mysql)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "accepts :mysql2" do
|
|
167
|
+
config.adapter = :mysql2
|
|
168
|
+
expect(config.adapter).to eq(:mysql2)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "raises ArgumentError for an unsupported adapter" do
|
|
172
|
+
expect { config.adapter = :oracle }.to raise_error(ArgumentError, /adapter must be/)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# ── read_only_db= ─────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
describe "#read_only_db=" do
|
|
179
|
+
it "accepts nil" do
|
|
180
|
+
config.read_only_db = nil
|
|
181
|
+
expect(config.read_only_db).to be_nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "accepts a connection URL string" do
|
|
185
|
+
config.read_only_db = "sqlite3:///tmp/test.db"
|
|
186
|
+
expect(config.read_only_db).to eq("sqlite3:///tmp/test.db")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "accepts :read_only symbol" do
|
|
190
|
+
config.read_only_db = :read_only
|
|
191
|
+
expect(config.read_only_db).to eq(:read_only)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "raises ArgumentError for an integer" do
|
|
195
|
+
expect { config.read_only_db = 42 }.to raise_error(ArgumentError)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ── llm_provider= ────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
describe "#llm_provider=" do
|
|
202
|
+
described_class::LLM_PROVIDERS.each do |provider|
|
|
203
|
+
it "accepts #{provider}" do
|
|
204
|
+
config.llm_provider = provider
|
|
205
|
+
expect(config.llm_provider).to eq(provider)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "raises ArgumentError for an unsupported provider" do
|
|
210
|
+
expect { config.llm_provider = :anthropic }.to raise_error(ArgumentError, /llm_provider must be/)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# ── llm_model= ───────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
describe "#llm_model=" do
|
|
217
|
+
it "accepts a String" do
|
|
218
|
+
config.llm_model = "gpt-4o"
|
|
219
|
+
expect(config.llm_model).to eq("gpt-4o")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it "raises ArgumentError for a non-String" do
|
|
223
|
+
expect { config.llm_model = 123 }.to raise_error(ArgumentError, /llm_model must be a String/)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# ── schema_permission= ───────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe "#schema_permission=" do
|
|
230
|
+
it "accepts true" do
|
|
231
|
+
config.schema_permission = true
|
|
232
|
+
expect(config.schema_permission).to be(true)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "accepts false" do
|
|
236
|
+
config.schema_permission = false
|
|
237
|
+
expect(config.schema_permission).to be(false)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "raises ArgumentError for non-boolean" do
|
|
241
|
+
expect { config.schema_permission = "yes" }.to raise_error(ArgumentError)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# ── models_permission= ───────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
describe "#models_permission=" do
|
|
248
|
+
it "accepts true and false" do
|
|
249
|
+
config.models_permission = true
|
|
250
|
+
expect(config.models_permission).to be(true)
|
|
251
|
+
config.models_permission = false
|
|
252
|
+
expect(config.models_permission).to be(false)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "raises ArgumentError for non-boolean" do
|
|
256
|
+
expect { config.models_permission = 1 }.to raise_error(ArgumentError)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# ── workflow_cache_ttl= ──────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
describe "#workflow_cache_ttl=" do
|
|
263
|
+
it "accepts a numeric" do
|
|
264
|
+
config.workflow_cache_ttl = 300
|
|
265
|
+
expect(config.workflow_cache_ttl).to eq(300)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it "raises ArgumentError for something that doesn't respond to to_i" do
|
|
269
|
+
expect { config.workflow_cache_ttl = Object.new }.to raise_error(ArgumentError)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# ── context_file_path= ───────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
describe "#context_file_path=" do
|
|
276
|
+
it "accepts a String path" do
|
|
277
|
+
config.context_file_path = "config/my.md"
|
|
278
|
+
expect(config.context_file_path).to eq("config/my.md")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "raises ArgumentError for a non-String" do
|
|
282
|
+
expect { config.context_file_path = :symbol }.to raise_error(ArgumentError)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# ── api_key= / gemini_api_key= / openai_api_key= / openrouter_api_key= ───────
|
|
287
|
+
|
|
288
|
+
%i[api_key gemini_api_key openai_api_key openrouter_api_key].each do |attr|
|
|
289
|
+
describe "##{attr}=" do
|
|
290
|
+
it "accepts nil" do
|
|
291
|
+
config.public_send(:"#{attr}=", nil)
|
|
292
|
+
expect(config.public_send(attr)).to be_nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "accepts a String" do
|
|
296
|
+
config.public_send(:"#{attr}=", "sk-abc123")
|
|
297
|
+
expect(config.public_send(attr)).to eq("sk-abc123")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "raises ArgumentError for a non-String / non-nil" do
|
|
301
|
+
expect { config.public_send(:"#{attr}=", 42) }.to raise_error(ArgumentError)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# ── log_output_path= ─────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
describe "#log_output_path=" do
|
|
309
|
+
it "accepts nil" do
|
|
310
|
+
config.log_output_path = nil
|
|
311
|
+
expect(config.log_output_path).to be_nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "accepts a String path" do
|
|
315
|
+
config.log_output_path = "/var/log/glancer.log"
|
|
316
|
+
expect(config.log_output_path).to eq("/var/log/glancer.log")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it "raises ArgumentError for non-String / non-nil" do
|
|
320
|
+
expect { config.log_output_path = true }.to raise_error(ArgumentError)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# ── log_verbosity= ───────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
describe "#log_verbosity=" do
|
|
327
|
+
described_class::LOG_VERBOSITY_LEVELS.each do |level|
|
|
328
|
+
it "accepts :#{level}" do
|
|
329
|
+
config.log_verbosity = level
|
|
330
|
+
expect(config.log_verbosity).to eq(level)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it "raises ArgumentError for an invalid verbosity" do
|
|
335
|
+
expect { config.log_verbosity = :verbose }.to raise_error(ArgumentError)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# ── k= ───────────────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
describe "#k=" do
|
|
342
|
+
it "accepts an integer >= 1" do
|
|
343
|
+
config.k = 10
|
|
344
|
+
expect(config.k).to eq(10)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it "accepts 1 (minimum)" do
|
|
348
|
+
config.k = 1
|
|
349
|
+
expect(config.k).to eq(1)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it "raises ArgumentError for 0" do
|
|
353
|
+
expect { config.k = 0 }.to raise_error(ArgumentError, /k must be an integer/)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
it "raises ArgumentError for a float" do
|
|
357
|
+
expect { config.k = 2.5 }.to raise_error(ArgumentError)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# ── min_score= ───────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
describe "#min_score=" do
|
|
364
|
+
it "accepts 0.0" do
|
|
365
|
+
config.min_score = 0.0
|
|
366
|
+
expect(config.min_score).to eq(0.0)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it "accepts 1.0" do
|
|
370
|
+
config.min_score = 1.0
|
|
371
|
+
expect(config.min_score).to eq(1.0)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "accepts a value in between" do
|
|
375
|
+
config.min_score = 0.75
|
|
376
|
+
expect(config.min_score).to eq(0.75)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it "raises ArgumentError above 1.0" do
|
|
380
|
+
expect { config.min_score = 1.1 }.to raise_error(ArgumentError)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it "raises ArgumentError below 0.0" do
|
|
384
|
+
expect { config.min_score = -0.1 }.to raise_error(ArgumentError)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it "raises ArgumentError for a non-Numeric" do
|
|
388
|
+
expect { config.min_score = "0.5" }.to raise_error(ArgumentError)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ── weight setters ────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
%i[schema_documents_weight context_documents_weight models_documents_weight].each do |attr|
|
|
395
|
+
describe "##{attr}=" do
|
|
396
|
+
it "accepts a numeric >= 1" do
|
|
397
|
+
config.public_send(:"#{attr}=", 1.5)
|
|
398
|
+
expect(config.public_send(attr)).to eq(1.5)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it "accepts exactly 1" do
|
|
402
|
+
config.public_send(:"#{attr}=", 1)
|
|
403
|
+
expect(config.public_send(attr)).to eq(1)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it "raises ArgumentError for a value below 1" do
|
|
407
|
+
expect { config.public_send(:"#{attr}=", 0.9) }.to raise_error(ArgumentError)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it "raises ArgumentError for a non-Numeric" do
|
|
411
|
+
expect { config.public_send(:"#{attr}=", "big") }.to raise_error(ArgumentError)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# ── chunk_size= ──────────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
describe "#chunk_size=" do
|
|
419
|
+
it "accepts 100 (minimum)" do
|
|
420
|
+
config.chunk_size = 100
|
|
421
|
+
expect(config.chunk_size).to eq(100)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it "raises ArgumentError for 99" do
|
|
425
|
+
expect { config.chunk_size = 99 }.to raise_error(ArgumentError)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
it "raises ArgumentError for a float" do
|
|
429
|
+
expect { config.chunk_size = 200.5 }.to raise_error(ArgumentError)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# ── chunk_overlap= ───────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
describe "#chunk_overlap=" do
|
|
436
|
+
it "accepts 0" do
|
|
437
|
+
config.chunk_overlap = 0
|
|
438
|
+
expect(config.chunk_overlap).to eq(0)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
it "accepts positive integers" do
|
|
442
|
+
config.chunk_overlap = 50
|
|
443
|
+
expect(config.chunk_overlap).to eq(50)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it "raises ArgumentError for negative values" do
|
|
447
|
+
expect { config.chunk_overlap = -1 }.to raise_error(ArgumentError)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# ── history_limit= ───────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
describe "#history_limit=" do
|
|
454
|
+
it "accepts 1" do
|
|
455
|
+
config.history_limit = 1
|
|
456
|
+
expect(config.history_limit).to eq(1)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it "raises ArgumentError for 0" do
|
|
460
|
+
expect { config.history_limit = 0 }.to raise_error(ArgumentError)
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# ── statement_timeout= ───────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
describe "#statement_timeout=" do
|
|
467
|
+
it "accepts a numeric" do
|
|
468
|
+
config.statement_timeout = 60
|
|
469
|
+
expect(config.statement_timeout).to eq(60)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it "raises ArgumentError for an object that doesn't respond to to_i" do
|
|
473
|
+
expect { config.statement_timeout = Object.new }.to raise_error(ArgumentError)
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# ── embedding_provider= ──────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
describe "#embedding_provider=" do
|
|
480
|
+
it "accepts nil" do
|
|
481
|
+
config.embedding_provider = nil
|
|
482
|
+
expect(config.embedding_provider).to be_nil
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
described_class::LLM_PROVIDERS.each do |p|
|
|
486
|
+
it "accepts :#{p}" do
|
|
487
|
+
config.embedding_provider = p
|
|
488
|
+
expect(config.embedding_provider).to eq(p)
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
it "raises ArgumentError for an invalid provider" do
|
|
493
|
+
expect { config.embedding_provider = :unknown }.to raise_error(ArgumentError)
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# ── embedding_model= ─────────────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
describe "#embedding_model=" do
|
|
500
|
+
it "accepts nil" do
|
|
501
|
+
config.embedding_model = nil
|
|
502
|
+
expect(config.embedding_model).to be_nil
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
it "accepts a String" do
|
|
506
|
+
config.embedding_model = "text-embedding-3-small"
|
|
507
|
+
expect(config.embedding_model).to eq("text-embedding-3-small")
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
it "raises ArgumentError for non-String / non-nil" do
|
|
511
|
+
expect { config.embedding_model = 42 }.to raise_error(ArgumentError)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# ── code_provider= / code_model= ───────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
describe "#code_provider=" do
|
|
518
|
+
it "accepts nil" do
|
|
519
|
+
config.code_provider = nil
|
|
520
|
+
expect(config.code_provider).to be_nil
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
it "accepts a valid LLM provider" do
|
|
524
|
+
config.code_provider = :openai
|
|
525
|
+
expect(config.code_provider).to eq(:openai)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
it "raises ArgumentError for invalid provider" do
|
|
529
|
+
expect { config.code_provider = :bad }.to raise_error(ArgumentError)
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
describe "#code_model=" do
|
|
534
|
+
it "accepts nil" do
|
|
535
|
+
config.code_model = nil
|
|
536
|
+
expect(config.code_model).to be_nil
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
it "accepts a String" do
|
|
540
|
+
config.code_model = "gpt-4o"
|
|
541
|
+
expect(config.code_model).to eq("gpt-4o")
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
it "raises ArgumentError for non-String / non-nil" do
|
|
545
|
+
expect { config.code_model = 1 }.to raise_error(ArgumentError)
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# ── chat_provider= / chat_model= ─────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
describe "#chat_provider=" do
|
|
552
|
+
it "accepts nil" do
|
|
553
|
+
config.chat_provider = nil
|
|
554
|
+
expect(config.chat_provider).to be_nil
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
it "accepts a valid LLM provider" do
|
|
558
|
+
config.chat_provider = :openrouter
|
|
559
|
+
expect(config.chat_provider).to eq(:openrouter)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it "raises ArgumentError for invalid provider" do
|
|
563
|
+
expect { config.chat_provider = :bad }.to raise_error(ArgumentError)
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
describe "#chat_model=" do
|
|
568
|
+
it "accepts nil and String" do
|
|
569
|
+
config.chat_model = nil
|
|
570
|
+
expect(config.chat_model).to be_nil
|
|
571
|
+
config.chat_model = "claude-3"
|
|
572
|
+
expect(config.chat_model).to eq("claude-3")
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
it "raises ArgumentError for non-String / non-nil" do
|
|
576
|
+
expect { config.chat_model = true }.to raise_error(ArgumentError)
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# ── blazer_path= ─────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
describe "#blazer_path=" do
|
|
583
|
+
it "accepts nil" do
|
|
584
|
+
config.blazer_path = nil
|
|
585
|
+
expect(config.blazer_path).to be_nil
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
it "accepts a String" do
|
|
589
|
+
config.blazer_path = "/analytics"
|
|
590
|
+
expect(config.blazer_path).to eq("/analytics")
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
it "raises ArgumentError for non-String / non-nil" do
|
|
594
|
+
expect { config.blazer_path = :auto }.to raise_error(ArgumentError)
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# ── Resolved methods ──────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
describe "#resolved_embedding_provider" do
|
|
601
|
+
it "falls back to llm_provider when embedding_provider is nil" do
|
|
602
|
+
config.embedding_provider = nil
|
|
603
|
+
config.llm_provider = :openai
|
|
604
|
+
expect(config.resolved_embedding_provider).to eq(:openai)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it "returns embedding_provider when set" do
|
|
608
|
+
config.llm_provider = :gemini
|
|
609
|
+
config.embedding_provider = :openai
|
|
610
|
+
expect(config.resolved_embedding_provider).to eq(:openai)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
describe "#resolved_embedding_model" do
|
|
615
|
+
it "returns the configured embedding_model when set" do
|
|
616
|
+
config.embedding_model = "my-custom-model"
|
|
617
|
+
expect(config.resolved_embedding_model).to eq("my-custom-model")
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it "falls back to EMBEDDING_DEFAULTS for gemini" do
|
|
621
|
+
config.embedding_model = nil
|
|
622
|
+
config.embedding_provider = :gemini
|
|
623
|
+
expect(config.resolved_embedding_model).to eq(described_class::EMBEDDING_DEFAULTS[:gemini])
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
it "falls back to EMBEDDING_DEFAULTS for openai" do
|
|
627
|
+
config.embedding_model = nil
|
|
628
|
+
config.embedding_provider = :openai
|
|
629
|
+
expect(config.resolved_embedding_model).to eq(described_class::EMBEDDING_DEFAULTS[:openai])
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
describe "#resolved_code_provider" do
|
|
634
|
+
it "falls back to llm_provider when code_provider is nil" do
|
|
635
|
+
config.code_provider = nil
|
|
636
|
+
config.llm_provider = :openai
|
|
637
|
+
expect(config.resolved_code_provider).to eq(:openai)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
it "returns code_provider when set" do
|
|
641
|
+
config.code_provider = :openrouter
|
|
642
|
+
expect(config.resolved_code_provider).to eq(:openrouter)
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
describe "#resolved_code_model" do
|
|
647
|
+
it "falls back to llm_model when code_model is nil" do
|
|
648
|
+
config.code_model = nil
|
|
649
|
+
config.llm_model = "base-model"
|
|
650
|
+
expect(config.resolved_code_model).to eq("base-model")
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
it "returns code_model when set" do
|
|
654
|
+
config.code_model = "sql-specialist"
|
|
655
|
+
expect(config.resolved_code_model).to eq("sql-specialist")
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
describe "#resolved_chat_provider" do
|
|
660
|
+
it "falls back to llm_provider when chat_provider is nil" do
|
|
661
|
+
config.chat_provider = nil
|
|
662
|
+
config.llm_provider = :openai
|
|
663
|
+
expect(config.resolved_chat_provider).to eq(:openai)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
it "returns chat_provider when set" do
|
|
667
|
+
config.chat_provider = :openrouter
|
|
668
|
+
expect(config.resolved_chat_provider).to eq(:openrouter)
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
describe "#resolved_chat_model" do
|
|
673
|
+
it "falls back to llm_model when chat_model is nil" do
|
|
674
|
+
config.chat_model = nil
|
|
675
|
+
config.llm_model = "base-model"
|
|
676
|
+
expect(config.resolved_chat_model).to eq("base-model")
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
it "returns chat_model when set" do
|
|
680
|
+
config.chat_model = "chat-specialist"
|
|
681
|
+
expect(config.resolved_chat_model).to eq("chat-specialist")
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
describe "#resolved_blazer_path" do
|
|
686
|
+
it "returns nil when Blazer is not defined" do
|
|
687
|
+
hide_const("Blazer::Engine") if defined?(Blazer::Engine)
|
|
688
|
+
config.blazer_path = nil
|
|
689
|
+
expect(config.resolved_blazer_path).to be_nil
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
it "returns explicitly set blazer_path over auto-detection" do
|
|
693
|
+
config.blazer_path = "/my-blazer"
|
|
694
|
+
expect(config.resolved_blazer_path).to eq("/my-blazer")
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
describe "#resolved_adapter" do
|
|
699
|
+
it "returns the configured adapter" do
|
|
700
|
+
config.adapter = :sqlite
|
|
701
|
+
expect(config.resolved_adapter).to eq(:sqlite)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
it "normalises 'postgresql' string to :postgres" do
|
|
705
|
+
allow(config).to receive(:adapter).and_return("postgresql")
|
|
706
|
+
expect(config.resolved_adapter).to eq(:postgres)
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# ── Class methods ─────────────────────────────────────────────────────────────
|
|
711
|
+
|
|
712
|
+
describe ".infer_adapter" do
|
|
713
|
+
it "returns :sqlite when using the test SQLite connection" do
|
|
714
|
+
adapter = described_class.infer_adapter
|
|
715
|
+
expect(adapter).to eq(:sqlite)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
it "returns nil when ActiveRecord raises" do
|
|
719
|
+
allow(ActiveRecord::Base).to receive(:connection).and_raise(StandardError)
|
|
720
|
+
expect(described_class.infer_adapter).to be_nil
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
describe ".valid_table_name?" do
|
|
725
|
+
it "returns true for a table that exists (glancer_chats)" do
|
|
726
|
+
expect(described_class.valid_table_name?("glancer_chats")).to be(true)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
it "returns false for a table that does not exist" do
|
|
730
|
+
expect(described_class.valid_table_name?("nonexistent_table")).to be(false)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
it "returns false when ActiveRecord raises" do
|
|
734
|
+
allow(ActiveRecord::Base).to receive(:connection).and_raise(StandardError)
|
|
735
|
+
expect(described_class.valid_table_name?("anything")).to be(false)
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# ── query_mode= ──────────────────────────────────────────────────────────────
|
|
740
|
+
|
|
741
|
+
describe "#query_mode=" do
|
|
742
|
+
it "accepts :sql" do
|
|
743
|
+
config.query_mode = :sql
|
|
744
|
+
expect(config.query_mode).to eq(:sql)
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
it "accepts :activerecord" do
|
|
748
|
+
config.query_mode = :activerecord
|
|
749
|
+
expect(config.query_mode).to eq(:activerecord)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
it "raises ArgumentError for an invalid mode" do
|
|
753
|
+
expect { config.query_mode = :graphql }
|
|
754
|
+
.to raise_error(ArgumentError, /query_mode must be one of/)
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
it "raises ArgumentError for a String value" do
|
|
758
|
+
expect { config.query_mode = "sql" }.to raise_error(ArgumentError)
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# ── query_enrichment_enabled= ─────────────────────────────────────────────
|
|
763
|
+
|
|
764
|
+
describe "#query_enrichment_enabled=" do
|
|
765
|
+
it "defaults to false" do
|
|
766
|
+
expect(config.query_enrichment_enabled).to be(false)
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
it "accepts true" do
|
|
770
|
+
config.query_enrichment_enabled = true
|
|
771
|
+
expect(config.query_enrichment_enabled).to be(true)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
it "accepts false" do
|
|
775
|
+
config.query_enrichment_enabled = true
|
|
776
|
+
config.query_enrichment_enabled = false
|
|
777
|
+
expect(config.query_enrichment_enabled).to be(false)
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
it "raises ArgumentError for non-boolean values" do
|
|
781
|
+
expect { config.query_enrichment_enabled = "yes" }.to raise_error(ArgumentError)
|
|
782
|
+
end
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# ── enrichment_provider= ──────────────────────────────────────────────────
|
|
786
|
+
|
|
787
|
+
describe "#enrichment_provider=" do
|
|
788
|
+
it "defaults to nil" do
|
|
789
|
+
expect(config.enrichment_provider).to be_nil
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
it "accepts a valid provider" do
|
|
793
|
+
config.enrichment_provider = :openai
|
|
794
|
+
expect(config.enrichment_provider).to eq(:openai)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
it "accepts nil" do
|
|
798
|
+
config.enrichment_provider = :gemini
|
|
799
|
+
config.enrichment_provider = nil
|
|
800
|
+
expect(config.enrichment_provider).to be_nil
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
it "raises ArgumentError for invalid providers" do
|
|
804
|
+
expect { config.enrichment_provider = :unknown }.to raise_error(ArgumentError)
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
# ── enrichment_model= ─────────────────────────────────────────────────────
|
|
809
|
+
|
|
810
|
+
describe "#enrichment_model=" do
|
|
811
|
+
it "defaults to nil" do
|
|
812
|
+
expect(config.enrichment_model).to be_nil
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
it "accepts a String model name" do
|
|
816
|
+
config.enrichment_model = "gemini-2.0-flash"
|
|
817
|
+
expect(config.enrichment_model).to eq("gemini-2.0-flash")
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
it "accepts nil" do
|
|
821
|
+
config.enrichment_model = "some-model"
|
|
822
|
+
config.enrichment_model = nil
|
|
823
|
+
expect(config.enrichment_model).to be_nil
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
it "raises ArgumentError for non-string values" do
|
|
827
|
+
expect { config.enrichment_model = :flash }.to raise_error(ArgumentError)
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# ── resolved_enrichment_provider / resolved_enrichment_model ──────────────
|
|
832
|
+
|
|
833
|
+
describe "#resolved_enrichment_provider" do
|
|
834
|
+
it "falls back to llm_provider when enrichment_provider is nil" do
|
|
835
|
+
config.llm_provider = :openai
|
|
836
|
+
config.enrichment_provider = nil
|
|
837
|
+
expect(config.resolved_enrichment_provider).to eq(:openai)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
it "returns enrichment_provider when set" do
|
|
841
|
+
config.enrichment_provider = :openrouter
|
|
842
|
+
expect(config.resolved_enrichment_provider).to eq(:openrouter)
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
describe "#resolved_enrichment_model" do
|
|
847
|
+
it "falls back to llm_model when enrichment_model is nil" do
|
|
848
|
+
config.llm_model = "gpt-4o"
|
|
849
|
+
config.enrichment_model = nil
|
|
850
|
+
expect(config.resolved_enrichment_model).to eq("gpt-4o")
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
it "returns enrichment_model when set" do
|
|
854
|
+
config.enrichment_model = "gemini-2.0-flash"
|
|
855
|
+
expect(config.resolved_enrichment_model).to eq("gemini-2.0-flash")
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
end
|