brute 0.4.1 → 1.0.1

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent.rb +14 -0
  3. data/lib/brute/diff.rb +18 -28
  4. data/lib/brute/loop/agent_stream.rb +118 -0
  5. data/lib/brute/loop/agent_turn.rb +520 -0
  6. data/lib/brute/{compactor.rb → loop/compactor.rb} +2 -0
  7. data/lib/brute/{doom_loop.rb → loop/doom_loop.rb} +2 -0
  8. data/lib/brute/loop/step.rb +332 -0
  9. data/lib/brute/loop/tool_call_step.rb +90 -0
  10. data/lib/brute/middleware/compaction_check.rb +60 -146
  11. data/lib/brute/middleware/doom_loop_detection.rb +95 -92
  12. data/lib/brute/middleware/llm_call.rb +78 -80
  13. data/lib/brute/middleware/message_tracking.rb +115 -162
  14. data/lib/brute/middleware/otel/span.rb +25 -106
  15. data/lib/brute/middleware/otel/token_usage.rb +29 -84
  16. data/lib/brute/middleware/otel/tool_calls.rb +23 -107
  17. data/lib/brute/middleware/otel/tool_results.rb +22 -86
  18. data/lib/brute/middleware/reasoning_normalizer.rb +78 -103
  19. data/lib/brute/middleware/retry.rb +95 -76
  20. data/lib/brute/middleware/session_persistence.rb +38 -37
  21. data/lib/brute/middleware/token_tracking.rb +64 -63
  22. data/lib/brute/middleware/tool_error_tracking.rb +108 -82
  23. data/lib/brute/middleware/tool_use_guard.rb +57 -90
  24. data/lib/brute/middleware/tracing.rb +53 -63
  25. data/lib/brute/middleware.rb +18 -0
  26. data/lib/brute/orchestrator/turn.rb +105 -0
  27. data/lib/brute/pipeline.rb +77 -133
  28. data/lib/brute/prompts/build_switch.rb +21 -25
  29. data/lib/brute/prompts/environment.rb +31 -35
  30. data/lib/brute/prompts/identity.rb +22 -29
  31. data/lib/brute/prompts/instructions.rb +15 -18
  32. data/lib/brute/prompts/max_steps.rb +18 -25
  33. data/lib/brute/prompts/plan_reminder.rb +18 -26
  34. data/lib/brute/prompts/skills.rb +8 -30
  35. data/lib/brute/prompts.rb +28 -0
  36. data/lib/brute/providers/ollama.rb +135 -0
  37. data/lib/brute/providers/shell.rb +2 -2
  38. data/lib/brute/providers/shell_response.rb +2 -2
  39. data/lib/brute/providers.rb +62 -0
  40. data/lib/brute/queue/base_queue.rb +222 -0
  41. data/lib/brute/{file_mutation_queue.rb → queue/file_mutation_queue.rb} +28 -26
  42. data/lib/brute/queue/parallel_queue.rb +66 -0
  43. data/lib/brute/queue/sequential_queue.rb +63 -0
  44. data/lib/brute/store/message_store.rb +362 -0
  45. data/lib/brute/store/session.rb +106 -0
  46. data/lib/brute/{snapshot_store.rb → store/snapshot_store.rb} +2 -0
  47. data/lib/brute/{todo_store.rb → store/todo_store.rb} +2 -0
  48. data/lib/brute/system_prompt.rb +81 -194
  49. data/lib/brute/tools/delegate.rb +46 -116
  50. data/lib/brute/tools/fs_patch.rb +36 -37
  51. data/lib/brute/tools/fs_remove.rb +2 -2
  52. data/lib/brute/tools/fs_undo.rb +2 -2
  53. data/lib/brute/tools/fs_write.rb +29 -41
  54. data/lib/brute/tools/todo_read.rb +1 -1
  55. data/lib/brute/tools/todo_write.rb +1 -1
  56. data/lib/brute/tools.rb +31 -0
  57. data/lib/brute/version.rb +1 -1
  58. data/lib/brute.rb +40 -204
  59. metadata +31 -20
  60. data/lib/brute/agent_stream.rb +0 -181
  61. data/lib/brute/hooks.rb +0 -84
  62. data/lib/brute/message_store.rb +0 -463
  63. data/lib/brute/orchestrator.rb +0 -550
  64. data/lib/brute/session.rb +0 -161
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if __FILE__ == $0
4
- require "bundler/setup"
5
- require "brute"
6
- end
3
+ require "bundler/setup"
4
+ require "brute"
7
5
 
