tsikol 0.1.0
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 +7 -0
- data/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +84 -0
- data/LICENSE +21 -0
- data/README.md +579 -0
- data/Rakefile +12 -0
- data/docs/README.md +69 -0
- data/docs/api/middleware.md +721 -0
- data/docs/api/prompt.md +858 -0
- data/docs/api/resource.md +651 -0
- data/docs/api/server.md +509 -0
- data/docs/api/test-helpers.md +591 -0
- data/docs/api/tool.md +527 -0
- data/docs/cookbook/authentication.md +651 -0
- data/docs/cookbook/caching.md +877 -0
- data/docs/cookbook/dynamic-tools.md +970 -0
- data/docs/cookbook/error-handling.md +887 -0
- data/docs/cookbook/logging.md +1044 -0
- data/docs/cookbook/rate-limiting.md +717 -0
- data/docs/examples/code-assistant.md +922 -0
- data/docs/examples/complete-server.md +726 -0
- data/docs/examples/database-manager.md +1198 -0
- data/docs/examples/devops-tools.md +1382 -0
- data/docs/examples/echo-server.md +501 -0
- data/docs/examples/weather-service.md +822 -0
- data/docs/guides/completion.md +472 -0
- data/docs/guides/getting-started.md +462 -0
- data/docs/guides/middleware.md +823 -0
- data/docs/guides/project-structure.md +434 -0
- data/docs/guides/prompts.md +920 -0
- data/docs/guides/resources.md +720 -0
- data/docs/guides/sampling.md +804 -0
- data/docs/guides/testing.md +863 -0
- data/docs/guides/tools.md +627 -0
- data/examples/README.md +92 -0
- data/examples/advanced_features.rb +129 -0
- data/examples/basic-migrated/app/prompts/weather_chat.rb +44 -0
- data/examples/basic-migrated/app/resources/weather_alerts.rb +18 -0
- data/examples/basic-migrated/app/tools/get_current_weather.rb +34 -0
- data/examples/basic-migrated/app/tools/get_forecast.rb +30 -0
- data/examples/basic-migrated/app/tools/get_weather_by_coords.rb +48 -0
- data/examples/basic-migrated/server.rb +25 -0
- data/examples/basic.rb +73 -0
- data/examples/full_featured.rb +175 -0
- data/examples/middleware_example.rb +112 -0
- data/examples/sampling_example.rb +104 -0
- data/examples/weather-service/app/prompts/weather/chat.rb +90 -0
- data/examples/weather-service/app/resources/weather/alerts.rb +59 -0
- data/examples/weather-service/app/tools/weather/get_current.rb +82 -0
- data/examples/weather-service/app/tools/weather/get_forecast.rb +90 -0
- data/examples/weather-service/server.rb +28 -0
- data/exe/tsikol +6 -0
- data/lib/tsikol/cli/templates/Gemfile.erb +10 -0
- data/lib/tsikol/cli/templates/README.md.erb +38 -0
- data/lib/tsikol/cli/templates/gitignore.erb +49 -0
- data/lib/tsikol/cli/templates/prompt.rb.erb +53 -0
- data/lib/tsikol/cli/templates/resource.rb.erb +29 -0
- data/lib/tsikol/cli/templates/server.rb.erb +24 -0
- data/lib/tsikol/cli/templates/tool.rb.erb +60 -0
- data/lib/tsikol/cli.rb +203 -0
- data/lib/tsikol/error_handler.rb +141 -0
- data/lib/tsikol/health.rb +198 -0
- data/lib/tsikol/http_transport.rb +72 -0
- data/lib/tsikol/lifecycle.rb +149 -0
- data/lib/tsikol/middleware.rb +168 -0
- data/lib/tsikol/prompt.rb +101 -0
- data/lib/tsikol/resource.rb +53 -0
- data/lib/tsikol/router.rb +190 -0
- data/lib/tsikol/server.rb +660 -0
- data/lib/tsikol/stdio_transport.rb +108 -0
- data/lib/tsikol/test_helpers.rb +261 -0
- data/lib/tsikol/tool.rb +111 -0
- data/lib/tsikol/version.rb +5 -0
- data/lib/tsikol.rb +72 -0
- metadata +219 -0
@@ -0,0 +1,863 @@
|
|
1
|
+
# Testing Guide
|
2
|
+
|
3
|
+
Comprehensive testing ensures your MCP server works correctly and reliably. Tsikol provides test helpers and patterns for testing tools, resources, prompts, and complete server interactions.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
1. [Test Setup](#test-setup)
|
8
|
+
2. [Testing Tools](#testing-tools)
|
9
|
+
3. [Testing Resources](#testing-resources)
|
10
|
+
4. [Testing Prompts](#testing-prompts)
|
11
|
+
5. [Testing Middleware](#testing-middleware)
|
12
|
+
6. [Integration Testing](#integration-testing)
|
13
|
+
7. [Test Helpers](#test-helpers)
|
14
|
+
8. [Best Practices](#best-practices)
|
15
|
+
|
16
|
+
## Test Setup
|
17
|
+
|
18
|
+
### Basic Test Structure
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# test/test_helper.rb
|
22
|
+
require 'minitest/autorun'
|
23
|
+
require 'tsikol'
|
24
|
+
require 'tsikol/test_helpers'
|
25
|
+
|
26
|
+
# Load your server components
|
27
|
+
Dir.glob('app/**/*.rb').each { |file| require_relative "../#{file}" }
|
28
|
+
|
29
|
+
class TsikolTest < Minitest::Test
|
30
|
+
include Tsikol::TestHelpers::Assertions
|
31
|
+
|
32
|
+
def setup
|
33
|
+
@server = Tsikol::Server.new(name: "test-server")
|
34
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
35
|
+
end
|
36
|
+
|
37
|
+
def teardown
|
38
|
+
# Clean up resources if needed
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
### Test Directory Structure
|
44
|
+
|
45
|
+
```
|
46
|
+
test/
|
47
|
+
├── test_helper.rb
|
48
|
+
├── tools/
|
49
|
+
│ ├── file_manager_test.rb
|
50
|
+
│ └── database_query_test.rb
|
51
|
+
├── resources/
|
52
|
+
│ ├── system_status_test.rb
|
53
|
+
│ └── config_test.rb
|
54
|
+
├── prompts/
|
55
|
+
│ └── assistant_test.rb
|
56
|
+
├── middleware/
|
57
|
+
│ └── auth_middleware_test.rb
|
58
|
+
└── integration/
|
59
|
+
└── server_test.rb
|
60
|
+
```
|
61
|
+
|
62
|
+
## Testing Tools
|
63
|
+
|
64
|
+
### Basic Tool Test
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# test/tools/calculator_test.rb
|
68
|
+
require_relative '../test_helper'
|
69
|
+
|
70
|
+
class CalculatorTest < TsikolTest
|
71
|
+
def setup
|
72
|
+
super
|
73
|
+
@server.register_tool_instance(Calculator.new)
|
74
|
+
@client.initialize_connection
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_addition
|
78
|
+
response = @client.call_tool("calculator", {
|
79
|
+
"operation" => "add",
|
80
|
+
"a" => 5,
|
81
|
+
"b" => 3
|
82
|
+
})
|
83
|
+
|
84
|
+
assert_successful_response(response)
|
85
|
+
assert_equal 8, response.dig(:result, :content, 0, :text).to_i
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_division_by_zero
|
89
|
+
response = @client.call_tool("calculator", {
|
90
|
+
"operation" => "divide",
|
91
|
+
"a" => 10,
|
92
|
+
"b" => 0
|
93
|
+
})
|
94
|
+
|
95
|
+
assert_error_response(response)
|
96
|
+
assert_match /division by zero/i, response[:error][:message]
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_invalid_operation
|
100
|
+
response = @client.call_tool("calculator", {
|
101
|
+
"operation" => "invalid",
|
102
|
+
"a" => 1,
|
103
|
+
"b" => 2
|
104
|
+
})
|
105
|
+
|
106
|
+
assert_error_response(response)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
### Testing Tool Parameters
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
class FileManagerTest < TsikolTest
|
115
|
+
def test_required_parameters
|
116
|
+
# Missing required parameter
|
117
|
+
response = @client.call_tool("file_manager", {
|
118
|
+
"operation" => "read"
|
119
|
+
# Missing "path" parameter
|
120
|
+
})
|
121
|
+
|
122
|
+
assert_error_response(response, -32602)
|
123
|
+
assert_match /path.*required/i, response[:error][:message]
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_parameter_types
|
127
|
+
# Wrong parameter type
|
128
|
+
response = @client.call_tool("file_manager", {
|
129
|
+
"path" => 123, # Should be string
|
130
|
+
"operation" => "read"
|
131
|
+
})
|
132
|
+
|
133
|
+
assert_error_response(response, -32602)
|
134
|
+
assert_match /must be.*string/i, response[:error][:message]
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_enum_validation
|
138
|
+
response = @client.call_tool("file_manager", {
|
139
|
+
"path" => "test.txt",
|
140
|
+
"operation" => "invalid_op"
|
141
|
+
})
|
142
|
+
|
143
|
+
assert_error_response(response, -32602)
|
144
|
+
assert_match /must be one of/i, response[:error][:message]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
### Testing Tool Completions
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class CompletionTest < TsikolTest
|
153
|
+
def setup
|
154
|
+
super
|
155
|
+
@server.completion true
|
156
|
+
@server.register_tool_instance(GitTool.new)
|
157
|
+
@client.initialize_connection
|
158
|
+
end
|
159
|
+
|
160
|
+
def test_branch_completion
|
161
|
+
response = @client.complete(
|
162
|
+
{ type: "ref/tool", name: "git_tool" },
|
163
|
+
{ name: "branch", value: "fea" }
|
164
|
+
)
|
165
|
+
|
166
|
+
assert_successful_response(response)
|
167
|
+
|
168
|
+
values = response.dig(:result, :completion, :values)
|
169
|
+
assert values.include?("feature")
|
170
|
+
assert values.include?("feature/new-ui")
|
171
|
+
refute values.include?("main") # Doesn't start with "fea"
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_context_aware_completion
|
175
|
+
response = @client.complete(
|
176
|
+
{ type: "ref/tool", name: "database_query" },
|
177
|
+
{ name: "column", value: "u" },
|
178
|
+
{ table: "users" } # Context from other parameter
|
179
|
+
)
|
180
|
+
|
181
|
+
assert_successful_response(response)
|
182
|
+
|
183
|
+
values = response.dig(:result, :completion, :values)
|
184
|
+
assert values.include?("user_id")
|
185
|
+
assert values.include?("username")
|
186
|
+
refute values.include?("post_id") # From different table
|
187
|
+
end
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
### Testing Async Tools
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class AsyncToolTest < TsikolTest
|
195
|
+
def test_async_execution
|
196
|
+
response = @client.call_tool("async_processor", {
|
197
|
+
"data" => "test data",
|
198
|
+
"callback_url" => "http://example.com/callback"
|
199
|
+
})
|
200
|
+
|
201
|
+
assert_successful_response(response)
|
202
|
+
|
203
|
+
result = JSON.parse(response.dig(:result, :content, 0, :text))
|
204
|
+
assert result["job_id"]
|
205
|
+
assert_equal "processing", result["status"]
|
206
|
+
|
207
|
+
# Wait for async processing
|
208
|
+
sleep(0.1)
|
209
|
+
|
210
|
+
# Verify callback was made (mock or check logs)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
## Testing Resources
|
216
|
+
|
217
|
+
### Basic Resource Test
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
# test/resources/system_status_test.rb
|
221
|
+
class SystemStatusTest < TsikolTest
|
222
|
+
def setup
|
223
|
+
super
|
224
|
+
@server.register_resource_instance(SystemStatus.new)
|
225
|
+
@client.initialize_connection
|
226
|
+
end
|
227
|
+
|
228
|
+
def test_read_status
|
229
|
+
response = @client.read_resource("system/status")
|
230
|
+
|
231
|
+
assert_successful_response(response)
|
232
|
+
|
233
|
+
content = response.dig(:result, :contents, 0, :text)
|
234
|
+
data = JSON.parse(content)
|
235
|
+
|
236
|
+
assert data["status"]
|
237
|
+
assert data["uptime"]
|
238
|
+
assert data["version"]
|
239
|
+
assert data["timestamp"]
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_resource_not_found
|
243
|
+
response = @client.read_resource("nonexistent/resource")
|
244
|
+
|
245
|
+
assert_error_response(response, -32002)
|
246
|
+
assert_match /resource not found/i, response[:error][:message]
|
247
|
+
end
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
### Testing Dynamic Resources
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
class DynamicResourceTest < TsikolTest
|
255
|
+
def test_real_time_updates
|
256
|
+
# Read resource twice
|
257
|
+
response1 = @client.read_resource("metrics/live")
|
258
|
+
sleep(0.1)
|
259
|
+
response2 = @client.read_resource("metrics/live")
|
260
|
+
|
261
|
+
data1 = JSON.parse(response1.dig(:result, :contents, 0, :text))
|
262
|
+
data2 = JSON.parse(response2.dig(:result, :contents, 0, :text))
|
263
|
+
|
264
|
+
# Timestamps should differ
|
265
|
+
refute_equal data1["timestamp"], data2["timestamp"]
|
266
|
+
|
267
|
+
# Some metrics might change
|
268
|
+
refute_equal data1["requests_count"], data2["requests_count"]
|
269
|
+
end
|
270
|
+
end
|
271
|
+
```
|
272
|
+
|
273
|
+
### Testing Cached Resources
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
class CachedResourceTest < TsikolTest
|
277
|
+
def test_caching_behavior
|
278
|
+
# Measure first call (cache miss)
|
279
|
+
start = Time.now
|
280
|
+
response1 = @client.read_resource("expensive/data")
|
281
|
+
duration1 = Time.now - start
|
282
|
+
|
283
|
+
# Measure second call (cache hit)
|
284
|
+
start = Time.now
|
285
|
+
response2 = @client.read_resource("expensive/data")
|
286
|
+
duration2 = Time.now - start
|
287
|
+
|
288
|
+
# Cache hit should be much faster
|
289
|
+
assert duration2 < duration1 / 10
|
290
|
+
|
291
|
+
# Content should be identical
|
292
|
+
assert_equal(
|
293
|
+
response1.dig(:result, :contents, 0, :text),
|
294
|
+
response2.dig(:result, :contents, 0, :text)
|
295
|
+
)
|
296
|
+
end
|
297
|
+
|
298
|
+
def test_cache_expiration
|
299
|
+
response1 = @client.read_resource("cached/data")
|
300
|
+
|
301
|
+
# Wait for cache to expire
|
302
|
+
sleep(cache_ttl + 0.1)
|
303
|
+
|
304
|
+
response2 = @client.read_resource("cached/data")
|
305
|
+
|
306
|
+
# Should have new timestamp
|
307
|
+
data1 = JSON.parse(response1.dig(:result, :contents, 0, :text))
|
308
|
+
data2 = JSON.parse(response2.dig(:result, :contents, 0, :text))
|
309
|
+
|
310
|
+
refute_equal data1["generated_at"], data2["generated_at"]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
## Testing Prompts
|
316
|
+
|
317
|
+
### Basic Prompt Test
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
# test/prompts/code_assistant_test.rb
|
321
|
+
class CodeAssistantTest < TsikolTest
|
322
|
+
def setup
|
323
|
+
super
|
324
|
+
@server.register_prompt_instance(CodeAssistant.new)
|
325
|
+
@client.initialize_connection
|
326
|
+
end
|
327
|
+
|
328
|
+
def test_get_prompt
|
329
|
+
response = @client.get_prompt("code_assistant", {
|
330
|
+
"language" => "ruby",
|
331
|
+
"task" => "write unit tests"
|
332
|
+
})
|
333
|
+
|
334
|
+
assert_successful_response(response)
|
335
|
+
|
336
|
+
messages = response.dig(:result, :messages)
|
337
|
+
assert messages.is_a?(Array)
|
338
|
+
assert messages.length >= 2
|
339
|
+
|
340
|
+
# Check system message
|
341
|
+
system_msg = messages.find { |m| m[:role] == "system" }
|
342
|
+
assert system_msg
|
343
|
+
assert_match /ruby/i, system_msg[:content][:text]
|
344
|
+
|
345
|
+
# Check user message
|
346
|
+
user_msg = messages.find { |m| m[:role] == "user" }
|
347
|
+
assert user_msg
|
348
|
+
assert_match /unit tests/i, user_msg[:content][:text]
|
349
|
+
end
|
350
|
+
|
351
|
+
def test_optional_arguments
|
352
|
+
response = @client.get_prompt("code_assistant", {
|
353
|
+
"language" => "python",
|
354
|
+
"task" => "parse JSON"
|
355
|
+
# Omitting optional arguments
|
356
|
+
})
|
357
|
+
|
358
|
+
assert_successful_response(response)
|
359
|
+
end
|
360
|
+
|
361
|
+
def test_invalid_enum_value
|
362
|
+
response = @client.get_prompt("code_assistant", {
|
363
|
+
"language" => "cobol", # Not in enum
|
364
|
+
"task" => "write code"
|
365
|
+
})
|
366
|
+
|
367
|
+
assert_error_response(response)
|
368
|
+
assert_match /must be one of/i, response[:error][:message]
|
369
|
+
end
|
370
|
+
end
|
371
|
+
```
|
372
|
+
|
373
|
+
### Testing Dynamic Prompts
|
374
|
+
|
375
|
+
```ruby
|
376
|
+
class DynamicPromptTest < TsikolTest
|
377
|
+
def test_includes_current_context
|
378
|
+
# Mock current data
|
379
|
+
prompt = DataAnalysisPrompt.new
|
380
|
+
prompt.define_singleton_method(:fetch_current_metrics) do
|
381
|
+
{ users: 100, revenue: 50000 }
|
382
|
+
end
|
383
|
+
|
384
|
+
@server.register_prompt_instance(prompt)
|
385
|
+
@client.initialize_connection
|
386
|
+
|
387
|
+
response = @client.get_prompt("data_analysis", {
|
388
|
+
"query" => "analyze user growth"
|
389
|
+
})
|
390
|
+
|
391
|
+
messages = response.dig(:result, :messages)
|
392
|
+
|
393
|
+
# Should include metrics in context
|
394
|
+
context_msg = messages.find { |m|
|
395
|
+
m[:content][:text].include?("users") &&
|
396
|
+
m[:content][:text].include?("100")
|
397
|
+
}
|
398
|
+
|
399
|
+
assert context_msg, "Should include current metrics in prompt"
|
400
|
+
end
|
401
|
+
end
|
402
|
+
```
|
403
|
+
|
404
|
+
## Testing Middleware
|
405
|
+
|
406
|
+
### Testing Custom Middleware
|
407
|
+
|
408
|
+
```ruby
|
409
|
+
# test/middleware/auth_middleware_test.rb
|
410
|
+
class AuthMiddlewareTest < Minitest::Test
|
411
|
+
def setup
|
412
|
+
@app = MockApp.new
|
413
|
+
@middleware = AuthenticationMiddleware.new(@app, secret_key: "test_secret")
|
414
|
+
end
|
415
|
+
|
416
|
+
def test_passes_authenticated_requests
|
417
|
+
token = generate_valid_token
|
418
|
+
request = {
|
419
|
+
"jsonrpc" => "2.0",
|
420
|
+
"id" => 1,
|
421
|
+
"method" => "tools/call",
|
422
|
+
"params" => { "_token" => token }
|
423
|
+
}
|
424
|
+
|
425
|
+
response = @middleware.call(request)
|
426
|
+
|
427
|
+
assert_equal :success, response[:result]
|
428
|
+
assert @app.called?
|
429
|
+
assert_equal "user123", request["authenticated_user"]["user_id"]
|
430
|
+
end
|
431
|
+
|
432
|
+
def test_rejects_invalid_token
|
433
|
+
request = {
|
434
|
+
"jsonrpc" => "2.0",
|
435
|
+
"id" => 1,
|
436
|
+
"method" => "tools/call",
|
437
|
+
"params" => { "_token" => "invalid_token" }
|
438
|
+
}
|
439
|
+
|
440
|
+
response = @middleware.call(request)
|
441
|
+
|
442
|
+
assert_equal -32001, response[:error][:code]
|
443
|
+
refute @app.called?
|
444
|
+
end
|
445
|
+
|
446
|
+
private
|
447
|
+
|
448
|
+
def generate_valid_token
|
449
|
+
payload = {
|
450
|
+
user_id: "user123",
|
451
|
+
exp: Time.now.to_i + 3600
|
452
|
+
}
|
453
|
+
JWT.encode(payload, "test_secret", 'HS256')
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
class MockApp
|
458
|
+
attr_reader :called
|
459
|
+
|
460
|
+
def call(_request)
|
461
|
+
@called = true
|
462
|
+
{ jsonrpc: "2.0", id: 1, result: :success }
|
463
|
+
end
|
464
|
+
|
465
|
+
def called?
|
466
|
+
@called
|
467
|
+
end
|
468
|
+
end
|
469
|
+
```
|
470
|
+
|
471
|
+
### Testing Middleware Chain
|
472
|
+
|
473
|
+
```ruby
|
474
|
+
class MiddlewareChainTest < TsikolTest
|
475
|
+
def setup
|
476
|
+
super
|
477
|
+
|
478
|
+
# Build server with middleware chain
|
479
|
+
@server.use LoggingMiddleware
|
480
|
+
@server.use AuthenticationMiddleware, secret_key: "secret"
|
481
|
+
@server.use RateLimitMiddleware, max_requests: 5
|
482
|
+
|
483
|
+
@server.register_tool_instance(TestTool.new)
|
484
|
+
@client.initialize_connection
|
485
|
+
end
|
486
|
+
|
487
|
+
def test_middleware_order
|
488
|
+
# Make authenticated request
|
489
|
+
token = generate_valid_token
|
490
|
+
|
491
|
+
response = @client.call_tool("test_tool", {
|
492
|
+
"_token" => token,
|
493
|
+
"param" => "value"
|
494
|
+
})
|
495
|
+
|
496
|
+
assert_successful_response(response)
|
497
|
+
|
498
|
+
# Verify middleware executed in order
|
499
|
+
# (Check logs, metrics, or other side effects)
|
500
|
+
end
|
501
|
+
|
502
|
+
def test_middleware_short_circuit
|
503
|
+
# Make request without auth token
|
504
|
+
response = @client.call_tool("test_tool", {
|
505
|
+
"param" => "value"
|
506
|
+
})
|
507
|
+
|
508
|
+
# Auth middleware should reject before rate limiting
|
509
|
+
assert_error_response(response, -32001)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
```
|
513
|
+
|
514
|
+
## Integration Testing
|
515
|
+
|
516
|
+
### Full Server Test
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
# test/integration/server_test.rb
|
520
|
+
class ServerIntegrationTest < TsikolTest
|
521
|
+
def setup
|
522
|
+
# Build complete server
|
523
|
+
@server = Tsikol::Server.new(name: "integration-test")
|
524
|
+
|
525
|
+
# Add middleware
|
526
|
+
@server.use Tsikol::LoggingMiddleware
|
527
|
+
@server.use Tsikol::ErrorHandlingMiddleware
|
528
|
+
|
529
|
+
# Register components
|
530
|
+
@server.register_tool_instance(FileManager.new)
|
531
|
+
@server.register_resource_instance(SystemStatus.new)
|
532
|
+
@server.register_prompt_instance(Assistant.new)
|
533
|
+
|
534
|
+
# Enable features
|
535
|
+
@server.completion true
|
536
|
+
@server.sampling true
|
537
|
+
|
538
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
539
|
+
@client.initialize_connection
|
540
|
+
end
|
541
|
+
|
542
|
+
def test_full_workflow
|
543
|
+
# 1. Initialize connection
|
544
|
+
response = @client.initialize_connection
|
545
|
+
assert_successful_response(response)
|
546
|
+
|
547
|
+
capabilities = response.dig(:result, :capabilities)
|
548
|
+
assert capabilities[:tools]
|
549
|
+
assert capabilities[:resources]
|
550
|
+
assert capabilities[:prompts]
|
551
|
+
assert capabilities[:completion]
|
552
|
+
assert capabilities[:sampling]
|
553
|
+
|
554
|
+
# 2. List tools
|
555
|
+
response = @client.list_tools
|
556
|
+
assert_successful_response(response)
|
557
|
+
|
558
|
+
tools = response.dig(:result, :tools)
|
559
|
+
assert tools.any? { |t| t[:name] == "file_manager" }
|
560
|
+
|
561
|
+
# 3. Call a tool
|
562
|
+
response = @client.call_tool("file_manager", {
|
563
|
+
"operation" => "list",
|
564
|
+
"path" => "."
|
565
|
+
})
|
566
|
+
assert_successful_response(response)
|
567
|
+
|
568
|
+
# 4. Read a resource
|
569
|
+
response = @client.read_resource("system/status")
|
570
|
+
assert_successful_response(response)
|
571
|
+
|
572
|
+
# 5. Get a prompt
|
573
|
+
response = @client.get_prompt("assistant", {
|
574
|
+
"task" => "help with testing"
|
575
|
+
})
|
576
|
+
assert_successful_response(response)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
```
|
580
|
+
|
581
|
+
### Error Handling Test
|
582
|
+
|
583
|
+
```ruby
|
584
|
+
class ErrorHandlingTest < TsikolTest
|
585
|
+
def test_handles_tool_errors_gracefully
|
586
|
+
@server.register_tool_instance(FaultyTool.new)
|
587
|
+
@client.initialize_connection
|
588
|
+
|
589
|
+
response = @client.call_tool("faulty_tool", {
|
590
|
+
"trigger_error" => true
|
591
|
+
})
|
592
|
+
|
593
|
+
assert_error_response(response)
|
594
|
+
assert response[:error][:message]
|
595
|
+
assert response[:error][:data] # Should include error details
|
596
|
+
end
|
597
|
+
|
598
|
+
def test_handles_malformed_requests
|
599
|
+
# Send malformed JSON-RPC request
|
600
|
+
response = @server.handle_request({
|
601
|
+
"id" => 1,
|
602
|
+
"method" => "test"
|
603
|
+
# Missing jsonrpc version
|
604
|
+
})
|
605
|
+
|
606
|
+
assert_error_response(response, -32600) # Invalid Request
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
class FaultyTool < Tsikol::Tool
|
611
|
+
def execute(trigger_error:)
|
612
|
+
raise "Intentional error" if trigger_error
|
613
|
+
"success"
|
614
|
+
end
|
615
|
+
end
|
616
|
+
```
|
617
|
+
|
618
|
+
## Test Helpers
|
619
|
+
|
620
|
+
### Custom Assertions
|
621
|
+
|
622
|
+
```ruby
|
623
|
+
module CustomAssertions
|
624
|
+
def assert_json_equal(expected, actual)
|
625
|
+
expected_parsed = expected.is_a?(String) ? JSON.parse(expected) : expected
|
626
|
+
actual_parsed = actual.is_a?(String) ? JSON.parse(actual) : actual
|
627
|
+
|
628
|
+
assert_equal expected_parsed, actual_parsed
|
629
|
+
end
|
630
|
+
|
631
|
+
def assert_includes_all(collection, items)
|
632
|
+
items.each do |item|
|
633
|
+
assert_includes collection, item
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
def assert_response_time(max_ms)
|
638
|
+
start = Time.now
|
639
|
+
yield
|
640
|
+
duration = (Time.now - start) * 1000
|
641
|
+
|
642
|
+
assert duration < max_ms,
|
643
|
+
"Response took #{duration.round}ms (max: #{max_ms}ms)"
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
class TsikolTest < Minitest::Test
|
648
|
+
include Tsikol::TestHelpers::Assertions
|
649
|
+
include CustomAssertions
|
650
|
+
end
|
651
|
+
```
|
652
|
+
|
653
|
+
### Test Fixtures
|
654
|
+
|
655
|
+
```ruby
|
656
|
+
module TestFixtures
|
657
|
+
def sample_file_content
|
658
|
+
<<~CONTENT
|
659
|
+
This is a test file.
|
660
|
+
It has multiple lines.
|
661
|
+
Used for testing file operations.
|
662
|
+
CONTENT
|
663
|
+
end
|
664
|
+
|
665
|
+
def sample_json_data
|
666
|
+
{
|
667
|
+
"users" => [
|
668
|
+
{ "id" => 1, "name" => "Alice" },
|
669
|
+
{ "id" => 2, "name" => "Bob" }
|
670
|
+
],
|
671
|
+
"metadata" => {
|
672
|
+
"version" => "1.0",
|
673
|
+
"generated_at" => Time.now.iso8601
|
674
|
+
}
|
675
|
+
}
|
676
|
+
end
|
677
|
+
|
678
|
+
def create_test_file(name, content = sample_file_content)
|
679
|
+
File.write(name, content)
|
680
|
+
|
681
|
+
# Ensure cleanup after test
|
682
|
+
@files_to_cleanup ||= []
|
683
|
+
@files_to_cleanup << name
|
684
|
+
end
|
685
|
+
|
686
|
+
def teardown
|
687
|
+
super
|
688
|
+
@files_to_cleanup&.each { |f| File.delete(f) if File.exist?(f) }
|
689
|
+
end
|
690
|
+
end
|
691
|
+
```
|
692
|
+
|
693
|
+
### Mock Helpers
|
694
|
+
|
695
|
+
```ruby
|
696
|
+
class MockServer < Tsikol::Server
|
697
|
+
attr_accessor :sampling_responses
|
698
|
+
|
699
|
+
def initialize(name:)
|
700
|
+
super
|
701
|
+
@sampling_responses = []
|
702
|
+
end
|
703
|
+
|
704
|
+
def sample_text(**params)
|
705
|
+
# Return predefined responses for testing
|
706
|
+
@sampling_responses.shift || { text: "Mock response" }
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
# Usage in tests
|
711
|
+
def test_tool_with_sampling
|
712
|
+
mock_server = MockServer.new(name: "test")
|
713
|
+
mock_server.sampling_responses = [
|
714
|
+
{ text: "Generated code" },
|
715
|
+
{ error: "Rate limited" }
|
716
|
+
]
|
717
|
+
|
718
|
+
# Test tool behavior with mocked sampling
|
719
|
+
end
|
720
|
+
```
|
721
|
+
|
722
|
+
## Best Practices
|
723
|
+
|
724
|
+
### 1. Test Organization
|
725
|
+
|
726
|
+
```ruby
|
727
|
+
# Group related tests
|
728
|
+
class FileManagerTest < TsikolTest
|
729
|
+
# Test successful operations
|
730
|
+
def test_read_existing_file
|
731
|
+
end
|
732
|
+
|
733
|
+
def test_write_new_file
|
734
|
+
end
|
735
|
+
|
736
|
+
# Test error cases
|
737
|
+
def test_read_nonexistent_file
|
738
|
+
end
|
739
|
+
|
740
|
+
def test_write_protected_directory
|
741
|
+
end
|
742
|
+
|
743
|
+
# Test edge cases
|
744
|
+
def test_empty_file
|
745
|
+
end
|
746
|
+
|
747
|
+
def test_large_file
|
748
|
+
end
|
749
|
+
end
|
750
|
+
```
|
751
|
+
|
752
|
+
### 2. Setup and Teardown
|
753
|
+
|
754
|
+
```ruby
|
755
|
+
class DatabaseToolTest < TsikolTest
|
756
|
+
def setup
|
757
|
+
super
|
758
|
+
@test_db = create_test_database
|
759
|
+
@tool = DatabaseTool.new(database: @test_db)
|
760
|
+
@server.register_tool_instance(@tool)
|
761
|
+
end
|
762
|
+
|
763
|
+
def teardown
|
764
|
+
@test_db.close
|
765
|
+
cleanup_test_database
|
766
|
+
super
|
767
|
+
end
|
768
|
+
end
|
769
|
+
```
|
770
|
+
|
771
|
+
### 3. Descriptive Test Names
|
772
|
+
|
773
|
+
```ruby
|
774
|
+
# Good test names
|
775
|
+
def test_returns_error_when_file_not_found
|
776
|
+
def test_completes_branch_names_starting_with_prefix
|
777
|
+
def test_rate_limits_after_exceeding_threshold
|
778
|
+
|
779
|
+
# Poor test names
|
780
|
+
def test_1
|
781
|
+
def test_error
|
782
|
+
def test_completion
|
783
|
+
```
|
784
|
+
|
785
|
+
### 4. Test Data Isolation
|
786
|
+
|
787
|
+
```ruby
|
788
|
+
def test_concurrent_requests
|
789
|
+
# Each test uses unique data
|
790
|
+
file1 = "test_#{SecureRandom.hex(8)}.txt"
|
791
|
+
file2 = "test_#{SecureRandom.hex(8)}.txt"
|
792
|
+
|
793
|
+
# Test concurrent operations
|
794
|
+
ensure
|
795
|
+
[file1, file2].each { |f| File.delete(f) if File.exist?(f) }
|
796
|
+
end
|
797
|
+
```
|
798
|
+
|
799
|
+
### 5. Performance Testing
|
800
|
+
|
801
|
+
```ruby
|
802
|
+
def test_handles_high_load
|
803
|
+
assert_response_time(1000) do # Max 1 second
|
804
|
+
100.times do
|
805
|
+
@client.read_resource("system/status")
|
806
|
+
end
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
def test_large_file_processing
|
811
|
+
large_file = create_test_file("large.txt", "x" * 10_000_000) # 10MB
|
812
|
+
|
813
|
+
assert_response_time(5000) do # Max 5 seconds
|
814
|
+
@client.call_tool("file_processor", {
|
815
|
+
"path" => large_file,
|
816
|
+
"operation" => "analyze"
|
817
|
+
})
|
818
|
+
end
|
819
|
+
end
|
820
|
+
```
|
821
|
+
|
822
|
+
## Running Tests
|
823
|
+
|
824
|
+
### Run All Tests
|
825
|
+
|
826
|
+
```bash
|
827
|
+
# Run all tests
|
828
|
+
bundle exec rake test
|
829
|
+
|
830
|
+
# Run with verbose output
|
831
|
+
bundle exec rake test TESTOPTS="--verbose"
|
832
|
+
|
833
|
+
# Run specific test file
|
834
|
+
ruby -Ilib:test test/tools/file_manager_test.rb
|
835
|
+
|
836
|
+
# Run specific test method
|
837
|
+
ruby -Ilib:test test/tools/file_manager_test.rb -n test_read_file
|
838
|
+
```
|
839
|
+
|
840
|
+
### Test Coverage
|
841
|
+
|
842
|
+
```ruby
|
843
|
+
# Gemfile
|
844
|
+
group :test do
|
845
|
+
gem 'simplecov'
|
846
|
+
end
|
847
|
+
|
848
|
+
# test/test_helper.rb
|
849
|
+
require 'simplecov'
|
850
|
+
SimpleCov.start do
|
851
|
+
add_filter '/test/'
|
852
|
+
add_group 'Tools', 'app/tools'
|
853
|
+
add_group 'Resources', 'app/resources'
|
854
|
+
add_group 'Prompts', 'app/prompts'
|
855
|
+
end
|
856
|
+
```
|
857
|
+
|
858
|
+
## Next Steps
|
859
|
+
|
860
|
+
- Review [Test Helpers API](../api/test-helpers.md)
|
861
|
+
- Learn about [Error Handling](../cookbook/error-handling.md)
|
862
|
+
- Explore [Logging](../cookbook/logging.md) for debugging
|
863
|
+
- Check [Examples](../examples/) for more test patterns
|