decision_agent 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +212 -35
- data/bin/decision_agent +3 -8
- data/lib/decision_agent/agent.rb +19 -26
- data/lib/decision_agent/audit/null_adapter.rb +1 -2
- data/lib/decision_agent/decision.rb +3 -1
- data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
- data/lib/decision_agent/dsl/rule_parser.rb +4 -6
- data/lib/decision_agent/dsl/schema_validator.rb +27 -31
- data/lib/decision_agent/errors.rb +11 -8
- data/lib/decision_agent/evaluation.rb +3 -1
- data/lib/decision_agent/evaluation_validator.rb +78 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
- data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
- data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
- data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +278 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
- data/lib/decision_agent/replay/replay.rb +12 -22
- data/lib/decision_agent/scoring/base.rb +1 -1
- data/lib/decision_agent/scoring/consensus.rb +5 -5
- data/lib/decision_agent/scoring/weighted_average.rb +1 -1
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +69 -33
- data/lib/decision_agent/versioning/adapter.rb +1 -3
- data/lib/decision_agent/versioning/file_storage_adapter.rb +143 -35
- data/lib/decision_agent/versioning/version_manager.rb +4 -12
- data/lib/decision_agent/web/public/index.html +1 -1
- data/lib/decision_agent/web/server.rb +19 -24
- data/lib/decision_agent.rb +7 -0
- data/lib/generators/decision_agent/install/install_generator.rb +5 -5
- data/lib/generators/decision_agent/install/templates/migration.rb +17 -6
- data/lib/generators/decision_agent/install/templates/rule.rb +3 -3
- data/lib/generators/decision_agent/install/templates/rule_version.rb +13 -7
- data/spec/activerecord_thread_safety_spec.rb +553 -0
- data/spec/agent_spec.rb +13 -13
- data/spec/api_contract_spec.rb +16 -16
- data/spec/audit_adapters_spec.rb +3 -3
- data/spec/comprehensive_edge_cases_spec.rb +86 -86
- data/spec/dsl_validation_spec.rb +83 -83
- data/spec/edge_cases_spec.rb +23 -23
- data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
- data/spec/examples.txt +548 -0
- data/spec/issue_verification_spec.rb +685 -0
- data/spec/json_rule_evaluator_spec.rb +15 -15
- data/spec/monitoring/alert_manager_spec.rb +378 -0
- data/spec/monitoring/metrics_collector_spec.rb +281 -0
- data/spec/monitoring/monitored_agent_spec.rb +222 -0
- data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
- data/spec/replay_edge_cases_spec.rb +58 -58
- data/spec/replay_spec.rb +11 -11
- data/spec/rfc8785_canonicalization_spec.rb +215 -0
- data/spec/scoring_spec.rb +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/thread_safety_spec.rb +482 -0
- data/spec/thread_safety_spec.rb.broken +878 -0
- data/spec/versioning_spec.rb +141 -37
- data/spec/web_ui_rack_spec.rb +135 -0
- metadata +69 -6
data/spec/versioning_spec.rb
CHANGED
|
@@ -156,9 +156,9 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
it "raises error for nonexistent version" do
|
|
159
|
-
expect
|
|
159
|
+
expect do
|
|
160
160
|
adapter.activate_version(version_id: "nonexistent")
|
|
161
|
-
|
|
161
|
+
end.to raise_error(DecisionAgent::NotFoundError)
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
|
|
@@ -227,17 +227,17 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
227
227
|
end
|
|
228
228
|
|
|
229
229
|
it "validates rule content" do
|
|
230
|
-
expect
|
|
230
|
+
expect do
|
|
231
231
|
manager.save_version(rule_id: rule_id, rule_content: nil)
|
|
232
|
-
|
|
232
|
+
end.to raise_error(DecisionAgent::ValidationError, /cannot be nil/)
|
|
233
233
|
|
|
234
|
-
expect
|
|
234
|
+
expect do
|
|
235
235
|
manager.save_version(rule_id: rule_id, rule_content: "not a hash")
|
|
236
|
-
|
|
236
|
+
end.to raise_error(DecisionAgent::ValidationError, /must be a Hash/)
|
|
237
237
|
|
|
238
|
-
expect
|
|
238
|
+
expect do
|
|
239
239
|
manager.save_version(rule_id: rule_id, rule_content: {})
|
|
240
|
-
|
|
240
|
+
end.to raise_error(DecisionAgent::ValidationError, /cannot be empty/)
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
it "generates default changelog if not provided" do
|
|
@@ -263,18 +263,48 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
describe "#rollback" do
|
|
266
|
-
it "activates a previous version
|
|
266
|
+
it "activates a previous version without creating a duplicate" do
|
|
267
267
|
v1 = manager.save_version(rule_id: rule_id, rule_content: rule_content, changelog: "v1")
|
|
268
268
|
manager.save_version(rule_id: rule_id, rule_content: rule_content, changelog: "v2")
|
|
269
|
+
manager.save_version(rule_id: rule_id, rule_content: rule_content, changelog: "v3")
|
|
269
270
|
|
|
271
|
+
# Rollback to v1 should just activate it, not create a duplicate
|
|
270
272
|
rolled_back = manager.rollback(version_id: v1[:id], performed_by: "admin")
|
|
271
273
|
|
|
272
274
|
expect(rolled_back[:status]).to eq("active")
|
|
275
|
+
expect(rolled_back[:id]).to eq(v1[:id])
|
|
273
276
|
|
|
274
|
-
# Should create a new version
|
|
277
|
+
# Should NOT create a new version - just activate the old one
|
|
275
278
|
versions = manager.get_versions(rule_id: rule_id)
|
|
276
|
-
expect(versions.length).to eq(3)
|
|
277
|
-
|
|
279
|
+
expect(versions.length).to eq(3) # Still just v1, v2, v3
|
|
280
|
+
|
|
281
|
+
# v1 should be active, v2 and v3 should be archived
|
|
282
|
+
active_version = manager.get_active_version(rule_id: rule_id)
|
|
283
|
+
expect(active_version[:id]).to eq(v1[:id])
|
|
284
|
+
expect(active_version[:version_number]).to eq(1)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it "maintains version history integrity after rollback" do
|
|
288
|
+
v1 = manager.save_version(rule_id: rule_id, rule_content: rule_content.merge(data: "v1"), changelog: "Version 1")
|
|
289
|
+
v2 = manager.save_version(rule_id: rule_id, rule_content: rule_content.merge(data: "v2"), changelog: "Version 2")
|
|
290
|
+
v3 = manager.save_version(rule_id: rule_id, rule_content: rule_content.merge(data: "v3"), changelog: "Version 3")
|
|
291
|
+
|
|
292
|
+
# Rollback to v2
|
|
293
|
+
manager.rollback(version_id: v2[:id])
|
|
294
|
+
|
|
295
|
+
# All original versions should still exist with original data
|
|
296
|
+
loaded_v1 = manager.get_version(version_id: v1[:id])
|
|
297
|
+
loaded_v2 = manager.get_version(version_id: v2[:id])
|
|
298
|
+
loaded_v3 = manager.get_version(version_id: v3[:id])
|
|
299
|
+
|
|
300
|
+
expect(loaded_v1[:content][:data]).to eq("v1")
|
|
301
|
+
expect(loaded_v2[:content][:data]).to eq("v2")
|
|
302
|
+
expect(loaded_v3[:content][:data]).to eq("v3")
|
|
303
|
+
|
|
304
|
+
# v2 should be active
|
|
305
|
+
expect(loaded_v2[:status]).to eq("active")
|
|
306
|
+
expect(loaded_v1[:status]).to eq("archived")
|
|
307
|
+
expect(loaded_v3[:status]).to eq("archived")
|
|
278
308
|
end
|
|
279
309
|
end
|
|
280
310
|
|
|
@@ -296,9 +326,9 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
296
326
|
|
|
297
327
|
describe "edge cases and error handling" do
|
|
298
328
|
it "handles empty rule_id gracefully" do
|
|
299
|
-
expect
|
|
329
|
+
expect do
|
|
300
330
|
manager.save_version(rule_id: "", rule_content: rule_content)
|
|
301
|
-
|
|
331
|
+
end.not_to raise_error
|
|
302
332
|
end
|
|
303
333
|
|
|
304
334
|
it "handles special characters in rule_id" do
|
|
@@ -390,7 +420,7 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
390
420
|
Thread.new do
|
|
391
421
|
manager.save_version(
|
|
392
422
|
rule_id: rule_id,
|
|
393
|
-
rule_content: rule_content.merge(version:
|
|
423
|
+
rule_content: rule_content.merge(version: i.to_s),
|
|
394
424
|
created_by: "thread_#{i}"
|
|
395
425
|
)
|
|
396
426
|
end
|
|
@@ -450,13 +480,13 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
450
480
|
}
|
|
451
481
|
|
|
452
482
|
content_v2 = {
|
|
453
|
-
version: "2.0",
|
|
483
|
+
version: "2.0", # changed
|
|
454
484
|
ruleset: "test",
|
|
455
485
|
rules: [
|
|
456
|
-
{ id: "rule_1", if: { field: "a", op: "eq", value: 2 }, then: { decision: "reject", weight: 0.9, reason: "Updated" } },
|
|
457
|
-
{ id: "rule_2", if: { field: "b", op: "gt", value: 100 }, then: { decision: "approve", weight: 0.7, reason: "New" } }
|
|
486
|
+
{ id: "rule_1", if: { field: "a", op: "eq", value: 2 }, then: { decision: "reject", weight: 0.9, reason: "Updated" } }, # modified
|
|
487
|
+
{ id: "rule_2", if: { field: "b", op: "gt", value: 100 }, then: { decision: "approve", weight: 0.7, reason: "New" } } # added
|
|
458
488
|
],
|
|
459
|
-
new_field: "added"
|
|
489
|
+
new_field: "added" # added field
|
|
460
490
|
}
|
|
461
491
|
|
|
462
492
|
v1 = manager.save_version(rule_id: rule_id, rule_content: content_v1)
|
|
@@ -470,7 +500,7 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
470
500
|
end
|
|
471
501
|
|
|
472
502
|
describe "rollback scenarios" do
|
|
473
|
-
it "
|
|
503
|
+
it "activates previous version without creating duplicates" do
|
|
474
504
|
v1 = manager.save_version(rule_id: rule_id, rule_content: rule_content, changelog: "Version 1")
|
|
475
505
|
manager.save_version(rule_id: rule_id, rule_content: rule_content, changelog: "Version 2")
|
|
476
506
|
manager.save_version(rule_id: rule_id, rule_content: rule_content, changelog: "Version 3")
|
|
@@ -478,26 +508,33 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
478
508
|
manager.rollback(version_id: v1[:id], performed_by: "admin")
|
|
479
509
|
|
|
480
510
|
history = manager.get_history(rule_id: rule_id)
|
|
481
|
-
expect(history[:total_versions]).to eq(
|
|
511
|
+
expect(history[:total_versions]).to eq(3) # Still just v1, v2, v3 - no duplicate
|
|
482
512
|
|
|
483
|
-
|
|
484
|
-
expect(
|
|
485
|
-
expect(
|
|
513
|
+
# v1 should be the active version
|
|
514
|
+
expect(history[:active_version][:id]).to eq(v1[:id])
|
|
515
|
+
expect(history[:active_version][:changelog]).to eq("Version 1")
|
|
486
516
|
end
|
|
487
517
|
|
|
488
|
-
it "handles multiple consecutive rollbacks" do
|
|
518
|
+
it "handles multiple consecutive rollbacks without duplication" do
|
|
489
519
|
v1 = manager.save_version(rule_id: rule_id, rule_content: rule_content)
|
|
490
520
|
v2 = manager.save_version(rule_id: rule_id, rule_content: rule_content)
|
|
491
|
-
manager.save_version(rule_id: rule_id, rule_content: rule_content)
|
|
521
|
+
v3 = manager.save_version(rule_id: rule_id, rule_content: rule_content)
|
|
492
522
|
|
|
493
523
|
# Rollback to v1
|
|
494
|
-
manager.rollback(version_id: v1[:id], performed_by: "user1")
|
|
524
|
+
result1 = manager.rollback(version_id: v1[:id], performed_by: "user1")
|
|
525
|
+
expect(result1[:id]).to eq(v1[:id])
|
|
495
526
|
|
|
496
527
|
# Rollback to v2
|
|
497
|
-
manager.rollback(version_id: v2[:id], performed_by: "user2")
|
|
528
|
+
result2 = manager.rollback(version_id: v2[:id], performed_by: "user2")
|
|
529
|
+
expect(result2[:id]).to eq(v2[:id])
|
|
530
|
+
|
|
531
|
+
# Rollback to v3
|
|
532
|
+
result3 = manager.rollback(version_id: v3[:id], performed_by: "user3")
|
|
533
|
+
expect(result3[:id]).to eq(v3[:id])
|
|
498
534
|
|
|
499
535
|
history = manager.get_history(rule_id: rule_id)
|
|
500
|
-
expect(history[:total_versions]).to eq(
|
|
536
|
+
expect(history[:total_versions]).to eq(3) # Still just the original 3 versions
|
|
537
|
+
expect(history[:active_version][:id]).to eq(v3[:id])
|
|
501
538
|
end
|
|
502
539
|
end
|
|
503
540
|
|
|
@@ -517,13 +554,11 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
517
554
|
end
|
|
518
555
|
|
|
519
556
|
it "handles versions across multiple rules" do
|
|
520
|
-
rule_ids = [
|
|
557
|
+
rule_ids = %w[rule_a rule_b rule_c]
|
|
521
558
|
|
|
522
559
|
rule_ids.each do |rid|
|
|
523
560
|
3.times { manager.save_version(rule_id: rid, rule_content: rule_content) }
|
|
524
|
-
end
|
|
525
561
|
|
|
526
|
-
rule_ids.each do |rid|
|
|
527
562
|
versions = manager.get_versions(rule_id: rid)
|
|
528
563
|
expect(versions.length).to eq(3)
|
|
529
564
|
expect(versions.all? { |v| v[:rule_id] == rid }).to be true
|
|
@@ -537,7 +572,7 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
537
572
|
manager.save_version(rule_id: rule_id, rule_content: rule_content)
|
|
538
573
|
|
|
539
574
|
begin
|
|
540
|
-
manager.save_version(rule_id: rule_id, rule_content: nil)
|
|
575
|
+
manager.save_version(rule_id: rule_id, rule_content: nil) # This should fail
|
|
541
576
|
rescue DecisionAgent::ValidationError
|
|
542
577
|
# Expected error
|
|
543
578
|
end
|
|
@@ -623,11 +658,12 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
623
658
|
)
|
|
624
659
|
|
|
625
660
|
expect(rolled_back[:status]).to eq("active")
|
|
661
|
+
expect(rolled_back[:id]).to eq(v2[:id])
|
|
626
662
|
|
|
627
663
|
# 6. Verify history
|
|
628
664
|
history = manager.get_history(rule_id: "approval_001")
|
|
629
|
-
expect(history[:total_versions]).to eq(
|
|
630
|
-
expect(history[:active_version][:version_number]).to
|
|
665
|
+
expect(history[:total_versions]).to eq(3) # v1, v2, v3 - no duplicate created
|
|
666
|
+
expect(history[:active_version][:version_number]).to eq(2) # v2 is active
|
|
631
667
|
end
|
|
632
668
|
end
|
|
633
669
|
|
|
@@ -647,7 +683,7 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
647
683
|
"review" => {
|
|
648
684
|
version: "1.0",
|
|
649
685
|
ruleset: "review",
|
|
650
|
-
rules: [{ id: "review_1", if: { field: "amount", op: "gte", value:
|
|
686
|
+
rules: [{ id: "review_1", if: { field: "amount", op: "gte", value: 10_000 }, then: { decision: "manual_review", weight: 0.9, reason: "Large transaction" } }]
|
|
651
687
|
}
|
|
652
688
|
}
|
|
653
689
|
|
|
@@ -662,12 +698,80 @@ RSpec.describe "DecisionAgent Versioning System" do
|
|
|
662
698
|
end
|
|
663
699
|
|
|
664
700
|
# Verify each has its own version history
|
|
665
|
-
rulesets.
|
|
701
|
+
rulesets.each_key do |name|
|
|
666
702
|
history = manager.get_history(rule_id: name)
|
|
667
703
|
expect(history[:total_versions]).to eq(1)
|
|
668
704
|
expect(history[:active_version][:rule_id]).to eq(name)
|
|
669
705
|
end
|
|
670
706
|
end
|
|
671
707
|
end
|
|
708
|
+
|
|
709
|
+
describe "status validation" do
|
|
710
|
+
let(:rule_id) { "test_status_rule" }
|
|
711
|
+
let(:rule_content) do
|
|
712
|
+
{
|
|
713
|
+
version: "1.0",
|
|
714
|
+
ruleset: "test",
|
|
715
|
+
rules: [{ id: "test", if: { field: "x", op: "eq", value: 1 }, then: { decision: "approve", weight: 0.8, reason: "Test" } }]
|
|
716
|
+
}
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
it "rejects invalid status values when creating versions" do
|
|
720
|
+
expect do
|
|
721
|
+
adapter.create_version(
|
|
722
|
+
rule_id: rule_id,
|
|
723
|
+
content: rule_content,
|
|
724
|
+
metadata: { status: "banana" }
|
|
725
|
+
)
|
|
726
|
+
end.to raise_error(DecisionAgent::ValidationError, /Invalid status 'banana'/)
|
|
727
|
+
|
|
728
|
+
expect do
|
|
729
|
+
adapter.create_version(
|
|
730
|
+
rule_id: rule_id,
|
|
731
|
+
content: rule_content,
|
|
732
|
+
metadata: { status: "pending" }
|
|
733
|
+
)
|
|
734
|
+
end.to raise_error(DecisionAgent::ValidationError, /Invalid status 'pending'/)
|
|
735
|
+
|
|
736
|
+
expect do
|
|
737
|
+
adapter.create_version(
|
|
738
|
+
rule_id: rule_id,
|
|
739
|
+
content: rule_content,
|
|
740
|
+
metadata: { status: "deleted" }
|
|
741
|
+
)
|
|
742
|
+
end.to raise_error(DecisionAgent::ValidationError, /Invalid status 'deleted'/)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
it "accepts valid status values" do
|
|
746
|
+
v1 = adapter.create_version(
|
|
747
|
+
rule_id: rule_id,
|
|
748
|
+
content: rule_content,
|
|
749
|
+
metadata: { status: "draft" }
|
|
750
|
+
)
|
|
751
|
+
expect(v1[:status]).to eq("draft")
|
|
752
|
+
|
|
753
|
+
v2 = adapter.create_version(
|
|
754
|
+
rule_id: "rule_002",
|
|
755
|
+
content: rule_content,
|
|
756
|
+
metadata: { status: "active" }
|
|
757
|
+
)
|
|
758
|
+
expect(v2[:status]).to eq("active")
|
|
759
|
+
|
|
760
|
+
v3 = adapter.create_version(
|
|
761
|
+
rule_id: "rule_003",
|
|
762
|
+
content: rule_content,
|
|
763
|
+
metadata: { status: "archived" }
|
|
764
|
+
)
|
|
765
|
+
expect(v3[:status]).to eq("archived")
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
it "uses default status 'active' when not provided" do
|
|
769
|
+
version = adapter.create_version(
|
|
770
|
+
rule_id: rule_id,
|
|
771
|
+
content: rule_content
|
|
772
|
+
)
|
|
773
|
+
expect(version[:status]).to eq("active")
|
|
774
|
+
end
|
|
775
|
+
end
|
|
672
776
|
end
|
|
673
777
|
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "rack/test"
|
|
3
|
+
require_relative "../lib/decision_agent/web/server"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "DecisionAgent Web UI Rack Integration" do
|
|
6
|
+
include Rack::Test::Methods
|
|
7
|
+
|
|
8
|
+
def app
|
|
9
|
+
DecisionAgent::Web::Server
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "Rack interface" do
|
|
13
|
+
it "responds to .call for Rack compatibility" do
|
|
14
|
+
expect(DecisionAgent::Web::Server).to respond_to(:call)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "serves the main page" do
|
|
18
|
+
get "/"
|
|
19
|
+
expect(last_response).to be_ok
|
|
20
|
+
expect(last_response.body).to include("DecisionAgent")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "serves the health endpoint" do
|
|
24
|
+
get "/health"
|
|
25
|
+
expect(last_response).to be_ok
|
|
26
|
+
expect(last_response.content_type).to include("application/json")
|
|
27
|
+
|
|
28
|
+
json = JSON.parse(last_response.body)
|
|
29
|
+
expect(json["status"]).to eq("ok")
|
|
30
|
+
expect(json["version"]).to eq(DecisionAgent::VERSION)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "validates rules via POST /api/validate" do
|
|
34
|
+
valid_rules = {
|
|
35
|
+
version: "1.0",
|
|
36
|
+
ruleset: "test_rules",
|
|
37
|
+
rules: [{
|
|
38
|
+
id: "test_rule",
|
|
39
|
+
if: { field: "amount", op: "gt", value: 100 },
|
|
40
|
+
then: { decision: "approve", weight: 0.9, reason: "Test" }
|
|
41
|
+
}]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
post "/api/validate", valid_rules.to_json, { "CONTENT_TYPE" => "application/json" }
|
|
45
|
+
|
|
46
|
+
expect(last_response).to be_ok
|
|
47
|
+
json = JSON.parse(last_response.body)
|
|
48
|
+
expect(json["valid"]).to be true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "returns error for invalid rules" do
|
|
52
|
+
invalid_rules = {
|
|
53
|
+
version: "1.0",
|
|
54
|
+
ruleset: "test_rules",
|
|
55
|
+
rules: [{
|
|
56
|
+
id: "bad_rule"
|
|
57
|
+
# Missing required fields
|
|
58
|
+
}]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
post "/api/validate", invalid_rules.to_json, { "CONTENT_TYPE" => "application/json" }
|
|
62
|
+
|
|
63
|
+
expect(last_response.status).to eq(422)
|
|
64
|
+
json = JSON.parse(last_response.body)
|
|
65
|
+
expect(json["valid"]).to be false
|
|
66
|
+
expect(json["errors"]).to be_an(Array)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "evaluates rules via POST /api/evaluate" do
|
|
70
|
+
rules = {
|
|
71
|
+
version: "1.0",
|
|
72
|
+
ruleset: "test_rules",
|
|
73
|
+
rules: [{
|
|
74
|
+
id: "high_value",
|
|
75
|
+
if: { field: "amount", op: "gt", value: 1000 },
|
|
76
|
+
then: { decision: "approve", weight: 0.9, reason: "High value" }
|
|
77
|
+
}]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
payload = {
|
|
81
|
+
rules: rules,
|
|
82
|
+
context: { amount: 1500 }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
post "/api/evaluate", payload.to_json, { "CONTENT_TYPE" => "application/json" }
|
|
86
|
+
|
|
87
|
+
expect(last_response).to be_ok
|
|
88
|
+
json = JSON.parse(last_response.body)
|
|
89
|
+
expect(json["success"]).to be true
|
|
90
|
+
expect(json["decision"]).to eq("approve")
|
|
91
|
+
expect(json["weight"]).to eq(0.9)
|
|
92
|
+
expect(json["reason"]).to eq("High value")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "serves example rules" do
|
|
96
|
+
get "/api/examples"
|
|
97
|
+
|
|
98
|
+
expect(last_response).to be_ok
|
|
99
|
+
json = JSON.parse(last_response.body)
|
|
100
|
+
expect(json).to be_an(Array)
|
|
101
|
+
expect(json.length).to be > 0
|
|
102
|
+
expect(json.first).to have_key("name")
|
|
103
|
+
expect(json.first).to have_key("rules")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "handles CORS preflight requests" do
|
|
107
|
+
options "/api/validate"
|
|
108
|
+
|
|
109
|
+
expect(last_response.status).to eq(200)
|
|
110
|
+
expect(last_response.headers["Access-Control-Allow-Origin"]).to eq("*")
|
|
111
|
+
expect(last_response.headers["Access-Control-Allow-Methods"]).to include("POST")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "Rails mounting compatibility" do
|
|
116
|
+
it "can be mounted in a Rack app" do
|
|
117
|
+
# Simulate a Rails-style mount
|
|
118
|
+
rack_app = Rack::Builder.new do
|
|
119
|
+
map "/decision_agent" do
|
|
120
|
+
run DecisionAgent::Web::Server
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Create a test session for the mounted app
|
|
125
|
+
test_session = Rack::Test::Session.new(Rack::MockSession.new(rack_app))
|
|
126
|
+
|
|
127
|
+
# Test that the health endpoint works when mounted
|
|
128
|
+
test_session.get "/decision_agent/health"
|
|
129
|
+
expect(test_session.last_response).to be_ok
|
|
130
|
+
|
|
131
|
+
json = JSON.parse(test_session.last_response.body)
|
|
132
|
+
expect(json["status"]).to eq("ok")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: decision_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Aswin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: json-canonicalization
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: sinatra
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -25,19 +39,19 @@ dependencies:
|
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
40
|
version: '3.0'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
42
|
+
name: rack-test
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
30
44
|
requirements:
|
|
31
45
|
- - "~>"
|
|
32
46
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
47
|
+
version: '2.0'
|
|
34
48
|
type: :development
|
|
35
49
|
prerelease: false
|
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
51
|
requirements:
|
|
38
52
|
- - "~>"
|
|
39
53
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
54
|
+
version: '2.0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: rake
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,11 +66,39 @@ dependencies:
|
|
|
52
66
|
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
68
|
version: '13.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.12'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.12'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.60'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.60'
|
|
55
97
|
description: A production-grade decision agent that provides deterministic rule evaluation,
|
|
56
98
|
conflict resolution, and full audit replay capabilities. Framework-agnostic and
|
|
57
99
|
AI-optional.
|
|
58
100
|
email:
|
|
59
|
-
-
|
|
101
|
+
- samaswin@gmail.com
|
|
60
102
|
executables:
|
|
61
103
|
- decision_agent
|
|
62
104
|
extensions: []
|
|
@@ -77,9 +119,18 @@ files:
|
|
|
77
119
|
- lib/decision_agent/dsl/schema_validator.rb
|
|
78
120
|
- lib/decision_agent/errors.rb
|
|
79
121
|
- lib/decision_agent/evaluation.rb
|
|
122
|
+
- lib/decision_agent/evaluation_validator.rb
|
|
80
123
|
- lib/decision_agent/evaluators/base.rb
|
|
81
124
|
- lib/decision_agent/evaluators/json_rule_evaluator.rb
|
|
82
125
|
- lib/decision_agent/evaluators/static_evaluator.rb
|
|
126
|
+
- lib/decision_agent/monitoring/alert_manager.rb
|
|
127
|
+
- lib/decision_agent/monitoring/dashboard/public/dashboard.css
|
|
128
|
+
- lib/decision_agent/monitoring/dashboard/public/dashboard.js
|
|
129
|
+
- lib/decision_agent/monitoring/dashboard/public/index.html
|
|
130
|
+
- lib/decision_agent/monitoring/dashboard_server.rb
|
|
131
|
+
- lib/decision_agent/monitoring/metrics_collector.rb
|
|
132
|
+
- lib/decision_agent/monitoring/monitored_agent.rb
|
|
133
|
+
- lib/decision_agent/monitoring/prometheus_exporter.rb
|
|
83
134
|
- lib/decision_agent/replay/replay.rb
|
|
84
135
|
- lib/decision_agent/scoring/base.rb
|
|
85
136
|
- lib/decision_agent/scoring/consensus.rb
|
|
@@ -100,6 +151,7 @@ files:
|
|
|
100
151
|
- lib/generators/decision_agent/install/templates/migration.rb
|
|
101
152
|
- lib/generators/decision_agent/install/templates/rule.rb
|
|
102
153
|
- lib/generators/decision_agent/install/templates/rule_version.rb
|
|
154
|
+
- spec/activerecord_thread_safety_spec.rb
|
|
103
155
|
- spec/agent_spec.rb
|
|
104
156
|
- spec/api_contract_spec.rb
|
|
105
157
|
- spec/audit_adapters_spec.rb
|
|
@@ -107,13 +159,23 @@ files:
|
|
|
107
159
|
- spec/context_spec.rb
|
|
108
160
|
- spec/dsl_validation_spec.rb
|
|
109
161
|
- spec/edge_cases_spec.rb
|
|
162
|
+
- spec/examples.txt
|
|
110
163
|
- spec/examples/feedback_aware_evaluator_spec.rb
|
|
164
|
+
- spec/issue_verification_spec.rb
|
|
111
165
|
- spec/json_rule_evaluator_spec.rb
|
|
166
|
+
- spec/monitoring/alert_manager_spec.rb
|
|
167
|
+
- spec/monitoring/metrics_collector_spec.rb
|
|
168
|
+
- spec/monitoring/monitored_agent_spec.rb
|
|
169
|
+
- spec/monitoring/prometheus_exporter_spec.rb
|
|
112
170
|
- spec/replay_edge_cases_spec.rb
|
|
113
171
|
- spec/replay_spec.rb
|
|
172
|
+
- spec/rfc8785_canonicalization_spec.rb
|
|
114
173
|
- spec/scoring_spec.rb
|
|
115
174
|
- spec/spec_helper.rb
|
|
175
|
+
- spec/thread_safety_spec.rb
|
|
176
|
+
- spec/thread_safety_spec.rb.broken
|
|
116
177
|
- spec/versioning_spec.rb
|
|
178
|
+
- spec/web_ui_rack_spec.rb
|
|
117
179
|
homepage: https://github.com/samaswin87/decision_agent
|
|
118
180
|
licenses:
|
|
119
181
|
- MIT
|
|
@@ -121,6 +183,7 @@ metadata:
|
|
|
121
183
|
homepage_uri: https://github.com/samaswin87/decision_agent
|
|
122
184
|
source_code_uri: https://github.com/samaswin87/decision_agent
|
|
123
185
|
changelog_uri: https://github.com/samaswin87/decision_agent/blob/main/CHANGELOG.md
|
|
186
|
+
rubygems_mfa_required: 'true'
|
|
124
187
|
post_install_message:
|
|
125
188
|
rdoc_options: []
|
|
126
189
|
require_paths:
|