ollama_chat 0.0.113 → 0.0.114

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +81 -0
  3. data/README.md +7 -0
  4. data/bin/ollama_chat_log +1 -1
  5. data/bin/ollama_chat_send +3 -1
  6. data/lib/ollama_chat/chat.rb +30 -14
  7. data/lib/ollama_chat/commands.rb +36 -18
  8. data/lib/ollama_chat/compaction.rb +289 -0
  9. data/lib/ollama_chat/config_handling.rb +32 -0
  10. data/lib/ollama_chat/database/migrations/010_add_enabled_to_collections.rb +7 -0
  11. data/lib/ollama_chat/database/models/app_state.rb +48 -16
  12. data/lib/ollama_chat/database/models/session.rb +1 -2
  13. data/lib/ollama_chat/database.rb +4 -2
  14. data/lib/ollama_chat/information.rb +53 -6
  15. data/lib/ollama_chat/logging.rb +3 -2
  16. data/lib/ollama_chat/message.rb +22 -1
  17. data/lib/ollama_chat/message_format.rb +1 -2
  18. data/lib/ollama_chat/message_list.rb +223 -9
  19. data/lib/ollama_chat/model_handling.rb +1 -1
  20. data/lib/ollama_chat/oc.rb +17 -7
  21. data/lib/ollama_chat/ollama_chat_config/default_config.yml +68 -3
  22. data/lib/ollama_chat/parsing.rb +2 -1
  23. data/lib/ollama_chat/prompt_handling.rb +1 -1
  24. data/lib/ollama_chat/prompt_management.rb +1 -0
  25. data/lib/ollama_chat/rag_handling.rb +19 -5
  26. data/lib/ollama_chat/session_management.rb +5 -3
  27. data/lib/ollama_chat/token_estimator/crude.rb +1 -1
  28. data/lib/ollama_chat/token_estimator.rb +1 -1
  29. data/lib/ollama_chat/tools/concern.rb +17 -0
  30. data/lib/ollama_chat/tools/directory_structure.rb +11 -7
  31. data/lib/ollama_chat/tools/eval_ruby.rb +16 -6
  32. data/lib/ollama_chat/tools/get_rfc.rb +2 -0
  33. data/lib/ollama_chat/tools/lookup_group.rb +100 -0
  34. data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
  35. data/lib/ollama_chat/tools/patch_file.rb +23 -5
  36. data/lib/ollama_chat/tools/read_file.rb +1 -1
  37. data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
  38. data/lib/ollama_chat/tools/search_knowledge.rb +7 -1
  39. data/lib/ollama_chat/tools/write_file.rb +25 -6
  40. data/lib/ollama_chat/tools.rb +1 -0
  41. data/lib/ollama_chat/utils/analyze_directory.rb +5 -4
  42. data/lib/ollama_chat/utils/fetcher.rb +1 -1
  43. data/lib/ollama_chat/utils/log_rotation.rb +29 -0
  44. data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
  45. data/lib/ollama_chat/utils.rb +1 -0
  46. data/lib/ollama_chat/version.rb +1 -1
  47. data/lib/ollama_chat.rb +7 -0
  48. data/ollama_chat.gemspec +5 -5
  49. data/spec/ollama_chat/commands_spec.rb +25 -0
  50. data/spec/ollama_chat/compaction_spec.rb +381 -0
  51. data/spec/ollama_chat/config_handling_spec.rb +60 -0
  52. data/spec/ollama_chat/message_list_spec.rb +229 -0
  53. data/spec/ollama_chat/message_spec.rb +143 -0
  54. data/spec/ollama_chat/rag_handling_spec.rb +3 -0
  55. data/spec/ollama_chat/system_prompt_management_spec.rb +0 -2
  56. data/spec/ollama_chat/tools/browse_spec.rb +7 -0
  57. data/spec/ollama_chat/tools/compute_bmi_spec.rb +15 -0
  58. data/spec/ollama_chat/tools/concern_spec.rb +77 -0
  59. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -0
  60. data/spec/ollama_chat/tools/delete_file_spec.rb +8 -0
  61. data/spec/ollama_chat/tools/directory_structure_spec.rb +4 -0
  62. data/spec/ollama_chat/tools/execute_grep_spec.rb +13 -0
  63. data/spec/ollama_chat/tools/execute_ri_spec.rb +8 -0
  64. data/spec/ollama_chat/tools/file_context_spec.rb +5 -0
  65. data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +2 -0
  66. data/spec/ollama_chat/tools/generate_image_spec.rb +12 -0
  67. data/spec/ollama_chat/tools/generate_password_spec.rb +5 -0
  68. data/spec/ollama_chat/tools/get_cve_spec.rb +4 -0
  69. data/spec/ollama_chat/tools/get_endoflife_spec.rb +4 -0
  70. data/spec/ollama_chat/tools/get_ghr_spec.rb +6 -0
  71. data/spec/ollama_chat/tools/get_location_spec.rb +4 -0
  72. data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -0
  73. data/spec/ollama_chat/tools/get_time_spec.rb +2 -0
  74. data/spec/ollama_chat/tools/get_url_spec.rb +7 -0
  75. data/spec/ollama_chat/tools/lookup_group_spec.rb +111 -0
  76. data/spec/ollama_chat/tools/move_file_spec.rb +10 -0
  77. data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -0
  78. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +5 -0
  79. data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -0
  80. data/spec/ollama_chat/tools/patch_file_spec.rb +62 -0
  81. data/spec/ollama_chat/tools/read_file_spec.rb +7 -0
  82. data/spec/ollama_chat/tools/resolve_tag_spec.rb +46 -0
  83. data/spec/ollama_chat/tools/roll_dice_spec.rb +11 -0
  84. data/spec/ollama_chat/tools/run_tests_spec.rb +8 -0
  85. data/spec/ollama_chat/tools/search_knowledge_spec.rb +2 -0
  86. data/spec/ollama_chat/tools/write_file_spec.rb +96 -0
  87. data/spec/ollama_chat/utils/analyze_directory_spec.rb +46 -7
  88. data/spec/ollama_chat/utils/log_rotation_spec.rb +71 -0
  89. data/spec/spec_helper.rb +2 -1
  90. metadata +19 -1
