ast-merge 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 (62) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -0
  4. data/CITATION.cff +20 -0
  5. data/CODE_OF_CONDUCT.md +134 -0
  6. data/CONTRIBUTING.md +227 -0
  7. data/FUNDING.md +74 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +852 -0
  10. data/REEK +0 -0
  11. data/RUBOCOP.md +71 -0
  12. data/SECURITY.md +21 -0
  13. data/lib/ast/merge/ast_node.rb +87 -0
  14. data/lib/ast/merge/comment/block.rb +195 -0
  15. data/lib/ast/merge/comment/empty.rb +78 -0
  16. data/lib/ast/merge/comment/line.rb +138 -0
  17. data/lib/ast/merge/comment/parser.rb +278 -0
  18. data/lib/ast/merge/comment/style.rb +282 -0
  19. data/lib/ast/merge/comment.rb +36 -0
  20. data/lib/ast/merge/conflict_resolver_base.rb +399 -0
  21. data/lib/ast/merge/debug_logger.rb +271 -0
  22. data/lib/ast/merge/fenced_code_block_detector.rb +211 -0
  23. data/lib/ast/merge/file_analyzable.rb +307 -0
  24. data/lib/ast/merge/freezable.rb +82 -0
  25. data/lib/ast/merge/freeze_node_base.rb +434 -0
  26. data/lib/ast/merge/match_refiner_base.rb +312 -0
  27. data/lib/ast/merge/match_score_base.rb +135 -0
  28. data/lib/ast/merge/merge_result_base.rb +169 -0
  29. data/lib/ast/merge/merger_config.rb +258 -0
  30. data/lib/ast/merge/node_typing.rb +373 -0
  31. data/lib/ast/merge/region.rb +124 -0
  32. data/lib/ast/merge/region_detector_base.rb +114 -0
  33. data/lib/ast/merge/region_mergeable.rb +364 -0
  34. data/lib/ast/merge/rspec/shared_examples/conflict_resolver_base.rb +416 -0
  35. data/lib/ast/merge/rspec/shared_examples/debug_logger.rb +174 -0
  36. data/lib/ast/merge/rspec/shared_examples/file_analyzable.rb +193 -0
  37. data/lib/ast/merge/rspec/shared_examples/freeze_node_base.rb +219 -0
  38. data/lib/ast/merge/rspec/shared_examples/merge_result_base.rb +106 -0
  39. data/lib/ast/merge/rspec/shared_examples/merger_config.rb +202 -0
  40. data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +115 -0
  41. data/lib/ast/merge/rspec/shared_examples.rb +26 -0
  42. data/lib/ast/merge/rspec.rb +4 -0
  43. data/lib/ast/merge/section_typing.rb +303 -0
  44. data/lib/ast/merge/smart_merger_base.rb +417 -0
  45. data/lib/ast/merge/text/conflict_resolver.rb +161 -0
  46. data/lib/ast/merge/text/file_analysis.rb +168 -0
  47. data/lib/ast/merge/text/line_node.rb +142 -0
  48. data/lib/ast/merge/text/merge_result.rb +42 -0
  49. data/lib/ast/merge/text/section.rb +93 -0
  50. data/lib/ast/merge/text/section_splitter.rb +397 -0
  51. data/lib/ast/merge/text/smart_merger.rb +141 -0
  52. data/lib/ast/merge/text/word_node.rb +86 -0
  53. data/lib/ast/merge/text.rb +35 -0
  54. data/lib/ast/merge/toml_frontmatter_detector.rb +88 -0
  55. data/lib/ast/merge/version.rb +12 -0
  56. data/lib/ast/merge/yaml_frontmatter_detector.rb +108 -0
  57. data/lib/ast/merge.rb +165 -0
  58. data/lib/ast-merge.rb +4 -0
  59. data/sig/ast/merge.rbs +195 -0
  60. data.tar.gz.sig +0 -0
  61. metadata +326 -0
  62. metadata.gz.sig +0 -0
