Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/architecture_test.go
T
Benjamin Admin 32ba8d16b1 feat(iace): add data-driven Architektur & Datenfluss explainer tab
Adds an auditor-facing view of the IACE engine: a clickable 10-stage
pipeline flow (Grenzen-Formular → ParseNarrative → Pattern-Gates →
Relevanz → Caps → Gefährdungen → Maßnahmen → Risiko → Normen → Matrix),
plus live library counts, the data-source/license register (incl. the
DIN/Beuth + DGUV exclusions), and the norm-matching logic that reconciles
DIN/ISO/OSHA machine-type vocabulary via canonicalMachineType folding.

Backend: BuildArchitecture() with LIVE counts so the diagram can never
drift; GET /iace/architecture; collectAllNorms() extracted from
SuggestNorms as the single source of truth for the norm-library count.
Frontend: useArchitecture hook + page + new IACE nav tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 09:35:37 +02:00

89 lines
2.7 KiB
Go

package iace
import (
"strings"
"testing"
)
// BuildArchitecture is the data-driven engine self-description rendered in the
// "Architektur & Datenfluss" auditability tab. These tests pin its shape and,
// crucially, that the library counts are LIVE (non-zero, drawn from the running
// engine) — a zero count would mean the diagram silently drifted from reality.
func TestBuildArchitecture_Shape(t *testing.T) {
a := BuildArchitecture()
if len(a.Stages) != 10 {
t.Errorf("expected 10 pipeline stages, got %d", len(a.Stages))
}
// Stage order is the audit narrative — first is the limits form, last the matrix.
if len(a.Stages) > 0 && a.Stages[0].ID != "grenzen" {
t.Errorf("first stage should be grenzen, got %q", a.Stages[0].ID)
}
if last := a.Stages[len(a.Stages)-1]; last.ID != "matrix" {
t.Errorf("last stage should be matrix, got %q", last.ID)
}
for _, s := range a.Stages {
if s.Title == "" || s.Summary == "" || s.Logic == "" || s.DataSource == "" {
t.Errorf("stage %q has empty required prose: %+v", s.ID, s)
}
}
if len(a.NormMatching) == 0 {
t.Error("norm_matching explanation must not be empty")
}
if len(a.Evidence) == 0 {
t.Error("evidence (ESAW citations) must not be empty")
}
}
func TestBuildArchitecture_LiveLibraryCounts(t *testing.T) {
a := BuildArchitecture()
if len(a.Libraries) == 0 {
t.Fatal("no libraries reported")
}
for _, l := range a.Libraries {
if l.Name == "" || l.SourceFile == "" {
t.Errorf("library missing name/source: %+v", l)
}
if l.Count <= 0 {
t.Errorf("library %q has non-live count %d (expected >0 from running engine)", l.Name, l.Count)
}
}
}
func TestBuildArchitecture_DataSourcesIncludeExclusions(t *testing.T) {
a := BuildArchitecture()
var hasUsed, hasExcluded bool
for _, d := range a.DataSources {
switch d.Status {
case "verwendet":
hasUsed = true
case "ausgeschlossen":
hasExcluded = true
default:
t.Errorf("data source %q has unexpected status %q", d.Name, d.Status)
}
if d.License == "" {
t.Errorf("data source %q missing license", d.Name)
}
}
if !hasUsed {
t.Error("expected at least one used data source")
}
// The copyright guardrail is auditable only if the EXCLUDED norm tables are
// shown as deliberately not-reproduced — not silently omitted.
if !hasExcluded {
t.Error("expected DIN/Beuth norm tables to appear as an explicit exclusion")
}
// The licensing guardrail must be spelled out in the norm-matching prose:
// norm tables are referenced, never reproduced.
joined := strings.ToLower(strings.Join(a.NormMatching, " "))
if !strings.Contains(joined, "reproduziert") {
t.Error("norm-matching prose should state norm tables are never reproduced")
}
}