ollama-client 0.2.5 → 0.2.6

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +138 -76
  4. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  5. data/docs/GETTING_STARTED.md +361 -0
  6. data/docs/INTEGRATION_TESTING.md +170 -0
  7. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  8. data/docs/PERSONAS.md +383 -0
  9. data/docs/QUICK_START.md +195 -0
  10. data/docs/TESTING.md +392 -170
  11. data/docs/TEST_CHECKLIST.md +450 -0
  12. data/examples/README.md +51 -66
  13. data/examples/basic_chat.rb +33 -0
  14. data/examples/basic_generate.rb +29 -0
  15. data/examples/tool_calling_parsing.rb +59 -0
  16. data/exe/ollama-client +128 -1
  17. data/lib/ollama/agent/planner.rb +7 -2
  18. data/lib/ollama/chat_session.rb +101 -0
  19. data/lib/ollama/client.rb +41 -35
  20. data/lib/ollama/config.rb +4 -1
  21. data/lib/ollama/document_loader.rb +1 -1
  22. data/lib/ollama/embeddings.rb +41 -26
  23. data/lib/ollama/errors.rb +1 -0
  24. data/lib/ollama/personas.rb +287 -0
  25. data/lib/ollama/version.rb +1 -1
  26. data/lib/ollama_client.rb +7 -0
  27. metadata +14 -48
  28. data/examples/advanced_complex_schemas.rb +0 -366
  29. data/examples/advanced_edge_cases.rb +0 -241
  30. data/examples/advanced_error_handling.rb +0 -200
  31. data/examples/advanced_multi_step_agent.rb +0 -341
  32. data/examples/advanced_performance_testing.rb +0 -186
  33. data/examples/chat_console.rb +0 -143
  34. data/examples/complete_workflow.rb +0 -245
  35. data/examples/dhan_console.rb +0 -843
  36. data/examples/dhanhq/README.md +0 -236
  37. data/examples/dhanhq/agents/base_agent.rb +0 -74
  38. data/examples/dhanhq/agents/data_agent.rb +0 -66
  39. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  40. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  41. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  42. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  43. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  44. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  45. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  46. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  47. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  48. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  49. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  50. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  51. data/examples/dhanhq/services/base_service.rb +0 -46
  52. data/examples/dhanhq/services/data_service.rb +0 -118
  53. data/examples/dhanhq/services/trading_service.rb +0 -59
  54. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  55. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  56. data/examples/dhanhq/test_tool_calling.rb +0 -538
  57. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  58. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  59. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  60. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  61. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  62. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  63. data/examples/dhanhq_agent.rb +0 -964
  64. data/examples/dhanhq_tools.rb +0 -1663
  65. data/examples/multi_step_agent_with_external_data.rb +0 -368
  66. data/examples/structured_outputs_chat.rb +0 -72
  67. data/examples/structured_tools.rb +0 -89
  68. data/examples/test_dhanhq_tool_calling.rb +0 -375
  69. data/examples/test_tool_calling.rb +0 -160
  70. data/examples/tool_calling_direct.rb +0 -124
  71. data/examples/tool_calling_pattern.rb +0 -269
  72. data/exe/dhan_console +0 -4
data/docs/TESTING.md CHANGED
@@ -1,19 +1,393 @@
1
- # Testing Guide
1
+ # Testing Guide: Client-Only Testing
2
2
 
