All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
- Create iace_handler_test.go (22 tests): input validation for InitFromProfile, GenerateSingleSection, ExportTechFile, CheckCompleteness, getTenantID, CreateProject, ListProjects, Component CRUD handlers - Add error-handling tests to tech_file_generator_test.go: nil context, nil project, empty components/hazards/classifications/evidence, unknown section type, all 19 getSystemPrompt types, AI-specific section prompts - Add JSON export tests to document_export_test.go: valid output, empty project, nil project error, special character handling (German text, XML escapes) - Add iace-hazard-library.md to mkdocs.yml navigation - Add TipTap Rich-Text-Editor section to iace.md documentation Total: 181 tests passing (was 165), 0 failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
649 lines
21 KiB
Go
649 lines
21 KiB
Go
package iace
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Test Helpers
|
|
// ============================================================================
|
|
|
|
// newTestSectionContext builds a SectionGenerationContext with realistic sample
|
|
// data for a "Robot Arm XY-200" industrial robot project.
|
|
func newTestSectionContext() *SectionGenerationContext {
|
|
projectID := uuid.New()
|
|
compSoftwareID := uuid.New()
|
|
compSensorID := uuid.New()
|
|
hazardHighID := uuid.New()
|
|
hazardLowID := uuid.New()
|
|
|
|
return &SectionGenerationContext{
|
|
Project: &Project{
|
|
ID: projectID,
|
|
TenantID: uuid.New(),
|
|
MachineName: "Robot Arm XY-200",
|
|
MachineType: "industrial_robot",
|
|
Manufacturer: "TestCorp",
|
|
Description: "6-axis industrial robot for automotive welding",
|
|
CEMarkingTarget: "2023/1230",
|
|
Status: ProjectStatusHazardAnalysis,
|
|
},
|
|
Components: []Component{
|
|
{
|
|
ID: compSoftwareID,
|
|
ProjectID: projectID,
|
|
Name: "SafetyPLC-500",
|
|
ComponentType: ComponentTypeSoftware,
|
|
Version: "3.2.1",
|
|
Description: "Safety-rated programmable logic controller firmware",
|
|
IsSafetyRelevant: true,
|
|
IsNetworked: true,
|
|
},
|
|
{
|
|
ID: compSensorID,
|
|
ProjectID: projectID,
|
|
Name: "ProxSensor-LiDAR",
|
|
ComponentType: ComponentTypeSensor,
|
|
Version: "1.0.0",
|
|
Description: "LiDAR proximity sensor for collision avoidance",
|
|
IsSafetyRelevant: false,
|
|
IsNetworked: false,
|
|
},
|
|
},
|
|
Hazards: []Hazard{
|
|
{
|
|
ID: hazardHighID,
|
|
ProjectID: projectID,
|
|
ComponentID: compSoftwareID,
|
|
Name: "Software malfunction causing uncontrolled movement",
|
|
Description: "Firmware fault leads to unpredictable arm trajectory",
|
|
Category: "mechanical",
|
|
SubCategory: "crushing",
|
|
Status: HazardStatusAssessed,
|
|
LifecyclePhase: "normal_operation",
|
|
AffectedPerson: "operator",
|
|
PossibleHarm: "Severe crushing injury to operator",
|
|
},
|
|
{
|
|
ID: hazardLowID,
|
|
ProjectID: projectID,
|
|
ComponentID: compSensorID,
|
|
Name: "Sensor drift causing delayed stop",
|
|
Description: "Gradual sensor calibration loss reduces reaction time",
|
|
Category: "electrical",
|
|
SubCategory: "sensor_failure",
|
|
Status: HazardStatusIdentified,
|
|
LifecyclePhase: "normal_operation",
|
|
AffectedPerson: "bystander",
|
|
PossibleHarm: "Minor bruising",
|
|
},
|
|
},
|
|
Assessments: map[uuid.UUID][]RiskAssessment{
|
|
hazardHighID: {
|
|
{
|
|
ID: uuid.New(),
|
|
HazardID: hazardHighID,
|
|
Version: 1,
|
|
AssessmentType: AssessmentTypeInitial,
|
|
Severity: 5,
|
|
Exposure: 4,
|
|
Probability: 3,
|
|
Avoidance: 2,
|
|
InherentRisk: 120.0,
|
|
ResidualRisk: 85.0,
|
|
RiskLevel: RiskLevelHigh,
|
|
IsAcceptable: false,
|
|
},
|
|
},
|
|
hazardLowID: {
|
|
{
|
|
ID: uuid.New(),
|
|
HazardID: hazardLowID,
|
|
Version: 1,
|
|
AssessmentType: AssessmentTypeInitial,
|
|
Severity: 2,
|
|
Exposure: 3,
|
|
Probability: 2,
|
|
Avoidance: 4,
|
|
InherentRisk: 12.0,
|
|
ResidualRisk: 6.0,
|
|
RiskLevel: RiskLevelLow,
|
|
IsAcceptable: true,
|
|
},
|
|
},
|
|
},
|
|
Mitigations: map[uuid.UUID][]Mitigation{
|
|
hazardHighID: {
|
|
{
|
|
ID: uuid.New(),
|
|
HazardID: hazardHighID,
|
|
ReductionType: ReductionTypeDesign,
|
|
Name: "Redundant safety controller",
|
|
Description: "Dual-channel safety PLC with cross-monitoring",
|
|
Status: MitigationStatusImplemented,
|
|
},
|
|
{
|
|
ID: uuid.New(),
|
|
HazardID: hazardHighID,
|
|
ReductionType: ReductionTypeProtective,
|
|
Name: "Light curtain barrier",
|
|
Description: "Type 4 safety light curtain around work envelope",
|
|
Status: MitigationStatusVerified,
|
|
},
|
|
},
|
|
hazardLowID: {
|
|
{
|
|
ID: uuid.New(),
|
|
HazardID: hazardLowID,
|
|
ReductionType: ReductionTypeInformation,
|
|
Name: "Calibration schedule warning",
|
|
Description: "Automated alert when sensor calibration is overdue",
|
|
Status: MitigationStatusPlanned,
|
|
},
|
|
},
|
|
},
|
|
Classifications: []RegulatoryClassification{
|
|
{
|
|
ID: uuid.New(),
|
|
ProjectID: projectID,
|
|
Regulation: RegulationMachineryRegulation,
|
|
ClassificationResult: "Annex I - High-Risk Machinery",
|
|
RiskLevel: RiskLevelHigh,
|
|
Confidence: 0.92,
|
|
},
|
|
},
|
|
Evidence: []Evidence{
|
|
{
|
|
ID: uuid.New(),
|
|
ProjectID: projectID,
|
|
FileName: "safety_plc_test_report.pdf",
|
|
Description: "Functional safety test report for SafetyPLC-500",
|
|
},
|
|
},
|
|
RAGContext: "",
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests: buildUserPrompt
|
|
// ============================================================================
|
|
|
|
func TestBuildUserPrompt_RiskAssessmentReport(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "risk_assessment_report")
|
|
|
|
// Must contain project identification
|
|
if !strings.Contains(prompt, "Robot Arm XY-200") {
|
|
t.Error("prompt should contain machine name 'Robot Arm XY-200'")
|
|
}
|
|
if !strings.Contains(prompt, "TestCorp") {
|
|
t.Error("prompt should contain manufacturer 'TestCorp'")
|
|
}
|
|
|
|
// Must reference hazard information
|
|
if !strings.Contains(prompt, "uncontrolled movement") || !strings.Contains(prompt, "Software malfunction") {
|
|
t.Error("prompt should contain hazard name or description for high-risk hazard")
|
|
}
|
|
|
|
// Must reference risk levels
|
|
if !strings.Contains(prompt, "high") && !strings.Contains(prompt, "High") && !strings.Contains(prompt, "HIGH") {
|
|
t.Error("prompt should reference the high risk level")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_ComponentList(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "component_list")
|
|
|
|
// Must list component names
|
|
if !strings.Contains(prompt, "SafetyPLC-500") {
|
|
t.Error("prompt should contain component name 'SafetyPLC-500'")
|
|
}
|
|
if !strings.Contains(prompt, "ProxSensor-LiDAR") {
|
|
t.Error("prompt should contain component name 'ProxSensor-LiDAR'")
|
|
}
|
|
|
|
// Must reference component types
|
|
if !strings.Contains(prompt, "software") && !strings.Contains(prompt, "Software") {
|
|
t.Error("prompt should contain component type 'software'")
|
|
}
|
|
if !strings.Contains(prompt, "sensor") && !strings.Contains(prompt, "Sensor") {
|
|
t.Error("prompt should contain component type 'sensor'")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_EmptyProject(t *testing.T) {
|
|
sctx := &SectionGenerationContext{
|
|
Project: nil,
|
|
Components: nil,
|
|
Hazards: nil,
|
|
Assessments: nil,
|
|
Mitigations: nil,
|
|
Classifications: nil,
|
|
Evidence: nil,
|
|
RAGContext: "",
|
|
}
|
|
|
|
// Should not panic on nil/empty data
|
|
prompt := buildUserPrompt(sctx, "general_description")
|
|
if prompt == "" {
|
|
t.Error("buildUserPrompt should return non-empty string even for empty context")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_MitigationReport(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "mitigation_report")
|
|
|
|
// Must reference mitigation names
|
|
if !strings.Contains(prompt, "Redundant safety controller") {
|
|
t.Error("prompt should contain design mitigation 'Redundant safety controller'")
|
|
}
|
|
if !strings.Contains(prompt, "Light curtain barrier") {
|
|
t.Error("prompt should contain protective mitigation 'Light curtain barrier'")
|
|
}
|
|
if !strings.Contains(prompt, "Calibration schedule warning") {
|
|
t.Error("prompt should contain information mitigation 'Calibration schedule warning'")
|
|
}
|
|
|
|
// Must reference reduction types
|
|
hasDesign := strings.Contains(prompt, "design") || strings.Contains(prompt, "Design")
|
|
hasProtective := strings.Contains(prompt, "protective") || strings.Contains(prompt, "Protective")
|
|
hasInformation := strings.Contains(prompt, "information") || strings.Contains(prompt, "Information")
|
|
if !hasDesign {
|
|
t.Error("prompt should reference 'design' reduction type")
|
|
}
|
|
if !hasProtective {
|
|
t.Error("prompt should reference 'protective' reduction type")
|
|
}
|
|
if !hasInformation {
|
|
t.Error("prompt should reference 'information' reduction type")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_WithRAGContext(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
sctx.RAGContext = "According to EN ISO 13849-1:2023, safety-related parts of control systems for machinery shall be designed and constructed using the principles of EN ISO 12100."
|
|
|
|
prompt := buildUserPrompt(sctx, "standards_applied")
|
|
|
|
if !strings.Contains(prompt, "EN ISO 13849-1") {
|
|
t.Error("prompt should include the RAG context referencing EN ISO 13849-1")
|
|
}
|
|
if !strings.Contains(prompt, "EN ISO 12100") {
|
|
t.Error("prompt should include the RAG context referencing EN ISO 12100")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_WithoutRAGContext(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
sctx.RAGContext = ""
|
|
|
|
prompt := buildUserPrompt(sctx, "standards_applied")
|
|
|
|
// Should still produce a valid prompt without RAG context
|
|
if prompt == "" {
|
|
t.Error("prompt should be non-empty even without RAG context")
|
|
}
|
|
// Should still contain the project info
|
|
if !strings.Contains(prompt, "Robot Arm XY-200") {
|
|
t.Error("prompt should still contain machine name when no RAG context")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests: buildRAGQuery
|
|
// ============================================================================
|
|
|
|
func TestBuildRAGQuery_AllSectionTypes(t *testing.T) {
|
|
sectionTypes := []string{
|
|
"risk_assessment_report",
|
|
"hazard_log_combined",
|
|
"general_description",
|
|
"essential_requirements",
|
|
"design_specifications",
|
|
"test_reports",
|
|
"standards_applied",
|
|
"declaration_of_conformity",
|
|
"ai_intended_purpose",
|
|
"ai_model_description",
|
|
"ai_risk_management",
|
|
"ai_human_oversight",
|
|
"component_list",
|
|
"classification_report",
|
|
"mitigation_report",
|
|
"verification_report",
|
|
"evidence_index",
|
|
"instructions_for_use",
|
|
"monitoring_plan",
|
|
}
|
|
|
|
for _, st := range sectionTypes {
|
|
t.Run(st, func(t *testing.T) {
|
|
q := buildRAGQuery(st)
|
|
if q == "" {
|
|
t.Errorf("buildRAGQuery(%q) returned empty string", st)
|
|
}
|
|
// Each query should be at least a few words long to be useful
|
|
if len(q) < 10 {
|
|
t.Errorf("buildRAGQuery(%q) returned suspiciously short query: %q", st, q)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildRAGQuery_UnknownSectionType(t *testing.T) {
|
|
q := buildRAGQuery("nonexistent_section_type")
|
|
// Should return a generic fallback query rather than an empty string
|
|
// (the function needs some query to send to RAG even for unknown types)
|
|
if q == "" {
|
|
t.Log("buildRAGQuery returned empty for unknown section type (may be acceptable if caller handles this)")
|
|
}
|
|
}
|
|
|
|
func TestBuildRAGQuery_QueriesAreDifferent(t *testing.T) {
|
|
// Different section types should produce different queries for targeted retrieval
|
|
q1 := buildRAGQuery("risk_assessment_report")
|
|
q2 := buildRAGQuery("declaration_of_conformity")
|
|
q3 := buildRAGQuery("monitoring_plan")
|
|
|
|
if q1 == q2 {
|
|
t.Error("risk_assessment_report and declaration_of_conformity should have different RAG queries")
|
|
}
|
|
if q2 == q3 {
|
|
t.Error("declaration_of_conformity and monitoring_plan should have different RAG queries")
|
|
}
|
|
if q1 == q3 {
|
|
t.Error("risk_assessment_report and monitoring_plan should have different RAG queries")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests: sectionSystemPrompts
|
|
// ============================================================================
|
|
|
|
func TestSectionSystemPrompts_Coverage(t *testing.T) {
|
|
requiredTypes := []string{
|
|
"risk_assessment_report",
|
|
"hazard_log_combined",
|
|
"general_description",
|
|
"essential_requirements",
|
|
"design_specifications",
|
|
"test_reports",
|
|
"standards_applied",
|
|
"declaration_of_conformity",
|
|
"component_list",
|
|
"classification_report",
|
|
"mitigation_report",
|
|
"verification_report",
|
|
"evidence_index",
|
|
"instructions_for_use",
|
|
"monitoring_plan",
|
|
}
|
|
|
|
for _, st := range requiredTypes {
|
|
t.Run(st, func(t *testing.T) {
|
|
prompt, ok := sectionSystemPrompts[st]
|
|
if !ok {
|
|
t.Errorf("sectionSystemPrompts missing entry for %q", st)
|
|
return
|
|
}
|
|
if prompt == "" {
|
|
t.Errorf("sectionSystemPrompts[%q] is empty", st)
|
|
}
|
|
// System prompts should contain meaningful instruction text
|
|
if len(prompt) < 50 {
|
|
t.Errorf("sectionSystemPrompts[%q] is suspiciously short (%d chars)", st, len(prompt))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSectionSystemPrompts_ContainRoleInstruction(t *testing.T) {
|
|
// Each system prompt should instruct the LLM about its role as a compliance expert.
|
|
// Prompts are in German, so check for both German and English keywords.
|
|
keywords := []string{
|
|
"expert", "engineer", "compliance", "technical", "documentation", "safety",
|
|
"generate", "write", "create", "produce",
|
|
// German equivalents
|
|
"experte", "ingenieur", "erstelle", "beschreibe", "dokumentation", "sicherheit",
|
|
"risikobeurteilung", "konformit", "norm", "verordnung", "richtlinie",
|
|
"gefaehrdung", "massnahm", "verifikation", "uebersicht", "protokoll",
|
|
"abschnitt", "bericht", "maschin",
|
|
}
|
|
for st, prompt := range sectionSystemPrompts {
|
|
t.Run(st, func(t *testing.T) {
|
|
lower := strings.ToLower(prompt)
|
|
found := false
|
|
for _, kw := range keywords {
|
|
if strings.Contains(lower, kw) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("sectionSystemPrompts[%q] does not appear to contain a role or task instruction", st)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests: buildUserPrompt — additional section types
|
|
// ============================================================================
|
|
|
|
func TestBuildUserPrompt_ClassificationReport(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "classification_report")
|
|
|
|
// Must reference classification data
|
|
if !strings.Contains(prompt, "machinery_regulation") && !strings.Contains(prompt, "Machinery") && !strings.Contains(prompt, "machinery") {
|
|
t.Error("prompt should reference the machinery regulation classification")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_EvidenceIndex(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "evidence_index")
|
|
|
|
// Must reference evidence files
|
|
if !strings.Contains(prompt, "safety_plc_test_report.pdf") {
|
|
t.Error("prompt should reference evidence file name 'safety_plc_test_report.pdf'")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_GeneralDescription(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "general_description")
|
|
|
|
// Must contain machine description
|
|
if !strings.Contains(prompt, "Robot Arm XY-200") {
|
|
t.Error("prompt should contain machine name")
|
|
}
|
|
if !strings.Contains(prompt, "industrial_robot") && !strings.Contains(prompt, "industrial robot") {
|
|
t.Error("prompt should contain machine type")
|
|
}
|
|
if !strings.Contains(prompt, "automotive welding") && !strings.Contains(prompt, "6-axis") {
|
|
t.Error("prompt should reference machine description content")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_DeclarationOfConformity(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "declaration_of_conformity")
|
|
|
|
// Declaration needs manufacturer and CE target
|
|
if !strings.Contains(prompt, "TestCorp") {
|
|
t.Error("prompt should contain manufacturer for declaration of conformity")
|
|
}
|
|
if !strings.Contains(prompt, "2023/1230") && !strings.Contains(prompt, "CE") && !strings.Contains(prompt, "ce_marking") {
|
|
t.Error("prompt should reference CE marking target or regulation for declaration")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests: Error handling & edge cases
|
|
// ============================================================================
|
|
|
|
func TestBuildUserPrompt_NilContext(t *testing.T) {
|
|
// Should not panic on completely nil context
|
|
prompt := buildUserPrompt(nil, "risk_assessment_report")
|
|
if prompt == "" {
|
|
t.Error("buildUserPrompt should return non-empty string for nil context")
|
|
}
|
|
if !strings.Contains(prompt, "Keine Projektdaten") {
|
|
t.Error("nil context prompt should contain fallback text 'Keine Projektdaten'")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_NilProject(t *testing.T) {
|
|
sctx := &SectionGenerationContext{
|
|
Project: nil,
|
|
}
|
|
prompt := buildUserPrompt(sctx, "general_description")
|
|
if prompt == "" {
|
|
t.Error("buildUserPrompt should return non-empty string for nil project")
|
|
}
|
|
if !strings.Contains(prompt, "Keine Projektdaten") {
|
|
t.Error("nil project prompt should contain fallback text 'Keine Projektdaten'")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_EmptyComponents(t *testing.T) {
|
|
sctx := &SectionGenerationContext{
|
|
Project: &Project{MachineName: "Test", Manufacturer: "Corp"},
|
|
Components: []Component{},
|
|
Hazards: nil,
|
|
}
|
|
prompt := buildUserPrompt(sctx, "component_list")
|
|
if !strings.Contains(prompt, "Test") {
|
|
t.Error("prompt should contain machine name even with empty components")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_EmptyHazards(t *testing.T) {
|
|
sctx := &SectionGenerationContext{
|
|
Project: &Project{MachineName: "Test", Manufacturer: "Corp"},
|
|
Hazards: []Hazard{},
|
|
}
|
|
prompt := buildUserPrompt(sctx, "hazard_log_combined")
|
|
if prompt == "" {
|
|
t.Error("prompt should be non-empty even with no hazards")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_EmptyClassifications(t *testing.T) {
|
|
sctx := &SectionGenerationContext{
|
|
Project: &Project{MachineName: "Test", Manufacturer: "Corp"},
|
|
Classifications: []RegulatoryClassification{},
|
|
}
|
|
prompt := buildUserPrompt(sctx, "classification_report")
|
|
if prompt == "" {
|
|
t.Error("prompt should be non-empty even with no classifications")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_EmptyEvidence(t *testing.T) {
|
|
sctx := &SectionGenerationContext{
|
|
Project: &Project{MachineName: "Test", Manufacturer: "Corp"},
|
|
Evidence: []Evidence{},
|
|
}
|
|
prompt := buildUserPrompt(sctx, "evidence_index")
|
|
if prompt == "" {
|
|
t.Error("prompt should be non-empty even with no evidence")
|
|
}
|
|
}
|
|
|
|
func TestGetSystemPrompt_UnknownType(t *testing.T) {
|
|
prompt := getSystemPrompt("totally_unknown_section")
|
|
if prompt == "" {
|
|
t.Error("getSystemPrompt should return a fallback for unknown types")
|
|
}
|
|
// Fallback should still be a useful CE expert prompt
|
|
lower := strings.ToLower(prompt)
|
|
if !strings.Contains(lower, "ce") && !strings.Contains(lower, "experte") && !strings.Contains(lower, "dokumentation") {
|
|
t.Error("fallback system prompt should reference CE or documentation expertise")
|
|
}
|
|
}
|
|
|
|
func TestGetSystemPrompt_AllKnownTypes(t *testing.T) {
|
|
knownTypes := []string{
|
|
"risk_assessment_report", "hazard_log_combined", "general_description",
|
|
"essential_requirements", "design_specifications", "test_reports",
|
|
"standards_applied", "declaration_of_conformity", "component_list",
|
|
"classification_report", "mitigation_report", "verification_report",
|
|
"evidence_index", "instructions_for_use", "monitoring_plan",
|
|
"ai_intended_purpose", "ai_model_description", "ai_risk_management",
|
|
"ai_human_oversight",
|
|
}
|
|
for _, st := range knownTypes {
|
|
t.Run(st, func(t *testing.T) {
|
|
prompt := getSystemPrompt(st)
|
|
if prompt == "" {
|
|
t.Errorf("getSystemPrompt(%q) returned empty string", st)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_AIIntendedPurpose(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "ai_intended_purpose")
|
|
if prompt == "" {
|
|
t.Error("prompt should be non-empty for ai_intended_purpose")
|
|
}
|
|
if !strings.Contains(prompt, "Robot Arm XY-200") {
|
|
t.Error("AI intended purpose prompt should contain machine name")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_AIHumanOversight(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
prompt := buildUserPrompt(sctx, "ai_human_oversight")
|
|
if prompt == "" {
|
|
t.Error("prompt should be non-empty for ai_human_oversight")
|
|
}
|
|
if !strings.Contains(prompt, "Robot Arm XY-200") {
|
|
t.Error("AI human oversight prompt should contain machine name")
|
|
}
|
|
}
|
|
|
|
func TestBuildUserPrompt_MultipleHazardAssessments(t *testing.T) {
|
|
sctx := newTestSectionContext()
|
|
|
|
// Find the high-risk hazard ID from the assessments map
|
|
var highHazardID uuid.UUID
|
|
for hid, assessments := range sctx.Assessments {
|
|
if len(assessments) > 0 && assessments[0].RiskLevel == RiskLevelHigh {
|
|
highHazardID = hid
|
|
break
|
|
}
|
|
}
|
|
|
|
if highHazardID != uuid.Nil {
|
|
// Add a second assessment version (post-mitigation) for the high-risk hazard
|
|
sctx.Assessments[highHazardID] = append(sctx.Assessments[highHazardID], RiskAssessment{
|
|
ID: uuid.New(),
|
|
HazardID: highHazardID,
|
|
Version: 2,
|
|
AssessmentType: AssessmentTypePostMitigation,
|
|
Severity: 5,
|
|
Exposure: 4,
|
|
Probability: 1,
|
|
Avoidance: 4,
|
|
InherentRisk: 120.0,
|
|
ResidualRisk: 20.0,
|
|
RiskLevel: RiskLevelMedium,
|
|
IsAcceptable: true,
|
|
})
|
|
}
|
|
|
|
prompt := buildUserPrompt(sctx, "risk_assessment_report")
|
|
if prompt == "" {
|
|
t.Error("prompt should not be empty with multiple assessments")
|
|
}
|
|
}
|