ollama_chat 0.0.113 → 0.0.115

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +120 -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 +72 -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/execute_shell.rb +158 -0
  33. data/lib/ollama_chat/tools/get_rfc.rb +2 -0
  34. data/lib/ollama_chat/tools/lookup_group.rb +100 -0
  35. data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
  36. data/lib/ollama_chat/tools/patch_file.rb +23 -5
  37. data/lib/ollama_chat/tools/read_file.rb +1 -1
  38. data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
  39. data/lib/ollama_chat/tools/search_knowledge.rb +7 -1
  40. data/lib/ollama_chat/tools/write_file.rb +25 -6
  41. data/lib/ollama_chat/tools.rb +2 -0
  42. data/lib/ollama_chat/utils/analyze_directory.rb +5 -4
  43. data/lib/ollama_chat/utils/fetcher.rb +1 -1
  44. data/lib/ollama_chat/utils/log_rotation.rb +29 -0
  45. data/lib/ollama_chat/utils/strip_ansi.rb +22 -0
  46. data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
  47. data/lib/ollama_chat/utils.rb +2 -0
  48. data/lib/ollama_chat/version.rb +1 -1
  49. data/lib/ollama_chat.rb +7 -0
  50. data/ollama_chat.gemspec +5 -5
  51. data/spec/ollama_chat/commands_spec.rb +25 -0
  52. data/spec/ollama_chat/compaction_spec.rb +381 -0
  53. data/spec/ollama_chat/config_handling_spec.rb +60 -0
  54. data/spec/ollama_chat/message_list_spec.rb +229 -0
  55. data/spec/ollama_chat/message_spec.rb +143 -0
  56. data/spec/ollama_chat/rag_handling_spec.rb +3 -0
  57. data/spec/ollama_chat/system_prompt_management_spec.rb +0 -2
  58. data/spec/ollama_chat/tools/browse_spec.rb +7 -0
  59. data/spec/ollama_chat/tools/compute_bmi_spec.rb +15 -0
  60. data/spec/ollama_chat/tools/concern_spec.rb +77 -0
  61. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -0
  62. data/spec/ollama_chat/tools/delete_file_spec.rb +8 -0
  63. data/spec/ollama_chat/tools/directory_structure_spec.rb +4 -0
  64. data/spec/ollama_chat/tools/execute_grep_spec.rb +13 -0
  65. data/spec/ollama_chat/tools/execute_ri_spec.rb +8 -0
  66. data/spec/ollama_chat/tools/execute_shell_spec.rb +232 -0
  67. data/spec/ollama_chat/tools/file_context_spec.rb +5 -0
  68. data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +2 -0
  69. data/spec/ollama_chat/tools/generate_image_spec.rb +12 -0
  70. data/spec/ollama_chat/tools/generate_password_spec.rb +5 -0
  71. data/spec/ollama_chat/tools/get_cve_spec.rb +4 -0
  72. data/spec/ollama_chat/tools/get_endoflife_spec.rb +4 -0
  73. data/spec/ollama_chat/tools/get_ghr_spec.rb +6 -0
  74. data/spec/ollama_chat/tools/get_location_spec.rb +4 -0
  75. data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -0
  76. data/spec/ollama_chat/tools/get_time_spec.rb +2 -0
  77. data/spec/ollama_chat/tools/get_url_spec.rb +7 -0
  78. data/spec/ollama_chat/tools/lookup_group_spec.rb +111 -0
  79. data/spec/ollama_chat/tools/move_file_spec.rb +10 -0
  80. data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -0
  81. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +5 -0
  82. data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -0
  83. data/spec/ollama_chat/tools/patch_file_spec.rb +62 -0
  84. data/spec/ollama_chat/tools/read_file_spec.rb +10 -3
  85. data/spec/ollama_chat/tools/resolve_tag_spec.rb +46 -0
  86. data/spec/ollama_chat/tools/roll_dice_spec.rb +11 -0
  87. data/spec/ollama_chat/tools/run_tests_spec.rb +8 -0
  88. data/spec/ollama_chat/tools/search_knowledge_spec.rb +2 -0
  89. data/spec/ollama_chat/tools/write_file_spec.rb +98 -2
  90. data/spec/ollama_chat/utils/analyze_directory_spec.rb +46 -7
  91. data/spec/ollama_chat/utils/log_rotation_spec.rb +71 -0
  92. data/spec/spec_helper.rb +2 -1
  93. metadata +25 -1
@@ -49,6 +49,8 @@ describe OllamaChat::Tools::MoveFile do
49
49
  expect(File.exist?(source_path)).to be false
50
50
  expect(File.exist?(dest_path)).to be true
51
51
  expect(File.read(dest_path)).to eq content