8
6
  module Brute
9
7
  # Deferred system prompt builder.
@@ -110,6 +108,18 @@ module Brute
110
108
  Prompts::Instructions,
111
109
  ],
112
110
 
111
+ # Ollama — lean stack for local models with smaller context windows
112
+ "ollama" => [
113
+ Prompts::Identity,
114
+ Prompts::ToneAndStyle,
115
+ Prompts::Conventions,
116
+ Prompts::DoingTasks,
117
+ Prompts::ToolUsage,
118
+ Prompts::GitSafety,
119
+ Prompts::Environment,
120
+ Prompts::Instructions,
121
+ ],
122
+
113
123
  # Fallback — conservative, concise, fewer than 4 lines
114
124
  "default" => [
115
125
  Prompts::Identity,
@@ -170,211 +180,88 @@ module Brute
170
180
  end
171
181
  end
172
182
 
173
- if __FILE__ == $0
174
- require_relative "../../spec/spec_helper"
175
-
176
- RSpec.describe Brute::SystemPrompt do
177
- describe ".build" do
178
- it "stores a block and executes it on prepare" do
179
- sp = described_class.build do |prompt, ctx|
180
- prompt << "hello #{ctx[:name]}"
181
- end
182
-
183
- result = sp.prepare(name: "world")
184
- expect(result.to_s).to eq("hello world")
185
- end
186
-
187
- it "returns a Result with sections" do
188
- sp = described_class.build do |prompt, _ctx|
189
- prompt << "section one"
190
- prompt << "section two"
191
- end
192
-
193
- result = sp.prepare({})
194
- expect(result.sections).to eq(["section one", "section two"])
195
- end
196
-
197
- it "strips nil and empty sections" do
198
- sp = described_class.build do |prompt, _ctx|
199
- prompt << "kept"
200
- prompt << nil
201
- prompt << ""
202
- prompt << "also kept"
203
- end
204
-
205
- result = sp.prepare({})
206
- expect(result.sections).to eq(["kept", "also kept"])
207
- end
208
- end
209
-
210
- describe Brute::SystemPrompt::Result do
211
- subject(:result) { described_class.new(["a", "b", "c"]) }
212
-
213
- it "joins sections with double newlines via to_s" do
214
- expect(result.to_s).to eq("a\n\nb\n\nc")
215
- end
216
-
217
- it "iterates over sections via each" do
218
- collected = []
219
- result.each { |s| collected << s }
220
- expect(collected).to eq(["a", "b", "c"])
221
- end
222
-
223
- it "reports empty? correctly" do
224
- expect(described_class.new([]).empty?).to be true
225
- expect(result.empty?).to be false
226
- end
227
- end
228
-
229
- describe ".default" do
230
- # Minimal context for all tests — enough to avoid nil errors
231
- let(:base_ctx) do
232
- {
233
- provider_name: "anthropic",
234
- model_name: "test-model",
235
- cwd: Dir.pwd,
236
- custom_rules: nil,
237
- agent: nil,
238
- agent_switched: nil,
239
- max_steps_reached: nil,
240
- }
241
- end
242
-
243
- let(:builder) { described_class.default }
244
-
245
- # ── Provider stack selection ──
246
-
247
- described_class::STACKS.each_key do |provider|
248
- context "with provider '#{provider}'" do
249
- it "produces non-empty sections" do
250
- ctx = base_ctx.merge(provider_name: provider)
251
- result = builder.prepare(ctx)
252
- expect(result.sections).not_to be_empty
253
- result.sections.each do |section|
254
- expect(section).to be_a(String)
255
- end
256
- end
257
- end
258
- end
259
-
260
- it "falls back to 'default' stack for unknown providers" do
261
- default_result = builder.prepare(base_ctx.merge(provider_name: "default"))
262
- unknown_result = builder.prepare(base_ctx.merge(provider_name: "unknown_provider"))
263
-
264
- # Both should produce the same number of sections (same stack)
265
- expect(unknown_result.sections.size).to eq(default_result.sections.size)
266
- end
267
-
268
- # ── Plan mode conditional ──
269
-
270
- context "when agent is 'plan'" do
271
- it "includes PlanReminder" do
272
- result = builder.prepare(base_ctx.merge(agent: "plan"))
273
- expect(result.to_s).to include("Plan Mode")
274
- expect(result.to_s).to include("<system-reminder>")
275
- expect(result.to_s).to include("READ-ONLY")
276
- end
277
- end
183
+ test do
184
+ def base_ctx
185
+ { provider_name: "anthropic", model_name: "test-model", cwd: Dir.pwd,
186
+ custom_rules: nil, agent: nil, agent_switched: nil, max_steps_reached: nil }
187
+ end
278
188
 
279
- context "when agent is 'build'" do
280
- it "does NOT include PlanReminder" do
281
- result = builder.prepare(base_ctx.merge(agent: "build"))
282
- expect(result.to_s).not_to include("Plan Mode - System Reminder")
283
- expect(result.to_s).not_to include("READ-ONLY")
284
- end
285
- end
189
+ it "stores a block and executes it on prepare" do
190
+ sp = Brute::SystemPrompt.build { |p, ctx| p << "hello #{ctx[:name]}" }
191
+ sp.prepare(name: "world").to_s.should == "hello world"
192
+ end
286
193
 
287
- context "when agent is nil" do
288
- it "does NOT include PlanReminder" do
289
- result = builder.prepare(base_ctx.merge(agent: nil))
290
- expect(result.to_s).not_to include("Plan Mode - System Reminder")
291
- end
292
- end
194
+ it "returns a Result with sections" do
195
+ sp = Brute::SystemPrompt.build { |p, _| p << "section one"; p << "section two" }
196
+ sp.prepare({}).sections.should == ["section one", "section two"]
197
+ end
293
198
 
294
- # ── Build switch conditional ──
199
+ it "strips nil and empty sections" do
200
+ sp = Brute::SystemPrompt.build { |p, _| p << "kept"; p << nil; p << ""; p << "also kept" }
201
+ sp.prepare({}).sections.should == ["kept", "also kept"]
202
+ end
295
203
 
296
- context "when agent_switched is 'build'" do
297
- it "includes BuildSwitch" do
298
- result = builder.prepare(base_ctx.merge(agent_switched: "build"))
299
- expect(result.to_s).to include("operational mode has changed from plan to build")
300
- end
301
- end
204
+ it "joins sections with double newlines via to_s" do
205
+ Brute::SystemPrompt::Result.new(["a", "b", "c"]).to_s.should == "a\n\nb\n\nc"
206
+ end
302
207
 
303
- context "when agent_switched is nil" do
304
- it "does NOT include BuildSwitch" do
305
- result = builder.prepare(base_ctx.merge(agent_switched: nil))
306
- expect(result.to_s).not_to include("operational mode has changed")
307
- end
308
- end
208
+ it "reports empty? correctly for empty result" do
209
+ Brute::SystemPrompt::Result.new([]).empty?.should.be.true
210
+ end
309
211
 
310
- # ── Max steps conditional ──
212
+ it "reports empty? correctly for non-empty result" do
213
+ Brute::SystemPrompt::Result.new(["a"]).empty?.should.be.false
214
+ end
311
215
 
312
- context "when max_steps_reached is truthy" do
313
- it "includes MaxSteps" do
314
- result = builder.prepare(base_ctx.merge(max_steps_reached: true))
315
- expect(result.to_s).to include("MAXIMUM STEPS REACHED")
316
- expect(result.to_s).to include("Tools are disabled")
317
- end
318
- end
216
+ it "falls back to default stack for unknown providers" do
217
+ builder = Brute::SystemPrompt.default
218
+ default_r = builder.prepare(base_ctx.merge(provider_name: "default"))
219
+ unknown_r = builder.prepare(base_ctx.merge(provider_name: "unknown_provider"))
220
+ unknown_r.sections.size.should == default_r.sections.size
221
+ end
319
222
 
320
- context "when max_steps_reached is falsy" do
321
- it "does NOT include MaxSteps" do
322
- result = builder.prepare(base_ctx.merge(max_steps_reached: nil))
323
- expect(result.to_s).not_to include("MAXIMUM STEPS REACHED")
324
- end
325
- end
223
+ it "includes PlanReminder when agent is plan" do
224
+ builder = Brute::SystemPrompt.default
225
+ builder.prepare(base_ctx.merge(agent: "plan")).to_s.should =~ /READ-ONLY/
226
+ end
326
227
 
327
- # ── Combined states (mid-session switch scenarios) ──
228
+ it "excludes PlanReminder when agent is build" do
229
+ builder = Brute::SystemPrompt.default
230
+ (builder.prepare(base_ctx.merge(agent: "build")).to_s =~ /Plan Mode - System Reminder/).should.be.nil
231
+ end
328
232
 
329
- context "plan mode includes PlanReminder but excludes BuildSwitch" do
330
- it "contains only plan-specific content" do
331
- result = builder.prepare(base_ctx.merge(agent: "plan"))
332
- expect(result.to_s).to include("READ-ONLY")
333
- expect(result.to_s).not_to include("operational mode has changed")
334
- end
335
- end
233
+ it "includes BuildSwitch when agent_switched is build" do
234
+ builder = Brute::SystemPrompt.default
235
+ builder.prepare(base_ctx.merge(agent_switched: "build")).to_s.should =~ /operational mode has changed/
236
+ end
336
237
 
337
- context "build mode with agent_switched includes BuildSwitch but excludes PlanReminder" do
338
- it "contains only build-switch content" do
339
- result = builder.prepare(base_ctx.merge(agent: "build", agent_switched: "build"))
340
- expect(result.to_s).to include("operational mode has changed from plan to build")
341
- expect(result.to_s).not_to include("Plan Mode - System Reminder")
342
- end
343
- end
238
+ it "excludes BuildSwitch when agent_switched is nil" do
239
+ builder = Brute::SystemPrompt.default
240
+ (builder.prepare(base_ctx.merge(agent_switched: nil)).to_s =~ /operational mode has changed/).should.be.nil
241
+ end
344
242
 
345
- context "plan mode with max_steps_reached includes both" do
346
- it "contains both PlanReminder and MaxSteps" do
347
- result = builder.prepare(base_ctx.merge(agent: "plan", max_steps_reached: true))
348
- expect(result.to_s).to include("READ-ONLY")
349
- expect(result.to_s).to include("MAXIMUM STEPS REACHED")
350
- end
351
- end
243
+ it "includes MaxSteps when max_steps_reached" do
244
+ builder = Brute::SystemPrompt.default
245
+ builder.prepare(base_ctx.merge(max_steps_reached: true)).to_s.should =~ /MAXIMUM STEPS REACHED/
246
+ end
352
247
 
353
- # ── Provider stack composition ──
248
+ it "excludes MaxSteps when max_steps_reached is nil" do
249
+ builder = Brute::SystemPrompt.default
250
+ (builder.prepare(base_ctx.merge(max_steps_reached: nil)).to_s =~ /MAXIMUM STEPS REACHED/).should.be.nil
251
+ end
354
252
 
355
- it "anthropic stack includes expected modules" do
356
- result = builder.prepare(base_ctx.merge(provider_name: "anthropic"))
357
- text = result.to_s
358
- # Anthropic has Identity, Objectivity, Conventions, GitSafety etc.
359
- expect(text).to include("Professional objectivity")
360
- expect(text).to include("Following conventions")
361
- expect(text).to include("Git safety")
362
- end
253
+ it "anthropic stack includes conventions" do
254
+ builder = Brute::SystemPrompt.default
255
+ builder.prepare(base_ctx.merge(provider_name: "anthropic")).to_s.should =~ /Following conventions/
256
+ end
363
257
 
364
- it "openai stack includes editing-focused modules" do
365
- result = builder.prepare(base_ctx.merge(provider_name: "openai"))
366
- text = result.to_s
367
- # OpenAI has EditingApproach, Autonomy, EditingConstraints
368
- expect(text).to include("Following conventions")
369
- expect(text).to include("Git safety")
370
- end
258
+ it "openai stack includes conventions" do
259
+ builder = Brute::SystemPrompt.default
260
+ builder.prepare(base_ctx.merge(provider_name: "openai")).to_s.should =~ /Following conventions/
261
+ end
371
262
 
372
- it "google stack includes security module" do
373
- result = builder.prepare(base_ctx.merge(provider_name: "google"))
374
- text = result.to_s
375
- expect(text).to include("Following conventions")
376
- expect(text).to include("Git safety")
377
- end
378
- end
263
+ it "google stack includes conventions" do
264
+ builder = Brute::SystemPrompt.default
265
+ builder.prepare(base_ctx.merge(provider_name: "google")).to_s.should =~ /Following conventions/
379
266
  end
380
267
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if __FILE__ == $0
4
- require "bundler/setup"
5
- require "brute"
6
- end
3
+ require "bundler/setup"
4
+ require "brute"
7
5
 
8
6
  module Brute
9
7
  module Tools
@@ -63,126 +61,58 @@ module Brute
63
61
  end
64
62
  end
65
63
 
66
- if __FILE__ == $0
67
- require_relative "../../../spec/spec_helper"
68
-
69
- RSpec.describe Brute::Tools::Delegate do
70
- let(:provider) { MockProvider.new }
71
-
72
- # Simple stand-in for LLM::Message in context history.
73
- FakeMessage = Struct.new(:role, :content) do
74
- def assistant?
75
- role == :assistant
76
- end
77
- end
78
-
79
- before do
80
- allow(Brute).to receive(:provider).and_return(provider)
81
- end
82
-
83
- def fake_context(messages)
84
- msgs_obj = Object.new
85
- msgs_obj.define_singleton_method(:to_a) { messages }
86
- ctx = Object.new
87
- ctx.define_singleton_method(:messages) { msgs_obj }
88
- ctx
89
- end
90
-
91
- describe "#call" do
92
- it "returns the sub-agent's text content" do
93
- result = described_class.new.call(task: "What files exist?")
94
- expect(result).to be_a(Hash)
95
- expect(result[:result]).to eq("mock response")
96
- end
97
- end
98
-
99
- describe "#extract_content" do
100
- let(:delegate) { described_class.new }
101
-
102
- it "returns content when response has text" do
103
- res = MockResponse.new(content: "analysis complete")
104
- context = fake_context([])
105
- result = delegate.send(:extract_content, res, context)
106
- expect(result).to eq("analysis complete")
107
- end
108
-
109
- context "when res.content raises NoMethodError (tool-only response)" do
110
- let(:bad_res) do
111
- obj = Object.new
112
- obj.define_singleton_method(:content) do
113
- raise NoMethodError, "undefined method 'content' for nil"
114
- end
115
- obj
116
- end
117
-
118
- it "falls back to the last assistant text in context messages" do
119
- context = fake_context([
120
- FakeMessage.new(:user, "input"),
121
- FakeMessage.new(:assistant, "found the answer"),
122
- ])
123
-
124
- result = delegate.send(:extract_content, bad_res, context)
125
- expect(result).to eq("found the answer")
126
- end
127
-
128
- it "returns fallback text when no assistant messages exist" do
129
- context = fake_context([])
130
- result = delegate.send(:extract_content, bad_res, context)
131
- expect(result).to eq("(sub-agent completed but produced no text response)")
132
- end
133
-
134
- it "skips assistant messages with empty content" do
135
- context = fake_context([
136
- FakeMessage.new(:assistant, "real answer"),
137
- FakeMessage.new(:assistant, ""),
138
- ])
139
-
140
- result = delegate.send(:extract_content, bad_res, context)
141
- expect(result).to eq("real answer")
142
- end
64
+ test do
65
+ require_relative "../../../spec/support/mock_provider"
66
+ require_relative "../../../spec/support/mock_response"
143
67
 
144
- it "skips assistant messages with non-string content" do
145
- context = fake_context([
146
- FakeMessage.new(:assistant, "text answer"),
147
- FakeMessage.new(:assistant, [{"type" => "tool_use"}]),
148
- ])
68
+ FakeMsg = Struct.new(:role, :content) do
69
+ def assistant?; role == :assistant; end
70
+ end
149
71
 
150
- result = delegate.send(:extract_content, bad_res, context)
151
- expect(result).to eq("text answer")
152
- end
153
- end
72
+ def fake_context(messages)
73
+ msgs_obj = Object.new
74
+ msgs_obj.define_singleton_method(:to_a) { messages }
75
+ ctx = Object.new
76
+ ctx.define_singleton_method(:messages) { msgs_obj }
77
+ ctx
78
+ end
154
79
 
155
- context "when res.content returns nil (empty response)" do
156
- let(:nil_res) { Struct.new(:content).new(nil) }
80
+ delegate = Brute::Tools::Delegate.new
157
81
 
158
- it "falls back to the last assistant text in context messages" do
159
- context = fake_context([
160
- FakeMessage.new(:assistant, "previous answer"),
161
- ])
82
+ it "returns content when response has text" do
83
+ res = MockResponse.new(content: "analysis complete")
84
+ delegate.send(:extract_content, res, fake_context([])).should == "analysis complete"
85
+ end
162
86
 
163
- result = delegate.send(:extract_content, nil_res, context)
164
- expect(result).to eq("previous answer")
165
- end
87
+ it "falls back to last assistant text on NoMethodError" do
88
+ bad_res = Object.new
89
+ bad_res.define_singleton_method(:content) { raise NoMethodError }
90
+ ctx = fake_context([FakeMsg.new(:user, "input"), FakeMsg.new(:assistant, "found the answer")])
91
+ delegate.send(:extract_content, bad_res, ctx).should == "found the answer"
92
+ end
166
93
 
167
- it "returns fallback text when no assistant messages exist" do
168
- context = fake_context([])
169
- result = delegate.send(:extract_content, nil_res, context)
170
- expect(result).to eq("(sub-agent completed but produced no text response)")
171
- end
172
- end
94
+ it "returns fallback when no assistant messages exist" do
95
+ bad_res = Object.new
96
+ bad_res.define_singleton_method(:content) { raise NoMethodError }
97
+ delegate.send(:extract_content, bad_res, fake_context([])).should == "(sub-agent completed but produced no text response)"
98
+ end
173
99
 
174
- context "when res.content returns empty string" do
175
- let(:empty_res) { Struct.new(:content).new("") }
100
+ it "skips assistant messages with empty content" do
101
+ bad_res = Object.new
102
+ bad_res.define_singleton_method(:content) { raise NoMethodError }
103
+ ctx = fake_context([FakeMsg.new(:assistant, "real answer"), FakeMsg.new(:assistant, "")])
104
+ delegate.send(:extract_content, bad_res, ctx).should == "real answer"
105
+ end
176
106
 
177
- it "falls back to the last assistant text in context messages" do
178
- context = fake_context([
179
- FakeMessage.new(:assistant, "previous answer"),
180
- ])
107
+ it "falls back to last assistant on nil content" do
108
+ nil_res = Struct.new(:content).new(nil)
109
+ ctx = fake_context([FakeMsg.new(:assistant, "previous answer")])
110
+ delegate.send(:extract_content, nil_res, ctx).should == "previous answer"
111
+ end
181
112
 
182
- result = delegate.send(:extract_content, empty_res, context)
183
- expect(result).to eq("previous answer")
184
- end
185
- end
186
- end
113
+ it "falls back to last assistant on empty string content" do
114
+ empty_res = Struct.new(:content).new("")
115
+ ctx = fake_context([FakeMsg.new(:assistant, "previous answer")])
116
+ delegate.send(:extract_content, empty_res, ctx).should == "previous answer"
187
117
  end
188
118
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if __FILE__ == $0
4
- require "bundler/setup"
5
- require "brute"
6
- end
3
+ require "bundler/setup"
4
+ require "brute"
7
5
 
8
6
  module Brute
9
7
  module Tools
@@ -19,13 +17,13 @@ module Brute
19
17
 
20
18
  def call(file_path:, old_string:, new_string:, replace_all: false)
21
19
  path = File.expand_path(file_path)
22
- Brute::FileMutationQueue.serialize(path) do
20
+ Brute::Queue::FileMutationQueue.serialize(path) do
23
21
  raise "File not found: #{path}" unless File.exist?(path)
24
22
 
25
23
  original = File.read(path)
26
24
  raise "old_string not found in #{path}" unless original.include?(old_string)
27
25
 
28
- Brute::SnapshotStore.save(path)
26
+ Brute::Store::SnapshotStore.save(path)
29
27
 
30
28
  updated = if replace_all
31
29
  original.gsub(old_string, new_string)
@@ -43,50 +41,51 @@ module Brute
43
41
  end
44
42
  end
45
43
 
46
- if __FILE__ == $0
47
- require_relative "../../../spec/spec_helper"
48
-
49
- RSpec.describe Brute::Tools::FSPatch do
50
- around(:each) { |ex| Dir.mktmpdir { |d| @dir = d; ex.run } }
44
+ test do
45
+ require "tmpdir"
51
46
 
52
- let(:tool) { described_class.new }
53
-
54
- it "replaces old_string with new_string" do
55
- path = File.join(@dir, "test.rb")
47
+ it "replaces old_string with new_string" do
48
+ Dir.mktmpdir do |dir|
49
+ path = File.join(dir, "test.rb")
56
50
  File.write(path, "hello world\n")
57
- result = tool.call(file_path: path, old_string: "world", new_string: "ruby")
58
- expect(result[:success]).to be true
59
- expect(File.read(path)).to eq("hello ruby\n")
51
+ result = Brute::Tools::FSPatch.new.call(file_path: path, old_string: "world", new_string: "ruby")
52
+ File.read(path).should == "hello ruby\n"
60
53
  end
54
+ end
61
55
 
62
- it "returns a unified diff" do
63
- path = File.join(@dir, "test.rb")
56
+ it "returns a unified diff" do
57
+ Dir.mktmpdir do |dir|
58
+ path = File.join(dir, "test.rb")
64
59
  File.write(path, "line1\nold line\nline3\n")
65
- result = tool.call(file_path: path, old_string: "old line", new_string: "new line")
66
- expect(result[:diff]).to include("-old line")
67
- expect(result[:diff]).to include("+new line")
60
+ result = Brute::Tools::FSPatch.new.call(file_path: path, old_string: "old line", new_string: "new line")
61
+ result[:diff].should =~ /\-old line/
68
62
  end
63
+ end
69
64
 
70
- it "raises when file not found" do
71
- expect {
72
- tool.call(file_path: File.join(@dir, "nope.rb"), old_string: "a", new_string: "b")
73
- }.to raise_error(/File not found/)
65
+ it "raises when file not found" do
66
+ Dir.mktmpdir do |dir|
67
+ lambda {
68
+ Brute::Tools::FSPatch.new.call(file_path: File.join(dir, "nope.rb"), old_string: "a", new_string: "b")
69
+ }.should.raise(RuntimeError)
74
70
  end
71
+ end
75
72
 
76
- it "raises when old_string not found" do
77
- path = File.join(@dir, "test.rb")
73
+ it "raises when old_string not found" do
74
+ Dir.mktmpdir do |dir|
75
+ path = File.join(dir, "test.rb")
78
76
  File.write(path, "hello\n")
79
- expect {
80
- tool.call(file_path: path, old_string: "missing", new_string: "new")
81
- }.to raise_error(/old_string not found/)
77
+ lambda {
78
+ Brute::Tools::FSPatch.new.call(file_path: path, old_string: "missing", new_string: "new")
79
+ }.should.raise(RuntimeError)
82
80
  end
81
+ end
83
82
 
84
- it "supports replace_all" do
85
- path = File.join(@dir, "test.rb")
83
+ it "supports replace_all" do
84
+ Dir.mktmpdir do |dir|
85
+ path = File.join(dir, "test.rb")
86
86
  File.write(path, "aaa bbb aaa\n")
87
- result = tool.call(file_path: path, old_string: "aaa", new_string: "ccc", replace_all: true)
88
- expect(result[:replacements]).to eq(2)
89
- expect(File.read(path)).to eq("ccc bbb ccc\n")
87
+ result = Brute::Tools::FSPatch.new.call(file_path: path, old_string: "aaa", new_string: "ccc", replace_all: true)
88
+ result[:replacements].should == 2
90
89
  end
91
90
  end
92
91
  end
@@ -17,10 +17,10 @@ module Brute
17
17
 
18
18
  def call(path:)
19
19
  target = File.expand_path(path)
20
- Brute::FileMutationQueue.serialize(target) do
20
+ Brute::Queue::FileMutationQueue.serialize(target) do
21
21
  raise "Path not found: #{target}" unless File.exist?(target)
22
22
 
23
- Brute::SnapshotStore.save(target) if File.file?(target)
23
+ Brute::Store::SnapshotStore.save(target) if File.file?(target)
24
24
 
25
25
  if File.directory?(target)
26
26
  Dir.rmdir(target)
@@ -16,8 +16,8 @@ module Brute
16
16
 
17
17
  def call(path:)
18
18
  target = File.expand_path(path)
19
- Brute::FileMutationQueue.serialize(target) do
20
- snapshot = Brute::SnapshotStore.pop(target)
19
+ Brute::Queue::FileMutationQueue.serialize(target) do
20
+ snapshot = Brute::Store::SnapshotStore.pop(target)
21
21
  raise "No undo history available for: #{target}" unless snapshot
22
22
 
23
23
  if snapshot == :did_not_exist