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) } if err := NewDocumentIndex(doc).ValidateRef(ref); err != nil { t.Fatalf("DocumentIndex.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) { doc := validDocument() validators := []struct { name string validate func(SourceRef) error }{ {name: "document", validate: func(ref SourceRef) error { return ValidateRef(doc, ref) }}, {name: "index", validate: NewDocumentIndex(doc).ValidateRef}, } for _, validator := range validators { t.Run(validator.name, func(t *testing.T) { requireErrorFragments(t, validator.validate(tt.ref), tt.fragments...) }) } }) } } func TestDocumentIndexSnapshotsIdentityAndUnitPositions(t *testing.T) { doc := &SourceDocument{ ID: "source-1", Units: []SourceUnit{ {ID: 30}, {ID: 10}, {ID: 30}, }, } index := NewDocumentIndex(doc) doc.ID = "changed" doc.Units[0].ID = 99 if documentID, ok := index.DocumentID(); !ok || documentID != "source-1" { t.Fatalf("DocumentID() = %q, %t, want source-1, true", documentID, ok) } if position, ok := index.Position(30); !ok || position != 0 { t.Fatalf("Position(30) = %d, %t, want 0, true", position, ok) } if position, ok := index.Position(10); !ok || position != 1 { t.Fatalf("Position(10) = %d, %t, want 1, true", position, ok) } ref := SourceRef{SourceID: "source-1", StartUnitID: 30, EndUnitID: 10} if err := index.ValidateRef(ref); err != nil { t.Fatalf("ValidateRef() error = %v, want nil", err) } } func TestZeroDocumentIndexIsSafe(t *testing.T) { var index DocumentIndex if documentID, ok := index.DocumentID(); ok || documentID != "" { t.Fatalf("DocumentID() = %q, %t, want empty, false", documentID, ok) } if position, ok := index.Position(1); ok || position != 0 { t.Fatalf("Position(1) = %d, %t, want 0, false", position, ok) } requireErrorFragments(t, index.ValidateRef(SourceRef{}), "source document must not be nil") } 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) } } }