3
- This document explains how to test the `ollama-client` gem comprehensively.
3
+ This document explains how to test the `ollama-client` gem **in isolation**, focusing on **transport and protocol correctness**, not agent behavior.
4
+
5
+ ## 🔒 Responsibility Boundary
6
+
7
+ `ollama-client` is responsible for:
8
+
9
+ ✅ **Transport layer** - HTTP requests/responses
10
+ ✅ **Protocol correctness** - Request shaping, response parsing
11
+ ✅ **Schema enforcement** - JSON validation
12
+ ✅ **Tool-call parsing** - Detecting and extracting tool calls
13
+ ✅ **Error handling** - Network errors, timeouts, retries
14
+ ✅ **Streaming behavior** - NDJSON/SSE parsing
15
+ ✅ **Protocol compatibility** - Native Ollama + Anthropic adapter
16
+
17
+ `ollama-client` is **NOT** responsible for:
18
+
19
+ ❌ Agent loops
20
+ ❌ Convergence logic
21
+ ❌ Policy decisions
22
+ ❌ Tool execution
23
+ ❌ Correctness of agent decisions
24
+
25
+ **If you test more than the transport layer, you're leaking agent concerns into the client.**
26
+
27
+ ## Test Categories
28
+
29
+ ### Category A: `/generate` Mode (Stateless, Deterministic)
30
+
31
+ Tests that prove `ollama-client` is safe-by-default for stateless operations.
32
+
33
+ #### ✅ G1 — Basic Generate
34
+
35
+ **Purpose:** Verify basic JSON parsing and response handling.
36
+
37
+ **Test:**
38
+ ```ruby
39
+ it "parses JSON response from generate endpoint" do
40
+ stub_request(:post, "http://localhost:11434/api/generate")
41
+ .to_return(
42
+ status: 200,
43
+ body: { response: '{"status":"ok"}' }.to_json
44
+ )
45
+
46
+ result = client.generate(
47
+ prompt: "Output a JSON object with a single key 'status' and value 'ok'.",
48
+ schema: { "type" => "object", "required" => ["status"] }
49
+ )
50
+
51
+ expect(result).to be_a(Hash)
52
+ expect(result["status"]).to eq("ok")
53
+ expect(result).not_to have_key("tool_calls")
54
+ end
55
+ ```
56
+
57
+ **Assertions:**
58
+ - Response is a Hash
59
+ - JSON is parsed correctly
60
+ - No `tool_calls` present
61
+ - No streaming artifacts
62
+
63
+ #### ✅ G2 — Strict Schema Enforcement
64
+
65
+ **Purpose:** Validate contract enforcement (major differentiator).
66
+
67
+ **Test:**
68
+ ```ruby
69
+ it "rejects responses that violate schema" do
70
+ stub_request(:post, "http://localhost:11434/api/generate")
71
+ .to_return(
72
+ status: 200,
73
+ body: { response: '{"count":"not-a-number"}' }.to_json
74
+ )
75
+
76
+ schema = {
77
+ "type" => "object",
78
+ "required" => ["count"],
79
+ "properties" => {
80
+ "count" => { "type" => "number" }
81
+ }
82
+ }
83
+
84
+ expect do
85
+ client.generate(prompt: "Output JSON with key 'count' as a number.", schema: schema)
86
+ end.to raise_error(Ollama::SchemaViolationError)
87
+ end
88
+ ```
89
+
90
+ **Assertions:**
91
+ - Raises error if schema violated
92
+ - Rejects extra fields (if strict mode enabled)
93
+ - Validates required fields
94
+
95
+ #### ❌ G3 — Tool Attempt in Generate (Must Fail)
96
+
97
+ **Purpose:** Prove `/generate` is non-agentic by design.
98
+
99
+ **Test:**
100
+ ```ruby
101
+ it "ignores tool calls in generate mode" do
102
+ stub_request(:post, "http://localhost:11434/api/generate")
103
+ .to_return(
104
+ status: 200,
105
+ body: { response: '{"action":"call read_file tool on foo.rb"}' }.to_json
106
+ )
107
+
108
+ result = client.generate(
109
+ prompt: "Call the read_file tool on foo.rb",
110
+ schema: { "type" => "object" }
111
+ )
112
+
113
+ expect(result).not_to have_key("tool_calls")
114
+ expect(result).not_to have_key("tool_use")
115
+ end
116
+ ```
117
+
118
+ **Assertions:**
119
+ - No `tool_calls` parsed
120
+ - No silent acceptance of tool intent
121
+ - Either ignored or explicit error
122
+
123
+ ### Category B: `/chat` Mode (Stateful, Tool-Aware)
124
+
125
+ Tests that prove `ollama-client` can **transport** tool calls and messages correctly — **not** that the agent works.
126
+
127
+ #### ✅ C1 — Simple Chat
128
+
129
+ **Purpose:** Verify basic message handling.
130
+
131
+ **Test:**
132
+ ```ruby
133
+ it "handles simple chat messages" do
134
+ stub_request(:post, "http://localhost:11434/api/chat")
135
+ .to_return(
136
+ status: 200,
137
+ body: {
138
+ message: { role: "assistant", content: "Hello!" }
139
+ }.to_json
140
+ )
141
+
142
+ response = client.chat_raw(
143
+ messages: [{ role: "user", content: "Say hello." }],
144
+ allow_chat: true
145
+ )
146
+
147
+ expect(response.message.content).to eq("Hello!")
148
+ expect(response.message.role).to eq("assistant")
149
+ end
150
+ ```
151
+
152
+ **Assertions:**
153
+ - Response contains assistant message
154
+ - Message history preserved in request
155
+
156
+ #### ✅ C2 — Tool-Call Parsing (Critical)
157
+
158
+ **Purpose:** Verify client correctly **detects tool intent** (not execution).
159
+
160
+ **Test:**
161
+ ```ruby
162
+ it "extracts tool calls from chat response" do
163
+ stub_request(:post, "http://localhost:11434/api/chat")
164
+ .to_return(
165
+ status: 200,
166
+ body: {
167
+ message: {
168
+ role: "assistant",
169
+ content: "I'll call the ping tool.",
170
+ tool_calls: [
171
+ {
172
+ type: "function",
173
+ function: {
174
+ name: "ping",
175
+ arguments: { "x" => 1 }.to_json
176
+ }
177
+ }
178
+ ]
179
+ }
180
+ }.to_json
181
+ )
182
+
183
+ response = client.chat_raw(
184
+ messages: [{ role: "user", content: "If a tool named 'ping' exists, call it with { 'x': 1 }." }],
185
+ tools: [tool_definition],
186
+ allow_chat: true
187
+ )
188
+
189
+ tool_calls = response.message.tool_calls
190
+ expect(tool_calls).not_to be_empty
191
+ expect(tool_calls.first["function"]["name"]).to eq("ping")
192
+ expect(JSON.parse(tool_calls.first["function"]["arguments"])).to eq("x" => 1)
193
+ end
194
+ ```
195
+
196
+ **Assertions:**
197
+ - `tool_calls` extracted correctly
198
+ - Tool name parsed
199
+ - Arguments parsed as hash
200
+ - **No execution happens** (client must not execute tools)
201
+
202
+ #### ✅ C3 — Tool Result Round-Trip Formatting
203
+
204
+ **Purpose:** Verify client serializes tool messages correctly.
205
+
206
+ **Test:**
207
+ ```ruby
208
+ it "serializes tool result messages correctly" do
209
+ messages = [
210
+ { role: "user", content: "Call ping tool" },
211
+ { role: "assistant", content: "", tool_calls: [...] },
212
+ { role: "tool", name: "ping", content: { ok: true }.to_json }
213
+ ]
214
+
215
+ stub_request(:post, "http://localhost:11434/api/chat")
216
+ .with(body: hash_including(messages: messages))
217
+ .to_return(
218
+ status: 200,
219
+ body: { message: { role: "assistant", content: "Done!" } }.to_json
220
+ )
221
+
222
+ response = client.chat_raw(messages: messages, allow_chat: true)
223
+ expect(response.message.content).to eq("Done!")
224
+ end
225
+ ```
226
+
227
+ **Assertions:**
228
+ - Client serializes tool message correctly
229
+ - Ollama accepts it
230
+ - Response parsed cleanly
231
+
232
+ ### Category C: Protocol Adapters (Anthropic / Native)
233
+
234
+ Tests that prove **protocol adapter correctness** (pure client tests, no model required).
235
+
236
+ #### ✅ A1 — Anthropic Message Shape
237
+
238
+ **Purpose:** Verify request payload compatibility.
239
+
240
+ **Test:**
241
+ ```ruby
242
+ it "serializes messages in Anthropic format" do
243
+ stub_request(:post, "http://localhost:11434/api/chat")
244
+ .with do |req|
245
+ body = JSON.parse(req.body)
246
+ expect(body["messages"]).to be_an(Array)
247
+ expect(body["messages"].first).to include("role", "content")
248
+ end
249
+ .to_return(status: 200, body: { message: {} }.to_json)
250
+
251
+ client.chat_raw(
252
+ messages: [{ role: "user", content: "Test" }],
253
+ allow_chat: true
254
+ )
255
+ end
256
+ ```
257
+
258
+ **Assertions:**
259
+ - Messages serialized as content blocks
260
+ - Tool calls emitted as `tool_use` (if Anthropic mode)
261
+ - Tool results serialized as `tool_result`
262
+
263
+ #### ✅ A2 — Anthropic Response Parsing
264
+
265
+ **Purpose:** Verify response normalization.
266
+
267
+ **Test:**
268
+ ```ruby
269
+ it "normalizes Anthropic-style responses into internal format" do
270
+ anthropic_response = {
271
+ content: [
272
+ {
273
+ type: "tool_use",
274
+ id: "call_123",
275
+ name: "search",
276
+ input: { q: "foo" }
277
+ }
278
+ ]
279
+ }
280
+
281
+ stub_request(:post, "http://localhost:11434/api/chat")
282
+ .to_return(status: 200, body: anthropic_response.to_json)
283
+
284
+ response = client.chat_raw(
285
+ messages: [{ role: "user", content: "Search for foo" }],
286
+ allow_chat: true
287
+ )
288
+
289
+ tool_calls = response.message.tool_calls
290
+ expect(tool_calls).not_to be_empty
291
+ expect(tool_calls.first["function"]["name"]).to eq("search")
292
+ end
293
+ ```
294
+
295
+ **Assertions:**
296
+ - Client normalizes Anthropic format into internal `tool_calls`
297
+ - Protocol adapter correctness
298
+
299
+ ### Category D: Failure Modes (Non-Negotiable)
300
+
301
+ #### ✅ F1 — Ollama Down
302
+
303
+ **Test:**
304
+ ```ruby
305
+ it "handles connection refused gracefully" do
306
+ stub_request(:post, "http://localhost:11434/api/generate")
307
+ .to_raise(Errno::ECONNREFUSED)
308
+
309
+ expect do
310
+ client.generate(prompt: "test", schema: schema)
311
+ end.to raise_error(Ollama::Error)
312
+
313
+ # Verify no hangs
314
+ expect(Time.now - start_time).to be < 5
315
+ end
316
+ ```
317
+
318
+ **Assertions:**
319
+ - Connection refused raises correct exception
320
+ - No hangs
321
+ - Retries handled correctly
322
+
323
+ #### ✅ F2 — Invalid JSON from Model
324
+
325
+ **Test:**
326
+ ```ruby
327
+ it "raises error on invalid JSON response" do
328
+ stub_request(:post, "http://localhost:11434/api/generate")
329
+ .to_return(status: 200, body: { response: "not json at all" }.to_json)
330
+
331
+ expect do
332
+ client.generate(prompt: "test", schema: schema)
333
+ end.to raise_error(Ollama::InvalidJSONError)
334
+ end
335
+ ```
336
+
337
+ **Assertions:**
338
+ - Client raises parse error
339
+ - Does not silently continue
340
+ - Retries handled (if retryable)
341
+
342
+ #### ✅ F3 — Streaming Interruption
343
+
344
+ **Test:**
345
+ ```ruby
346
+ it "handles partial stream gracefully" do
347
+ stub_request(:post, "http://localhost:11434/api/chat")
348
+ .to_return(
349
+ status: 200,
350
+ body: "data: {\"message\":{\"content\":\"partial\"}}\n",
351
+ headers: { "Content-Type" => "text/event-stream" }
352
+ )
353
+
354
+ # Simulate stream interruption
355
+ expect do
356
+ client.chat_raw(messages: [{ role: "user", content: "test" }], allow_chat: true)
357
+ end.to raise_error(Ollama::Error)
358
+ end
359
+ ```
360
+
361
+ **Assertions:**
362
+ - Partial stream handled
363
+ - Client terminates cleanly
364
+ - No corrupted state
365
+
366
+ ## What You Should NOT Test
367
+
368
+ ❌ **Do not test:**
369
+ - Infinite loops
370
+ - Retries based on content
371
+ - Agent stopping behavior
372
+ - Tool side effects
373
+ - Correctness of answers
374
+ - Agent convergence logic
375
+ - Policy decisions
376
+
377
+ **Those belong to `agent-runtime` and app repos.**
4
378
 