@@ -0,0 +1,416 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared examples for validating ConflictResolverBase integration
4
+ #
5
+ # Usage in your spec:
6
+ # require "ast/merge/rspec/shared_examples/conflict_resolver_base"
7
+ #
8
+ # RSpec.describe MyMerge::ConflictResolver do
9
+ # it_behaves_like "Ast::Merge::ConflictResolverBase" do
10
+ # let(:conflict_resolver_class) { MyMerge::ConflictResolver }
11
+ # let(:strategy) { :node } # or :batch or :boundary
12
+ # # Factory to create a conflict resolver instance
13
+ # let(:build_conflict_resolver) do
14
+ # ->(preference:, template_analysis:, dest_analysis:, **opts) {
15
+ # conflict_resolver_class.new(
16
+ # preference: preference,
17
+ # template_analysis: template_analysis,
18
+ # dest_analysis: dest_analysis,
19
+ # **opts
20
+ # )
21
+ # }
22
+ # end
23
+ # # Factory to create mock analysis objects
24
+ # let(:build_mock_analysis) { -> { double("Analysis") } }
25
+ # end
26
+ # end
27
+ #
28
+ # @note The extending class should inherit from or behave like Ast::Merge::ConflictResolverBase
29
+
30
+ RSpec.shared_examples("Ast::Merge::ConflictResolverBase") do
31
+ # Required let blocks:
32
+ # - conflict_resolver_class: The class under test
33
+ # - strategy: The resolution strategy (:node, :batch, or :boundary)
34
+ # - build_conflict_resolver: Lambda that creates a conflict resolver instance
35
+ # - build_mock_analysis: Lambda that creates mock analysis objects
36
+
37
+ describe "decision constants" do
38
+ it "has DECISION_DESTINATION" do
39
+ expect(conflict_resolver_class::DECISION_DESTINATION).to(eq(:destination))
40
+ end
41
+
42
+ it "has DECISION_TEMPLATE" do
43
+ expect(conflict_resolver_class::DECISION_TEMPLATE).to(eq(:template))
44
+ end
45
+
46
+ it "has DECISION_ADDED" do
47
+ expect(conflict_resolver_class::DECISION_ADDED).to(eq(:added))
48
+ end
49
+
50
+ it "has DECISION_FROZEN" do
51
+ expect(conflict_resolver_class::DECISION_FROZEN).to(eq(:frozen))
52
+ end
53
+
54
+ it "has DECISION_IDENTICAL" do
55
+ expect(conflict_resolver_class::DECISION_IDENTICAL).to(eq(:identical))
56
+ end
57
+
58
+ it "has DECISION_KEPT_DEST" do
59
+ expect(conflict_resolver_class::DECISION_KEPT_DEST).to(eq(:kept_destination))
60
+ end
61
+
62
+ it "has DECISION_KEPT_TEMPLATE" do
63
+ expect(conflict_resolver_class::DECISION_KEPT_TEMPLATE).to(eq(:kept_template))
64
+ end
65
+
66
+ it "has DECISION_APPENDED" do
67
+ expect(conflict_resolver_class::DECISION_APPENDED).to(eq(:appended))
68
+ end
69
+
70
+ it "has DECISION_FREEZE_BLOCK" do
71
+ expect(conflict_resolver_class::DECISION_FREEZE_BLOCK).to(eq(:freeze_block))
72
+ end
73
+
74
+ it "has DECISION_RECURSIVE" do
75
+ expect(conflict_resolver_class::DECISION_RECURSIVE).to(eq(:recursive))
76
+ end
77
+
78
+ it "has DECISION_REPLACED" do
79
+ expect(conflict_resolver_class::DECISION_REPLACED).to(eq(:replaced))
80
+ end
81
+ end
82
+
83
+ describe "initialization" do
84
+ let(:template_analysis) { build_mock_analysis.call }
85
+ let(:dest_analysis) { build_mock_analysis.call }
86
+
87
+ context "with :destination preference" do
88
+ let(:resolver) do
89
+ build_conflict_resolver.call(
90
+ preference: :destination,
91
+ template_analysis: template_analysis,
92
+ dest_analysis: dest_analysis,
93
+ )
94
+ end
95
+
96
+ it "sets preference to :destination" do
97
+ expect(resolver.preference).to(eq(:destination))
98
+ end
99
+
100
+ it "sets strategy correctly" do
101
+ expect(resolver.strategy).to(eq(strategy))
102
+ end
103
+
104
+ it "stores template_analysis" do
105
+ expect(resolver.template_analysis).to(eq(template_analysis))
106
+ end
107
+
108
+ it "stores dest_analysis" do
109
+ expect(resolver.dest_analysis).to(eq(dest_analysis))
110
+ end
111
+ end
112
+
113
+ context "with :template preference" do
114
+ let(:resolver) do
115
+ build_conflict_resolver.call(
116
+ preference: :template,
117
+ template_analysis: template_analysis,
118
+ dest_analysis: dest_analysis,
119
+ )
120
+ end
121
+
122
+ it "sets preference to :template" do
123
+ expect(resolver.preference).to(eq(:template))
124
+ end
125
+ end
126
+ end
127
+
128
+ describe "attr_readers" do
129
+ let(:template_analysis) { build_mock_analysis.call }
130
+ let(:dest_analysis) { build_mock_analysis.call }
131
+ let(:resolver) do
132
+ build_conflict_resolver.call(
133
+ preference: :destination,
134
+ template_analysis: template_analysis,
135
+ dest_analysis: dest_analysis,
136
+ )
137
+ end
138
+
139
+ it "has #strategy reader" do
140
+ expect(resolver).to(respond_to(:strategy))
141
+ end
142
+
143
+ it "has #preference reader" do
144
+ expect(resolver).to(respond_to(:preference))
145
+ end
146
+
147
+ it "has #template_analysis reader" do
148
+ expect(resolver).to(respond_to(:template_analysis))
149
+ end
150
+
151
+ it "has #dest_analysis reader" do
152
+ expect(resolver).to(respond_to(:dest_analysis))
153
+ end
154
+
155
+ it "has #add_template_only_nodes reader" do
156
+ expect(resolver).to(respond_to(:add_template_only_nodes))
157
+ end
158
+ end
159
+
160
+ describe "#resolve" do
161
+ let(:template_analysis) { build_mock_analysis.call }
162
+ let(:dest_analysis) { build_mock_analysis.call }
163
+ let(:resolver) do
164
+ build_conflict_resolver.call(
165
+ preference: :destination,
166
+ template_analysis: template_analysis,
167
+ dest_analysis: dest_analysis,
168
+ )
169
+ end
170
+
171
+ it "responds to #resolve" do
172
+ expect(resolver).to(respond_to(:resolve))
173
+ end
174
+ end
175
+
176
+ describe "#freeze_node?" do
177
+ let(:template_analysis) { build_mock_analysis.call }
178
+ let(:dest_analysis) { build_mock_analysis.call }
179
+ let(:resolver) do
180
+ build_conflict_resolver.call(
181
+ preference: :destination,
182
+ template_analysis: template_analysis,
183
+ dest_analysis: dest_analysis,
184
+ )
185
+ end
186
+
187
+ it "returns false for nodes without freeze_node? method" do
188
+ node = double("Node")
189
+ expect(resolver.freeze_node?(node)).to(be(false))
190
+ end
191
+
192
+ it "returns true for nodes that respond to freeze_node? and return true" do
193
+ node = double("FreezeNode", freeze_node?: true)
194
+ expect(resolver.freeze_node?(node)).to(be(true))
195
+ end
196
+
197
+ it "returns false for nodes that respond to freeze_node? and return false" do
198
+ node = double("RegularNode", freeze_node?: false)
199
+ expect(resolver.freeze_node?(node)).to(be(false))
200
+ end
201
+ end
202
+ end
203
+
204
+ RSpec.shared_examples("Ast::Merge::ConflictResolverBase validation") do
205
+ # Tests for invalid initialization arguments
206
+ # These test the base class validation directly
207
+
208
+ let(:template_analysis) { build_mock_analysis.call }
209
+ let(:dest_analysis) { build_mock_analysis.call }
210
+
211
+ describe "argument validation" do
212
+ it "raises ArgumentError for invalid strategy" do
213
+ expect do
214
+ Ast::Merge::ConflictResolverBase.new(
215
+ strategy: :invalid,
216
+ preference: :destination,
217
+ template_analysis: template_analysis,
218
+ dest_analysis: dest_analysis,
219
+ )
220
+ end.to(raise_error(ArgumentError, /Invalid strategy/))
221
+ end
222
+
223
+ it "raises ArgumentError for invalid preference" do
224
+ expect do
225
+ Ast::Merge::ConflictResolverBase.new(
226
+ strategy: :node,
227
+ preference: :invalid,
228
+ template_analysis: template_analysis,
229
+ dest_analysis: dest_analysis,
230
+ )
231
+ end.to(raise_error(ArgumentError, /Invalid preference/))
232
+ end
233
+
234
+ it "accepts :node strategy" do
235
+ expect do
236
+ Ast::Merge::ConflictResolverBase.new(
237
+ strategy: :node,
238
+ preference: :destination,
239
+ template_analysis: template_analysis,
240
+ dest_analysis: dest_analysis,
241
+ )
242
+ end.not_to(raise_error)
243
+ end
244
+
245
+ it "accepts :batch strategy" do
246
+ expect do
247
+ Ast::Merge::ConflictResolverBase.new(
248
+ strategy: :batch,
249
+ preference: :destination,
250
+ template_analysis: template_analysis,
251
+ dest_analysis: dest_analysis,
252
+ )
253
+ end.not_to(raise_error)
254
+ end
255
+
256
+ it "accepts :boundary strategy" do
257
+ expect do
258
+ Ast::Merge::ConflictResolverBase.new(
259
+ strategy: :boundary,
260
+ preference: :destination,
261
+ template_analysis: template_analysis,
262
+ dest_analysis: dest_analysis,
263
+ )
264
+ end.not_to(raise_error)
265
+ end
266
+
267
+ it "accepts :destination preference" do
268
+ expect do
269
+ Ast::Merge::ConflictResolverBase.new(
270
+ strategy: :node,
271
+ preference: :destination,
272
+ template_analysis: template_analysis,
273
+ dest_analysis: dest_analysis,
274
+ )
275
+ end.not_to(raise_error)
276
+ end
277
+
278
+ it "accepts :template preference" do
279
+ expect do
280
+ Ast::Merge::ConflictResolverBase.new(
281
+ strategy: :node,
282
+ preference: :template,
283
+ template_analysis: template_analysis,
284
+ dest_analysis: dest_analysis,
285
+ )
286
+ end.not_to(raise_error)
287
+ end
288
+ end
289
+ end
290
+
291
+ RSpec.shared_examples("Ast::Merge::ConflictResolverBase node strategy") do
292
+ # Additional examples specific to :node strategy resolvers
293
+ # Use when strategy is :node
294
+
295
+ let(:template_analysis) { build_mock_analysis.call }
296
+ let(:dest_analysis) { build_mock_analysis.call }
297
+ let(:resolver) do
298
+ build_conflict_resolver.call(
299
+ preference: :destination,
300
+ template_analysis: template_analysis,
301
+ dest_analysis: dest_analysis,
302
+ )
303
+ end
304
+
305
+ describe "node strategy" do
306
+ it "has :node strategy" do
307
+ expect(resolver.strategy).to(eq(:node))
308
+ end
309
+
310
+ it "delegates resolve to resolve_node_pair" do
311
+ expect(resolver).to(respond_to(:resolve))
312
+ end
313
+ end
314
+ end
315
+
316
+ RSpec.shared_examples("Ast::Merge::ConflictResolverBase batch strategy") do
317
+ # Additional examples specific to :batch strategy resolvers
318
+ # Use when strategy is :batch
319
+
320
+ let(:template_analysis) { build_mock_analysis.call }
321
+ let(:dest_analysis) { build_mock_analysis.call }
322
+ let(:resolver) do
323
+ build_conflict_resolver.call(
324
+ preference: :destination,
325
+ template_analysis: template_analysis,
326
+ dest_analysis: dest_analysis,
327
+ )
328
+ end
329
+
330
+ describe "batch strategy" do
331
+ it "has :batch strategy" do
332
+ expect(resolver.strategy).to(eq(:batch))
333
+ end
334
+
335
+ it "delegates resolve to resolve_batch" do
336
+ expect(resolver).to(respond_to(:resolve))
337
+ end
338
+ end
339
+
340
+ describe "#add_template_only_nodes" do
341
+ context "with default value" do
342
+ it "defaults to false" do
343
+ expect(resolver.add_template_only_nodes).to(be(false))
344
+ end
345
+ end
346
+
347
+ context "with add_template_only_nodes: true" do
348
+ let(:resolver_with_add) do
349
+ build_conflict_resolver.call(
350
+ preference: :destination,
351
+ template_analysis: template_analysis,
352
+ dest_analysis: dest_analysis,
353
+ add_template_only_nodes: true,
354
+ )
355
+ end
356
+
357
+ it "returns true when set" do
358
+ expect(resolver_with_add.add_template_only_nodes).to(be(true))
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ RSpec.shared_examples("Ast::Merge::ConflictResolverBase boundary strategy") do
365
+ # Additional examples specific to :boundary strategy resolvers
366
+ # Use when strategy is :boundary
367
+ #
368
+ # Boundary strategy is used for ASTs where content is processed
369
+ # in sections/ranges rather than individual nodes or all at once.
370
+ # This is typical for languages like Ruby where file alignment
371
+ # identifies boundaries (sections with differences) between
372
+ # template and destination files.
373
+
374
+ let(:template_analysis) { build_mock_analysis.call }
375
+ let(:dest_analysis) { build_mock_analysis.call }
376
+ let(:resolver) do
377
+ build_conflict_resolver.call(
378
+ preference: :destination,
379
+ template_analysis: template_analysis,
380
+ dest_analysis: dest_analysis,
381
+ )
382
+ end
383
+
384
+ describe "boundary strategy" do
385
+ it "has :boundary strategy" do
386
+ expect(resolver.strategy).to(eq(:boundary))
387
+ end
388
+
389
+ it "delegates resolve to resolve_boundary" do
390
+ expect(resolver).to(respond_to(:resolve))
391
+ end
392
+ end
393
+
394
+ describe "#add_template_only_nodes" do
395
+ context "with default value" do
396
+ it "defaults to false" do
397
+ expect(resolver.add_template_only_nodes).to(be(false))
398
+ end
399
+ end
400
+
401
+ context "with add_template_only_nodes: true" do
402
+ let(:resolver_with_add) do
403
+ build_conflict_resolver.call(
404
+ preference: :destination,
405
+ template_analysis: template_analysis,
406
+ dest_analysis: dest_analysis,
407
+ add_template_only_nodes: true,
408
+ )
409
+ end
410
+
411
+ it "returns true when set" do
412
+ expect(resolver_with_add.add_template_only_nodes).to(be(true))
413
+ end
414
+ end
415
+ end
416
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "silent_stream"
4
+ require "rspec/stubbed_env"
5
+
6
+ # Shared examples for validating DebugLogger integration
7
+ #
8
+ # Usage in your spec:
9
+ # require "ast/merge/rspec/shared_examples/debug_logger"
10
+ #
11
+ # RSpec.describe MyMerge::DebugLogger do
12
+ # it_behaves_like "Ast::Merge::DebugLogger" do
13
+ # let(:described_logger) { MyMerge::DebugLogger }
14
+ # let(:env_var_name) { "MY_MERGE_DEBUG" }
15
+ # let(:log_prefix) { "[MyMerge]" }
16
+ # end
17
+ # end
18
+ #
19
+ # @note The extending module must configure:
20
+ # - self.env_var_name = "YOUR_ENV_VAR"
21
+ # - self.log_prefix = "[YourPrefix]"
22
+
23
+ RSpec.shared_examples("Ast::Merge::DebugLogger") do
24
+ include SilentStream
25
+
26
+ include_context "with stubbed env"
27
+
28
+ # Required let blocks that must be provided by including spec:
29
+ # - described_logger: The module that extends Ast::Merge::DebugLogger
30
+ # - env_var_name: Expected environment variable name
31
+ # - log_prefix: Expected log prefix
32
+
33
+ describe "module configuration" do
34
+ it "has env_var_name configured" do
35
+ expect(described_logger.env_var_name).to(eq(env_var_name))
36
+ end
37
+
38
+ it "has log_prefix configured" do
39
+ expect(described_logger.log_prefix).to(eq(log_prefix))
40
+ end
41
+
42
+ it "has BENCHMARK_AVAILABLE constant from base" do
43
+ expect(described_logger::BENCHMARK_AVAILABLE).to(eq(Ast::Merge::DebugLogger::BENCHMARK_AVAILABLE))
44
+ end
45
+ end
46
+
47
+ describe "core methods from Ast::Merge::DebugLogger" do
48
+ it "responds to #enabled?" do
49
+ expect(described_logger).to(respond_to(:enabled?))
50
+ end
51
+
52
+ it "responds to #debug" do
53
+ expect(described_logger).to(respond_to(:debug))
54
+ end
55
+
56
+ it "responds to #info" do
57
+ expect(described_logger).to(respond_to(:info))
58
+ end
59
+
60
+ it "responds to #warning" do
61
+ expect(described_logger).to(respond_to(:warning))
62
+ end
63
+
64
+ it "responds to #time" do
65
+ expect(described_logger).to(respond_to(:time))
66
+ end
67
+
68
+ it "responds to #log_node" do
69
+ expect(described_logger).to(respond_to(:log_node))
70
+ end
71
+
72
+ it "responds to #extract_node_info" do
73
+ expect(described_logger).to(respond_to(:extract_node_info))
74
+ end
75
+
76
+ it "responds to #safe_type_name" do
77
+ expect(described_logger).to(respond_to(:safe_type_name))
78
+ end
79
+
80
+ it "responds to #extract_lines" do
81
+ expect(described_logger).to(respond_to(:extract_lines))
82
+ end
83
+ end
84
+
85
+ describe "when debug is enabled" do
86
+ before do
87
+ stub_env(env_var_name => "1")
88
+ end
89
+
90
+ it "#enabled? returns true" do
91
+ expect(described_logger.enabled?).to(be(true))
92
+ end
93
+
94
+ it "#debug outputs message with configured prefix" do
95
+ expect { described_logger.debug("test message") }
96
+ .to(output(/#{Regexp.escape(log_prefix)} test message/).to_stderr)
97
+ end
98
+
99
+ it "#debug includes context hash when provided" do
100
+ expect { described_logger.debug("test", key: "value") }
101
+ .to(output(/key.*value/).to_stderr)
102
+ end
103
+
104
+ it "#info outputs with INFO label" do
105
+ expect { described_logger.info("info message") }
106
+ .to(output(/#{Regexp.escape(log_prefix)} INFO\] info message/).to_stderr)
107
+ end
108
+
109
+ it "#time logs start and completion with timing" do
110
+ output = capture(:stderr) { described_logger.time("test operation") { 42 } }
111
+ expect(output).to(include("Starting: test operation"))
112
+ expect(output).to(include("Completed: test operation"))
113
+ expect(output).to(match(/real_ms/))
114
+ end
115
+
116
+ it "#time returns the block result" do
117
+ result = described_logger.time("operation") { 42 }
118
+ expect(result).to(eq(42))
119
+ end
120
+ end
121
+
122
+ describe "when debug is disabled" do
123
+ before do
124
+ stub_env(env_var_name => nil)
125
+ end
126
+
127
+ it "#enabled? returns false" do
128
+ expect(described_logger.enabled?).to(be(false))
129
+ end
130
+
131
+ it "#debug does not output anything" do
132
+ expect { described_logger.debug("test") }.not_to(output.to_stderr)
133
+ end
134
+
135
+ it "#info does not output anything" do
136
+ expect { described_logger.info("test") }.not_to(output.to_stderr)
137
+ end
138
+
139
+ it "#time still executes block and returns result without logging" do
140
+ output = capture(:stderr) { @result = described_logger.time("operation") { 42 } }
141
+ expect(@result).to(eq(42))
142
+ expect(output).to(be_empty)
143
+ end
144
+ end
145
+
146
+ describe "#warning (always outputs)" do
147
+ before do
148
+ stub_env(env_var_name => nil)
149
+ end
150
+
151
+ it "outputs warning even when debug is disabled" do
152
+ expect { described_logger.warning("warning message") }
153
+ .to(output(/#{Regexp.escape(log_prefix)} WARNING\] warning message/).to_stderr)
154
+ end
155
+ end
156
+
157
+ describe "#extract_node_info" do
158
+ it "extracts type name from object" do
159
+ node = Object.new
160
+ info = described_logger.extract_node_info(node)
161
+ expect(info[:type]).to(eq("Object"))
162
+ end
163
+ end
164
+
165
+ describe "#safe_type_name" do
166
+ it "returns class name for standard objects" do
167
+ expect(described_logger.safe_type_name("test")).to(eq("String"))
168
+ end
169
+
170
+ it "returns short name without module prefix" do
171
+ expect(described_logger.safe_type_name([])).to(eq("Array"))
172
+ end
173
+ end
174
+ end