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.
- checksums.yaml +4 -4
- data/CHANGES.md +81 -0
- data/README.md +7 -0
- data/bin/ollama_chat_log +1 -1
- data/bin/ollama_chat_send +3 -1
- data/lib/ollama_chat/chat.rb +30 -14
- data/lib/ollama_chat/commands.rb +36 -18
- data/lib/ollama_chat/compaction.rb +289 -0
- data/lib/ollama_chat/config_handling.rb +32 -0
- data/lib/ollama_chat/database/migrations/010_add_enabled_to_collections.rb +7 -0
- data/lib/ollama_chat/database/models/app_state.rb +48 -16
- data/lib/ollama_chat/database/models/session.rb +1 -2
- data/lib/ollama_chat/database.rb +4 -2
- data/lib/ollama_chat/information.rb +53 -6
- data/lib/ollama_chat/logging.rb +3 -2
- data/lib/ollama_chat/message.rb +22 -1
- data/lib/ollama_chat/message_format.rb +1 -2
- data/lib/ollama_chat/message_list.rb +223 -9
- data/lib/ollama_chat/model_handling.rb +1 -1
- data/lib/ollama_chat/oc.rb +17 -7
- data/lib/ollama_chat/ollama_chat_config/default_config.yml +68 -3
- data/lib/ollama_chat/parsing.rb +2 -1
- data/lib/ollama_chat/prompt_handling.rb +1 -1
- data/lib/ollama_chat/prompt_management.rb +1 -0
- data/lib/ollama_chat/rag_handling.rb +19 -5
- data/lib/ollama_chat/session_management.rb +5 -3
- data/lib/ollama_chat/token_estimator/crude.rb +1 -1
- data/lib/ollama_chat/token_estimator.rb +1 -1
- data/lib/ollama_chat/tools/concern.rb +17 -0
- data/lib/ollama_chat/tools/directory_structure.rb +11 -7
- data/lib/ollama_chat/tools/eval_ruby.rb +16 -6
- data/lib/ollama_chat/tools/get_rfc.rb +2 -0
- data/lib/ollama_chat/tools/lookup_group.rb +100 -0
- data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
- data/lib/ollama_chat/tools/patch_file.rb +23 -5
- data/lib/ollama_chat/tools/read_file.rb +1 -1
- data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
- data/lib/ollama_chat/tools/search_knowledge.rb +7 -1
- data/lib/ollama_chat/tools/write_file.rb +25 -6
- data/lib/ollama_chat/tools.rb +1 -0
- data/lib/ollama_chat/utils/analyze_directory.rb +5 -4
- data/lib/ollama_chat/utils/fetcher.rb +1 -1
- data/lib/ollama_chat/utils/log_rotation.rb +29 -0
- data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
- data/lib/ollama_chat/utils.rb +1 -0
- data/lib/ollama_chat/version.rb +1 -1
- data/lib/ollama_chat.rb +7 -0
- data/ollama_chat.gemspec +5 -5
- data/spec/ollama_chat/commands_spec.rb +25 -0
- data/spec/ollama_chat/compaction_spec.rb +381 -0
- data/spec/ollama_chat/config_handling_spec.rb +60 -0
- data/spec/ollama_chat/message_list_spec.rb +229 -0
- data/spec/ollama_chat/message_spec.rb +143 -0
- data/spec/ollama_chat/rag_handling_spec.rb +3 -0
- data/spec/ollama_chat/system_prompt_management_spec.rb +0 -2
- data/spec/ollama_chat/tools/browse_spec.rb +7 -0
- data/spec/ollama_chat/tools/compute_bmi_spec.rb +15 -0
- data/spec/ollama_chat/tools/concern_spec.rb +77 -0
- data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -0
- data/spec/ollama_chat/tools/delete_file_spec.rb +8 -0
- data/spec/ollama_chat/tools/directory_structure_spec.rb +4 -0
- data/spec/ollama_chat/tools/execute_grep_spec.rb +13 -0
- data/spec/ollama_chat/tools/execute_ri_spec.rb +8 -0
- data/spec/ollama_chat/tools/file_context_spec.rb +5 -0
- data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +2 -0
- data/spec/ollama_chat/tools/generate_image_spec.rb +12 -0
- data/spec/ollama_chat/tools/generate_password_spec.rb +5 -0
- data/spec/ollama_chat/tools/get_cve_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_endoflife_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_ghr_spec.rb +6 -0
- data/spec/ollama_chat/tools/get_location_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -0
- data/spec/ollama_chat/tools/get_time_spec.rb +2 -0
- data/spec/ollama_chat/tools/get_url_spec.rb +7 -0
- data/spec/ollama_chat/tools/lookup_group_spec.rb +111 -0
- data/spec/ollama_chat/tools/move_file_spec.rb +10 -0
- data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -0
- data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +5 -0
- data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -0
- data/spec/ollama_chat/tools/patch_file_spec.rb +62 -0
- data/spec/ollama_chat/tools/read_file_spec.rb +7 -0
- data/spec/ollama_chat/tools/resolve_tag_spec.rb +46 -0
- data/spec/ollama_chat/tools/roll_dice_spec.rb +11 -0
- data/spec/ollama_chat/tools/run_tests_spec.rb +8 -0
- data/spec/ollama_chat/tools/search_knowledge_spec.rb +2 -0
- data/spec/ollama_chat/tools/write_file_spec.rb +96 -0
- data/spec/ollama_chat/utils/analyze_directory_spec.rb +46 -7
- data/spec/ollama_chat/utils/log_rotation_spec.rb +71 -0
- data/spec/spec_helper.rb +2 -1
- 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 '
|
|
39
|
-
result = generate.call(@tmp_dir, max_depth:
|
|
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::
|
|
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
|
|