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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +96 -0
  3. data/.rubocop.yml +54 -0
  4. data/CHANGELOG.md +88 -0
  5. data/CLAUDE.md +115 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/README.md +354 -0
  8. data/app/assets/config/glancer_manifest.js +1 -0
  9. data/app/assets/javascripts/glancer/application.js +15 -0
  10. data/app/assets/javascripts/glancer/controllers/chat_controller.js +101 -0
  11. data/app/assets/javascripts/glancer/controllers/message_controller.js +1052 -0
  12. data/app/assets/javascripts/glancer/controllers/toast_controller.js +63 -0
  13. data/app/assets/stylesheets/glancer/application.css +350 -0
  14. data/app/assets/stylesheets/glancer/code-blocks.css +6 -0
  15. data/app/assets/stylesheets/glancer/list.css +31 -0
  16. data/app/assets/stylesheets/glancer/scrollbar.css +16 -0
  17. data/app/assets/stylesheets/glancer/table.css +97 -0
  18. data/app/controllers/glancer/application_controller.rb +33 -0
  19. data/app/controllers/glancer/chats_controller.rb +49 -0
  20. data/app/controllers/glancer/messages_controller.rb +144 -0
  21. data/app/controllers/glancer/schema_controller.rb +29 -0
  22. data/app/controllers/glancer/settings_controller.rb +23 -0
  23. data/app/helpers/glancer/application_helper.rb +17 -0
  24. data/app/jobs/glancer/application_job.rb +6 -0
  25. data/app/jobs/glancer/process_message_job.rb +38 -0
  26. data/app/models/glancer/audit.rb +12 -0
  27. data/app/models/glancer/chat.rb +8 -0
  28. data/app/models/glancer/code_version.rb +12 -0
  29. data/app/models/glancer/embedding.rb +6 -0
  30. data/app/models/glancer/message.rb +25 -0
  31. data/app/models/glancer/setting.rb +23 -0
  32. data/app/models/glancer/sql_version.rb +6 -0
  33. data/app/views/glancer/_data/_importmap.json.erb +7 -0
  34. data/app/views/glancer/chats/_chat_sidebar.html.erb +2 -0
  35. data/app/views/glancer/chats/_show.html.erb +52 -0
  36. data/app/views/glancer/chats/_sidebar_chat_list.html.erb +30 -0
  37. data/app/views/glancer/chats/index.html.erb +10 -0
  38. data/app/views/glancer/chats/show.html.erb +1 -0
  39. data/app/views/glancer/messages/_data_table.html.erb +268 -0
  40. data/app/views/glancer/messages/_execution_error.html.erb +26 -0
  41. data/app/views/glancer/messages/_form.html.erb +93 -0
  42. data/app/views/glancer/messages/_message.html.erb +206 -0
  43. data/app/views/glancer/messages/_message_info.html.erb +176 -0
  44. data/app/views/glancer/messages/_temp_form.html.erb +100 -0
  45. data/app/views/glancer/messages/create.turbo_stream.erb +25 -0
  46. data/app/views/glancer/schema/show.html.erb +123 -0
  47. data/app/views/glancer/settings/show.html.erb +306 -0
  48. data/app/views/glancer/shared/_icons.html.erb +126 -0
  49. data/app/views/layouts/glancer/application.html.erb +234 -0
  50. data/config/locales/glancer.en.yml +90 -0
  51. data/config/locales/glancer.es.yml +90 -0
  52. data/config/locales/glancer.pt-BR.yml +90 -0
  53. data/config/routes.rb +20 -0
  54. data/db/migrate/20250629212642_create_glancer_audits.rb +19 -0
  55. data/db/migrate/20250629212643_create_glancer_chats.rb +10 -0
  56. data/db/migrate/20250629212645_create_glancer_embeddings.rb +17 -0
  57. data/db/migrate/20250629212647_create_glancer_messages.rb +29 -0
  58. data/db/migrate/20260513204129_add_user_edited_sql_to_glancer_messages.rb +11 -0
  59. data/db/migrate/20260513210647_create_glancer_sql_versions.rb +18 -0
  60. data/db/migrate/20260513210648_add_message_id_to_glancer_audits.rb +8 -0
  61. data/db/migrate/20260513220000_create_glancer_settings.rb +12 -0
  62. data/db/migrate/20260514083509_add_llm_model_to_glancer_messages.rb +9 -0
  63. data/db/migrate/20260523120000_rename_code_columns_in_glancer_messages.rb +8 -0
  64. data/db/migrate/20260523120001_rename_code_column_in_glancer_audits.rb +7 -0
  65. data/db/migrate/20260523120002_add_code_type_to_glancer_tables.rb +10 -0
  66. data/db/migrate/20260523120003_rename_glancer_sql_versions_to_code_versions.rb +8 -0
  67. data/db/migrate/20260523130000_add_enriched_question_to_glancer_messages.rb +7 -0
  68. data/db/migrate/20260524100000_add_status_to_glancer_messages.rb +9 -0
  69. data/lib/generators/glancer/install/install_generator.rb +74 -0
  70. data/lib/generators/glancer/install/templates/glancer.rb +227 -0
  71. data/lib/generators/glancer/install/templates/llm_context.glancer.md +51 -0
  72. data/lib/glancer/async_runner.rb +50 -0
  73. data/lib/glancer/chart_analyzer.rb +230 -0
  74. data/lib/glancer/configuration.rb +372 -0
  75. data/lib/glancer/engine.rb +90 -0
  76. data/lib/glancer/indexer/context_indexer.rb +58 -0
  77. data/lib/glancer/indexer/model_indexer.rb +64 -0
  78. data/lib/glancer/indexer/schema_indexer.rb +171 -0
  79. data/lib/glancer/indexer.rb +50 -0
  80. data/lib/glancer/retriever.rb +114 -0
  81. data/lib/glancer/utils/logger.rb +83 -0
  82. data/lib/glancer/utils/markdown_helper.rb +56 -0
  83. data/lib/glancer/utils/result_formatter.rb +25 -0
  84. data/lib/glancer/utils/table_stats.rb +18 -0
  85. data/lib/glancer/utils/transaction.rb +59 -0
  86. data/lib/glancer/version.rb +5 -0
  87. data/lib/glancer/workflow/ar_executor.rb +104 -0
  88. data/lib/glancer/workflow/ar_extractor.rb +25 -0
  89. data/lib/glancer/workflow/ar_prompt_builder.rb +64 -0
  90. data/lib/glancer/workflow/ar_sanitizer.rb +88 -0
  91. data/lib/glancer/workflow/builder.rb +129 -0
  92. data/lib/glancer/workflow/cache.rb +55 -0
  93. data/lib/glancer/workflow/executor.rb +72 -0
  94. data/lib/glancer/workflow/llm.rb +123 -0
  95. data/lib/glancer/workflow/prompt_builder.rb +143 -0
  96. data/lib/glancer/workflow/query_enricher.rb +117 -0
  97. data/lib/glancer/workflow/sql_extractor.rb +42 -0
  98. data/lib/glancer/workflow/sql_sanitizer.rb +42 -0
  99. data/lib/glancer/workflow/sql_validator.rb +67 -0
  100. data/lib/glancer/workflow.rb +158 -0
  101. data/lib/glancer.rb +50 -0
  102. data/lib/tasks/glancer/tailwind.rake +8 -0
  103. data/lib/tasks/glancer.rake +99 -0
  104. data/spec/glancer_spec.rb +62 -0
  105. data/spec/lib/glancer/async_runner_spec.rb +133 -0
  106. data/spec/lib/glancer/chart_analyzer_spec.rb +296 -0
  107. data/spec/lib/glancer/configuration_spec.rb +858 -0
  108. data/spec/lib/glancer/engine_spec.rb +209 -0
  109. data/spec/lib/glancer/indexer/context_indexer_spec.rb +96 -0
  110. data/spec/lib/glancer/indexer/model_indexer_spec.rb +103 -0
  111. data/spec/lib/glancer/indexer/schema_indexer_spec.rb +382 -0
  112. data/spec/lib/glancer/indexer_spec.rb +95 -0
  113. data/spec/lib/glancer/retriever_spec.rb +179 -0
  114. data/spec/lib/glancer/utils/logger_spec.rb +85 -0
  115. data/spec/lib/glancer/utils/markdown_helper_spec.rb +92 -0
  116. data/spec/lib/glancer/utils/result_formatter_spec.rb +73 -0
  117. data/spec/lib/glancer/utils/table_stats_spec.rb +34 -0
  118. data/spec/lib/glancer/utils/transaction_spec.rb +73 -0
  119. data/spec/lib/glancer/workflow/ar_executor_spec.rb +155 -0
  120. data/spec/lib/glancer/workflow/ar_extractor_spec.rb +50 -0
  121. data/spec/lib/glancer/workflow/ar_prompt_builder_spec.rb +79 -0
  122. data/spec/lib/glancer/workflow/ar_sanitizer_spec.rb +175 -0
  123. data/spec/lib/glancer/workflow/builder_spec.rb +204 -0
  124. data/spec/lib/glancer/workflow/cache_spec.rb +142 -0
  125. data/spec/lib/glancer/workflow/executor_spec.rb +149 -0
  126. data/spec/lib/glancer/workflow/llm_spec.rb +124 -0
  127. data/spec/lib/glancer/workflow/prompt_builder_spec.rb +196 -0
  128. data/spec/lib/glancer/workflow/query_enricher_spec.rb +184 -0
  129. data/spec/lib/glancer/workflow/sql_extractor_spec.rb +82 -0
  130. data/spec/lib/glancer/workflow/sql_sanitizer_spec.rb +98 -0
  131. data/spec/lib/glancer/workflow/sql_validator_spec.rb +166 -0
  132. data/spec/lib/glancer/workflow_spec.rb +308 -0
  133. data/spec/models/glancer/audit_spec.rb +82 -0
  134. data/spec/models/glancer/chat_spec.rb +60 -0
  135. data/spec/models/glancer/code_version_spec.rb +71 -0
  136. data/spec/models/glancer/embedding_spec.rb +73 -0
  137. data/spec/models/glancer/message_spec.rb +144 -0
  138. data/spec/models/glancer/setting_spec.rb +88 -0
  139. data/spec/models/glancer/sql_version_spec.rb +4 -0
  140. data/spec/spec_helper.rb +128 -0
  141. data/spec/support/schema.rb +55 -0
  142. 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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Glancer::SqlVersion is now an alias for Glancer::CodeVersion.
4
+ # Tests have been moved to spec/models/glancer/code_version_spec.rb.
@@ -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