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