kbs 0.0.1 → 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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +235 -334
  5. data/docs/DOCUMENTATION_STATUS.md +158 -0
  6. data/docs/advanced/custom-persistence.md +775 -0
  7. data/docs/advanced/debugging.md +726 -0
  8. data/docs/advanced/index.md +8 -0
  9. data/docs/advanced/performance.md +832 -0
  10. data/docs/advanced/testing.md +691 -0
  11. data/docs/api/blackboard.md +1157 -0
  12. data/docs/api/engine.md +978 -0
  13. data/docs/api/facts.md +1212 -0
  14. data/docs/api/index.md +12 -0
  15. data/docs/api/rules.md +1034 -0
  16. data/docs/architecture/blackboard.md +553 -0
  17. data/docs/architecture/index.md +277 -0
  18. data/docs/architecture/network-structure.md +343 -0
  19. data/docs/architecture/rete-algorithm.md +737 -0
  20. data/docs/assets/css/custom.css +83 -0
  21. data/docs/assets/images/blackboard-architecture.svg +136 -0
  22. data/docs/assets/images/compiled-network.svg +101 -0
  23. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  24. data/docs/assets/images/kbs.jpg +0 -0
  25. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  26. data/docs/assets/images/rete-network-layers.svg +96 -0
  27. data/docs/assets/images/system-layers.svg +69 -0
  28. data/docs/assets/images/trading-signal-network.svg +139 -0
  29. data/docs/assets/js/mathjax.js +17 -0
  30. data/docs/examples/expert-systems.md +1031 -0
  31. data/docs/examples/index.md +9 -0
  32. data/docs/examples/multi-agent.md +1335 -0
  33. data/docs/examples/stock-trading.md +488 -0
  34. data/docs/guides/blackboard-memory.md +558 -0
  35. data/docs/guides/dsl.md +1321 -0
  36. data/docs/guides/facts.md +652 -0
  37. data/docs/guides/getting-started.md +383 -0
  38. data/docs/guides/index.md +23 -0
  39. data/docs/guides/negation.md +529 -0
  40. data/docs/guides/pattern-matching.md +561 -0
  41. data/docs/guides/persistence.md +451 -0
  42. data/docs/guides/variable-binding.md +491 -0
  43. data/docs/guides/writing-rules.md +755 -0
  44. data/docs/index.md +157 -0
  45. data/docs/installation.md +156 -0
  46. data/docs/quick-start.md +228 -0
  47. data/examples/README.md +2 -2
  48. data/examples/advanced_example.rb +2 -2
  49. data/examples/advanced_example_dsl.rb +224 -0
  50. data/examples/ai_enhanced_kbs.rb +1 -1
  51. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  52. data/examples/blackboard_demo_dsl.rb +50 -0
  53. data/examples/car_diagnostic.rb +1 -1
  54. data/examples/car_diagnostic_dsl.rb +54 -0
  55. data/examples/concurrent_inference_demo.rb +5 -5
  56. data/examples/concurrent_inference_demo_dsl.rb +363 -0
  57. data/examples/csv_trading_system.rb +1 -1
  58. data/examples/csv_trading_system_dsl.rb +525 -0
  59. data/examples/knowledge_base.db +0 -0
  60. data/examples/portfolio_rebalancing_system.rb +2 -2
  61. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  62. data/examples/redis_trading_demo_dsl.rb +177 -0
  63. data/examples/run_all.rb +50 -0
  64. data/examples/run_all_dsl.rb +49 -0
  65. data/examples/stock_trading_advanced.rb +1 -1
  66. data/examples/stock_trading_advanced_dsl.rb +404 -0
  67. data/examples/temp.txt +7693 -0
  68. data/examples/temp_dsl.txt +8447 -0
  69. data/examples/timestamped_trading.rb +1 -1
  70. data/examples/timestamped_trading_dsl.rb +258 -0
  71. data/examples/trading_demo.rb +1 -1
  72. data/examples/trading_demo_dsl.rb +322 -0
  73. data/examples/working_demo.rb +1 -1
  74. data/examples/working_demo_dsl.rb +160 -0
  75. data/lib/kbs/blackboard/engine.rb +3 -3
  76. data/lib/kbs/blackboard/fact.rb +1 -1
  77. data/lib/kbs/condition.rb +1 -1
  78. data/lib/kbs/dsl/knowledge_base.rb +1 -1
  79. data/lib/kbs/dsl/variable.rb +1 -1
  80. data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
  81. data/lib/kbs/fact.rb +1 -1
  82. data/lib/kbs/version.rb +1 -1
  83. data/lib/kbs.rb +2 -2
  84. data/mkdocs.yml +181 -0
  85. metadata +66 -6
  86. data/examples/stock_trading_system.rb.bak +0 -563
