Add diagnostic contract primitives
This commit is contained in:
348
internal/framework/contracts/diagnostics.go
Normal file
348
internal/framework/contracts/diagnostics.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxDiagnosticReasonCodeBytes = 128
|
||||
MaxDiagnosticScopeBytes = 512
|
||||
MaxDiagnosticMessageBytes = 4 * 1024
|
||||
MaxDiagnosticSamples = 3
|
||||
MaxProducerDiagnosticGroups = 64
|
||||
)
|
||||
|
||||
// DiagnosticDisposition identifies the operator significance of a producer
|
||||
// finding. Warnings are reserved for process-level degradation or incomplete
|
||||
// configured work.
|
||||
type DiagnosticDisposition string
|
||||
|
||||
const (
|
||||
DiagnosticDispositionWarning DiagnosticDisposition = "warning"
|
||||
DiagnosticDispositionAdvisory DiagnosticDisposition = "advisory"
|
||||
DiagnosticDispositionObservation DiagnosticDisposition = "observation"
|
||||
)
|
||||
|
||||
// DiagnosticCategory gives a stable, bounded classification for a producer
|
||||
// finding.
|
||||
type DiagnosticCategory string
|
||||
|
||||
const (
|
||||
DiagnosticCategoryConfiguration DiagnosticCategory = "configuration"
|
||||
DiagnosticCategoryDegradation DiagnosticCategory = "degradation"
|
||||
DiagnosticCategoryValidationIncomplete DiagnosticCategory = "validation_incomplete"
|
||||
DiagnosticCategoryFallback DiagnosticCategory = "fallback"
|
||||
DiagnosticCategoryDataQuality DiagnosticCategory = "data_quality"
|
||||
DiagnosticCategoryNormalization DiagnosticCategory = "normalization"
|
||||
)
|
||||
|
||||
// DiagnosticOriginStage identifies the framework operation that promoted a
|
||||
// diagnostic. It is framework-owned rather than producer-owned.
|
||||
type DiagnosticOriginStage string
|
||||
|
||||
const (
|
||||
DiagnosticOriginStageReferences DiagnosticOriginStage = "references"
|
||||
DiagnosticOriginStageChunk DiagnosticOriginStage = "chunk"
|
||||
DiagnosticOriginStageExtract DiagnosticOriginStage = "extract"
|
||||
DiagnosticOriginStageMerge DiagnosticOriginStage = "merge"
|
||||
DiagnosticOriginStageNormalize DiagnosticOriginStage = "normalize"
|
||||
)
|
||||
|
||||
// DiagnosticSample is a bounded, safe example of a diagnostic occurrence.
|
||||
// Chunk identity is attached by the framework when it promotes a producer
|
||||
// diagnostic into a final group.
|
||||
type DiagnosticSample struct {
|
||||
Scope string `json:"scope"`
|
||||
Message string `json:"message"`
|
||||
ChunkID string `json:"chunk_id,omitempty"`
|
||||
ChunkIndex *int `json:"chunk_index,omitempty"`
|
||||
}
|
||||
|
||||
// ProducerDiagnostic is the locally grouped form returned by one producer or
|
||||
// validator. It intentionally has no pipeline origin.
|
||||
type ProducerDiagnostic struct {
|
||||
Disposition DiagnosticDisposition `json:"disposition"`
|
||||
Category DiagnosticCategory `json:"category"`
|
||||
ReasonCode string `json:"reason_code"`
|
||||
OccurrenceCount int `json:"occurrence_count"`
|
||||
Samples []DiagnosticSample `json:"samples"`
|
||||
OmittedSampleCount int `json:"omitted_sample_count"`
|
||||
}
|
||||
|
||||
// DiagnosticOrigin is framework-owned context used to distinguish findings
|
||||
// from different pipeline locations during final aggregation.
|
||||
type DiagnosticOrigin struct {
|
||||
Stage DiagnosticOriginStage `json:"stage"`
|
||||
StepID string `json:"step_id,omitempty"`
|
||||
LaneID string `json:"lane_id,omitempty"`
|
||||
ModuleKey string `json:"module_key,omitempty"`
|
||||
ValidatorKey string `json:"validator_key,omitempty"`
|
||||
}
|
||||
|
||||
// DiagnosticGroup is a producer diagnostic after framework origin enrichment.
|
||||
type DiagnosticGroup struct {
|
||||
Disposition DiagnosticDisposition `json:"disposition"`
|
||||
Category DiagnosticCategory `json:"category"`
|
||||
ReasonCode string `json:"reason_code"`
|
||||
Origin DiagnosticOrigin `json:"origin"`
|
||||
OccurrenceCount int `json:"occurrence_count"`
|
||||
Samples []DiagnosticSample `json:"samples"`
|
||||
OmittedSampleCount int `json:"omitted_sample_count"`
|
||||
}
|
||||
|
||||
// DiagnosticCollection is the grouped collection supplied to later durable
|
||||
// and presentation boundaries. Global aggregation policy is applied by the
|
||||
// framework before it reaches those boundaries.
|
||||
type DiagnosticCollection struct {
|
||||
Groups []DiagnosticGroup `json:"groups"`
|
||||
Truncated bool `json:"truncated"`
|
||||
UnrepresentedOccurrenceCount int `json:"unrepresented_occurrence_count"`
|
||||
}
|
||||
|
||||
// Validate checks a producer-local diagnostic against the public safety and
|
||||
// classification contract.
|
||||
func (diagnostic ProducerDiagnostic) Validate() error {
|
||||
return validateDiagnostic(
|
||||
diagnostic.Disposition,
|
||||
diagnostic.Category,
|
||||
diagnostic.ReasonCode,
|
||||
diagnostic.OccurrenceCount,
|
||||
diagnostic.Samples,
|
||||
diagnostic.OmittedSampleCount,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
// ValidateProducerDiagnostics validates the complete set returned by one
|
||||
// producer or validator result.
|
||||
func ValidateProducerDiagnostics(diagnostics []ProducerDiagnostic) error {
|
||||
if len(diagnostics) > MaxProducerDiagnosticGroups {
|
||||
return errors.New("producer diagnostics exceed maximum group count")
|
||||
}
|
||||
for index, diagnostic := range diagnostics {
|
||||
if err := diagnostic.Validate(); err != nil {
|
||||
return fmt.Errorf("producer diagnostic %d: %w", index, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks framework-owned origin fields.
|
||||
func (origin DiagnosticOrigin) Validate() error {
|
||||
switch origin.Stage {
|
||||
case DiagnosticOriginStageReferences, DiagnosticOriginStageChunk, DiagnosticOriginStageExtract, DiagnosticOriginStageMerge, DiagnosticOriginStageNormalize:
|
||||
default:
|
||||
return errors.New("diagnostic origin stage is invalid")
|
||||
}
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "diagnostic origin step ID", value: origin.StepID},
|
||||
{name: "diagnostic origin lane ID", value: origin.LaneID},
|
||||
{name: "diagnostic origin module key", value: origin.ModuleKey},
|
||||
{name: "diagnostic origin validator key", value: origin.ValidatorKey},
|
||||
} {
|
||||
if field.value != "" && (!utf8.ValidString(field.value) || strings.TrimSpace(field.value) == "") {
|
||||
return fmt.Errorf("%s must be valid nonblank UTF-8 when present", field.name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks a final origin-enriched group.
|
||||
func (group DiagnosticGroup) Validate() error {
|
||||
if err := group.Origin.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateDiagnostic(
|
||||
group.Disposition,
|
||||
group.Category,
|
||||
group.ReasonCode,
|
||||
group.OccurrenceCount,
|
||||
group.Samples,
|
||||
group.OmittedSampleCount,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate checks the collection shape without imposing later global
|
||||
// aggregation limits.
|
||||
func (collection DiagnosticCollection) Validate() error {
|
||||
if collection.UnrepresentedOccurrenceCount < 0 {
|
||||
return errors.New("diagnostic collection unrepresented occurrence count must not be negative")
|
||||
}
|
||||
if !collection.Truncated && collection.UnrepresentedOccurrenceCount != 0 {
|
||||
return errors.New("diagnostic collection has unrepresented occurrences without truncation")
|
||||
}
|
||||
seen := make(map[diagnosticGroupKey]struct{}, len(collection.Groups))
|
||||
for index, group := range collection.Groups {
|
||||
if err := group.Validate(); err != nil {
|
||||
return fmt.Errorf("diagnostic group %d: %w", index, err)
|
||||
}
|
||||
key := diagnosticGroupKeyFromGroup(group)
|
||||
if _, exists := seen[key]; exists {
|
||||
return errors.New("diagnostic collection contains duplicate group identity")
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneProducerDiagnostics returns independent diagnostic slice ownership.
|
||||
func CloneProducerDiagnostics(diagnostics []ProducerDiagnostic) []ProducerDiagnostic {
|
||||
if len(diagnostics) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]ProducerDiagnostic, len(diagnostics))
|
||||
for index, diagnostic := range diagnostics {
|
||||
cloned[index] = cloneProducerDiagnostic(diagnostic)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
// CloneDiagnosticCollection returns independent collection ownership.
|
||||
func CloneDiagnosticCollection(collection DiagnosticCollection) DiagnosticCollection {
|
||||
collection.Groups = make([]DiagnosticGroup, len(collection.Groups))
|
||||
for index, group := range collection.Groups {
|
||||
collection.Groups[index] = cloneDiagnosticGroup(group)
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
func validateDiagnostic(disposition DiagnosticDisposition, category DiagnosticCategory, reasonCode string, occurrenceCount int, samples []DiagnosticSample, omittedSampleCount int, allowChunkContext bool) error {
|
||||
if !diagnosticCategoryAllowed(disposition, category) {
|
||||
return errors.New("diagnostic disposition and category combination is invalid")
|
||||
}
|
||||
if err := validateDiagnosticText(reasonCode, MaxDiagnosticReasonCodeBytes, "diagnostic reason code"); err != nil {
|
||||
return err
|
||||
}
|
||||
if occurrenceCount <= 0 {
|
||||
return errors.New("diagnostic occurrence count must be positive")
|
||||
}
|
||||
if len(samples) == 0 {
|
||||
return errors.New("diagnostic samples must not be empty")
|
||||
}
|
||||
if len(samples) > MaxDiagnosticSamples {
|
||||
return errors.New("diagnostic samples exceed maximum count")
|
||||
}
|
||||
seen := make(map[diagnosticSampleKey]struct{}, len(samples))
|
||||
for index, sample := range samples {
|
||||
if err := validateDiagnosticSample(sample, allowChunkContext); err != nil {
|
||||
return fmt.Errorf("diagnostic sample %d: %w", index, err)
|
||||
}
|
||||
key := diagnosticSampleKeyFromSample(sample)
|
||||
if _, exists := seen[key]; exists {
|
||||
return errors.New("diagnostic samples must be distinct")
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
if occurrenceCount < len(samples) {
|
||||
return errors.New("diagnostic occurrence count is smaller than sample count")
|
||||
}
|
||||
if omittedSampleCount != occurrenceCount-len(samples) {
|
||||
return errors.New("diagnostic omitted sample count is inconsistent")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func diagnosticCategoryAllowed(disposition DiagnosticDisposition, category DiagnosticCategory) bool {
|
||||
switch disposition {
|
||||
case DiagnosticDispositionWarning:
|
||||
return category == DiagnosticCategoryConfiguration || category == DiagnosticCategoryDegradation || category == DiagnosticCategoryValidationIncomplete || category == DiagnosticCategoryFallback
|
||||
case DiagnosticDispositionAdvisory:
|
||||
return category == DiagnosticCategoryDataQuality
|
||||
case DiagnosticDispositionObservation:
|
||||
return category == DiagnosticCategoryNormalization
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateDiagnosticSample(sample DiagnosticSample, allowChunkContext bool) error {
|
||||
if err := validateDiagnosticText(sample.Scope, MaxDiagnosticScopeBytes, "diagnostic sample scope"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDiagnosticText(sample.Message, MaxDiagnosticMessageBytes, "diagnostic sample message"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowChunkContext && (sample.ChunkID != "" || sample.ChunkIndex != nil) {
|
||||
return errors.New("producer diagnostic sample must not include framework chunk context")
|
||||
}
|
||||
if sample.ChunkID != "" && (!utf8.ValidString(sample.ChunkID) || strings.TrimSpace(sample.ChunkID) == "") {
|
||||
return errors.New("diagnostic sample chunk ID must be valid nonblank UTF-8 when present")
|
||||
}
|
||||
if sample.ChunkIndex != nil && *sample.ChunkIndex < 0 {
|
||||
return errors.New("diagnostic sample chunk index must not be negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDiagnosticText(value string, maximum int, name string) error {
|
||||
if !utf8.ValidString(value) {
|
||||
return fmt.Errorf("%s must be valid UTF-8", name)
|
||||
}
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fmt.Errorf("%s must not be blank", name)
|
||||
}
|
||||
if len(value) > maximum {
|
||||
return fmt.Errorf("%s exceeds maximum length", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneProducerDiagnostic(diagnostic ProducerDiagnostic) ProducerDiagnostic {
|
||||
diagnostic.Samples = cloneDiagnosticSamples(diagnostic.Samples)
|
||||
return diagnostic
|
||||
}
|
||||
|
||||
func cloneDiagnosticGroup(group DiagnosticGroup) DiagnosticGroup {
|
||||
group.Samples = cloneDiagnosticSamples(group.Samples)
|
||||
return group
|
||||
}
|
||||
|
||||
func cloneDiagnosticSamples(samples []DiagnosticSample) []DiagnosticSample {
|
||||
if len(samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]DiagnosticSample, len(samples))
|
||||
for index, sample := range samples {
|
||||
if sample.ChunkIndex != nil {
|
||||
chunkIndex := *sample.ChunkIndex
|
||||
sample.ChunkIndex = &chunkIndex
|
||||
}
|
||||
cloned[index] = sample
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
type diagnosticSampleKey struct {
|
||||
scope string
|
||||
message string
|
||||
chunkID string
|
||||
chunkIndex int
|
||||
hasChunkIndex bool
|
||||
}
|
||||
|
||||
func diagnosticSampleKeyFromSample(sample DiagnosticSample) diagnosticSampleKey {
|
||||
key := diagnosticSampleKey{scope: sample.Scope, message: sample.Message, chunkID: sample.ChunkID}
|
||||
if sample.ChunkIndex != nil {
|
||||
key.chunkIndex = *sample.ChunkIndex
|
||||
key.hasChunkIndex = true
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
type diagnosticGroupKey struct {
|
||||
disposition DiagnosticDisposition
|
||||
category DiagnosticCategory
|
||||
reasonCode string
|
||||
origin DiagnosticOrigin
|
||||
}
|
||||
|
||||
func diagnosticGroupKeyFromGroup(group DiagnosticGroup) diagnosticGroupKey {
|
||||
return diagnosticGroupKey{disposition: group.Disposition, category: group.Category, reasonCode: group.ReasonCode, origin: group.Origin}
|
||||
}
|
||||
139
internal/framework/contracts/diagnostics_test.go
Normal file
139
internal/framework/contracts/diagnostics_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProducerDiagnosticValidationAcceptsClassificationMatrix(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
disposition DiagnosticDisposition
|
||||
category DiagnosticCategory
|
||||
}{
|
||||
{name: "configuration warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryConfiguration},
|
||||
{name: "degradation warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryDegradation},
|
||||
{name: "incomplete validation warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryValidationIncomplete},
|
||||
{name: "fallback warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryFallback},
|
||||
{name: "quality advisory", disposition: DiagnosticDispositionAdvisory, category: DiagnosticCategoryDataQuality},
|
||||
{name: "normalization observation", disposition: DiagnosticDispositionObservation, category: DiagnosticCategoryNormalization},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
diagnostic := validProducerDiagnostic()
|
||||
diagnostic.Disposition = test.disposition
|
||||
diagnostic.Category = test.category
|
||||
if err := diagnostic.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProducerDiagnosticValidationRejectsInvalidFieldsAndCounts(t *testing.T) {
|
||||
tooLongReason := strings.Repeat("r", MaxDiagnosticReasonCodeBytes+1)
|
||||
tooLongScope := strings.Repeat("s", MaxDiagnosticScopeBytes+1)
|
||||
tooLongMessage := strings.Repeat("m", MaxDiagnosticMessageBytes+1)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
mutate func(*ProducerDiagnostic)
|
||||
}{
|
||||
{name: "invalid classification", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Category = DiagnosticCategoryDataQuality }},
|
||||
{name: "blank reason", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = " \t" }},
|
||||
{name: "invalid reason UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = string([]byte{0xff}) }},
|
||||
{name: "oversized reason", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = tooLongReason }},
|
||||
{name: "blank scope", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = "\n" }},
|
||||
{name: "invalid scope UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = string([]byte{0xff}) }},
|
||||
{name: "oversized scope", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = tooLongScope }},
|
||||
{name: "blank message", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = " " }},
|
||||
{name: "invalid message UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = string([]byte{0xff}) }},
|
||||
{name: "oversized message", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = tooLongMessage }},
|
||||
{name: "zero occurrences", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.OccurrenceCount = 0 }},
|
||||
{name: "missing samples", mutate: func(diagnostic *ProducerDiagnostic) {
|
||||
diagnostic.Samples = nil
|
||||
diagnostic.OmittedSampleCount = diagnostic.OccurrenceCount
|
||||
}},
|
||||
{name: "too many samples", mutate: func(diagnostic *ProducerDiagnostic) {
|
||||
diagnostic.OccurrenceCount = 4
|
||||
diagnostic.Samples = []DiagnosticSample{{Scope: "one", Message: "one"}, {Scope: "two", Message: "two"}, {Scope: "three", Message: "three"}, {Scope: "four", Message: "four"}}
|
||||
diagnostic.OmittedSampleCount = 0
|
||||
}},
|
||||
{name: "duplicate samples", mutate: func(diagnostic *ProducerDiagnostic) {
|
||||
diagnostic.OccurrenceCount = 2
|
||||
diagnostic.Samples = []DiagnosticSample{{Scope: "scope", Message: "message"}, {Scope: "scope", Message: "message"}}
|
||||
diagnostic.OmittedSampleCount = 0
|
||||
}},
|
||||
{name: "inconsistent omission", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.OmittedSampleCount = 1 }},
|
||||
{name: "producer chunk context", mutate: func(diagnostic *ProducerDiagnostic) { chunkIndex := 0; diagnostic.Samples[0].ChunkIndex = &chunkIndex }},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
diagnostic := validProducerDiagnostic()
|
||||
test.mutate(&diagnostic)
|
||||
if err := diagnostic.Validate(); err == nil {
|
||||
t.Fatal("Validate() error = nil, want invalid diagnostic error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProducerDiagnosticsEnforcesLocalGroupBound(t *testing.T) {
|
||||
diagnostics := make([]ProducerDiagnostic, MaxProducerDiagnosticGroups)
|
||||
for index := range diagnostics {
|
||||
diagnostics[index] = validProducerDiagnostic()
|
||||
diagnostics[index].ReasonCode = "reason-" + string(rune('a'+index))
|
||||
}
|
||||
if err := ValidateProducerDiagnostics(diagnostics); err != nil {
|
||||
t.Fatalf("ValidateProducerDiagnostics() error = %v", err)
|
||||
}
|
||||
diagnostics = append(diagnostics, validProducerDiagnostic())
|
||||
if err := ValidateProducerDiagnostics(diagnostics); err == nil {
|
||||
t.Fatal("ValidateProducerDiagnostics() error = nil, want excessive-group error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticGroupValidationPreservesChunkIndexZero(t *testing.T) {
|
||||
chunkIndex := 0
|
||||
group := DiagnosticGroup{
|
||||
Disposition: DiagnosticDispositionAdvisory,
|
||||
Category: DiagnosticCategoryDataQuality,
|
||||
ReasonCode: "unresolved",
|
||||
Origin: DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells", ValidatorKey: "dnd/spells/source-relatedness"},
|
||||
OccurrenceCount: 1,
|
||||
Samples: []DiagnosticSample{{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &chunkIndex}},
|
||||
}
|
||||
if err := group.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
collection := DiagnosticCollection{Groups: []DiagnosticGroup{group}}
|
||||
if err := collection.Validate(); err != nil {
|
||||
t.Fatalf("DiagnosticCollection.Validate() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticGroupRejectsRepeatedSampleWithEqualChunkIndex(t *testing.T) {
|
||||
firstIndex := 0
|
||||
secondIndex := 0
|
||||
group := DiagnosticGroup{
|
||||
Disposition: DiagnosticDispositionAdvisory,
|
||||
Category: DiagnosticCategoryDataQuality,
|
||||
ReasonCode: "unresolved",
|
||||
Origin: DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells"},
|
||||
OccurrenceCount: 2,
|
||||
Samples: []DiagnosticSample{
|
||||
{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &firstIndex},
|
||||
{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &secondIndex},
|
||||
},
|
||||
}
|
||||
if err := group.Validate(); err == nil {
|
||||
t.Fatal("Validate() error = nil, want duplicate sample error")
|
||||
}
|
||||
}
|
||||
|
||||
func validProducerDiagnostic() ProducerDiagnostic {
|
||||
return ProducerDiagnostic{
|
||||
Disposition: DiagnosticDispositionWarning,
|
||||
Category: DiagnosticCategoryConfiguration,
|
||||
ReasonCode: "empty_reference",
|
||||
OccurrenceCount: 1,
|
||||
Samples: []DiagnosticSample{{Scope: "references.glossary", Message: "Reference is empty"}},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user