52
+ expect(described_class.summary_template(result:))\
53
+ .to match(%r{File moved successfully from .*test_move_source.* to .*test_move_dest.*})
52
54
  ensure
53
55
  File.delete(source_path) if File.exist?(source_path)
54
56
  File.delete(dest_path) if File.exist?(dest_path)
@@ -80,6 +82,8 @@ describe OllamaChat::Tools::MoveFile do
80
82
  expect(json.success).to be_falsey
81
83
  expect(json.error).to eq 'OllamaChat::InvalidPathError'
82
84
  expect(json.message).to include('does already exist')
85
+ expect(described_class.summary_template(result:))\
86
+ .to include('does already exist')
83
87
 
84
88
  # Verify files are still there (no move happened)
85
89
  expect(File.exist?(source_path)).to be true
@@ -112,6 +116,8 @@ describe OllamaChat::Tools::MoveFile do
112
116
  expect(json.success).to be_falsey
113
117
  expect(json.error).to eq 'OllamaChat::InvalidPathError'
114
118
  expect(json.message).to include('does not exist')
119
+ expect(described_class.summary_template(result:))\
120
+ .to include('does not exist')
115
121
  ensure
116
122
  File.delete(dest_path) if File.exist?(dest_path)
117
123
  end
@@ -134,6 +140,8 @@ describe OllamaChat::Tools::MoveFile do
134
140
  json = json_object(result)
135
141
  expect(json.error).to eq 'OllamaChat::InvalidPathError'
136
142
  expect(json.message).to include('is not within allowed directories')
143
+ expect(described_class.summary_template(result:))\
144
+ .to include('is not within allowed directories')
137
145
  end
138
146
 
139
147
  it 'handles general exceptions gracefully' do
@@ -161,6 +169,8 @@ describe OllamaChat::Tools::MoveFile do
161
169
  json = json_object(result)
162
170
  expect(json.error).to eq 'RuntimeError'
163
171
  expect(json.message).to include('Unexpected system error')
172
+ expect(described_class.summary_template(result:))\
173
+ .to include('Unexpected system error')
164
174
  ensure
165
175
  File.delete(source_path) if File.exist?(source_path)
166
176
  File.delete(dest_path) if File.exist?(dest_path)
@@ -32,6 +32,8 @@ describe OllamaChat::Tools::OpenFileInEditor do
32
32
  expect(json.path).to eq asset('example.rb')
33
33
  expect(json.start_line).to eq 42