5
379
  ## Test Structure
6
380
 
7
381
  The test suite is organized into focused spec files:
8
382
 
9
383
  - `spec/ollama/client_spec.rb` - Basic client initialization and parameter validation
10
- - `spec/ollama/client_generate_spec.rb` - Comprehensive tests for `generate()` method
11
- - `spec/ollama/client_chat_spec.rb` - Comprehensive tests for `chat()` method
384
+ - `spec/ollama/client_generate_spec.rb` - Tests for `generate()` method (Category A)
385
+ - `spec/ollama/client_chat_spec.rb` - Tests for `chat()` method (Category B)
386
+ - `spec/ollama/client_chat_raw_spec.rb` - Tests for `chat_raw()` method (Category B)
12
387
  - `spec/ollama/client_list_models_spec.rb` - Tests for `list_models()` method
13
388
  - `spec/ollama/client_model_suggestions_spec.rb` - Tests for model suggestion feature
14
- - `spec/ollama/errors_spec.rb` - Tests for all error classes
15
- - `spec/ollama/config_spec.rb` - Config class tests (in client_spec.rb)
16
- - `spec/ollama/schema_validator_spec.rb` - Schema validation tests (in client_spec.rb)
389
+ - `spec/ollama/errors_spec.rb` - Tests for all error classes (Category D)
390
+ - `spec/ollama/schema_validator_spec.rb` - Schema validation tests (Category A, G2)
17
391
 
