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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CONTRIBUTING.md +84 -0
  4. data/LICENSE +21 -0
  5. data/README.md +579 -0
  6. data/Rakefile +12 -0
  7. data/docs/README.md +69 -0
  8. data/docs/api/middleware.md +721 -0
  9. data/docs/api/prompt.md +858 -0
  10. data/docs/api/resource.md +651 -0
  11. data/docs/api/server.md +509 -0
  12. data/docs/api/test-helpers.md +591 -0
  13. data/docs/api/tool.md +527 -0
  14. data/docs/cookbook/authentication.md +651 -0
  15. data/docs/cookbook/caching.md +877 -0
  16. data/docs/cookbook/dynamic-tools.md +970 -0
  17. data/docs/cookbook/error-handling.md +887 -0
  18. data/docs/cookbook/logging.md +1044 -0
  19. data/docs/cookbook/rate-limiting.md +717 -0
  20. data/docs/examples/code-assistant.md +922 -0
  21. data/docs/examples/complete-server.md +726 -0
  22. data/docs/examples/database-manager.md +1198 -0
  23. data/docs/examples/devops-tools.md +1382 -0
  24. data/docs/examples/echo-server.md +501 -0
  25. data/docs/examples/weather-service.md +822 -0
  26. data/docs/guides/completion.md +472 -0
  27. data/docs/guides/getting-started.md +462 -0
  28. data/docs/guides/middleware.md +823 -0
  29. data/docs/guides/project-structure.md +434 -0
  30. data/docs/guides/prompts.md +920 -0
  31. data/docs/guides/resources.md +720 -0
  32. data/docs/guides/sampling.md +804 -0
  33. data/docs/guides/testing.md +863 -0
  34. data/docs/guides/tools.md +627 -0
  35. data/examples/README.md +92 -0
  36. data/examples/advanced_features.rb +129 -0
  37. data/examples/basic-migrated/app/prompts/weather_chat.rb +44 -0
  38. data/examples/basic-migrated/app/resources/weather_alerts.rb +18 -0
  39. data/examples/basic-migrated/app/tools/get_current_weather.rb +34 -0
  40. data/examples/basic-migrated/app/tools/get_forecast.rb +30 -0
  41. data/examples/basic-migrated/app/tools/get_weather_by_coords.rb +48 -0
  42. data/examples/basic-migrated/server.rb +25 -0
  43. data/examples/basic.rb +73 -0
  44. data/examples/full_featured.rb +175 -0
  45. data/examples/middleware_example.rb +112 -0
  46. data/examples/sampling_example.rb +104 -0
  47. data/examples/weather-service/app/prompts/weather/chat.rb +90 -0
  48. data/examples/weather-service/app/resources/weather/alerts.rb +59 -0
  49. data/examples/weather-service/app/tools/weather/get_current.rb +82 -0
  50. data/examples/weather-service/app/tools/weather/get_forecast.rb +90 -0
  51. data/examples/weather-service/server.rb +28 -0
  52. data/exe/tsikol +6 -0
  53. data/lib/tsikol/cli/templates/Gemfile.erb +10 -0
  54. data/lib/tsikol/cli/templates/README.md.erb +38 -0
  55. data/lib/tsikol/cli/templates/gitignore.erb +49 -0
  56. data/lib/tsikol/cli/templates/prompt.rb.erb +53 -0
  57. data/lib/tsikol/cli/templates/resource.rb.erb +29 -0
  58. data/lib/tsikol/cli/templates/server.rb.erb +24 -0
  59. data/lib/tsikol/cli/templates/tool.rb.erb +60 -0
  60. data/lib/tsikol/cli.rb +203 -0
  61. data/lib/tsikol/error_handler.rb +141 -0
  62. data/lib/tsikol/health.rb +198 -0
  63. data/lib/tsikol/http_transport.rb +72 -0
  64. data/lib/tsikol/lifecycle.rb +149 -0
  65. data/lib/tsikol/middleware.rb +168 -0
  66. data/lib/tsikol/prompt.rb +101 -0
  67. data/lib/tsikol/resource.rb +53 -0
  68. data/lib/tsikol/router.rb +190 -0
  69. data/lib/tsikol/server.rb +660 -0
  70. data/lib/tsikol/stdio_transport.rb +108 -0
  71. data/lib/tsikol/test_helpers.rb +261 -0
  72. data/lib/tsikol/tool.rb +111 -0
  73. data/lib/tsikol/version.rb +5 -0
  74. data/lib/tsikol.rb +72 -0
  75. 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