@@ -142,6 +142,8 @@ describe OllamaChat::Tools::PatchFile do
142
142
 
143
143
  result = tool.execute(tool_call, chat:)
144
144
  expect(json_object(result).success).to eq true
145
+ expect(described_class.summary_template(result:))\
146
+ .to match(/Successfully applied patch to/)
145
147
  ensure
146
148
  File.delete(test_file) if File.exist?(test_file)
147
149
  end
@@ -152,6 +154,8 @@ describe OllamaChat::Tools::PatchFile do
152
154
 
153
155
  result = tool.execute(tool_call, chat:)
154
156
  expect(json_object(result).error).to eq 'OllamaChat::ToolFunctionArgumentError'
157
+ expect(described_class.summary_template(result:))\
158
+ .to match(/Failed to apply patch to file/)
155
159
  end
156
160
 
157
161
  it 'can handle execution errors gracefully when path is not expected' do
@@ -188,4 +192,62 @@ describe OllamaChat::Tools::PatchFile do
188
192
  ensure
189
193
  File.delete(test_file) if File.exist?(test_file)
190
194
  end
195
+
196
+ describe 'syntax check integration' do
197
+ let :test_rb_file do
198
+ "./tmp/patch_syntax_#{Tins::Token.new(bits: 128)}.rb"
199
+ end
200
+
201
+ after do
202
+ File.delete(test_rb_file) if File.exist?(test_rb_file)
203
+ end
204
+
205
+ it 'includes syntax_check fail on broken .rb patch' do
206
+ const_conf_as('OC::DIFF_TOOL' => Pathname.new(`which true`.chomp))
207
+ File.write(test_rb_file, "def foo\n 42\nend\n")
208
+ content = File.read(test_rb_file)
209
+
210
+ edits = [{ start_line: 2, end_line: 2, text: 'end' }]
211
+ checksum = '%08x' % Zlib.crc32(content)
212
+ args_double = double('Arguments', path: test_rb_file, edits:, checksum:)
213
+ tool_call = double('ToolCall', function: double(name: 'patch_file', arguments: args_double))
214
+
215
+ tmp_double = double('Tempfile', write: true, flush: true, path: '/tmp/test_patch')
216
+ expect(chat).to receive(:edit_text_block).and_yield(tmp_double)
217
+ expect(tool).to receive(:system).and_return(true)
218
+ expect(tool).to receive(:digest).and_return 'old', 'new'
219
+
220
+ checker = double('checker', cmd: ['ruby', '-wc'])
221
+ expect(chat).to receive(:syntax_checker_for).and_return(checker)
222
+ expect(chat).to receive(:run_syntax_check).with(checker, any_args)
223
+ .and_return({ status: 'fail',
224
+ output: "test.rb:2: syntax error" })
225
+
226
+ result = tool.execute(tool_call, chat:)
227
+ json = json_object(result)
228
+ expect(json.message).to include('❌ Syntax error detected')
229
+ expect(json.syntax_check[:status]).to eq 'fail'
230
+ end
231
+
232
+ it 'omits syntax_check when no checker matches' do
233
+ const_conf_as('OC::DIFF_TOOL' => Pathname.new(`which true`.chomp))
234
+ File.write(test_file, "Line 1\nLine 2\n")
235
+ content = File.read(test_file)
236
+
237
+ edits = [{ start_line: 2, end_line: 2, text: 'Modified' }]
238
+ checksum = '%08x' % Zlib.crc32(content)
239
+ args_double = double('Arguments', path: test_file, edits:, checksum:)
240
+ tool_call = double('ToolCall', function: double(name: 'patch_file', arguments: args_double))
241
+
242
+ tmp_double = double('Tempfile', write: true, flush: true, path: '/tmp/test_patch')
243
+ expect(chat).to receive(:edit_text_block).and_yield(tmp_double)
244
+ expect(tool).to receive(:system).and_return(true)
245
+ expect(tool).to receive(:digest).and_return 'old', 'new'
246
+ expect(chat).to receive(:syntax_checker_for).and_return(nil)
247
+
248
+ result = tool.execute(tool_call, chat:)
249
+ json = json_object(result)
250
+ expect(json.syntax_check).to be_nil
251
+ end
252
+ end
191
253
  end
