ollama-client 0.2.5 → 0.2.7
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/CHANGELOG.md +22 -0
- data/README.md +336 -91
- data/RELEASE_NOTES_v0.2.6.md +41 -0
- data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
- data/docs/EXAMPLE_REORGANIZATION.md +412 -0
- data/docs/FEATURES_ADDED.md +12 -1
- data/docs/GETTING_STARTED.md +361 -0
- data/docs/INTEGRATION_TESTING.md +170 -0
- data/docs/NEXT_STEPS_SUMMARY.md +114 -0
- data/docs/PERSONAS.md +383 -0
- data/docs/QUICK_START.md +195 -0
- data/docs/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -0
- data/examples/README.md +62 -63
- data/examples/basic_chat.rb +33 -0
- data/examples/basic_generate.rb +29 -0
- data/examples/mcp_executor.rb +39 -0
- data/examples/mcp_http_executor.rb +45 -0
- data/examples/tool_calling_parsing.rb +59 -0
- data/examples/tool_dto_example.rb +0 -0
- data/exe/ollama-client +128 -1
- data/lib/ollama/agent/planner.rb +7 -2
- data/lib/ollama/chat_session.rb +101 -0
- data/lib/ollama/client.rb +41 -35
- data/lib/ollama/config.rb +9 -4
- data/lib/ollama/document_loader.rb +1 -1
- data/lib/ollama/embeddings.rb +61 -28
- data/lib/ollama/errors.rb +1 -0
- data/lib/ollama/mcp/http_client.rb +149 -0
- data/lib/ollama/mcp/stdio_client.rb +146 -0
- data/lib/ollama/mcp/tools_bridge.rb +72 -0
- data/lib/ollama/mcp.rb +31 -0
- data/lib/ollama/options.rb +3 -1
- data/lib/ollama/personas.rb +287 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +17 -5
- metadata +22 -48
- data/examples/advanced_complex_schemas.rb +0 -366
- data/examples/advanced_edge_cases.rb +0 -241
- data/examples/advanced_error_handling.rb +0 -200
- data/examples/advanced_multi_step_agent.rb +0 -341
- data/examples/advanced_performance_testing.rb +0 -186
- data/examples/chat_console.rb +0 -143
- data/examples/complete_workflow.rb +0 -245
- data/examples/dhan_console.rb +0 -843
- data/examples/dhanhq/README.md +0 -236
- data/examples/dhanhq/agents/base_agent.rb +0 -74
- data/examples/dhanhq/agents/data_agent.rb +0 -66
- data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
- data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
- data/examples/dhanhq/agents/trading_agent.rb +0 -81
- data/examples/dhanhq/analysis/market_structure.rb +0 -138
- data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
- data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
- data/examples/dhanhq/builders/market_context_builder.rb +0 -67
- data/examples/dhanhq/dhanhq_agent.rb +0 -829
- data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
- data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
- data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
- data/examples/dhanhq/services/base_service.rb +0 -46
- data/examples/dhanhq/services/data_service.rb +0 -118
- data/examples/dhanhq/services/trading_service.rb +0 -59
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
- data/examples/dhanhq/technical_analysis_runner.rb +0 -420
- data/examples/dhanhq/test_tool_calling.rb +0 -538
- data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
- data/examples/dhanhq/utils/instrument_helper.rb +0 -32
- data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
- data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
- data/examples/dhanhq/utils/rate_limiter.rb +0 -23
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
- data/examples/dhanhq_agent.rb +0 -964
- data/examples/dhanhq_tools.rb +0 -1663
- data/examples/multi_step_agent_with_external_data.rb +0 -368
- data/examples/structured_outputs_chat.rb +0 -72
- data/examples/structured_tools.rb +0 -89
- data/examples/test_dhanhq_tool_calling.rb +0 -375
- data/examples/test_tool_calling.rb +0 -160
- data/examples/tool_calling_direct.rb +0 -124
- data/examples/tool_calling_pattern.rb +0 -269
- 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
|
|
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` -
|
|
11
|
-
- `spec/ollama/client_chat_spec.rb` -
|
|
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/
|
|
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 "
|
|
411
|
+
bundle exec rspec -e "schema"
|
|
43
412
|
```
|
|
44
413
|
|
|
45
414
|
## Testing Strategy
|
|
46
415
|
|
|
47
|
-
###
|
|
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
|
|
246
|
-
3. **Test
|
|
247
|
-
4. **Test
|
|
248
|
-
5. **
|
|
249
|
-
6. **
|
|
250
|
-
7. **
|
|
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
|
-
|