@@ -0,0 +1,691 @@
1
+ # Testing Rules
2
+
3
+ Comprehensive testing strategies for rule-based systems. This guide covers unit testing, integration testing, test fixtures, and coverage analysis for KBS applications.
4
+
5
+ ## Testing Overview
6
+
7
+ Rule-based systems require testing at multiple levels:
8
+
9
+ 1. **Unit Tests** - Test individual rules in isolation
10
+ 2. **Integration Tests** - Test rule interactions
11
+ 3. **Fact Fixtures** - Reusable test data
12
+ 4. **Coverage** - Ensure all rules and conditions are tested
13
+ 5. **Performance Tests** - Verify rule execution speed
14
+
15
+ ## Setup
16
+
17
+ ### Test Framework
18
+
19
+ ```ruby
20
+ # Gemfile
21
+ group :test do
22
+ gem 'minitest', '~> 5.0'
23
+ gem 'simplecov', require: false # Coverage
24
+ end
25
+ ```
26
+
27
+ ### Test Helper
28
+
29
+ ```ruby
30
+ # test/test_helper.rb
31
+ require 'simplecov'
32
+ SimpleCov.start
33
+
34
+ require 'minitest/autorun'
35
+ require 'kbs'
36
+
37
+ class Minitest::Test
38
+ def setup_engine
39
+ KBS::Engine.new
40
+ end
41
+
42
+ def assert_rule_fired(engine, rule_name)
43
+ # Check if rule action was executed
44
+ # Implementation depends on tracking mechanism
45
+ end
46
+
47
+ def refute_rule_fired(engine, rule_name)
48
+ # Check that rule did not fire
49
+ end
50
+ end
51
+ ```
52
+
53
+ ## Unit Testing Rules
54
+
55
+ ### Test Single Rule
56
+
57
+ ```ruby
58
+ require 'test_helper'
59
+
60
+ class TestTemperatureRule < Minitest::Test
61
+ def setup
62
+ @engine = setup_engine
63
+ @fired = false
64
+
65
+ # Create test rule
66
+ @rule = KBS::Rule.new("high_temp_alert", priority: 100) do |r|
67
+ r.conditions = [
68
+ KBS::Condition.new(:sensor, {
69
+ type: "temperature",
70
+ value: :temp?
71
+ }, predicate: lambda { |f| f[:value] > 30 })
72
+ ]
73
+
74
+ r.action = lambda do |facts, bindings|
75
+ @fired = true
76
+ @engine.add_fact(:alert, {
77
+ type: "high_temperature",
78
+ temperature: bindings[:temp?]
79
+ })
80
+ end
81
+ end
82
+
83
+ @engine.add_rule(@rule)
84
+ end
85
+
86
+ def test_fires_when_temperature_high
87
+ @engine.add_fact(:sensor, { type: "temperature", value: 35 })
88
+ @engine.run
89
+
90
+ assert @fired, "Rule should fire for high temperature"
91
+
92
+ alerts = @engine.facts.select { |f| f.type == :alert }
93
+ assert_equal 1, alerts.size
94
+ assert_equal 35, alerts.first[:temperature]
95
+ end
96
+
97
+ def test_does_not_fire_when_temperature_normal
98
+ @engine.add_fact(:sensor, { type: "temperature", value: 25 })
99
+ @engine.run
100
+
101
+ refute @fired, "Rule should not fire for normal temperature"
102
+
103
+ alerts = @engine.facts.select { |f| f.type == :alert }
104
+ assert_empty alerts
105
+ end
106
+
107
+ def test_threshold_boundary
108
+ # Test at exact threshold
109
+ @engine.add_fact(:sensor, { type: "temperature", value: 30 })
110
+ @engine.run
111
+
112
+ refute @fired, "Rule should not fire at exact threshold (>= not >)"
113
+ end
114
+ end
115
+ ```
116
+
117
+ ### Test Rule with Multiple Conditions
118
+
119
+ ```ruby
120
+ class TestMultiConditionRule < Minitest::Test
121
+ def setup
122
+ @engine = setup_engine
123
+ @fired = false
124
+
125
+ @rule = KBS::Rule.new("high_temp_and_low_humidity") do |r|
126
+ r.conditions = [
127
+ KBS::Condition.new(:temperature, {
128
+ location: :loc?,
129
+ value: :temp?
130
+ }, predicate: lambda { |f| f[:value] > 30 }),
131
+
132
+ KBS::Condition.new(:humidity, {
133
+ location: :loc?,
134
+ value: :hum?
135
+ }, predicate: lambda { |f| f[:value] < 40 })
136
+ ]
137
+
138
+ r.action = lambda do |facts, bindings|
139
+ @fired = true
140
+ end
141
+ end
142
+
143
+ @engine.add_rule(@rule)
144
+ end
145
+
146
+ def test_fires_when_both_conditions_met
147
+ @engine.add_fact(:temperature, { location: "room1", value: 35 })
148
+ @engine.add_fact(:humidity, { location: "room1", value: 30 })
149
+ @engine.run
150
+
151
+ assert @fired, "Rule should fire when both conditions met"
152
+ end
153
+
154
+ def test_does_not_fire_with_mismatched_locations
155
+ @engine.add_fact(:temperature, { location: "room1", value: 35 })
156
+ @engine.add_fact(:humidity, { location: "room2", value: 30 })
157
+ @engine.run
158
+
159
+ refute @fired, "Rule should not fire with different locations"
160
+ end
161
+
162
+ def test_does_not_fire_when_only_temperature_high
163
+ @engine.add_fact(:temperature, { location: "room1", value: 35 })
164
+ # No humidity fact
165
+ @engine.run
166
+
167
+ refute @fired, "Rule should not fire without humidity fact"
168
+ end
169
+
170
+ def test_does_not_fire_when_temperature_normal
171
+ @engine.add_fact(:temperature, { location: "room1", value: 25 })
172
+ @engine.add_fact(:humidity, { location: "room1", value: 30 })
173
+ @engine.run
174
+
175
+ refute @fired, "Rule should not fire with normal temperature"
176
+ end
177
+ end
178
+ ```
179
+
180
+ ### Test Negated Conditions
181
+
182
+ ```ruby
183
+ class TestNegationRule < Minitest::Test
184
+ def setup
185
+ @engine = setup_engine
186
+ @fired = false
187
+
188
+ @rule = KBS::Rule.new("alert_if_no_acknowledgment") do |r|
189
+ r.conditions = [
190
+ KBS::Condition.new(:error, { id: :id? }),
191
+ KBS::Condition.new(:acknowledged, { error_id: :id? }, negated: true)
192
+ ]
193
+
194
+ r.action = lambda do |facts, bindings|
195
+ @fired = true
196
+ end
197
+ end
198
+
199
+ @engine.add_rule(@rule)
200
+ end
201
+
202
+ def test_fires_when_error_not_acknowledged
203
+ @engine.add_fact(:error, { id: 1 })
204
+ @engine.run
205
+
206
+ assert @fired, "Rule should fire when error not acknowledged"
207
+ end
208
+
209
+ def test_does_not_fire_when_error_acknowledged
210
+ @engine.add_fact(:error, { id: 1 })
211
+ @engine.add_fact(:acknowledged, { error_id: 1 })
212
+ @engine.run
213
+
214
+ refute @fired, "Rule should not fire when error acknowledged"
215
+ end
216
+ end
217
+ ```
218
+
219
+ ## Integration Testing
220
+
221
+ ### Test Rule Interactions
222
+
223
+ ```ruby
224
+ class TestRuleInteractions < Minitest::Test
225
+ def setup
226
+ @engine = setup_engine
227
+ @alerts = []
228
+
229
+ # Rule 1: Detect high temperature
230
+ @engine.add_rule(KBS::Rule.new("detect_high_temp") do |r|
231
+ r.conditions = [
232
+ KBS::Condition.new(:sensor, { value: :temp? }, predicate: lambda { |f| f[:value] > 30 })
233
+ ]
234
+
235
+ r.action = lambda do |facts, bindings|
236
+ @engine.add_fact(:temp_alert, { severity: "high" })
237
+ end
238
+ end)
239
+
240
+ # Rule 2: Escalate to critical
241
+ @engine.add_rule(KBS::Rule.new("escalate_critical") do |r|
242
+ r.conditions = [
243
+ KBS::Condition.new(:temp_alert, { severity: "high" }),
244
+ KBS::Condition.new(:sensor, { value: :temp? }, predicate: lambda { |f| f[:value] > 40 })
245
+ ]
246
+
247
+ r.action = lambda do |facts, bindings|
248
+ @engine.add_fact(:critical_alert, { type: "temperature" })
249
+ @alerts << :critical
250
+ end
251
+ end)
252
+ end
253
+
254
+ def test_cascading_rules
255
+ # Add high temperature
256
+ @engine.add_fact(:sensor, { value: 45 })
257
+ @engine.run
258
+
259
+ # Both rules should fire
260
+ assert @engine.facts.any? { |f| f.type == :temp_alert }
261
+ assert @engine.facts.any? { |f| f.type == :critical_alert }
262
+ assert_includes @alerts, :critical
263
+ end
264
+
265
+ def test_partial_cascade
266
+ # Add moderately high temperature
267
+ @engine.add_fact(:sensor, { value: 35 })
268
+ @engine.run
269
+
270
+ # Only first rule fires
271
+ assert @engine.facts.any? { |f| f.type == :temp_alert }
272
+ refute @engine.facts.any? { |f| f.type == :critical_alert }
273
+ end
274
+ end
275
+ ```
276
+
277
+ ### Test Rule Priority
278
+
279
+ ```ruby
280
+ class TestRulePriority < Minitest::Test
281
+ def setup
282
+ @engine = setup_engine
283
+ @execution_order = []
284
+
285
+ # High priority rule
286
+ @engine.add_rule(KBS::Rule.new("high_priority", priority: 100) do |r|
287
+ r.conditions = [KBS::Condition.new(:trigger, {})]
288
+ r.action = lambda do |facts, bindings|
289
+ @execution_order << :high
290
+ end
291
+ end)
292
+
293
+ # Low priority rule
294
+ @engine.add_rule(KBS::Rule.new("low_priority", priority: 10) do |r|
295
+ r.conditions = [KBS::Condition.new(:trigger, {})]
296
+ r.action = lambda do |facts, bindings|
297
+ @execution_order << :low
298
+ end
299
+ end)
300
+ end
301
+
302
+ def test_executes_in_priority_order
303
+ @engine.add_fact(:trigger, {})
304
+ @engine.run
305
+
306
+ assert_equal [:high, :low], @execution_order
307
+ end
308
+ end
309
+ ```
310
+
311
+ ## Test Fixtures
312
+
313
+ ### Fact Fixtures
314
+
315
+ ```ruby
316
+ module FactFixtures
317
+ def sensor_facts(count: 10)
318
+ count.times.map do |i|
319
+ { type: :sensor, attributes: { id: i, value: rand(20..40) } }
320
+ end
321
+ end
322
+
323
+ def high_temp_scenario
324
+ [
325
+ { type: :sensor, attributes: { location: "room1", value: 35 } },
326
+ { type: :sensor, attributes: { location: "room2", value: 38 } },
327
+ { type: :threshold, attributes: { value: 30 } }
328
+ ]
329
+ end
330
+
331
+ def normal_scenario
332
+ [
333
+ { type: :sensor, attributes: { location: "room1", value: 22 } },
334
+ { type: :sensor, attributes: { location: "room2", value: 24 } },
335
+ { type: :threshold, attributes: { value: 30 } }
336
+ ]
337
+ end
338
+
339
+ def load_facts(engine, facts)
340
+ facts.each do |fact_data|
341
+ engine.add_fact(fact_data[:type], fact_data[:attributes])
342
+ end
343
+ end
344
+ end
345
+
346
+ class TestWithFixtures < Minitest::Test
347
+ include FactFixtures
348
+
349
+ def test_with_high_temp_scenario
350
+ engine = setup_engine
351
+ # Add rules...
352
+
353
+ load_facts(engine, high_temp_scenario)
354
+ engine.run
355
+
356
+ # Assertions...
357
+ end
358
+ end
359
+ ```
360
+
361
+ ### Rule Fixtures
362
+
363
+ ```ruby
364
+ module RuleFixtures
365
+ def temperature_monitoring_rules
366
+ [
367
+ KBS::Rule.new("detect_high") do |r|
368
+ r.conditions = [
369
+ KBS::Condition.new(:sensor, { value: :v? }, predicate: lambda { |f| f[:value] > 30 })
370
+ ]
371
+ r.action = lambda { |facts, bindings| facts[0][:alerted] = true }
372
+ end,
373
+
374
+ KBS::Rule.new("detect_low") do |r|
375
+ r.conditions = [
376
+ KBS::Condition.new(:sensor, { value: :v? }, predicate: lambda { |f| f[:value] < 15 })
377
+ ]
378
+ r.action = lambda { |facts, bindings| facts[0][:alerted] = true }
379
+ end
380
+ ]
381
+ end
382
+
383
+ def load_rules(engine, rules)
384
+ rules.each { |rule| engine.add_rule(rule) }
385
+ end
386
+ end
387
+ ```
388
+
389
+ ## Coverage Strategies
390
+
391
+ ### Track Rule Firings
392
+
393
+ ```ruby
394
+ class CoverageTracker
395
+ def initialize(engine)
396
+ @engine = engine
397
+ @rule_firings = Hash.new(0)
398
+ @condition_matches = Hash.new(0)
399
+ end
400
+
401
+ def wrap_rules
402
+ @engine.instance_variable_get(:@rules).each do |rule|
403
+ original_action = rule.action
404
+
405
+ rule.action = lambda do |facts, bindings|
406
+ @rule_firings[rule.name] += 1
407
+ original_action.call(facts, bindings)
408
+ end
409
+ end
410
+ end
411
+
412
+ def report
413
+ puts "\n=== Coverage Report ==="
414
+
415
+ total_rules = @engine.instance_variable_get(:@rules).size
416
+ fired_rules = @rule_firings.keys.size
417
+ coverage = (fired_rules.to_f / total_rules * 100).round(2)
418
+
419
+ puts "Rules: #{fired_rules}/#{total_rules} (#{coverage}%)"
420
+
421
+ puts "\nRule Firings:"
422
+ @rule_firings.each do |name, count|
423
+ puts " #{name}: #{count}"
424
+ end
425
+
426
+ untested = @engine.instance_variable_get(:@rules).map(&:name) - @rule_firings.keys
427
+ if untested.any?
428
+ puts "\nUntested Rules:"
429
+ untested.each { |name| puts " - #{name}" }
430
+ end
431
+ end
432
+
433
+ attr_reader :rule_firings, :condition_matches
434
+ end
435
+
436
+ # Usage
437
+ class TestWithCoverage < Minitest::Test
438
+ def test_coverage
439
+ engine = setup_engine
440
+ # Add rules...
441
+
442
+ tracker = CoverageTracker.new(engine)
443
+ tracker.wrap_rules
444
+
445
+ # Add facts and run
446
+ engine.run
447
+
448
+ tracker.report
449
+
450
+ # Assert all rules fired
451
+ assert_equal @engine.instance_variable_get(:@rules).size, tracker.rule_firings.size
452
+ end
453
+ end
454
+ ```
455
+
456
+ ### Condition Coverage
457
+
458
+ ```ruby
459
+ def test_all_condition_paths
460
+ engine = setup_engine
461
+
462
+ rule = KBS::Rule.new("multi_path") do |r|
463
+ r.conditions = [
464
+ KBS::Condition.new(:a, {}),
465
+ KBS::Condition.new(:b, {}),
466
+ KBS::Condition.new(:c, {}, negated: true)
467
+ ]
468
+ r.action = lambda { |facts, bindings| }
469
+ end
470
+
471
+ engine.add_rule(rule)
472
+
473
+ # Test path 1: All conditions pass
474
+ engine.add_fact(:a, {})
475
+ engine.add_fact(:b, {})
476
+ # c absent
477
+ engine.run
478
+ # Assert...
479
+
480
+ # Test path 2: Negation fails
481
+ engine = setup_engine
482
+ engine.add_rule(rule)
483
+ engine.add_fact(:a, {})
484
+ engine.add_fact(:b, {})
485
+ engine.add_fact(:c, {}) # Blocks negation
486
+ engine.run
487
+ # Assert...
488
+
489
+ # Test path 3: Positive condition missing
490
+ engine = setup_engine
491
+ engine.add_rule(rule)
492
+ engine.add_fact(:a, {})
493
+ # b missing
494
+ engine.run
495
+ # Assert...
496
+ end
497
+ ```
498
+
499
+ ## Performance Testing
500
+
501
+ ### Benchmark Rule Execution
502
+
503
+ ```ruby
504
+ require 'benchmark'
505
+
506
+ class PerformanceTest < Minitest::Test
507
+ def test_rule_performance
508
+ engine = setup_engine
509
+
510
+ # Add rule
511
+ engine.add_rule(KBS::Rule.new("perf_test") do |r|
512
+ r.conditions = [
513
+ KBS::Condition.new(:data, { value: :v? })
514
+ ]
515
+ r.action = lambda { |facts, bindings| }
516
+ end)
517
+
518
+ # Add many facts
519
+ 1000.times { |i| engine.add_fact(:data, { value: i }) }
520
+
521
+ # Benchmark
522
+ time = Benchmark.measure { engine.run }
523
+
524
+ assert time.real < 1.0, "Engine should complete in under 1 second"
525
+ end
526
+
527
+ def test_fact_addition_performance
528
+ engine = setup_engine
529
+
530
+ time = Benchmark.measure do
531
+ 10_000.times { |i| engine.add_fact(:data, { value: i }) }
532
+ end
533
+
534
+ rate = 10_000 / time.real
535
+ assert rate > 10_000, "Should add >10k facts/sec, got #{rate.round(2)}"
536
+ end
537
+ end
538
+ ```
539
+
540
+ ## Testing Blackboard Persistence
541
+
542
+ ### Test with SQLite
543
+
544
+ ```ruby
545
+ class TestBlackboardPersistence < Minitest::Test
546
+ def test_facts_persist_across_sessions
547
+ # Session 1: Add facts
548
+ engine1 = KBS::Blackboard::Engine.new(db_path: 'test.db')
549
+ engine1.add_fact(:sensor, { id: 1, value: 25 })
550
+ engine1.close
551
+
552
+ # Session 2: Load facts
553
+ engine2 = KBS::Blackboard::Engine.new(db_path: 'test.db')
554
+ assert_equal 1, engine2.facts.size
555
+ assert_equal 25, engine2.facts.first[:value]
556
+
557
+ engine2.close
558
+ File.delete('test.db') if File.exist?('test.db')
559
+ end
560
+
561
+ def test_audit_trail
562
+ engine = KBS::Blackboard::Engine.new(db_path: ':memory:')
563
+
564
+ fact = engine.add_fact(:data, { value: 1 })
565
+ engine.update_fact(fact.id, { value: 2 })
566
+ engine.delete_fact(fact.id)
567
+
568
+ history = engine.fact_history(fact.id)
569
+
570
+ assert_equal 3, history.size
571
+ assert_equal "add", history[0][:operation]
572
+ assert_equal "update", history[1][:operation]
573
+ assert_equal "delete", history[2][:operation]
574
+ end
575
+ end
576
+ ```
577
+
578
+ ## Testing Best Practices
579
+
580
+ ### 1. Isolate Rules
581
+
582
+ ```ruby
583
+ def test_single_rule_only
584
+ engine = setup_engine
585
+
586
+ # Add ONLY the rule being tested
587
+ engine.add_rule(my_test_rule)
588
+
589
+ # No other rules to interfere
590
+ engine.run
591
+ end
592
+ ```
593
+
594
+ ### 2. Test Edge Cases
595
+
596
+ ```ruby
597
+ def test_edge_cases
598
+ # Empty facts
599
+ engine.run
600
+ assert_empty engine.facts.select { |f| f.type == :alert }
601
+
602
+ # Exact threshold
603
+ engine.add_fact(:sensor, { value: 30 })
604
+ engine.run
605
+
606
+ # Just below threshold
607
+ engine.add_fact(:sensor, { value: 29.99 })
608
+ engine.run
609
+
610
+ # Just above threshold
611
+ engine.add_fact(:sensor, { value: 30.01 })
612
+ engine.run
613
+ end
614
+ ```
615
+
616
+ ### 3. Test Side Effects
617
+
618
+ ```ruby
619
+ def test_action_side_effects
620
+ engine = setup_engine
621
+ added_facts = []
622
+
623
+ rule = KBS::Rule.new("test") do |r|
624
+ r.conditions = [KBS::Condition.new(:trigger, {})]
625
+ r.action = lambda do |facts, bindings|
626
+ new_fact = engine.add_fact(:result, { value: 42 })
627
+ added_facts << new_fact
628
+ end
629
+ end
630
+
631
+ engine.add_rule(rule)
632
+ engine.add_fact(:trigger, {})
633
+ engine.run
634
+
635
+ assert_equal 1, added_facts.size
636
+ assert_equal 42, added_facts.first[:value]
637
+ end
638
+ ```
639
+
640
+ ### 4. Use Descriptive Test Names
641
+
642
+ ```ruby
643
+ def test_high_temperature_alert_fires_when_sensor_exceeds_threshold
644
+ # Clear what this tests
645
+ end
646
+
647
+ def test_alert_not_sent_twice_for_same_sensor
648
+ # Explains the scenario
649
+ end
650
+ ```
651
+
652
+ ### 5. Setup and Teardown
653
+
654
+ ```ruby
655
+ class TestWithSetup < Minitest::Test
656
+ def setup
657
+ @engine = setup_engine
658
+ @test_db = "test_#{SecureRandom.hex(8)}.db"
659
+ end
660
+
661
+ def teardown
662
+ @engine.close if @engine.respond_to?(:close)
663
+ File.delete(@test_db) if File.exist?(@test_db)
664
+ end
665
+ end
666
+ ```
667
+
668
+ ## Testing Checklist
669
+
670
+ - [ ] Test each rule fires with correct facts
671
+ - [ ] Test each rule doesn't fire without required facts
672
+ - [ ] Test boundary conditions
673
+ - [ ] Test negated conditions
674
+ - [ ] Test variable bindings
675
+ - [ ] Test rule priorities
676
+ - [ ] Test rule interactions
677
+ - [ ] Test action side effects
678
+ - [ ] Test persistence (if using blackboard)
679
+ - [ ] Measure performance
680
+ - [ ] Achieve high rule coverage
681
+
682
+ ## Next Steps
683
+
684
+ - **[Debugging Guide](debugging.md)** - Debug failing tests
685
+ - **[Performance Guide](performance.md)** - Optimize slow tests
686
+ - **[Architecture](../architecture/index.md)** - Understand rule execution
687
+ - **[Examples](../examples/stock-trading.md)** - See tested examples
688
+
689
+ ---
690
+
691
+ *Good tests make rule changes safe. Test each rule thoroughly.*