@@ -17,6 +17,7 @@ describe OllamaChat::Tools::ReadFile do
17
17
  expect(described_class.new.to_hash).to be_a Hash
18
18
  end
19
19
 
20
+
20
21
  it 'can be executed successfully' do
21
22
  tool_call = double(
22
23
  'ToolCall',
@@ -43,6 +44,8 @@ describe OllamaChat::Tools::ReadFile do
43
44
  expect(json.mtime).to match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
44
45
  expect(json.line_count).to be_nil
45
46
  expect(json.checksum).not_to be_present
47
+ expect(described_class.summary_template(result:))\
48
+ .to include('Read 20.0 B (6.0 T) from')
46
49
  end
47
50
 
48
51
  it 'can extract range when start_line is provided and end_line is nil' do
@@ -198,6 +201,8 @@ describe OllamaChat::Tools::ReadFile do
198
201
  expect(json.path).to eq '/etc/passwd'
199
202
  expect(json.message).to include('is not within allowed directories')
200
203
  expect(json.checksum).not_to be_present
204
+ expect(described_class.summary_template(result:))\
205
+ .to include('is not within allowed directories')
201
206
  end
202
207
 
203
208
  it 'can handle exceptions gracefully' do
@@ -222,5 +227,7 @@ describe OllamaChat::Tools::ReadFile do
222
227
  expect(json.error).to eq 'OllamaChat::InvalidPathError'
223
228
  expect(json.checksum).to be_nil
224
229
  expect(json.message).to match(/Failed to read file:.* does not exist/)
230
+ expect(described_class.summary_template(result:))\
231
+ .to match(/Failed to read file:.* does not exist/)
225
232
  end
226
233
  end
@@ -60,6 +60,8 @@ describe OllamaChat::Tools::ResolveTag do
60
60
  expect(json.results).to be_present
61
61
  expect(json.symbol).to eq 'execute'
62
62
  expect(json.kind).to eq ?f
63
+ expect(described_class.summary_template(result:)).to eq \
64
+ 'Found 2 results of symbol "execute".'
63
65
  end
64
66
  end
65
67
 
@@ -85,6 +87,50 @@ describe OllamaChat::Tools::ResolveTag do
85
87
  json = json_object(result)
86
88
  expect(json.error).to eq('RuntimeError')
87
89
  expect(json.message).to include('some error')
90
+ expect(described_class.summary_template(result:)).to include('some error')
91
+ end
92
+ end
93
+
94
+ context 'when a tag line triggers the internal rescue path' do
95
+ let(:tags_path) { File.join(Dir.pwd, 'tmp', 'test_tags.ctags') }
96
+
97
+ before do
98
+ FileUtils.mkdir_p(File.dirname(tags_path))
99
+ # Valid ctags format: symbol\tfilename\t/^regexp$/;"\tkind rest
100
+ # 5 regex captures, but TagResult needs 6 (…+ :linenumber),
101
+ # so TagResult.new always raises ArgumentError → rescue fires.
102
+ File.write(tags_path, \
103
+ "execute\tlib/foo.rb\t/^ def execute\\($/;\"\tf method\n")
104
+ const_conf_as(
105
+ 'OC::OLLAMA::CHAT::TOOLS::CTAGS_TOOL' => 'true',
106
+ 'OC::OLLAMA::CHAT::TOOLS::TAGS_FILE' => Pathname.new(tags_path),
107
+ )
108
+ end
109
+
110
+ after do
111
+ File.delete(tags_path) if File.exist?(tags_path)
112
+ end
113
+
114
+ it 'logs via chat.log and returns empty results' do
115
+ tool_call = double(
116
+ 'ToolCall',
117
+ function: double(
118
+ name: 'resolve_tag',
119
+ arguments: double(symbol: 'execute', kind: nil, directory: nil)
120
+ )
121
+ )
122
+
123
+ expect(chat).to receive(:log).
124
+ with(:error, kind_of(ArgumentError),
125
+ hash_including(data: { context: 'tag_resolver' })).
126
+ and_return(nil)
127
+
128
+ result = described_class.new.execute(tool_call, chat:)
129
+ json = json_object(result)
130
+
131
+ expect(json.error).to be_nil
132
+ expect(json.results).to eq []
133
+ expect(json.message).to eq 'Found 0 results of symbol "execute".'
88
134
  end
89
135
  end
90
136
  end
@@ -44,6 +44,8 @@ describe OllamaChat::Tools::RollDice do
44
44
  expect(json.modifier).to eq 0
45
45
  expect(json.total).to eq 7
46
46
  expect(json.message).to eq 'Dice roll was: 2d6 = 2…12 = (2 + 5) = 7'
47
+ expect(described_class.summary_template(result:)).to eq \
48
+ 'Dice roll was: 2d6 = 2…12 = (2 + 5) = 7'
47
49
  end
48
50
 
49
51
  it 'can be executed successfully with d20 notation' do
@@ -65,6 +67,8 @@ describe OllamaChat::Tools::RollDice do
65
67
  expect(json.rolls).to eq [10]
66
68
  expect(json.total).to eq 10
67
69
  expect(json.message).to eq 'Dice roll was: d20 = 1…20 = (10) = 10'
70
+ expect(described_class.summary_template(result:)).to eq \
71
+ 'Dice roll was: d20 = 1…20 = (10) = 10'
68
72
  end
69
73
 
70
74
  it 'can be executed successfully with positive modifier' do
@@ -87,6 +91,8 @@ describe OllamaChat::Tools::RollDice do
87
91
  expect(json.modifier).to eq 3
88
92
  expect(json.total).to eq 18
89
93
  expect(json.message).to eq 'Dice roll was: d20+3 = 4…23 = (15) + 3 = 18'
94
+ expect(described_class.summary_template(result:)).to eq \
95
+ 'Dice roll was: d20+3 = 4…23 = (15) + 3 = 18'
90
96
  end
91
97
 
92
98
  it 'can be executed successfully with negative modifier' do
@@ -109,6 +115,8 @@ describe OllamaChat::Tools::RollDice do
109
115
  expect(json.modifier).to eq(-3)
110
116
  expect(json.total).to eq 2
111
117
  expect(json.message).to eq 'Dice roll was: d20-3 = -2…17 = (5) - 3 = 2'
118
+ expect(described_class.summary_template(result:)).to eq \
119
+ 'Dice roll was: d20-3 = -2…17 = (5) - 3 = 2'
112
120
  end
113
121
 
114
122
  it 'can handle invalid dice notation' do
@@ -126,6 +134,9 @@ describe OllamaChat::Tools::RollDice do
126
134
  json = json_object(result)
127
135
  expect(json.error).to eq 'OllamaChat::ToolFunctionArgumentError'
128
136
  expect(json.message).to match(/Invalid dice notation/)
137
+ expect(described_class.summary_template(result:)).to match(
138
+ /Invalid dice notation/
139
+ )
129
140
  end
130
141
 
131
142
  it 'can be converted to hash' do
@@ -60,6 +60,9 @@ describe OllamaChat::Tools::RunTests do
60
60
  expect(json.success).to be true
61
61
  expect(json.path).to eq 'spec/ollama_chat/tools/my_spec.rb'
62
62
  expect(json.status).to eq 'passed'
63
+ expect(described_class.summary_template(result:)).to eq \
64
+ '✨ All tests passed successfully in ' \
65
+ '"spec/ollama_chat/tools/my_spec.rb"!'
63
66
  end
64
67
 
65
68
  it 'can be executed successfully with a path and coverage' do
@@ -108,6 +111,10 @@ describe OllamaChat::Tools::RunTests do
108
111
  json = json_object(result)
109
112
  expect(json.success).to be false
110
113
  expect(json.status).to eq 'failed'
114
+ expect(described_class.summary_template(result:)).to eq \
115
+ '❌ Some tests failed in ' \
116
+ '"spec/ollama_chat/tools/my_spec.rb". ' \
117
+ 'Please check the error messages above.'
111
118
  end
112
119
 
113
120
  it 'can handle unexpected errors gracefully' do
@@ -129,6 +136,7 @@ describe OllamaChat::Tools::RunTests do
129
136
  json = json_object(result)
130
137
  expect(json.error).to eq 'StandardError'
131
138
  expect(json.message).to eq 'Unexpected boom'
139
+ expect(described_class.summary_template(result:)).to eq 'Unexpected boom'
132
140
  end
133
141
 
134
142
  context 'path auto-discovery' do
@@ -120,6 +120,8 @@ describe OllamaChat::Tools::SearchKnowledge do
120
120
 
121
121
  mock_docs = double('Documents')
122
122
  expect(chat).to receive(:documents).and_return(mock_docs).at_least(:once)
123
+ expect(chat).to receive(:database_collection?).with('tolkien').
124
+ and_return(double('Col', enabled: true))
123
125
 
124
126
  expect(mock_docs).to receive(:collection).and_return('default_collection').
125
127
  at_least(:once)
@@ -42,6 +42,8 @@ describe OllamaChat::Tools::WriteFile do
42
42
  expect(json.success).to eq true
43
43
  expect(json.path).to include(File.basename(test_write_file))
44
44
  expect(json.message).to include('Wrote 13.0 B (4.0 T) to file')
45
+ expect(described_class.summary_template(result:)).
46
+ to include('Wrote 13.0 B (4.0 T) to file')
45
47
 
46
48
  # Verify file was actually written
47
49
  expect(File.exist?(test_write_file)).to be true
@@ -74,6 +76,8 @@ describe OllamaChat::Tools::WriteFile do
74
76
  json = json_object(result)
75
77
  expect(json.success).to eq true
76
78
  expect(File.read(test_write_file)).to eq 'New content'
79
+ expect(described_class.summary_template(result:)).
80
+ to include('Wrote 11.0 B (4.0 T) to file')
77
81
  ensure
78
82
  File.delete(test_write_file) if File.exist?(test_write_file)
79
83
  end
@@ -101,6 +105,8 @@ describe OllamaChat::Tools::WriteFile do
101
105
  json = json_object(result)
102
106
  expect(json.error).to eq 'OllamaChat::ToolFunctionArgumentError'
103
107
  expect(json.message).to include('Write rejected')
108
+ expect(described_class.summary_template(result:)).
109
+ to include('Write rejected')
104
110
  expect(File.read(test_write_file)).to eq 'Old content'
105
111
  ensure
106
112
  File.delete(test_write_file) if File.exist?(test_write_file)
@@ -131,6 +137,8 @@ describe OllamaChat::Tools::WriteFile do
131
137
  expect(json.success).to be true
132
138
  expect(json.path).to include(File.basename(test_write_file))
133
139
  expect(json.message).to include('Wrote 18.0 B (6.0 T) to file')
140
+ expect(described_class.summary_template(result:)).
141
+ to include('Wrote 18.0 B (6.0 T) to file')
134
142
 
135
143
  # Verify file was actually appended
136
144
  expect(File.exist?(test_write_file)).to be true
@@ -162,6 +170,8 @@ describe OllamaChat::Tools::WriteFile do
162
170
  json = json_object(result)
163
171
  expect(json.success).to eq true
164
172
  expect(File.read(test_write_file)).to eq 'First content\n'
173
+ expect(described_class.summary_template(result:)).
174
+ to include('Wrote 15.0 B (5.0 T) to file')
165
175
  ensure
166
176
  File.delete(test_write_file) if File.exist?(test_write_file)
167
177
  end
@@ -186,6 +196,8 @@ describe OllamaChat::Tools::WriteFile do
186
196
  json = json_object(result)
187
197
  expect(json.error).to eq 'OllamaChat::ToolFunctionArgumentError'
188
198
  expect(json.message).to include('Write rejected')
199
+ expect(described_class.summary_template(result:)).
200
+ to include('Write rejected')
189
201
  expect(File.exist?(test_write_file)).to be false
190
202
  end
191
203
 
@@ -210,6 +222,8 @@ describe OllamaChat::Tools::WriteFile do
210
222
  expect(json.error).to eq 'OllamaChat::InvalidPathError'
211
223
  expect(json.path).to eq '/etc/passwd'
212
224
  expect(json.message).to include('is not within allowed directories')
225
+ expect(described_class.summary_template(result:)).
226
+ to include('is not within allowed directories')
213
227
  end
214
228
 
215
229
  it 'can handle execution errors gracefully when mode is invalid' do
@@ -232,6 +246,8 @@ describe OllamaChat::Tools::WriteFile do
232
246
  json = json_object(result)
233
247
  expect(json.error).to eq 'ArgumentError'
234
248
  expect(json.message).to include('Invalid mode')
249
+ expect(described_class.summary_template(result:)).
250
+ to include('Invalid mode')
235
251
  end
236
252
 
237
253
  it 'can handle exceptions gracefully' do
@@ -256,5 +272,85 @@ describe OllamaChat::Tools::WriteFile do
256
272
  expect(json.error).to eq 'RuntimeError'
257
273
  expect(json.path).to be_nil
258
274
  expect(json.message).to eq 'Failed to write to file: some error'
275
+ expect(described_class.summary_template(result:)).to eq \
276
+ 'Failed to write to file: some error'
277
+ end
278
+
279
+ describe 'syntax check integration' do
280
+ let :test_rb_file do
281
+ "./tmp/test_syntax_#{Tins::Token.new(bits: 128)}.rb"
282
+ end
283
+
284
+ after do
285
+ File.delete(test_rb_file) if File.exist?(test_rb_file)
286
+ end
287
+
288
+ it 'includes syntax_check on valid .rb file (omits clean pass)' do
289
+ expect(chat).to receive(:syntax_checker_for).and_return(nil)
290
+
291
+ tool_call = double(
292
+ 'ToolCall',
293
+ function: double(
294
+ name: 'write_file',
295
+ arguments: double(
296
+ path: test_rb_file,
297
+ content: "def foo\n 42\nend\n",
298
+ mode: 'overwrite'
299
+ )
300
+ )
301
+ )
302
+
303
+ result = described_class.new.execute(tool_call, chat:)
304
+ json = json_object(result)
305
+ expect(json.success).to eq true
306
+ expect(json.syntax_check).to be_nil
307
+ end
308
+
309
+ it 'includes syntax_check fail on broken .rb file' do
310
+ checker = double('checker', cmd: ['ruby', '-wc'])
311
+ expect(chat).to receive(:syntax_checker_for).and_return(checker)
312
+ expect(chat).to receive(:run_syntax_check).with(checker, any_args)
313
+ .and_return({ status: 'fail',
314
+ output: "test.rb:2: syntax error, unexpected 'end'" })
315
+
316
+ tool_call = double(
317
+ 'ToolCall',
318
+ function: double(
319
+ name: 'write_file',
320
+ arguments: double(
321
+ path: test_rb_file,
322
+ content: "def foo\nend\nend\n",
323
+ mode: 'overwrite'
324
+ )
325
+ )
326
+ )
327
+
328
+ result = described_class.new.execute(tool_call, chat:)
329
+ json = json_object(result)
330
+ expect(json.success).to eq true
331
+ expect(json.message).to include('❌ Syntax error detected')
332
+ expect(json.syntax_check[:status]).to eq 'fail'
333
+ end
334
+
335
+ it 'omits syntax_check for non-matching extension' do
336
+ expect(chat).to receive(:syntax_checker_for).and_return(nil)
337
+
338
+ tool_call = double(
339
+ 'ToolCall',
340
+ function: double(
341
+ name: 'write_file',
342
+ arguments: double(
343
+ path: test_write_file,
344
+ content: 'plain text',
345
+ mode: 'overwrite'
346
+ )
347
+ )
348
+ )
349
+
350
+ result = described_class.new.execute(tool_call, chat:)
351
+ json = json_object(result)
352
+ expect(json.success).to eq true
353
+ expect(json.syntax_check).to be_nil
354
+ end
259
355
  end
260
356
  end
@@ -35,16 +35,11 @@ describe OllamaChat::Utils::AnalyzeDirectory do
35
35
  expect(sub[:children].map { |e| e[:depth] }).to eq [ 1 ]
36
36
  end
37
37
 
38
- it 'returns an array of entries with depth max_depth' do
39
- result = generate.call(@tmp_dir, max_depth: 0)
38
+ it 'max_depth 1 prunes all children from directories' do
39
+ result = generate.call(@tmp_dir, max_depth: 1)
40
40
 
41
- expect(result).to be_an(Array)
42
41
  expect(result.map { |e| e[:name] }).to contain_exactly('a.txt', 'b.rb', 'sub')
43
-
44
42
  sub = result.find { |e| e[:name] == 'sub' }
45
- expect(sub[:depth]).to eq 0
46
- expect(sub[:type]).to eq('directory')
47
- expect(sub[:children]).to be_an(Array)
48
43
  expect(sub[:children]).to be_empty
49
44
  end
50
45
 
@@ -55,6 +50,50 @@ describe OllamaChat::Utils::AnalyzeDirectory do
55
50
  expect(names).to include('example.rb')
56
51
  expect(names).not_to include('examplle.json', 'example.md')
57
52
  end
53
+
54
+ context 'max_depth with nested directories' do
55
+ before do
56
+ @tmp_dir = Dir.mktmpdir
57
+ @sub_dir = File.join(@tmp_dir, 'sub')
58
+ @deep_dir = File.join(@sub_dir, 'deep')
59
+ Dir.mkdir(@sub_dir)
60
+ Dir.mkdir(@deep_dir)
61
+ @file_a = File.join(@tmp_dir, 'a.txt')
62
+ @file_b = File.join(@sub_dir, 'b.txt')
63
+ @file_c = File.join(@deep_dir, 'c.txt')
64
+
65
+ File.write(@file_a, 'a')
66
+ File.write(@file_b, 'b')
67
+ File.write(@file_c, 'c')
68
+ end
69
+
70
+ it 'max_depth 1 shows only immediate children' do
71
+ result = generate.call(@tmp_dir, max_depth: 1)
72
+
73
+ expect(result.map { |e| e[:name] }).to contain_exactly('a.txt', 'sub')
74
+ sub = result.find { |e| e[:name] == 'sub' }
75
+ expect(sub[:children]).to be_empty
76
+ end
77
+
78
+ it 'max_depth 2 shows children and grandchildren' do
79
+ result = generate.call(@tmp_dir, max_depth: 2)
80
+
81
+ expect(result.map { |e| e[:name] }).to contain_exactly('a.txt', 'sub')
82
+ sub = result.find { |e| e[:name] == 'sub' }
83
+ deep = sub[:children].find { |e| e[:name] == 'deep' }
84
+
85
+ expect(sub[:children].map { |e| e[:name] })
86
+ .to contain_exactly('b.txt', 'deep')
87
+ expect(deep[:children]).to be_empty
88
+ end
89
+
90
+ it 'max_depth exceeding height returns the full tree' do
91
+ full = generate.call(@tmp_dir)
92
+ pruned = generate.call(@tmp_dir, max_depth: 99)
93
+
94
+ expect(pruned.to_json).to eq full.to_json
95
+ end
96
+ end
58
97
  end
59
98
 
60
99
  context 'skipping hidden files and symlinks' do
@@ -0,0 +1,71 @@
1
+ require 'tmpdir'
2
+
3
+ describe OllamaChat::Utils::LogRotation do
4
+ describe '.truncate_tail' do
5
+ it 'is a no-op when the file does not exist' do
6
+ described_class.truncate_tail('/nonexistent/test.log', keep_lines: 10)
7
+ end
8
+
9
+ it 'is a no-op for an empty file' do
10
+ Dir.mktmpdir do |dir|
11
+ log = File.join(dir, 'test.log')
12
+ File.write(log, '')
13
+ described_class.truncate_tail(log, keep_lines: 10)
14
+ expect(File.read(log)).to eq ''
15
+ end
16
+ end
17
+
18
+ it 'is a no-op when the file has fewer lines than keep_lines' do
19
+ Dir.mktmpdir do |dir|
20
+ log = File.join(dir, 'test.log')
21
+ File.write(log, "line1\nline2\nline3\n")
22
+ described_class.truncate_tail(log, keep_lines: 10)
23
+ expect(File.read(log)).to eq "line1\nline2\nline3\n"
24
+ end
25
+ end
26
+
27
+ it 'is a no-op when the file has exactly keep_lines lines' do
28
+ Dir.mktmpdir do |dir|
29
+ log = File.join(dir, 'test.log')
30
+ File.write(log, (1..10).map { |i| "line #{i}\n" }.join)
31
+ described_class.truncate_tail(log, keep_lines: 10)
32
+ expect(File.read(log).lines.length).to eq 10
33
+ end
34
+ end
35
+
36
+ it 'truncates to the last keep_lines lines' do
37
+ Dir.mktmpdir do |dir|
38
+ log = File.join(dir, 'test.log')
39
+ File.write(log, (1..100).map { |i| "line #{i}\n" }.join)
40
+ described_class.truncate_tail(log, keep_lines: 10)
41
+ content = File.read(log)
42
+ lines = content.lines
43
+ expect(lines.length).to eq 10
44
+ expect(lines.first).to eq "line 91\n"
45
+ expect(lines.last).to eq "line 100\n"
46
+ end
47
+ end
48
+
49
+ it 'preserves the inode' do
50
+ Dir.mktmpdir do |dir|
51
+ log = File.join(dir, 'test.log')
52
+ File.write(log, (1..100).map { |i| "line #{i}\n" }.join)
53
+ before_ino = File.stat(log).ino
54
+ described_class.truncate_tail(log, keep_lines: 10)
55
+ expect(File.stat(log).ino).to eq before_ino
56
+ end
57
+ end
58
+
59
+ it 'handles a file without a trailing newline' do
60
+ Dir.mktmpdir do |dir|
61
+ log = File.join(dir, 'test.log')
62
+ File.write(log, (1..50).map { |i| "line #{i}\n" }.join)
63
+ File.open(log, 'a') { |f| f.puts('final') }
64
+ described_class.truncate_tail(log, keep_lines: 10)
65
+ lines = File.read(log).lines
66
+ expect(lines.length).to eq 10
67
+ expect(lines.last).to eq "final\n"
68
+ end
69
+ end
70
+ end
71
+ end
data/spec/spec_helper.rb CHANGED
@@ -218,7 +218,8 @@ RSpec.configure do |config|
218
218
  OllamaChat.test_mode = true
219
219
  const_conf_as(
220
220
  'OC::OLLAMA::CHAT::HISTORY' => Pathname.pwd.join('tmp', 'history.json'),
221
- 'OC::OLLAMA::CHAT::LOGFILE' => Pathname.pwd.join('tmp', 'chat.log'),
221
+ 'OC::OLLAMA::CHAT::LOG::CHAT' => Pathname.pwd.join('tmp', 'chat.log'),
222
+ 'OC::OLLAMA::CHAT::LOG::DATABASE' => Pathname.pwd.join('tmp', 'database.log'),
222
223
  )
223
224
  end
224
225