2026-07-17 04:57:47 +00:00
package transcript
2026-07-03 21:13:12 +00:00
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 )
}
2026-07-17 05:19:57 +00:00
if doc . Digest == testDigest ( raw ) || ! strings . HasPrefix ( doc . Digest , "sha256:" ) {
t . Fatalf ( "doc.Digest = %q, want canonical source digest distinct from raw input digest" , doc . Digest )
2026-07-03 21:13:12 +00:00
}
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 ) )
}
2026-07-17 05:19:57 +00:00
for i , unit := range doc . Units {
want := source . SourceRef { SourceID : doc . ID , StartUnitID : unit . ID , EndUnitID : unit . ID }
if unit . Ref != want {
t . Fatalf ( "doc.Units[%d].Ref = %#v, want %#v" , i , unit . Ref , want )
}
}
2026-07-03 21:13:12 +00:00
first := doc . Units [ 0 ]
2026-07-07 18:34:23 +00:00
if first . ID != 1 {
t . Fatalf ( "first.ID = %d, want 1" , first . ID )
2026-07-03 21:13:12 +00:00
}
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 )
}
}
2026-07-03 22:27:54 -05:00
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 ) )
}
2026-07-07 18:34:23 +00:00
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 } )
2026-07-03 22:27:54 -05:00
}
2026-07-17 05:19:57 +00:00
for i , unit := range doc . Units {
want := source . SourceRef { SourceID : doc . ID , StartUnitID : unit . ID , EndUnitID : unit . ID }
if unit . Ref != want {
t . Fatalf ( "doc.Units[%d].Ref = %#v, want %#v" , i , unit . Ref , want )
}
}
2026-07-03 22:27:54 -05:00
ref := source . SourceRef {
SourceID : doc . ID ,
2026-07-07 18:34:23 +00:00
StartUnitID : 1 ,
EndUnitID : 2 ,
2026-07-03 22:27:54 -05:00
}
if err := source . ValidateRef ( doc , ref ) ; err != nil {
t . Fatalf ( "ValidateRef() error = %v, want nil" , err )
}
}
2026-07-03 21:13:12 +00:00
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 ) {
2026-07-07 18:34:23 +00:00
raw := [ ] byte ( ` { "metadata": { },"segments":[ { "id":1,"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."}]} ` )
2026-07-03 21:13:12 +00:00
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 ) {
2026-07-07 18:34:23 +00:00
raw := [ ] byte ( ` { "metadata": { "source_id":" source-from-metadata "},"segments":[ { "id":1,"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."}]} ` )
2026-07-03 21:13:12 +00:00
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." ` ) ,
2026-07-07 18:34:23 +00:00
wantErr : [ ] string { "id" , "positive" } ,
2026-07-03 21:13:12 +00:00
} ,
2026-07-03 22:27:54 -05:00
{
name : "invalid segment id type" ,
raw : validJSONWithSegment ( ` "id": { },"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text." ` ) ,
wantErr : [ ] string { "id" , "string or number" } ,
} ,
2026-07-03 21:13:12 +00:00
{
name : "whitespace segment id" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":" 1 ","start":0,"end":1,"speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "id" , "whitespace" } ,
} ,
{
name : "empty text" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":0,"end":1,"speaker":"Narrator","text":" " ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "text" , "empty" } ,
} ,
{
name : "missing speaker" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":0,"end":1,"text":"Synthetic text." ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "speaker" , "empty" } ,
} ,
{
name : "missing start" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"end":1,"speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "start" , "empty" } ,
} ,
{
name : "missing end" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":0,"speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "end" , "empty" } ,
} ,
{
name : "negative start" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":-1,"end":1,"speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "start" , "negative" } ,
} ,
{
name : "non-numeric end" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":0,"end":"late","speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 23:14:33 +00:00
wantErr : [ ] string { "segment[0]" , "end" , "number" } ,
2026-07-03 21:13:12 +00:00
} ,
{
name : "non-finite timestamp" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":1e10000,"end":1e10000,"speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "start" , "valid number" } ,
} ,
{
name : "end before start" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":2,"end":1,"speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 21:13:12 +00:00
wantErr : [ ] string { "end" , "start" } ,
} ,
2026-07-03 23:14:33 +00:00
{
name : "end before start beyond float precision" ,
2026-07-07 18:34:23 +00:00
raw : validJSONWithSegment ( ` "id":1,"start":9007199254740993,"end":9007199254740992,"speaker":"Narrator","text":"Synthetic text." ` ) ,
2026-07-03 23:14:33 +00:00
wantErr : [ ] string { "end" , "start" } ,
} ,
2026-07-03 21:13:12 +00:00
}
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" )
}
2026-07-07 18:34:23 +00:00
if ! strings . Contains ( err . Error ( ) , "duplicated" ) || ! strings . Contains ( err . Error ( ) , "1" ) {
2026-07-03 21:13:12 +00:00
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 [ : ] )
}