18
392
  ## Running Tests
19
393
 
@@ -32,19 +406,14 @@ bundle exec rspec spec/ollama/client_generate_spec.rb
32
406
  bundle exec rspec --format documentation
33
407
  ```
34
408
 
35
- ### Run Specific Test
36
- ```bash
37
- bundle exec rspec spec/ollama/client_generate_spec.rb:45
38
- ```
39
-
40
409
  ### Run Tests Matching a Pattern
41
410
  ```bash
42
- bundle exec rspec -e "retry"
411
+ bundle exec rspec -e "schema"
43
412
  ```
44
413
 
45
414
  ## Testing Strategy
46
415
 
47
- ### 1. HTTP Mocking with WebMock
416
+ ### HTTP Mocking with WebMock
48
417
 
49
418
  All HTTP requests are mocked using [WebMock](https://github.com/bblimke/webmock). This allows us to:
50
419
  - Test without a real Ollama server
@@ -58,43 +427,6 @@ stub_request(:post, "http://localhost:11434/api/generate")
58
427
  .to_return(status: 200, body: { response: '{"test":"value"}' }.to_json)
59
428
  ```
60
429
 
61
- ### 2. Test Coverage Areas
62
-
63
- #### ✅ Success Cases
64
- - Successful API calls return parsed JSON
65
- - Schema validation passes
66
- - Config defaults are applied correctly
67
- - Model overrides work
68
- - Options are merged correctly
69
-
70
- #### ✅ Error Handling
71
- - **404 (NotFoundError)**: Model not found, no retries, includes suggestions
72
- - **500 (HTTPError)**: Retryable, retries up to config limit
73
- - **400 (HTTPError)**: Non-retryable, fails immediately
74
- - **TimeoutError**: Retries on timeout
75
- - **InvalidJSONError**: Retries on JSON parse errors
76
- - **SchemaViolationError**: Retries on schema validation failures
77
- - **Connection Errors**: Retries on network failures
78
-
79
- #### ✅ Retry Logic
80
- - Retries up to `config.retries` times
81
- - Only retries retryable errors (5xx, 408, 429)
82
- - Raises `RetryExhaustedError` after max retries
83
- - Succeeds if retry succeeds
84
-
85
- #### ✅ Edge Cases
86
- - JSON wrapped in markdown code blocks
87
- - Plain JSON responses
88
- - Empty model lists
89
- - Missing response fields
90
- - Malformed JSON
91
-
92
- #### ✅ Model Suggestions
93
- - Suggests similar models on 404
94
- - Fuzzy matching on model names
95
- - Limits suggestions to 5 models
96
- - Handles model listing failures gracefully
97
-
98
430
  ## Writing New Tests
