feat: Regulatory News Dashboard — proaktive Compliance-Alerts
Some checks failed
Build + Deploy / build-backend-compliance (push) Successful in 2m43s
Build + Deploy / build-admin-compliance (push) Successful in 1m46s
Build + Deploy / build-ai-sdk (push) Successful in 47s
Build + Deploy / build-developer-portal (push) Successful in 1m0s
Build + Deploy / build-tts (push) Successful in 1m14s
Build + Deploy / build-document-crawler (push) Successful in 37s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 19s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m35s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 23s
Build + Deploy / trigger-orca (push) Failing after 2h32m34s
Some checks failed
Build + Deploy / build-backend-compliance (push) Successful in 2m43s
Build + Deploy / build-admin-compliance (push) Successful in 1m46s
Build + Deploy / build-ai-sdk (push) Successful in 47s
Build + Deploy / build-developer-portal (push) Successful in 1m0s
Build + Deploy / build-tts (push) Successful in 1m14s
Build + Deploy / build-document-crawler (push) Successful in 37s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 19s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m35s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 23s
Build + Deploy / trigger-orca (push) Failing after 2h32m34s
Zeigt anstehende regulatorische Fristen im Dashboard an, abgeleitet aus den bestehenden Obligation v2 JSON-Dateien. Keine neue DB-Tabelle. Erster News-Eintrag: Widerrufsbutton-Pflicht ab 19.06.2026 (EU-RL 2023/2673, §356a BGB) — eigener Text, keine externe Quelle. Features: - Go Service: scannt Obligations nach Fristen, berechnet Urgency - API: GET /sdk/v1/regulatory-news mit Countdown + Farbcodierung - Dashboard: RegulatoryNewsFeed Sektion mit Countdown-Badges - Vorlage: news-Feld in v2 JSON fuer zukuenftige regulatorische Updates - 11 Tests (Sortierung, Urgency, Deadline-Parsing, Real-File-Test) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
ai-compliance-sdk/internal/ucca/regulatory_news_test.go
Normal file
191
ai-compliance-sdk/internal/ucca/regulatory_news_test.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func makeTestRegulations() map[string]*V2RegulationFile {
|
||||
future30 := time.Now().AddDate(0, 0, 30).Format("2006-01-02")
|
||||
future90 := time.Now().AddDate(0, 0, 90).Format("2006-01-02")
|
||||
past := time.Now().AddDate(0, 0, -10).Format("2006-01-02")
|
||||
|
||||
return map[string]*V2RegulationFile{
|
||||
"TestReg": {
|
||||
Regulation: "TestReg",
|
||||
Obligations: []V2Obligation{
|
||||
{
|
||||
ID: "TR-001", Title: "Upcoming Critical",
|
||||
Deadline: &V2Deadline{Date: future30},
|
||||
News: &V2ObligationNews{
|
||||
Headline: "Critical Deadline", Summary: "Test summary",
|
||||
ActionRequired: "Do something", Affected: "All", ActionLink: "/sdk/test",
|
||||
},
|
||||
LegalBasis: []V2LegalBasis{{Norm: "TestLaw", Article: "Art. 1"}},
|
||||
},
|
||||
{
|
||||
ID: "TR-002", Title: "Upcoming Medium",
|
||||
Description: "Medium priority regulation change.",
|
||||
Deadline: &V2Deadline{Date: future90},
|
||||
LegalBasis: []V2LegalBasis{{Norm: "TestLaw", Article: "Art. 2"}},
|
||||
},
|
||||
{
|
||||
ID: "TR-003", Title: "Past Deadline",
|
||||
Deadline: &V2Deadline{Date: past},
|
||||
},
|
||||
{
|
||||
ID: "TR-004", Title: "No Deadline",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegulatoryNews_SortedByUrgency(t *testing.T) {
|
||||
regs := makeTestRegulations()
|
||||
items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
|
||||
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("expected 2 items (future only), got %d", len(items))
|
||||
}
|
||||
// First item should be the more urgent one (30 days)
|
||||
if items[0].ID != "TR-001" {
|
||||
t.Errorf("expected TR-001 first (most urgent), got %s", items[0].ID)
|
||||
}
|
||||
if items[1].ID != "TR-002" {
|
||||
t.Errorf("expected TR-002 second, got %s", items[1].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegulatoryNews_UsesNewsField(t *testing.T) {
|
||||
regs := makeTestRegulations()
|
||||
items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
|
||||
|
||||
// TR-001 has hand-crafted news
|
||||
if items[0].Headline != "Critical Deadline" {
|
||||
t.Errorf("expected hand-crafted headline, got %q", items[0].Headline)
|
||||
}
|
||||
if items[0].ActionLink != "/sdk/test" {
|
||||
t.Errorf("expected /sdk/test, got %q", items[0].ActionLink)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegulatoryNews_AutoGeneratesWithoutNews(t *testing.T) {
|
||||
regs := makeTestRegulations()
|
||||
items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
|
||||
|
||||
// TR-002 has no news field — should auto-generate
|
||||
if items[1].Headline == "" {
|
||||
t.Error("expected auto-generated headline")
|
||||
}
|
||||
if items[1].Summary == "" {
|
||||
t.Error("expected auto-generated summary from description")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegulatoryNews_ExcludesPastDeadlines(t *testing.T) {
|
||||
regs := makeTestRegulations()
|
||||
items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
|
||||
|
||||
for _, item := range items {
|
||||
if item.ID == "TR-003" {
|
||||
t.Error("past deadline should be excluded")
|
||||
}
|
||||
if item.ID == "TR-004" {
|
||||
t.Error("no-deadline obligation should be excluded")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegulatoryNews_LimitWorks(t *testing.T) {
|
||||
regs := makeTestRegulations()
|
||||
items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 1})
|
||||
|
||||
if len(items) != 1 {
|
||||
t.Errorf("expected 1 item with limit=1, got %d", len(items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeUrgency(t *testing.T) {
|
||||
tests := []struct {
|
||||
days int
|
||||
expected string
|
||||
}{
|
||||
{5, "critical"},
|
||||
{30, "critical"},
|
||||
{31, "high"},
|
||||
{90, "high"},
|
||||
{91, "medium"},
|
||||
{180, "medium"},
|
||||
{181, "low"},
|
||||
{365, "low"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := computeUrgency(tc.days)
|
||||
if got != tc.expected {
|
||||
t.Errorf("computeUrgency(%d) = %q, want %q", tc.days, got, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDeadline_DeadlineDate(t *testing.T) {
|
||||
obl := V2Obligation{Deadline: &V2Deadline{Date: "2026-06-19"}}
|
||||
d, ok := resolveDeadline(obl)
|
||||
if !ok {
|
||||
t.Fatal("expected deadline resolved")
|
||||
}
|
||||
if d.Format("2006-01-02") != "2026-06-19" {
|
||||
t.Errorf("got %s", d.Format("2006-01-02"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDeadline_ValidFrom(t *testing.T) {
|
||||
obl := V2Obligation{ValidFrom: "2026-08-02"}
|
||||
d, ok := resolveDeadline(obl)
|
||||
if !ok {
|
||||
t.Fatal("expected deadline resolved from valid_from")
|
||||
}
|
||||
if d.Format("2006-01-02") != "2026-08-02" {
|
||||
t.Errorf("got %s", d.Format("2006-01-02"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDeadline_NoDate(t *testing.T) {
|
||||
obl := V2Obligation{}
|
||||
_, ok := resolveDeadline(obl)
|
||||
if ok {
|
||||
t.Error("expected no deadline for empty obligation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatLegalReference(t *testing.T) {
|
||||
bases := []V2LegalBasis{
|
||||
{Norm: "DSGVO", Article: "Art. 22"},
|
||||
{Norm: "BGB", Article: "§ 356a"},
|
||||
}
|
||||
ref := formatLegalReference(bases)
|
||||
if ref != "Art. 22 DSGVO, § 356a BGB" {
|
||||
t.Errorf("got %q", ref)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegulatoryNews_FromRealFiles(t *testing.T) {
|
||||
regs, err := LoadAllV2Regulations()
|
||||
if err != nil {
|
||||
t.Skipf("could not load v2 regulations: %v", err)
|
||||
}
|
||||
items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 20, HorizonDays: 730})
|
||||
// Should find at least the Widerrufsbutton obligation
|
||||
found := false
|
||||
for _, item := range items {
|
||||
if item.ID == "VBR-OBL-001" {
|
||||
found = true
|
||||
if item.Headline != "Widerrufsbutton-Pflicht ab 19. Juni 2026" {
|
||||
t.Errorf("unexpected headline: %q", item.Headline)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected VBR-OBL-001 in regulatory news")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user