208 lines
7.8 KiB
Go
208 lines
7.8 KiB
Go
package evidencecontext
|
|
|
|
import (
|
|
"bytes"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
|
)
|
|
|
|
func TestBuildSelectsExpandedSourceUnitUnion(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
window int
|
|
refs []source.SourceRef
|
|
wantIDs []int
|
|
}{
|
|
{name: "zero window", refs: []source.SourceRef{ref(3, 3)}, wantIDs: []int{3}},
|
|
{name: "non monotonic IDs use document positions", window: 1, refs: []source.SourceRef{ref(3, 3)}, wantIDs: []int{10, 3, 30}},
|
|
{name: "boundary clamping", window: 1, refs: []source.SourceRef{ref(10, 10), ref(50, 50)}, wantIDs: []int{10, 3, 7, 50}},
|
|
{name: "overlapping and adjacent windows merge", window: 1, refs: []source.SourceRef{ref(3, 3), ref(30, 30), ref(30, 30)}, wantIDs: []int{10, 3, 30, 7}},
|
|
{name: "adjacent expanded ranges merge", window: 1, refs: []source.SourceRef{ref(10, 10), ref(7, 7)}, wantIDs: []int{10, 3, 30, 7, 50}},
|
|
{name: "largest window clips without overflow", window: math.MaxInt, refs: []source.SourceRef{ref(30, 30)}, wantIDs: []int{10, 3, 30, 7, 50}},
|
|
{name: "no references returns an initialized empty document", wantIDs: []int{}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := Build(BuildRequest{Source: testDocument(t), WindowUnits: test.window, SourceRefs: test.refs})
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Build() returned a nil document")
|
|
}
|
|
if actual := unitIDs(got); !reflect.DeepEqual(actual, test.wantIDs) {
|
|
t.Fatalf("unit IDs = %#v, want %#v", actual, test.wantIDs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildCopiesSelectedUnitsAndMetadata(t *testing.T) {
|
|
document := testDocument(t)
|
|
first, err := Build(BuildRequest{Source: document, WindowUnits: 1, SourceRefs: []source.SourceRef{ref(3, 3)}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := Build(BuildRequest{Source: document, WindowUnits: 1, SourceRefs: []source.SourceRef{ref(3, 3)}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(first[0], document.Units[0]) {
|
|
t.Fatalf("first unit = %#v, want unchanged source unit %#v", first[0], document.Units[0])
|
|
}
|
|
first[0].Metadata["nested"].(map[string]any)["value"] = "changed"
|
|
if document.Units[0].Metadata["nested"].(map[string]any)["value"] != "original" {
|
|
t.Fatal("Build() returned metadata aliases to the source document")
|
|
}
|
|
document.Units[0].Metadata["nested"].(map[string]any)["value"] = "later"
|
|
if second[0].Metadata["nested"].(map[string]any)["value"] != "original" {
|
|
t.Fatal("Build() retained metadata aliases to the source document")
|
|
}
|
|
}
|
|
|
|
func TestBuildRejectsInvalidInputs(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func(*BuildRequest)
|
|
want string
|
|
}{
|
|
{name: "negative window", mutate: func(request *BuildRequest) { request.WindowUnits = -1 }, want: "window_units"},
|
|
{name: "source digest mismatch", mutate: func(request *BuildRequest) { request.Source.Digest = "sha256:" + strings.Repeat("0", 64) }, want: "does not match"},
|
|
{name: "invalid reference", mutate: func(request *BuildRequest) { request.SourceRefs = []source.SourceRef{ref(99, 99)} }, want: "source reference[0]"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
request := BuildRequest{Source: testDocument(t), SourceRefs: []source.SourceRef{ref(3, 3)}}
|
|
test.mutate(&request)
|
|
if _, err := Build(request); err == nil || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("Build() error = %v, want %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCodecRoundTripsFixtureAndOwnsValues(t *testing.T) {
|
|
fixture, err := os.ReadFile("testdata/source_evidence_context.v1.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
codec := New()
|
|
value, err := codec.Decode(fixture)
|
|
if err != nil {
|
|
t.Fatalf("Decode(fixture) error = %v", err)
|
|
}
|
|
encoded, err := codec.Encode(value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(encoded, bytes.TrimSpace(fixture)) {
|
|
t.Fatalf("fixture does not use canonical encoding\nwant: %s\n got: %s", fixture, encoded)
|
|
}
|
|
value[0].Text = "changed"
|
|
decoded, err := codec.Decode(encoded)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decoded[0].Text != "The party meets Rowan." {
|
|
t.Fatal("Encode() retained mutable document storage")
|
|
}
|
|
|
|
built, err := Build(BuildRequest{Source: testDocument(t), WindowUnits: 1, SourceRefs: []source.SourceRef{ref(3, 3)}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, err := codec.Encode(built)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := codec.Decode(content)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := codec.Decode(content)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first[0].Metadata["nested"].(map[string]any)["value"] = "changed"
|
|
if second[0].Metadata["nested"].(map[string]any)["value"] != "original" {
|
|
t.Fatal("Decode() returned metadata aliases")
|
|
}
|
|
}
|
|
|
|
func TestCodecRejectsInvalidDurablePayloads(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
content string
|
|
}{
|
|
{name: "null", content: "null"},
|
|
{name: "wrapper object", content: `{"units":[]}`},
|
|
{name: "missing required unit field", content: `[{"id":1,"kind":"segment","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1}}]`},
|
|
{name: "unknown unit field", content: `[{"id":1,"kind":"segment","text":"text","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1},"unknown":true}]`},
|
|
{name: "unknown reference field", content: `[{"id":1,"kind":"segment","text":"text","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1,"unknown":true}}]`},
|
|
{name: "invalid self reference", content: `[{"id":1,"kind":"segment","text":"text","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":2}}]`},
|
|
{name: "duplicate units", content: `[{"id":1,"kind":"segment","text":"one","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1}},{"id":1,"kind":"segment","text":"two","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1}}]`},
|
|
{name: "multiple JSON values", content: `[] []`},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, err := New().Decode([]byte(test.content)); err == nil {
|
|
t.Fatal("Decode() error = nil, want strict payload rejection")
|
|
}
|
|
})
|
|
}
|
|
if _, err := New().Encode(nil); err == nil {
|
|
t.Fatal("Encode(nil) error = nil, want array rejection")
|
|
}
|
|
}
|
|
|
|
func TestSerializeUsesFixedArtifactIdentityAndEmptyArray(t *testing.T) {
|
|
artifact, err := Serialize(BuildRequest{Source: testDocument(t)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if artifact.Kind != ArtifactKind || artifact.MediaType != MediaType || artifact.Schema.ID != SchemaID || artifact.Schema.Name != SchemaName || artifact.Schema.Version != SchemaVersion {
|
|
t.Fatalf("Serialize() = %#v, want fixed artifact identity", artifact)
|
|
}
|
|
if string(artifact.Content) != "[]" {
|
|
t.Fatalf("Serialize() content = %s, want []", artifact.Content)
|
|
}
|
|
decoded, err := New().Decode(artifact.Content)
|
|
if err != nil || decoded == nil || len(decoded) != 0 {
|
|
t.Fatalf("Decode(Serialize()) = %#v, %v; want explicit empty array", decoded, err)
|
|
}
|
|
}
|
|
|
|
func testDocument(t *testing.T) *source.SourceDocument {
|
|
t.Helper()
|
|
document := &source.SourceDocument{
|
|
ID: "session", Kind: "transcript", Format: "application/json",
|
|
Units: []source.SourceUnit{
|
|
{ID: 10, Kind: "segment", Text: "first", Ref: ref(10, 10), Metadata: map[string]any{"nested": map[string]any{"value": "original"}}},
|
|
{ID: 3, Kind: "segment", Text: "second", Ref: ref(3, 3)},
|
|
{ID: 30, Kind: "segment", Text: "third", Ref: ref(30, 30)},
|
|
{ID: 7, Kind: "segment", Text: "fourth", Ref: ref(7, 7)},
|
|
{ID: 50, Kind: "segment", Text: "fifth", Ref: ref(50, 50)},
|
|
},
|
|
}
|
|
digest, err := source.DigestDocument(document)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
document.Digest = digest
|
|
return document
|
|
}
|
|
|
|
func ref(start, end int) source.SourceRef {
|
|
return source.SourceRef{SourceID: "session", StartUnitID: start, EndUnitID: end}
|
|
}
|
|
|
|
func unitIDs(units Document) []int {
|
|
values := make([]int, len(units))
|
|
for index, unit := range units {
|
|
values[index] = unit.ID
|
|
}
|
|
return values
|
|
}
|