34
34
  expect(json.message).to match(%r(spec/assets/example.rb" at line 42))
35
+ expect(described_class.summary_template(result:)).
36
+ to match(%r(spec/assets/example.rb" at line 42))
35
37
  end
36
38
 
37
39
  it 'can be executed successfully with valid path, start_line, end_line' do
@@ -54,6 +56,8 @@ describe OllamaChat::Tools::OpenFileInEditor do
54
56
  expect(json.start_line).to eq 23
55
57
  expect(json.end_line).to eq 42
56
58
  expect(json.message).to match(%r(spec/assets/example.rb" and selected range 23-42))
59
+ expect(described_class.summary_template(result:)).
60
+ to match(%r(spec/assets/example.rb" and selected range 23-42))
57
61
  end
58
62
 
59
63
  it 'can handle invalid path gracefully' do
@@ -74,6 +78,8 @@ describe OllamaChat::Tools::OpenFileInEditor do
74
78
 
75
79
  expect(json.error).to eq 'Errno::ENOENT'
76
80
  expect(json.message).to include('No such file or directory')
81
+ expect(described_class.summary_template(result:)).
82
+ to include('No such file or directory')
77
83
  end
78
84
 
79
85
  it 'can be converted to hash' do
@@ -40,6 +40,7 @@ describe OllamaChat::Tools::PasteFromClipboard do
40
40
  expect(json.error).to be_nil # No exception was raised
41
41
  expect(json.success).to be true
42
42
  expect(json.message).to eq "Pasted 11.0 B of content."
43
+ expect(described_class.summary_template(result:)).to eq "Pasted 11.0 B of content."
43
44
  end
44
45
 
45
46
  it 'can be executed with edit option' do
@@ -65,6 +66,7 @@ describe OllamaChat::Tools::PasteFromClipboard do
65
66
  expect(json.error).to be_nil
66
67
  expect(json.success).to be true
67
68
  expect(json.message).to eq "Pasted 18.0 B of content."
69
+ expect(described_class.summary_template(result:)).to eq "Pasted 18.0 B of content."
68
70
  end
69
71
 
70
72
  it 'can handle execution errors gracefully' do
@@ -89,6 +91,8 @@ describe OllamaChat::Tools::PasteFromClipboard do
89
91
  json = json_object(result)
90
92
  expect(json.error).to eq 'OllamaChat::OllamaChatError'
91
93
  expect(json.message).to eq 'No content available to paste from the system clipboard.'
94
+ expect(described_class.summary_template(result:)).to eq \
95
+ 'No content available to paste from the system clipboard.'
92
96
  end
93
97
 
94
98
  it 'can handle execution exceptions gracefully' do
@@ -113,5 +117,6 @@ describe OllamaChat::Tools::PasteFromClipboard do
113
117
  json = json_object(result)
114
118
  expect(json.error).to eq 'RuntimeError'
115
119
  expect(json.message).to eq 'some kind of exception'
120
+ expect(described_class.summary_template(result:)).to eq 'some kind of exception'
116
121
  end
117
122
  end
@@ -41,6 +41,8 @@ describe OllamaChat::Tools::PasteIntoEditor do
41
41
  expect(json.message).to eq(
42
42
  'The provided text has been successfully pasted into the editor.'
43
43
  )
44
+ expect(described_class.summary_template(result:)).to eq \
45
+ 'The provided text has been successfully pasted into the editor.'
44
46
  end
45
47
  end
46
48
 
@@ -64,6 +66,8 @@ describe OllamaChat::Tools::PasteIntoEditor do
64
66
 
65
67
  expect(json.error).to eq('OllamaChat::OllamaChatError')
66
68
  expect(json.message).to eq('Insert failed')
69
+ expect(described_class.summary_template(result:)).to eq \
70
+ 'Insert failed'
67
71
  end
68
72
 
69
73
  it 'captures generic RuntimeError and returns JSON with the error details' do
@@ -75,6 +79,8 @@ describe OllamaChat::Tools::PasteIntoEditor do
75
79
 
76
80
  expect(json.error).to eq('RuntimeError')
77
81
  expect(json.message).to eq('Some exception')
82
+ expect(described_class.summary_template(result:)).to eq \
83
+ 'Some exception'
78
84
  end
79
85
  end
80
86
 
@@ -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',
@@ -39,10 +40,12 @@ describe OllamaChat::Tools::ReadFile do
39
40
  expect(json.content).to eq <<~EOT
40
41
  puts "Hello World!"
41
42
  EOT
42
- expect(json.message).to include('Read 20.0 B (6.0 T) from')
43
+ expect(json.message).to include('Read 6.0 T (20.0 B) from')
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 6.0 T (20.0 B) from')
46
49
  end
47
50
 
48
51
  it 'can extract range when start_line is provided and end_line is nil' do
@@ -61,7 +64,7 @@ describe OllamaChat::Tools::ReadFile do
61
64
  result = described_class.new.execute(tool_call, chat:)
62
65
  json = json_object(result)
63
66
  expect(json.content).to eq "puts \"Hello World!\"\n"
64
- expect(json.message).to include('Read 20.0 B (6.0 T) from')
67
+ expect(json.message).to include('Read 6.0 T (20.0 B) from')
65
68
  expect(json.mtime).to match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
66
69
  expect(json.line_count).to be_nil
67
70
  expect(json.checksum).not_to be_present
@@ -83,7 +86,7 @@ describe OllamaChat::Tools::ReadFile do
83
86
  result = described_class.new.execute(tool_call, chat:)
84
87
  json = json_object(result)
85
88
  expect(json.content).to eq "puts \"Hello World!\"\n"
86
- expect(json.message).to include('Read 20.0 B (6.0 T) from')
89
+ expect(json.message).to include('Read 6.0 T (20.0 B) from')
87
90
  expect(json.mtime).to match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
88
91
  expect(json.line_count).to be_nil
89
92
  expect(json.checksum).not_to be_present
@@ -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)
@@ -41,7 +41,9 @@ describe OllamaChat::Tools::WriteFile do
41
41
  json = json_object(result)
42
42
  expect(json.success).to eq true
43
43
  expect(json.path).to include(File.basename(test_write_file))
44
- expect(json.message).to include('Wrote 13.0 B (4.0 T) to file')
44
+ expect(json.message).to include('Wrote 4.0 T (13.0 B) to file')
45
+ expect(described_class.summary_template(result:)).
46
+ to include('Wrote 4.0 T (13.0 B) 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 4.0 T (11.0 B) 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)
@@ -130,7 +136,9 @@ describe OllamaChat::Tools::WriteFile do
130
136
  json = json_object(result)
131
137
  expect(json.success).to be true
132
138
  expect(json.path).to include(File.basename(test_write_file))
133
- expect(json.message).to include('Wrote 18.0 B (6.0 T) to file')
139
+ expect(json.message).to include('Wrote 6.0 T (18.0 B) to file')
140
+ expect(described_class.summary_template(result:)).
141
+ to include('Wrote 6.0 T (18.0 B) 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 5.0 T (15.0 B) 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