package source import ( "encoding/json" "strings" "testing" ) func TestValidateDocumentValid(t *testing.T) { doc := validDocument() if err := ValidateDocument(doc); err != nil { t.Fatalf("ValidateDocument() error = %v, want nil", err) } } func TestValidateDocumentNil(t *testing.T) { err := ValidateDocument(nil) requireErrorFragments(t, err, "source document", "nil") } func TestValidateDocumentMissingFields(t *testing.T) { tests := []struct { name string mutate func(*SourceDocument) fragments []string }{ { name: "id", mutate: func(doc *SourceDocument) { doc.ID = " \t" }, fragments: []string{"source document id", "must not be empty"}, }, { name: "id surrounding whitespace", mutate: func(doc *SourceDocument) { doc.ID = " source-1 " }, fragments: []string{"source document id", "leading or trailing whitespace"}, }, { name: "kind", mutate: func(doc *SourceDocument) { doc.Kind = "" }, fragments: []string{"source document kind", "must not be empty"}, }, { name: "format", mutate: func(doc *SourceDocument) { doc.Format = "\n" }, fragments: []string{"source document format", "must not be empty"}, }, { name: "digest", mutate: func(doc *SourceDocument) { doc.Digest = "" }, fragments: []string{"source document digest", "must not be empty"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { doc := validDocument() tt.mutate(doc) err := ValidateDocument(doc) requireErrorFragments(t, err, tt.fragments...) }) } } func TestValidateDocumentEmptyUnits(t *testing.T) { doc := validDocument() doc.Units = nil err := ValidateDocument(doc) requireErrorFragments(t, err, "source document units", "must not be empty") } func TestValidateDocumentMissingUnitFields(t *testing.T) { tests := []struct { name string mutate func(*SourceDocument) fragments []string }{ { name: "id", mutate: func(doc *SourceDocument) { doc.Units[1].ID = 0 }, fragments: []string{"source unit[1].id", "must be positive"}, }, { name: "kind", mutate: func(doc *SourceDocument) { doc.Units[1].Kind = " " }, fragments: []string{"source unit[1].kind", "must not be empty"}, }, { name: "text", mutate: func(doc *SourceDocument) { doc.Units[1].Text = "\n\t" }, fragments: []string{"source unit[1].text", "must not be empty"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { doc := validDocument() tt.mutate(doc) err := ValidateDocument(doc) requireErrorFragments(t, err, tt.fragments...) }) } } func TestValidateDocumentDuplicateUnitIDs(t *testing.T) { doc := validDocument() doc.Units[1].ID = 1 err := ValidateDocument(doc) requireErrorFragments(t, err, "source unit id 1", "duplicated") } func TestValidateDocumentUnitReferences(t *testing.T) { tests := []struct { name string mutate func(*SourceDocument) fragments []string }{ { name: "nested reference context", mutate: func(doc *SourceDocument) { doc.Units[0].Ref.SourceID = "source-2" }, fragments: []string{"source unit[0].ref", "source_id", "does not match"}, }, { name: "document unit self-reference", mutate: func(doc *SourceDocument) { doc.Units[0].Ref.StartUnitID = 2 doc.Units[0].Ref.EndUnitID = 2 }, fragments: []string{"source unit[0].ref", "must identify source unit id 1"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { doc := validDocument() tt.mutate(doc) err := ValidateDocument(doc) requireErrorFragments(t, err, tt.fragments...) }) } } func TestDigestDocumentIsDeterministicAndIncludesUnitReference(t *testing.T) { doc := validDocument() doc.Metadata = map[string]any{"second": "value", "first": true} first, err := DigestDocument(doc) if err != nil { t.Fatalf("DigestDocument() error = %v, want nil", err) } reordered := validDocument() reordered.Metadata = map[string]any{"first": true, "second": "value"} second, err := DigestDocument(reordered) if err != nil { t.Fatalf("DigestDocument(reordered) error = %v, want nil", err) } if first != second { t.Fatalf("digests = %q and %q, want deterministic map ordering", first, second) } changed := validDocument() changed.Metadata = map[string]any{"first": true, "second": "value"} changed.Units[0].Ref.SourceID = "different-source" changedDigest, err := DigestDocument(changed) if err != nil { t.Fatalf("DigestDocument(changed) error = %v, want nil", err) } if first == changedDigest { t.Fatalf("digest = %q after reference change, want different digest", changedDigest) } } func TestDigestChunkIsDeterministicAndIncludesReference(t *testing.T) { doc := validDocument() chunk := Chunk{ ID: "chunk-1", SourceID: doc.ID, Index: 0, Ref: SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2}, Content: []byte("chunk content"), MediaType: "text/plain", Units: doc.Units, Metadata: map[string]any{"second": "value", "first": true}, } first, err := DigestChunk(chunk) if err != nil { t.Fatalf("DigestChunk() error = %v, want nil", err) } chunk.Metadata = map[string]any{"first": true, "second": "value"} second, err := DigestChunk(chunk) if err != nil { t.Fatalf("DigestChunk(reordered metadata) error = %v, want nil", err) } if first != second { t.Fatalf("digests = %q and %q, want deterministic map ordering", first, second) } chunk.Ref.EndUnitID = 1 changed, err := DigestChunk(chunk) if err != nil { t.Fatalf("DigestChunk(changed ref) error = %v, want nil", err) } if first == changed { t.Fatalf("digest = %q after reference change, want different digest", changed) } } func TestDigestChunkIncludesAnnotationScopes(t *testing.T) { doc := validDocument() chunk := Chunk{ ID: "chunk-1", SourceID: doc.ID, Ref: SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2}, Content: []byte("content"), MediaType: "text/plain", Units: doc.Units, Annotations: ChunkAnnotations{"scope": json.RawMessage(`{"value":1}`)}, PlanAnnotations: ChunkAnnotations{"scope": json.RawMessage(`{"value":2}`)}, } base, err := DigestChunk(chunk) if err != nil { t.Fatalf("DigestChunk() error = %v", err) } chunk.Annotations["scope"] = json.RawMessage(`{"value":3}`) rangeChanged, _ := DigestChunk(chunk) chunk.Annotations["scope"] = json.RawMessage(`{"value":1}`) chunk.PlanAnnotations["scope"] = json.RawMessage(`{"value":3}`) planChanged, _ := DigestChunk(chunk) if base == rangeChanged || base == planChanged || rangeChanged == planChanged { t.Fatalf("annotation scope digests did not change distinctly: %q %q %q", base, rangeChanged, planChanged) } } func TestValidateRefValid(t *testing.T) { doc := validDocument() ref := SourceRef{ SourceID: "source-1", StartUnitID: 1, EndUnitID: 2, } if err := ValidateRef(doc, ref); err != nil { t.Fatalf("ValidateRef() error = %v, want nil", err) } } func TestValidateRefRejectsMalformedReferences(t *testing.T) { tests := []struct { name string ref SourceRef fragments []string }{ { name: "missing source id", ref: SourceRef{StartUnitID: 1, EndUnitID: 2}, fragments: []string{"source_id", "must not be empty"}, }, { name: "source id surrounding whitespace", ref: SourceRef{SourceID: " source-1 ", StartUnitID: 1, EndUnitID: 2}, fragments: []string{"source_id", "leading or trailing whitespace"}, }, { name: "missing start id", ref: SourceRef{SourceID: "source-1", EndUnitID: 2}, fragments: []string{"start_unit_id", "must be positive"}, }, { name: "missing end id", ref: SourceRef{SourceID: "source-1", StartUnitID: 1}, fragments: []string{"end_unit_id", "must be positive"}, }, { name: "unknown start id", ref: SourceRef{SourceID: "source-1", StartUnitID: 9, EndUnitID: 2}, fragments: []string{"start_unit_id", "was not found"}, }, { name: "unknown end id", ref: SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 9}, fragments: []string{"end_unit_id", "was not found"}, }, { name: "source id mismatch", ref: SourceRef{SourceID: "source-2", StartUnitID: 1, EndUnitID: 2}, fragments: []string{"source_id", "does not match"}, }, { name: "reversed unit order", ref: SourceRef{SourceID: "source-1", StartUnitID: 2, EndUnitID: 1}, fragments: []string{"start_unit_id", "appears after"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateRef(validDocument(), tt.ref) requireErrorFragments(t, err, tt.fragments...) }) } } func TestUnitIndex(t *testing.T) { doc := validDocument() index, ok := UnitIndex(doc, 2) if !ok { t.Fatal("UnitIndex() ok = false, want true") } if index != 1 { t.Fatalf("UnitIndex() index = %d, want 1", index) } index, ok = UnitIndex(doc, 9) if ok { t.Fatal("UnitIndex() ok = true, want false") } if index != 0 { t.Fatalf("UnitIndex() index = %d, want 0", index) } } func validDocument() *SourceDocument { return &SourceDocument{ ID: "source-1", Kind: "document", Format: "text/plain", Digest: "sha256:abc123", Units: []SourceUnit{ { ID: 1, Kind: "paragraph", Text: "First unit.", Ref: SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 1}, }, { ID: 2, Kind: "paragraph", Text: "Second unit.", Ref: SourceRef{SourceID: "source-1", StartUnitID: 2, EndUnitID: 2}, }, }, } } func requireErrorFragments(t *testing.T, err error, fragments ...string) { t.Helper() if err == nil { t.Fatalf("error = nil, want fragments %q", fragments) } for _, fragment := range fragments { if !strings.Contains(err.Error(), fragment) { t.Fatalf("error = %q, want fragment %q", err.Error(), fragment) } } }