Rule now carries a required Jurisdiction (land) field, and Facts a matching Jurisdiction supplied by the caller (like Platform — no legal jurisdiction can be read off a caption or image, so the model never guesses it). Evaluate() only lets a rule fire when its jurisdiction matches the facts' jurisdiction. This is the structural half of "deutsche Rechtslage zuerst, Struktur für Österreich und Schweiz vorgesehen": a future AT/CH rule set can be added as plain new YAML files without touching existing DE rules, but no AT/CH content is added now — that needs its own legal research first, same as WK-001/WK-004 needed for Germany. WK-001 and WK-004 are tagged land: DE, all golden fixtures carry jurisdiction: DE, and extract.Input passes Jurisdiction through unchanged into the returned Facts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
package rules_test
|
|
|
|
import (
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"github.com/netcell-it/deklarix/internal/rules"
|
|
)
|
|
|
|
func TestLoadRejectsMissingID(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"bad.yaml": &fstest.MapFile{Data: []byte("version: 1\ntitel: x\nschwere: hoch\n")},
|
|
}
|
|
if _, err := rules.Load(fsys); err == nil {
|
|
t.Fatal("expected error for rule without id, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsMissingVersion(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"bad.yaml": &fstest.MapFile{Data: []byte("id: WK-999\ntitel: x\nschwere: hoch\n")},
|
|
}
|
|
if _, err := rules.Load(fsys); err == nil {
|
|
t.Fatal("expected error for rule without version, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsMissingJurisdiction(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"bad.yaml": &fstest.MapFile{Data: []byte("id: WK-999\nversion: 1\ntitel: x\nschwere: hoch\n")},
|
|
}
|
|
if _, err := rules.Load(fsys); err == nil {
|
|
t.Fatal("expected error for rule without land (jurisdiction), got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsDuplicateID(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"a.yaml": &fstest.MapFile{Data: []byte("id: WK-999\nversion: 1\nland: DE\ntitel: x\nschwere: hoch\n")},
|
|
"b.yaml": &fstest.MapFile{Data: []byte("id: WK-999\nversion: 2\nland: DE\ntitel: y\nschwere: hoch\n")},
|
|
}
|
|
if _, err := rules.Load(fsys); err == nil {
|
|
t.Fatal("expected error for duplicate rule id, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoadIgnoresNonYAMLFiles(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"a.yaml": &fstest.MapFile{Data: []byte("id: WK-999\nversion: 1\nland: DE\ntitel: x\nschwere: hoch\n")},
|
|
"README.md": &fstest.MapFile{Data: []byte("not a rule")},
|
|
}
|
|
got, err := rules.Load(fsys)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 rule, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestConditionMatches(t *testing.T) {
|
|
yes := true
|
|
no := false
|
|
|
|
cases := []struct {
|
|
name string
|
|
cond rules.Condition
|
|
fact rules.Facts
|
|
want bool
|
|
}{
|
|
{
|
|
name: "consideration list matches",
|
|
cond: rules.Condition{Consideration: []rules.Consideration{rules.ConsiderationPaid}},
|
|
fact: rules.Facts{Consideration: rules.ConsiderationPaid},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "consideration list does not match",
|
|
cond: rules.Condition{Consideration: []rules.Consideration{rules.ConsiderationPaid}},
|
|
fact: rules.Facts{Consideration: rules.ConsiderationNone},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "disclosure present must match",
|
|
cond: rules.Condition{DisclosurePresent: &no},
|
|
fact: rules.Facts{DisclosurePresent: true},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "unset fields impose no constraint",
|
|
cond: rules.Condition{},
|
|
fact: rules.Facts{Consideration: rules.ConsiderationUnclear},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "all constraints must hold (AND)",
|
|
cond: rules.Condition{
|
|
Consideration: []rules.Consideration{rules.ConsiderationPaid},
|
|
DisclosurePresent: &yes,
|
|
DisclosureBeforeCut: &no,
|
|
},
|
|
fact: rules.Facts{Consideration: rules.ConsiderationPaid, DisclosurePresent: true, DisclosureBeforeCut: false},
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := tc.cond.Matches(tc.fact); got != tc.want {
|
|
t.Fatalf("Matches() = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluateFiltersByJurisdiction(t *testing.T) {
|
|
rule := rules.Rule{
|
|
ID: "WK-TEST",
|
|
Version: 1,
|
|
Jurisdiction: "AT",
|
|
Condition: rules.Condition{Consideration: []rules.Consideration{rules.ConsiderationPaid}},
|
|
Severity: rules.SeverityHigh,
|
|
}
|
|
fact := rules.Facts{Jurisdiction: "DE", Consideration: rules.ConsiderationPaid}
|
|
|
|
findings, needsClarification := rules.Evaluate([]rules.Rule{rule}, fact)
|
|
if needsClarification {
|
|
t.Fatal("needsClarification = true, want false")
|
|
}
|
|
if len(findings) != 0 {
|
|
t.Fatalf("expected no findings for a rule from a different jurisdiction, got %v", findings)
|
|
}
|
|
}
|