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,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::Embedding do
|
|
6
|
+
# ── Persistence ───────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe "persistence" do
|
|
9
|
+
it "saves an embedding with an array of floats" do
|
|
10
|
+
embedding = described_class.create!(
|
|
11
|
+
content: "create_table users ...",
|
|
12
|
+
embedding: [0.1, 0.2, 0.3],
|
|
13
|
+
source_type: "schema",
|
|
14
|
+
source_path: "/path/to/schema.rb#users"
|
|
15
|
+
)
|
|
16
|
+
expect(embedding).to be_persisted
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "retrieves the embedding array after save" do
|
|
20
|
+
described_class.create!(
|
|
21
|
+
content: "some content",
|
|
22
|
+
embedding: [1.0, 2.0, 3.0]
|
|
23
|
+
)
|
|
24
|
+
loaded = described_class.last
|
|
25
|
+
expect(loaded.embedding).to be_a(Array)
|
|
26
|
+
expect(loaded.embedding.map(&:to_f)).to eq([1.0, 2.0, 3.0])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "stores and retrieves an empty array" do
|
|
30
|
+
described_class.create!(content: "empty", embedding: [])
|
|
31
|
+
loaded = described_class.last
|
|
32
|
+
expect(loaded.embedding).to eq([])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ── Serialisation ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe "serialize :embedding, Array" do
|
|
39
|
+
it "serialises the embedding to a storable format" do
|
|
40
|
+
emb = described_class.new(content: "x", embedding: [0.5, 0.6])
|
|
41
|
+
emb.save!
|
|
42
|
+
raw = ActiveRecord::Base.connection
|
|
43
|
+
.exec_query("SELECT embedding FROM glancer_embeddings WHERE id = #{emb.id}")
|
|
44
|
+
.first["embedding"]
|
|
45
|
+
# The serialised form should be a String (YAML or JSON)
|
|
46
|
+
expect(raw).to be_a(String)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ── Scopes via where ──────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
describe "filtering by source_type" do
|
|
53
|
+
before do
|
|
54
|
+
described_class.create!(content: "schema chunk", embedding: [], source_type: "schema", source_path: "schema.rb#users")
|
|
55
|
+
described_class.create!(content: "model chunk", embedding: [], source_type: "models", source_path: "user.rb")
|
|
56
|
+
described_class.create!(content: "context chunk", embedding: [], source_type: "context", source_path: "context.md")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "can filter schema embeddings" do
|
|
60
|
+
schema_embeds = described_class.where(source_type: "schema")
|
|
61
|
+
expect(schema_embeds.count).to eq(1)
|
|
62
|
+
expect(schema_embeds.first.content).to include("schema chunk")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "can filter model embeddings" do
|
|
66
|
+
expect(described_class.where(source_type: "models").count).to eq(1)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "can filter context embeddings" do
|
|
70
|
+
expect(described_class.where(source_type: "context").count).to eq(1)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::Message do
|
|
6
|
+
let(:chat) { Glancer::Chat.create!(title: "Test Chat") }
|
|
7
|
+
let(:user_message) { described_class.create!(chat: chat, role: "user", content: "How many orders?") }
|
|
8
|
+
|
|
9
|
+
subject(:message) { described_class.new(chat: chat, role: "user", content: "Hello") }
|
|
10
|
+
|
|
11
|
+
# ── Validations ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
describe "validations" do
|
|
14
|
+
it "is valid with required attributes" do
|
|
15
|
+
expect(message).to be_valid
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "is invalid without content" do
|
|
19
|
+
message.content = nil
|
|
20
|
+
expect(message).not_to be_valid
|
|
21
|
+
expect(message.errors[:content]).to include("can't be blank")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "is invalid with an empty content string" do
|
|
25
|
+
message.content = ""
|
|
26
|
+
expect(message).not_to be_valid
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# ── Enum: role ────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
describe "role enum" do
|
|
33
|
+
it "accepts 'user'" do
|
|
34
|
+
msg = described_class.create!(chat: chat, role: "user", content: "hi")
|
|
35
|
+
expect(msg.role).to eq("user")
|
|
36
|
+
expect(msg.user?).to be(true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "accepts 'assistant'" do
|
|
40
|
+
msg = described_class.create!(chat: chat, role: "assistant", content: "hello")
|
|
41
|
+
expect(msg.role).to eq("assistant")
|
|
42
|
+
expect(msg.assistant?).to be(true)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "accepts 'system'" do
|
|
46
|
+
msg = described_class.create!(chat: chat, role: "system", content: "init")
|
|
47
|
+
expect(msg.role).to eq("system")
|
|
48
|
+
expect(msg.system?).to be(true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "raises on an invalid role" do
|
|
52
|
+
expect { described_class.create!(chat: chat, role: "unknown", content: "x") }
|
|
53
|
+
.to raise_error(ArgumentError)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ── Associations ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe "associations" do
|
|
60
|
+
it "belongs_to a chat" do
|
|
61
|
+
expect(user_message.chat).to eq(chat)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "belongs_to a user_message (optional)" do
|
|
65
|
+
assistant = described_class.create!(
|
|
66
|
+
chat: chat,
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content: "There are 10 orders.",
|
|
69
|
+
user_message: user_message
|
|
70
|
+
)
|
|
71
|
+
expect(assistant.user_message).to eq(user_message)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "allows nil user_message (optional association)" do
|
|
75
|
+
msg = described_class.create!(chat: chat, role: "user", content: "test")
|
|
76
|
+
expect(msg.user_message).to be_nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "has many code_versions" do
|
|
80
|
+
msg = described_class.create!(chat: chat, role: "assistant", content: "result", code: "SELECT 1")
|
|
81
|
+
version = Glancer::CodeVersion.create!(message: msg, code: "SELECT 1", source: "generated")
|
|
82
|
+
expect(msg.code_versions).to include(version)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "destroys code_versions when the message is destroyed" do
|
|
86
|
+
msg = described_class.create!(chat: chat, role: "assistant", content: "result", code: "SELECT 1")
|
|
87
|
+
Glancer::CodeVersion.create!(message: msg, code: "SELECT 1", source: "generated")
|
|
88
|
+
expect { msg.destroy }.to change(Glancer::CodeVersion, :count).by(-1)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "nullifies audits when the message is destroyed" do
|
|
92
|
+
msg = described_class.create!(chat: chat, role: "assistant", content: "result", code: "SELECT 1")
|
|
93
|
+
audit = Glancer::Audit.create!(
|
|
94
|
+
code: "SELECT 1",
|
|
95
|
+
adapter: "sqlite",
|
|
96
|
+
run_id: SecureRandom.uuid,
|
|
97
|
+
executed_at: Time.current,
|
|
98
|
+
message: msg
|
|
99
|
+
)
|
|
100
|
+
msg.destroy
|
|
101
|
+
expect(audit.reload.message_id).to be_nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ── before_destroy callback ───────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
describe "before_destroy callback" do
|
|
108
|
+
it "nullifies user_message_id on assistant messages pointing to this user message" do
|
|
109
|
+
assistant = described_class.create!(
|
|
110
|
+
chat: chat,
|
|
111
|
+
role: "assistant",
|
|
112
|
+
content: "reply",
|
|
113
|
+
user_message: user_message
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
user_message.destroy
|
|
117
|
+
expect(assistant.reload.user_message_id).to be_nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ── #sql_result_json ──────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe "#sql_result_json" do
|
|
124
|
+
it "parses valid JSON content" do
|
|
125
|
+
msg = described_class.new(content: '[{"id":1}]')
|
|
126
|
+
expect(msg.sql_result_json).to eq([{ "id" => 1 }])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "returns [] for non-JSON content" do
|
|
130
|
+
msg = described_class.new(content: "plain text response")
|
|
131
|
+
expect(msg.sql_result_json).to eq([])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "returns [] for nil content" do
|
|
135
|
+
msg = described_class.new(content: nil)
|
|
136
|
+
expect(msg.sql_result_json).to eq([])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "returns [] for malformed JSON" do
|
|
140
|
+
msg = described_class.new(content: "{bad json}")
|
|
141
|
+
expect(msg.sql_result_json).to eq([])
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::Setting do
|
|
6
|
+
# ── Validations ───────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe "validations" do
|
|
9
|
+
it "is valid with a unique key" do
|
|
10
|
+
setting = described_class.new(key: "some_key", value: "val")
|
|
11
|
+
expect(setting).to be_valid
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "is invalid without a key" do
|
|
15
|
+
setting = described_class.new(key: nil, value: "val")
|
|
16
|
+
expect(setting).not_to be_valid
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "enforces key uniqueness" do
|
|
20
|
+
described_class.create!(key: "duplicate", value: "first")
|
|
21
|
+
dup = described_class.new(key: "duplicate", value: "second")
|
|
22
|
+
expect(dup).not_to be_valid
|
|
23
|
+
expect(dup.errors[:key]).to be_present
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# ── .get ──────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe ".get" do
|
|
30
|
+
it "returns the value for an existing key" do
|
|
31
|
+
described_class.create!(key: "my_key", value: "hello")
|
|
32
|
+
expect(described_class.get("my_key")).to eq("hello")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "returns the default when the key does not exist" do
|
|
36
|
+
expect(described_class.get("missing", default: "fallback")).to eq("fallback")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "returns nil by default when key is missing and no default given" do
|
|
40
|
+
expect(described_class.get("nonexistent")).to be_nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "accepts symbol keys" do
|
|
44
|
+
described_class.create!(key: "sym_key", value: "sym_val")
|
|
45
|
+
expect(described_class.get(:sym_key)).to eq("sym_val")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ── .set ──────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe ".set" do
|
|
52
|
+
it "creates a new setting record" do
|
|
53
|
+
expect { described_class.set("brand_new", "value") }.to change(described_class, :count).by(1)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "updates an existing record instead of creating a duplicate" do
|
|
57
|
+
described_class.set("existing", "old")
|
|
58
|
+
expect { described_class.set("existing", "new") }.not_to change(described_class, :count)
|
|
59
|
+
expect(described_class.get("existing")).to eq("new")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "converts the value to a String" do
|
|
63
|
+
described_class.set("numeric_key", 42)
|
|
64
|
+
expect(described_class.get("numeric_key")).to eq("42")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "converts the key to a String" do
|
|
68
|
+
described_class.set(:symbol_key, "v")
|
|
69
|
+
expect(described_class.get("symbol_key")).to eq("v")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ── .store_many ───────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
describe ".store_many" do
|
|
76
|
+
it "stores all key-value pairs in the hash" do
|
|
77
|
+
described_class.store_many(foo: "bar", baz: "qux")
|
|
78
|
+
expect(described_class.get(:foo)).to eq("bar")
|
|
79
|
+
expect(described_class.get(:baz)).to eq("qux")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "updates existing keys" do
|
|
83
|
+
described_class.set("k", "original")
|
|
84
|
+
described_class.store_many(k: "updated")
|
|
85
|
+
expect(described_class.get("k")).to eq("updated")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if ENV["COVERAGE"] || ENV["CI"]
|
|
4
|
+
require "simplecov"
|
|
5
|
+
require "simplecov-json"
|
|
6
|
+
|
|
7
|
+
SimpleCov.formatters = [
|
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
|
9
|
+
SimpleCov::Formatter::JSONFormatter
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
SimpleCov.start do
|
|
13
|
+
add_filter "/spec/"
|
|
14
|
+
add_filter "/lib/generators/"
|
|
15
|
+
add_filter "/lib/glancer/version.rb" # loaded by bundler before SimpleCov starts
|
|
16
|
+
|
|
17
|
+
add_group "Workflow", "lib/glancer/workflow"
|
|
18
|
+
add_group "Indexers", "lib/glancer/indexer"
|
|
19
|
+
add_group "Controllers", "app/controllers"
|
|
20
|
+
add_group "Models", "app/models"
|
|
21
|
+
add_group "Configuration", "lib/glancer/configuration.rb"
|
|
22
|
+
|
|
23
|
+
minimum_coverage 80
|
|
24
|
+
track_files "lib/**/*.rb"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Must load Rails BEFORE requiring glancer, because engine.rb inherits Rails::Engine
|
|
29
|
+
require "active_record"
|
|
30
|
+
require "active_record/railtie"
|
|
31
|
+
require "action_dispatch"
|
|
32
|
+
require "rails"
|
|
33
|
+
|
|
34
|
+
# Minimal Rails application — provides Rails.root and Rails.application.
|
|
35
|
+
# Do NOT call initialize! (avoids assets, LLM config, migration initializers).
|
|
36
|
+
module GlancerTestApp
|
|
37
|
+
class Application < Rails::Application
|
|
38
|
+
config.eager_load = false
|
|
39
|
+
config.logger = Logger.new(nil)
|
|
40
|
+
config.active_support.deprecation = :silence
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
GlancerTestApp::Application.new # sets Rails.application
|
|
44
|
+
|
|
45
|
+
# ApplicationRecord must exist BEFORE Glancer models are autoloaded.
|
|
46
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
47
|
+
self.abstract_class = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
require "glancer"
|
|
51
|
+
|
|
52
|
+
# Rails 7.1+ removed the positional `serialize :attr, SomeClass` form in favour of
|
|
53
|
+
# keyword arguments. The production Glancer::Embedding model uses the old two-arg
|
|
54
|
+
# form (`serialize :embedding, Array`). We shim ActiveRecord::Base.serialize to
|
|
55
|
+
# translate the legacy positional argument into the correct keyword form so the
|
|
56
|
+
# model loads under Rails 7.2 without modifying production code.
|
|
57
|
+
#
|
|
58
|
+
# When the legacy coder is a type class (Array, Hash, …) Rails 7.2 expects it as
|
|
59
|
+
# `type:` with the default YAML coder, not as `coder:` (which would try to call
|
|
60
|
+
# Array.new with the wrong arguments).
|
|
61
|
+
module SerializeCompatShim
|
|
62
|
+
TYPE_CLASSES = [Array, Hash, String, Integer, Float].freeze
|
|
63
|
+
|
|
64
|
+
def serialize(attr_name, legacy_coder = nil, coder: nil, type: Object, **opts)
|
|
65
|
+
if legacy_coder && coder.nil?
|
|
66
|
+
if TYPE_CLASSES.include?(legacy_coder)
|
|
67
|
+
# Old form: serialize :col, Array → coder: YAML, type: Array
|
|
68
|
+
super(attr_name, type: legacy_coder, **opts)
|
|
69
|
+
else
|
|
70
|
+
# Old form: serialize :col, SomeCoder → coder: SomeCoder
|
|
71
|
+
super(attr_name, coder: legacy_coder, type: type, **opts)
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
super(attr_name, coder: coder, type: type, **opts)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
ActiveRecord::Base.singleton_class.prepend(SerializeCompatShim)
|
|
79
|
+
|
|
80
|
+
# Explicitly require Glancer model files (the engine's autoloading is not active
|
|
81
|
+
# in the minimal test Rails application).
|
|
82
|
+
Dir[File.expand_path("../app/models/**/*.rb", __dir__)].sort.each { |f| require f }
|
|
83
|
+
|
|
84
|
+
# SQLite3 in-memory database for tests.
|
|
85
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
|
86
|
+
|
|
87
|
+
# Suppress migration output.
|
|
88
|
+
ActiveRecord::Migration.verbose = false
|
|
89
|
+
|
|
90
|
+
# Create all tables used by Glancer.
|
|
91
|
+
load File.expand_path("support/schema.rb", __dir__)
|
|
92
|
+
|
|
93
|
+
# Silence Glancer logger during the test run.
|
|
94
|
+
Glancer.configure do |c|
|
|
95
|
+
c.adapter = :sqlite
|
|
96
|
+
c.llm_provider = :gemini
|
|
97
|
+
c.llm_model = "test-model"
|
|
98
|
+
c.gemini_api_key = "test-api-key"
|
|
99
|
+
c.log_verbosity = :none
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
RSpec.configure do |config|
|
|
103
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
|
104
|
+
config.disable_monkey_patching!
|
|
105
|
+
|
|
106
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
107
|
+
|
|
108
|
+
config.order = :random
|
|
109
|
+
Kernel.srand config.seed
|
|
110
|
+
|
|
111
|
+
config.before do
|
|
112
|
+
# Clean DB between tests (order matters: child tables first).
|
|
113
|
+
[Glancer::CodeVersion, Glancer::Audit, Glancer::Embedding,
|
|
114
|
+
Glancer::Message, Glancer::Chat, Glancer::Setting].each(&:delete_all)
|
|
115
|
+
|
|
116
|
+
# Clear the in-memory workflow cache.
|
|
117
|
+
Glancer::Workflow::Cache.clear
|
|
118
|
+
|
|
119
|
+
# Reset Glancer configuration to safe test defaults.
|
|
120
|
+
Glancer.configuration = Glancer::Configuration.new.tap do |c|
|
|
121
|
+
c.adapter = :sqlite
|
|
122
|
+
c.llm_provider = :gemini
|
|
123
|
+
c.llm_model = "test-model"
|
|
124
|
+
c.gemini_api_key = "test-api-key"
|
|
125
|
+
c.log_verbosity = :silent
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ActiveRecord::Schema.define do
|
|
4
|
+
create_table :glancer_chats, force: :cascade do |t|
|
|
5
|
+
t.string :title
|
|
6
|
+
t.timestamps
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
create_table :glancer_messages, force: :cascade do |t|
|
|
10
|
+
t.integer :chat_id, null: false
|
|
11
|
+
t.integer :user_message_id
|
|
12
|
+
t.string :role
|
|
13
|
+
t.text :content
|
|
14
|
+
t.text :code
|
|
15
|
+
t.string :code_type, null: false, default: "sql"
|
|
16
|
+
t.boolean :successful, default: true
|
|
17
|
+
t.boolean :user_edited_code, default: false, null: false
|
|
18
|
+
t.string :llm_model
|
|
19
|
+
t.text :enriched_question
|
|
20
|
+
t.integer :status, null: false, default: 2
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
create_table :glancer_embeddings, force: :cascade do |t|
|
|
25
|
+
t.text :content, null: false
|
|
26
|
+
t.json :embedding
|
|
27
|
+
t.string :source_type
|
|
28
|
+
t.string :source_path
|
|
29
|
+
t.timestamps
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
create_table :glancer_audits, force: :cascade do |t|
|
|
33
|
+
t.text :question
|
|
34
|
+
t.text :code, null: false
|
|
35
|
+
t.string :code_type, null: false, default: "sql"
|
|
36
|
+
t.string :adapter, null: false
|
|
37
|
+
t.string :run_id, null: false
|
|
38
|
+
t.datetime :executed_at, null: false
|
|
39
|
+
t.integer :message_id
|
|
40
|
+
t.timestamps
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
create_table :glancer_code_versions, force: :cascade do |t|
|
|
44
|
+
t.integer :message_id, null: false
|
|
45
|
+
t.text :code, null: false
|
|
46
|
+
t.string :source, null: false, default: "generated"
|
|
47
|
+
t.timestamps
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
create_table :glancer_settings, force: :cascade do |t|
|
|
51
|
+
t.string :key, null: false
|
|
52
|
+
t.text :value
|
|
53
|
+
t.timestamps
|
|
54
|
+
end
|
|
55
|
+
end
|