99
431
 
100
432
  ### Basic Test Structure
@@ -106,6 +438,8 @@ RSpec.describe Ollama::Client, "#method_name" do
106
438
  Ollama::Config.new.tap do |c|
107
439
  c.base_url = "http://localhost:11434"
108
440
  c.model = "test-model"
441
+ c.retries = 2
442
+ c.timeout = 5
109
443
  end
110
444
  end
111
445
 
@@ -127,127 +461,16 @@ RSpec.describe Ollama::Client, "#method_name" do
127
461
  end
128
462
  ```
129
463
 
130
- ### Testing Retry Logic
131
-
132
- ```ruby
133
- it "retries on 500 errors" do
134
- stub_request(:post, "http://localhost:11434/api/generate")
135
- .to_return(status: 500, body: "Internal Server Error")
136
- .times(config.retries + 1)
137
-
138
- expect do
139
- client.generate(prompt: "test", schema: schema)
140
- end.to raise_error(Ollama::RetryExhaustedError)
141
-
142
- expect(WebMock).to have_requested(:post, "http://localhost:11434/api/generate")
143
- .times(config.retries + 1)
144
- end
145
- ```
146
-
147
- ### Testing Success After Retry
148
-
149
- ```ruby
150
- it "succeeds on retry" do
151
- stub_request(:post, "http://localhost:11434/api/generate")
152
- .to_return(
153
- { status: 500, body: "Internal Server Error" },
154
- { status: 200, body: { response: '{"test":"value"}' }.to_json }
155
- )
156
-
157
- result = client.generate(prompt: "test", schema: schema)
158
- expect(result).to eq("test" => "value")
159
- expect(WebMock).to have_requested(:post, "http://localhost:11434/api/generate").twice
160
- end
161
- ```
162
-
163
- ### Testing Error Details
164
-
165
- ```ruby
166
- it "raises error with correct details" do
167
- stub_request(:post, "http://localhost:11434/api/generate")
168
- .to_return(status: 404, body: "Not Found")
169
-
170
- expect do
171
- client.generate(prompt: "test", schema: schema)
172
- end.to raise_error(Ollama::NotFoundError) do |error|
173
- expect(error.requested_model).to eq("test-model")
174
- expect(error.status_code).to eq(404)
175
- end
176
- end
177
- ```
178
-
179
- ## Integration Tests (Optional)
180
-
181
- For integration tests that hit a real Ollama server, create a separate spec file:
182
-
183
- ```ruby
184
- # spec/integration/ollama_client_integration_spec.rb
185
- RSpec.describe "Ollama Client Integration", :integration do
186
- # Skip if OLLAMA_URL is not set
187
- before(:all) do
188
- skip "Set OLLAMA_URL environment variable to run integration tests" unless ENV["OLLAMA_URL"]
189
- end
190
-
191
- let(:client) do
192
- config = Ollama::Config.new
193
- config.base_url = ENV["OLLAMA_URL"] || "http://localhost:11434"
194
- Ollama::Client.new(config: config)
195
- end
196
-
197
- it "can generate structured output" do
198
- schema = {
199
- "type" => "object",
200
- "required" => ["test"],
201
- "properties" => { "test" => { "type" => "string" } }
202
- }
203
-
204
- result = client.generate(
205
- prompt: "Return a JSON object with test='hello'",
206
- schema: schema
207
- )
208
-
209
- expect(result["test"]).to eq("hello")
210
- end
211
- end
212
- ```
213
-
214
- Run integration tests separately:
215
- ```bash
216
- bundle exec rspec --tag integration
217
- ```
218
-
219
- ## Test Coverage Metrics
220
-
221
- To check test coverage, add `simplecov`:
222
-
223
- ```ruby
224
- # spec/spec_helper.rb
225
- require "simplecov"
226
- SimpleCov.start
227
- ```
228
-
229
- Then run:
230
- ```bash
231
- bundle exec rspec
232
- open coverage/index.html
233
- ```
234
-
235
- ## Continuous Integration
236
-
237
- The test suite is designed to run in CI without external dependencies:
238
- - All tests use WebMock (no real Ollama server needed)
239
- - Tests are deterministic and fast
240
- - No flaky network-dependent tests
241
-
242
464
  ## Best Practices
243
465
 
244
466
  1. **Always mock HTTP requests** - Don't make real network calls in unit tests
245
- 2. **Test error paths** - Ensure all error scenarios are covered
246
- 3. **Test retry logic** - Verify retries work correctly
247
- 4. **Test edge cases** - JSON parsing, empty responses, etc.
248
- 5. **Keep tests focused** - One assertion per test when possible
249
- 6. **Use descriptive test names** - "it 'retries on 500 errors'"
250
- 7. **Reset WebMock** - Always reset in `after` blocks
467
+ 2. **Test transport layer only** - Don't test agent behavior
468
+ 3. **Test error paths** - Ensure all error scenarios are covered
469
+ 4. **Test retry logic** - Verify retries work correctly
470
+ 5. **Test edge cases** - JSON parsing, empty responses, etc.
471
+ 6. **Keep tests focused** - One assertion per test when possible
472
+ 7. **Use descriptive test names** - "it 'extracts tool calls from chat response'"
473
+ 8. **Reset WebMock** - Always reset in `after` blocks
251
474
 
252
475
  ## Debugging Tests
253
476
 
@@ -283,4 +506,3 @@ WebMock.allow_net_connect!
283
506
  - Ensure WebMock is reset in `after` blocks
284
507
  - Don't share state between tests
285
508
  - Use `let` instead of instance variables
286
-