325 lines
10 KiB
Go
325 lines
10 KiB
Go
package seriatim
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
)
|
|
|
|
func TestParseValidMinimalTranscript(t *testing.T) {
|
|
raw := readFixture(t, "testdata/valid_minimal.json")
|
|
|
|
doc, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v, want nil", err)
|
|
}
|
|
|
|
if doc.ID != "session-alpha" {
|
|
t.Fatalf("doc.ID = %q, want session-alpha", doc.ID)
|
|
}
|
|
if doc.Kind != DocumentKind {
|
|
t.Fatalf("doc.Kind = %q, want %q", doc.Kind, DocumentKind)
|
|
}
|
|
if doc.Format != Format {
|
|
t.Fatalf("doc.Format = %q, want %q", doc.Format, Format)
|
|
}
|
|
if doc.Digest != testDigest(raw) {
|
|
t.Fatalf("doc.Digest = %q, want %q", doc.Digest, testDigest(raw))
|
|
}
|
|
if got := doc.Metadata["title"]; got != "Synthetic session transcript" {
|
|
t.Fatalf("doc.Metadata[title] = %#v, want Synthetic session transcript", got)
|
|
}
|
|
if len(doc.Units) != 2 {
|
|
t.Fatalf("len(doc.Units) = %d, want 2", len(doc.Units))
|
|
}
|
|
|
|
first := doc.Units[0]
|
|
if first.ID != 1 {
|
|
t.Fatalf("first.ID = %d, want 1", first.ID)
|
|
}
|
|
if first.Kind != UnitKind {
|
|
t.Fatalf("first.Kind = %q, want %q", first.Kind, UnitKind)
|
|
}
|
|
if first.Text != "The stone door opens." {
|
|
t.Fatalf("first.Text = %q, want fixture text", first.Text)
|
|
}
|
|
if speaker, ok := Speaker(first); !ok || speaker != "Narrator" {
|
|
t.Fatalf("Speaker(first) = %q, %v; want Narrator, true", speaker, ok)
|
|
}
|
|
if start, ok := Start(first); !ok || start != json.Number("0") {
|
|
t.Fatalf("Start(first) = %q, %v; want 0, true", start, ok)
|
|
}
|
|
if end, ok := End(first); !ok || end != json.Number("4.5") {
|
|
t.Fatalf("End(first) = %q, %v; want 4.5, true", end, ok)
|
|
}
|
|
|
|
ref := source.SourceRef{
|
|
SourceID: doc.ID,
|
|
StartUnitID: doc.Units[0].ID,
|
|
EndUnitID: doc.Units[len(doc.Units)-1].ID,
|
|
}
|
|
if err := source.ValidateRef(doc, ref); err != nil {
|
|
t.Fatalf("ValidateRef() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestParseAcceptsNumericSegmentIDs(t *testing.T) {
|
|
raw := []byte(`{"metadata":{"application":"seriatim","version":"v1.5.0","output_schema":"seriatim-intermediate"},"segments":[{"id":1,"start":451.821,"end":469.685,"speaker":"Narrator","text":"The stone door opens.","categories":["scene"]},{"id":2,"start":470,"end":471,"speaker":"Player","text":"I step inside."}]}`)
|
|
|
|
doc, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v, want nil", err)
|
|
}
|
|
if got, want := doc.Metadata["output_schema"], "seriatim-intermediate"; got != want {
|
|
t.Fatalf("doc.Metadata[output_schema] = %#v, want %q", got, want)
|
|
}
|
|
if doc.Format != Format {
|
|
t.Fatalf("doc.Format = %q, want %q", doc.Format, Format)
|
|
}
|
|
if len(doc.Units) != 2 {
|
|
t.Fatalf("len(doc.Units) = %d, want 2", len(doc.Units))
|
|
}
|
|
if doc.Units[0].ID != 1 || doc.Units[1].ID != 2 {
|
|
t.Fatalf("unit IDs = %#v, want numeric IDs", []int{doc.Units[0].ID, doc.Units[1].ID})
|
|
}
|
|
ref := source.SourceRef{
|
|
SourceID: doc.ID,
|
|
StartUnitID: 1,
|
|
EndUnitID: 2,
|
|
}
|
|
if err := source.ValidateRef(doc, ref); err != nil {
|
|
t.Fatalf("ValidateRef() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestParseRequestSourceIDOverridesMetadataIDs(t *testing.T) {
|
|
raw := readFixture(t, "testdata/valid_minimal.json")
|
|
|
|
doc, err := New().Parse(context.Background(), contracts.ParseRequest{
|
|
SourceID: " requested-source ",
|
|
Raw: raw,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v, want nil", err)
|
|
}
|
|
if doc.ID != "requested-source" {
|
|
t.Fatalf("doc.ID = %q, want requested-source", doc.ID)
|
|
}
|
|
}
|
|
|
|
func TestParseFallbackDocumentIDIsDeterministic(t *testing.T) {
|
|
raw := []byte(`{"metadata":{},"segments":[{"id":1,"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."}]}`)
|
|
|
|
first, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
|
|
if err != nil {
|
|
t.Fatalf("first Parse() error = %v, want nil", err)
|
|
}
|
|
second, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
|
|
if err != nil {
|
|
t.Fatalf("second Parse() error = %v, want nil", err)
|
|
}
|
|
|
|
if first.ID != second.ID {
|
|
t.Fatalf("fallback IDs differ: %q vs %q", first.ID, second.ID)
|
|
}
|
|
if !strings.HasPrefix(first.ID, "seriatim:") {
|
|
t.Fatalf("fallback ID = %q, want seriatim prefix", first.ID)
|
|
}
|
|
if first.ID != "seriatim:"+strings.TrimPrefix(testDigest(raw), "sha256:")[:16] {
|
|
t.Fatalf("fallback ID = %q, want digest-derived ID", first.ID)
|
|
}
|
|
}
|
|
|
|
func TestParseUsesMetadataSourceIDWhenMetadataIDIsAbsent(t *testing.T) {
|
|
raw := []byte(`{"metadata":{"source_id":" source-from-metadata "},"segments":[{"id":1,"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."}]}`)
|
|
|
|
doc, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v, want nil", err)
|
|
}
|
|
if doc.ID != "source-from-metadata" {
|
|
t.Fatalf("doc.ID = %q, want source-from-metadata", doc.ID)
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsInvalidInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw []byte
|
|
wantErr []string
|
|
}{
|
|
{
|
|
name: "malformed JSON",
|
|
raw: []byte(`{"metadata":`),
|
|
wantErr: []string{"seriatim input", "parse JSON"},
|
|
},
|
|
{
|
|
name: "trailing JSON",
|
|
raw: []byte(`{"metadata":{},"segments":[]} {}`),
|
|
wantErr: []string{"seriatim input", "trailing"},
|
|
},
|
|
{
|
|
name: "missing metadata",
|
|
raw: []byte(`{"segments":[]}`),
|
|
wantErr: []string{"metadata"},
|
|
},
|
|
{
|
|
name: "null metadata",
|
|
raw: []byte(`{"metadata":null,"segments":[]}`),
|
|
wantErr: []string{"metadata", "object"},
|
|
},
|
|
{
|
|
name: "metadata wrong type",
|
|
raw: []byte(`{"metadata":[],"segments":[]}`),
|
|
wantErr: []string{"metadata", "object"},
|
|
},
|
|
{
|
|
name: "missing segments",
|
|
raw: []byte(`{"metadata":{}}`),
|
|
wantErr: []string{"segments"},
|
|
},
|
|
{
|
|
name: "null segments",
|
|
raw: []byte(`{"metadata":{},"segments":null}`),
|
|
wantErr: []string{"segments", "array"},
|
|
},
|
|
{
|
|
name: "segments wrong type",
|
|
raw: []byte(`{"metadata":{},"segments":{}}`),
|
|
wantErr: []string{"segments", "array"},
|
|
},
|
|
{
|
|
name: "empty segments",
|
|
raw: []byte(`{"metadata":{},"segments":[]}`),
|
|
wantErr: []string{"segments", "empty"},
|
|
},
|
|
{
|
|
name: "missing segment id",
|
|
raw: validJSONWithSegment(`"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"id", "positive"},
|
|
},
|
|
{
|
|
name: "invalid segment id type",
|
|
raw: validJSONWithSegment(`"id":{},"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"id", "string or number"},
|
|
},
|
|
{
|
|
name: "whitespace segment id",
|
|
raw: validJSONWithSegment(`"id":" 1 ","start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"id", "whitespace"},
|
|
},
|
|
{
|
|
name: "empty text",
|
|
raw: validJSONWithSegment(`"id":1,"start":0,"end":1,"speaker":"Narrator","text":" "`),
|
|
wantErr: []string{"text", "empty"},
|
|
},
|
|
{
|
|
name: "missing speaker",
|
|
raw: validJSONWithSegment(`"id":1,"start":0,"end":1,"text":"Synthetic text."`),
|
|
wantErr: []string{"speaker", "empty"},
|
|
},
|
|
{
|
|
name: "missing start",
|
|
raw: validJSONWithSegment(`"id":1,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"start", "empty"},
|
|
},
|
|
{
|
|
name: "missing end",
|
|
raw: validJSONWithSegment(`"id":1,"start":0,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"end", "empty"},
|
|
},
|
|
{
|
|
name: "negative start",
|
|
raw: validJSONWithSegment(`"id":1,"start":-1,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"start", "negative"},
|
|
},
|
|
{
|
|
name: "non-numeric end",
|
|
raw: validJSONWithSegment(`"id":1,"start":0,"end":"late","speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"segment[0]", "end", "number"},
|
|
},
|
|
{
|
|
name: "non-finite timestamp",
|
|
raw: validJSONWithSegment(`"id":1,"start":1e10000,"end":1e10000,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"start", "valid number"},
|
|
},
|
|
{
|
|
name: "end before start",
|
|
raw: validJSONWithSegment(`"id":1,"start":2,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"end", "start"},
|
|
},
|
|
{
|
|
name: "end before start beyond float precision",
|
|
raw: validJSONWithSegment(`"id":1,"start":9007199254740993,"end":9007199254740992,"speaker":"Narrator","text":"Synthetic text."`),
|
|
wantErr: []string{"end", "start"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: tt.raw})
|
|
if err == nil {
|
|
t.Fatal("Parse() error = nil, want error")
|
|
}
|
|
for _, want := range tt.wantErr {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("Parse() error = %q, want substring %q", err.Error(), want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsDuplicateSegmentIDs(t *testing.T) {
|
|
raw := readFixture(t, "testdata/duplicate_segment_id.json")
|
|
|
|
_, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
|
|
if err == nil {
|
|
t.Fatal("Parse() error = nil, want duplicate ID error")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicated") || !strings.Contains(err.Error(), "1") {
|
|
t.Fatalf("Parse() error = %q, want duplicate segment context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsInvalidContextOrEmptyInput(t *testing.T) {
|
|
if _, err := New().Parse(nil, contracts.ParseRequest{Raw: []byte(`{}`)}); err == nil {
|
|
t.Fatal("Parse(nil context) error = nil, want error")
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := New().Parse(ctx, contracts.ParseRequest{Raw: []byte(`{}`)}); err == nil {
|
|
t.Fatal("Parse(canceled context) error = nil, want error")
|
|
}
|
|
|
|
if _, err := New().Parse(context.Background(), contracts.ParseRequest{}); err == nil {
|
|
t.Fatal("Parse(empty input) error = nil, want error")
|
|
}
|
|
}
|
|
|
|
func readFixture(t *testing.T, path string) []byte {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(%q) error = %v, want nil", path, err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func validJSONWithSegment(segmentFields string) []byte {
|
|
return []byte(`{"metadata":{"id":"fixture"},"segments":[{` + segmentFields + `}]}`)
|
|
}
|
|
|
|
func testDigest(raw []byte) string {
|
|
sum := sha256.Sum256(raw)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|