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,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::Engine do
|
|
6
|
+
# ── configure_provider_key ────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe ".configure_provider_key" do
|
|
9
|
+
let(:llm_config) { double("RubyLLM::Config") }
|
|
10
|
+
|
|
11
|
+
context "gemini provider" do
|
|
12
|
+
it "sets gemini_api_key from gemini_api_key config" do
|
|
13
|
+
Glancer.configuration.gemini_api_key = "g-key"
|
|
14
|
+
allow(llm_config).to receive(:gemini_api_key=)
|
|
15
|
+
described_class.configure_provider_key(llm_config, Glancer.configuration, :gemini)
|
|
16
|
+
expect(llm_config).to have_received(:gemini_api_key=).with("g-key")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "falls back to api_key when gemini_api_key is nil" do
|
|
20
|
+
Glancer.configuration.gemini_api_key = nil
|
|
21
|
+
Glancer.configuration.api_key = "fallback-key"
|
|
22
|
+
allow(llm_config).to receive(:gemini_api_key=)
|
|
23
|
+
described_class.configure_provider_key(llm_config, Glancer.configuration, :gemini)
|
|
24
|
+
expect(llm_config).to have_received(:gemini_api_key=).with("fallback-key")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "raises Glancer::Error when no API key is available" do
|
|
28
|
+
Glancer.configuration.gemini_api_key = nil
|
|
29
|
+
Glancer.configuration.api_key = nil
|
|
30
|
+
expect { described_class.configure_provider_key(llm_config, Glancer.configuration, :gemini) }
|
|
31
|
+
.to raise_error(Glancer::Error, /Gemini API key/)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context "openai provider" do
|
|
36
|
+
it "sets openai_api_key" do
|
|
37
|
+
Glancer.configuration.openai_api_key = "oai-key"
|
|
38
|
+
allow(llm_config).to receive(:openai_api_key=)
|
|
39
|
+
described_class.configure_provider_key(llm_config, Glancer.configuration, :openai)
|
|
40
|
+
expect(llm_config).to have_received(:openai_api_key=).with("oai-key")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "raises Glancer::Error when no API key is available" do
|
|
44
|
+
Glancer.configuration.openai_api_key = nil
|
|
45
|
+
Glancer.configuration.api_key = nil
|
|
46
|
+
expect { described_class.configure_provider_key(llm_config, Glancer.configuration, :openai) }
|
|
47
|
+
.to raise_error(Glancer::Error, /OpenAI API key/)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context "openrouter provider" do
|
|
52
|
+
it "sets openrouter_api_key" do
|
|
53
|
+
Glancer.configuration.openrouter_api_key = "or-key"
|
|
54
|
+
allow(llm_config).to receive(:openrouter_api_key=)
|
|
55
|
+
described_class.configure_provider_key(llm_config, Glancer.configuration, :openrouter)
|
|
56
|
+
expect(llm_config).to have_received(:openrouter_api_key=).with("or-key")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "raises Glancer::Error when no API key is available" do
|
|
60
|
+
Glancer.configuration.openrouter_api_key = nil
|
|
61
|
+
Glancer.configuration.api_key = nil
|
|
62
|
+
expect { described_class.configure_provider_key(llm_config, Glancer.configuration, :openrouter) }
|
|
63
|
+
.to raise_error(Glancer::Error, /OpenRouter API key/)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
context "unsupported provider" do
|
|
68
|
+
it "raises Glancer::Error" do
|
|
69
|
+
expect { described_class.configure_provider_key(llm_config, Glancer.configuration, :unknown_llm) }
|
|
70
|
+
.to raise_error(Glancer::Error, /Unsupported LLM provider/)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# ── Initializer registration ──────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe "registered initializers" do
|
|
78
|
+
let(:initializer_names) { described_class.initializers.map { |i| i.name.to_s } }
|
|
79
|
+
|
|
80
|
+
it "registers glancer.append_migrations" do
|
|
81
|
+
expect(initializer_names).to include("glancer.append_migrations")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "registers glancer.assets" do
|
|
85
|
+
expect(initializer_names).to include("glancer.assets")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "registers glancer.load_tasks" do
|
|
89
|
+
expect(initializer_names).to include("glancer.load_tasks")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "registers glancer.configure_ruby_llm" do
|
|
93
|
+
expect(initializer_names).to include("glancer.configure_ruby_llm")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ── glancer.configure_ruby_llm initializer body ───────────────────────────
|
|
98
|
+
|
|
99
|
+
describe "glancer.configure_ruby_llm initializer" do
|
|
100
|
+
let(:ruby_llm_config) { double("RubyLLM::Config") }
|
|
101
|
+
|
|
102
|
+
before do
|
|
103
|
+
allow(ruby_llm_config).to receive(:gemini_api_key=)
|
|
104
|
+
allow(ruby_llm_config).to receive(:default_embedding_model=)
|
|
105
|
+
allow(ruby_llm_config).to receive(:default_embedding_model).and_return("text-embedding-004")
|
|
106
|
+
allow(RubyLLM).to receive(:configure).and_yield(ruby_llm_config)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def run_llm_initializer
|
|
110
|
+
init = Glancer::Engine.initializers.find { |i| i.name.to_s == "glancer.configure_ruby_llm" }
|
|
111
|
+
init.run(nil)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "calls RubyLLM.configure" do
|
|
115
|
+
expect(RubyLLM).to receive(:configure).and_yield(ruby_llm_config)
|
|
116
|
+
run_llm_initializer
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "sets gemini_api_key on the RubyLLM config" do
|
|
120
|
+
run_llm_initializer
|
|
121
|
+
expect(ruby_llm_config).to have_received(:gemini_api_key=).with("test-api-key")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "sets default_embedding_model on the RubyLLM config" do
|
|
125
|
+
run_llm_initializer
|
|
126
|
+
expect(ruby_llm_config).to have_received(:default_embedding_model=)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "raises Glancer::Error when configure_provider_key raises" do
|
|
130
|
+
allow(RubyLLM).to receive(:configure).and_raise(StandardError, "LLM error")
|
|
131
|
+
expect { run_llm_initializer }.to raise_error(Glancer::Error, /RubyLLM configuration failed/)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "skips when Glancer.configuration is nil" do
|
|
135
|
+
Glancer.configuration = nil
|
|
136
|
+
expect(RubyLLM).not_to receive(:configure)
|
|
137
|
+
expect { run_llm_initializer }.not_to raise_error
|
|
138
|
+
ensure
|
|
139
|
+
Glancer.configuration = Glancer::Configuration.new.tap do |c|
|
|
140
|
+
c.adapter = :sqlite
|
|
141
|
+
c.llm_provider = :gemini
|
|
142
|
+
c.llm_model = "test-model"
|
|
143
|
+
c.gemini_api_key = "test-api-key"
|
|
144
|
+
c.log_verbosity = :none
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ── glancer.append_migrations initializer body ───────────────────────────
|
|
150
|
+
|
|
151
|
+
describe "glancer.append_migrations initializer" do
|
|
152
|
+
def run_migrations_initializer(app)
|
|
153
|
+
init = Glancer::Engine.initializers.find { |i| i.name.to_s == "glancer.append_migrations" }
|
|
154
|
+
init.bind(Glancer::Engine).run(app)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "skips migration append when app root matches engine root" do
|
|
158
|
+
app = double("app")
|
|
159
|
+
allow(app).to receive(:root).and_return(Glancer::Engine.root)
|
|
160
|
+
expect(app).not_to receive(:config)
|
|
161
|
+
run_migrations_initializer(app)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "appends migration paths when app root differs from engine root" do
|
|
165
|
+
app = double("app")
|
|
166
|
+
allow(app).to receive(:root).and_return(Pathname.new("/some/other/app"))
|
|
167
|
+
migrate_paths = double("migrate_paths")
|
|
168
|
+
allow(migrate_paths).to receive(:<<)
|
|
169
|
+
app_paths = double("app_paths")
|
|
170
|
+
allow(app_paths).to receive(:[]).with("db/migrate").and_return(migrate_paths)
|
|
171
|
+
allow(app).to receive(:config).and_return(double("app_config", paths: app_paths))
|
|
172
|
+
expect { run_migrations_initializer(app) }.not_to raise_error
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# ── glancer.assets initializer body ──────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
describe "glancer.assets initializer" do
|
|
179
|
+
def run_assets_initializer(app)
|
|
180
|
+
init = Glancer::Engine.initializers.find { |i| i.name.to_s == "glancer.assets" }
|
|
181
|
+
init.bind(Glancer::Engine).run(app)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "registers asset paths and precompile list without raising" do
|
|
185
|
+
asset_paths = []
|
|
186
|
+
precompile = []
|
|
187
|
+
assets = double("assets")
|
|
188
|
+
allow(assets).to receive(:paths).and_return(asset_paths)
|
|
189
|
+
allow(assets).to receive(:precompile).and_return(precompile)
|
|
190
|
+
allow(assets).to receive(:precompile=)
|
|
191
|
+
app = double("app", config: double("config", assets: assets))
|
|
192
|
+
expect { run_assets_initializer(app) }.not_to raise_error
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# ── glancer.load_tasks initializer body ──────────────────────────────────
|
|
197
|
+
|
|
198
|
+
describe "glancer.load_tasks initializer" do
|
|
199
|
+
def run_load_tasks_initializer
|
|
200
|
+
init = Glancer::Engine.initializers.find { |i| i.name.to_s == "glancer.load_tasks" }
|
|
201
|
+
init.bind(Glancer::Engine).run(nil)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "loads rake task files without raising" do
|
|
205
|
+
allow(Dir).to receive(:[]).and_return([])
|
|
206
|
+
expect { run_load_tasks_initializer }.not_to raise_error
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::Indexer::ContextIndexer do
|
|
6
|
+
let(:context_path) { "/fake/config/context.md" }
|
|
7
|
+
let(:context_content) do
|
|
8
|
+
<<~MD
|
|
9
|
+
# Domain Context
|
|
10
|
+
This application manages e-commerce orders and customers.
|
|
11
|
+
|
|
12
|
+
## Users
|
|
13
|
+
Users have email, name, and created_at attributes.
|
|
14
|
+
|
|
15
|
+
## Orders
|
|
16
|
+
Orders belong to users and have a total and status.
|
|
17
|
+
MD
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
before do
|
|
21
|
+
Glancer.configuration.context_file_path = context_path
|
|
22
|
+
allow(File).to receive(:exist?).with(context_path).and_return(true)
|
|
23
|
+
allow(File).to receive(:read).with(context_path).and_return(context_content)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# ── index! ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
describe ".index!" do
|
|
29
|
+
it "returns an array of chunk hashes" do
|
|
30
|
+
result = described_class.index!
|
|
31
|
+
expect(result).to be_an(Array)
|
|
32
|
+
expect(result).not_to be_empty
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "sets source_type to 'context' on all chunks" do
|
|
36
|
+
result = described_class.index!
|
|
37
|
+
expect(result.map { |c| c[:source_type] }).to all(eq("context"))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "sets source_path to the configured context_file_path" do
|
|
41
|
+
result = described_class.index!
|
|
42
|
+
expect(result.map { |c| c[:source_path] }).to all(eq(context_path))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "includes the content from the context file" do
|
|
46
|
+
result = described_class.index!
|
|
47
|
+
combined = result.map { |c| c[:content] }.join
|
|
48
|
+
expect(combined).to include("Domain Context")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "returns [] when the file starts with --glancer-ignore" do
|
|
52
|
+
allow(File).to receive(:read).with(context_path).and_return("--glancer-ignore\nsome content")
|
|
53
|
+
expect(described_class.index!).to eq([])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "raises Glancer::Error when the file does not exist" do
|
|
57
|
+
allow(File).to receive(:exist?).with(context_path).and_return(false)
|
|
58
|
+
expect { described_class.index! }.to raise_error(Glancer::Error, /Context file not found/)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "raises Glancer::Error when context_file_path is nil" do
|
|
62
|
+
Glancer.configuration.context_file_path = "non_existent_file.md"
|
|
63
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
64
|
+
expect { described_class.index! }.to raise_error(Glancer::Error)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "raises Glancer::Error when File.read raises" do
|
|
68
|
+
allow(File).to receive(:read).and_raise(IOError, "unreadable file")
|
|
69
|
+
expect { described_class.index! }.to raise_error(Glancer::Error, /Context indexing failed/)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ── split_into_chunks ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
describe ".split_into_chunks" do
|
|
76
|
+
before do
|
|
77
|
+
Glancer.configuration.chunk_size = 100
|
|
78
|
+
Glancer.configuration.chunk_overlap = 10
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "returns one chunk for text shorter than chunk_size" do
|
|
82
|
+
result = described_class.split_into_chunks("short")
|
|
83
|
+
expect(result.size).to eq(1)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "splits long text into multiple chunks" do
|
|
87
|
+
result = described_class.split_into_chunks("x" * 300)
|
|
88
|
+
expect(result.size).to be > 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "produces chunks of at most chunk_size characters" do
|
|
92
|
+
result = described_class.split_into_chunks("y" * 300)
|
|
93
|
+
result.each { |chunk| expect(chunk.length).to be <= 100 }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::Indexer::ModelIndexer do
|
|
6
|
+
let(:fake_root) { Pathname.new("/fake/app") }
|
|
7
|
+
let(:model_content) do
|
|
8
|
+
<<~RUBY
|
|
9
|
+
class User < ApplicationRecord
|
|
10
|
+
has_many :orders
|
|
11
|
+
validates :email, presence: true
|
|
12
|
+
end
|
|
13
|
+
RUBY
|
|
14
|
+
end
|
|
15
|
+
let(:model_file) { "/fake/app/app/models/user.rb" }
|
|
16
|
+
|
|
17
|
+
before do
|
|
18
|
+
allow(Rails).to receive(:root).and_return(fake_root)
|
|
19
|
+
allow(Dir).to receive(:[]).with(fake_root.join("app/models/**/*.rb")).and_return([model_file])
|
|
20
|
+
allow(File).to receive(:read).with(model_file).and_return(model_content)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# ── index! ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe ".index!" do
|
|
26
|
+
it "returns an array of chunk hashes" do
|
|
27
|
+
result = described_class.index!
|
|
28
|
+
expect(result).to be_an(Array)
|
|
29
|
+
expect(result).not_to be_empty
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "sets source_type to 'models' on all chunks" do
|
|
33
|
+
result = described_class.index!
|
|
34
|
+
expect(result.map { |c| c[:source_type] }).to all(eq("models"))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "sets source_path to the file path" do
|
|
38
|
+
result = described_class.index!
|
|
39
|
+
expect(result.map { |c| c[:source_path] }).to all(eq(model_file))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "includes the content of the model file" do
|
|
43
|
+
result = described_class.index!
|
|
44
|
+
combined = result.map { |c| c[:content] }.join
|
|
45
|
+
expect(combined).to include("class User")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "returns [] when no model files are found" do
|
|
49
|
+
allow(Dir).to receive(:[]).and_return([])
|
|
50
|
+
expect(described_class.index!).to eq([])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "handles multiple model files" do
|
|
54
|
+
second_file = "/fake/app/app/models/order.rb"
|
|
55
|
+
second_content = "class Order < ApplicationRecord; end"
|
|
56
|
+
allow(Dir).to receive(:[]).and_return([model_file, second_file])
|
|
57
|
+
allow(File).to receive(:read).with(second_file).and_return(second_content)
|
|
58
|
+
|
|
59
|
+
result = described_class.index!
|
|
60
|
+
paths = result.map { |c| c[:source_path] }.uniq
|
|
61
|
+
expect(paths).to include(model_file, second_file)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "raises Glancer::Error when File.read raises" do
|
|
65
|
+
allow(File).to receive(:read).and_raise(IOError, "unreadable")
|
|
66
|
+
expect { described_class.index! }.to raise_error(Glancer::Error, /Model indexing failed/)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ── split_into_chunks ─────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe ".split_into_chunks" do
|
|
73
|
+
before do
|
|
74
|
+
Glancer.configuration.chunk_size = 100
|
|
75
|
+
Glancer.configuration.chunk_overlap = 20
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "returns at least one chunk for any non-empty text" do
|
|
79
|
+
result = described_class.split_into_chunks("x" * 50)
|
|
80
|
+
expect(result.size).to be >= 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "splits long text into multiple chunks" do
|
|
84
|
+
long_text = "a" * 250
|
|
85
|
+
result = described_class.split_into_chunks(long_text)
|
|
86
|
+
expect(result.size).to be > 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "respects chunk_size from configuration" do
|
|
90
|
+
Glancer.configuration.chunk_size = 200
|
|
91
|
+
Glancer.configuration.chunk_overlap = 0 # no overlap so the text fits in exactly 1 chunk
|
|
92
|
+
result = described_class.split_into_chunks("b" * 200)
|
|
93
|
+
expect(result.size).to eq(1)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "applies overlap so successive chunks share content" do
|
|
97
|
+
text = "a" * 180
|
|
98
|
+
result = described_class.split_into_chunks(text)
|
|
99
|
+
# With size=100 and overlap=20, second chunk starts at index 80
|
|
100
|
+
expect(result[1]).to start_with("a" * 20)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|