All checks were successful
ci/woodpecker/tag/release Pipeline was successful
458 lines
14 KiB
Go
458 lines
14 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/seriatim/internal/config"
|
|
"gitea.maximumdirect.net/eric/seriatim/internal/report"
|
|
"gitea.maximumdirect.net/eric/seriatim/schema"
|
|
)
|
|
|
|
func TestNormalizeCommandIsRecognized(t *testing.T) {
|
|
cmd := NewRootCommand()
|
|
cmd.SetArgs([]string{"normalize", "--help"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("normalize command should be recognized: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMissingInputFileFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--output-file", output,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected missing input-file error")
|
|
}
|
|
if !strings.Contains(err.Error(), "--input-file is required") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMissingOutputFileFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected missing output-file error")
|
|
}
|
|
if !strings.Contains(err.Error(), "--output-file is required") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeInvalidOutputSchemaFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--output-schema", "compact",
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected invalid output schema error")
|
|
}
|
|
if !strings.Contains(err.Error(), "--output-schema must be one of") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeInvalidOutputModuleFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--output-modules", "yaml",
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected invalid output module error")
|
|
}
|
|
if !strings.Contains(err.Error(), "unknown output module") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDefaultOutputSchemaIsIntermediate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{
|
|
"segments": [
|
|
{"id": 99, "start": 5, "end": 6, "speaker": "Bob", "text": "second", "categories": ["filler"]},
|
|
{"id": 10, "start": 1, "end": 2, "speaker": "Alice", "text": "first", "categories": ["backchannel"]}
|
|
]
|
|
}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var transcript schema.IntermediateTranscript
|
|
readJSON(t, output, &transcript)
|
|
if transcript.Metadata.OutputSchema != config.OutputSchemaIntermediate {
|
|
t.Fatalf("output schema = %q, want %q", transcript.Metadata.OutputSchema, config.OutputSchemaIntermediate)
|
|
}
|
|
if len(transcript.Segments) != 2 {
|
|
t.Fatalf("segment count = %d, want 2", len(transcript.Segments))
|
|
}
|
|
if transcript.Segments[0].ID != 1 || transcript.Segments[1].ID != 2 {
|
|
t.Fatalf("segment IDs = %d,%d, want 1,2", transcript.Segments[0].ID, transcript.Segments[1].ID)
|
|
}
|
|
if transcript.Segments[0].Text != "first" || transcript.Segments[1].Text != "second" {
|
|
t.Fatalf("unexpected sort order: %#v", transcript.Segments)
|
|
}
|
|
if len(transcript.Segments[0].Categories) != 1 || transcript.Segments[0].Categories[0] != "backchannel" {
|
|
t.Fatalf("expected categories preserved on first segment, got %#v", transcript.Segments[0].Categories)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeBareArrayInputToIntermediateOutput(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `[
|
|
{"start": 2, "end": 3, "speaker": "Bob", "text": "second"},
|
|
{"start": 1, "end": 2, "speaker": "Alice", "text": "first"}
|
|
]`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--output-schema", config.OutputSchemaIntermediate,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var transcript schema.IntermediateTranscript
|
|
readJSON(t, output, &transcript)
|
|
if len(transcript.Segments) != 2 {
|
|
t.Fatalf("segment count = %d, want 2", len(transcript.Segments))
|
|
}
|
|
if transcript.Segments[0].Speaker != "Alice" || transcript.Segments[1].Speaker != "Bob" {
|
|
t.Fatalf("unexpected sorted speakers: %#v", transcript.Segments)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeInputIndexTieBreakerIsDeterministic(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `[
|
|
{"start": 1, "end": 2, "speaker": "Zulu", "text": "first in"},
|
|
{"start": 1, "end": 2, "speaker": "Alpha", "text": "second in"}
|
|
]`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var transcript schema.IntermediateTranscript
|
|
readJSON(t, output, &transcript)
|
|
if transcript.Segments[0].Speaker != "Zulu" || transcript.Segments[1].Speaker != "Alpha" {
|
|
t.Fatalf("tie-break order mismatch: %#v", transcript.Segments)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMinimalSchemaOmitsCategories(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{
|
|
"segments": [
|
|
{"start": 1, "end": 2, "speaker": "Alice", "text": "first", "categories": ["filler"]}
|
|
]
|
|
}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--output-schema", config.OutputSchemaMinimal,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var transcript schema.MinimalTranscript
|
|
readJSON(t, output, &transcript)
|
|
if transcript.Metadata.OutputSchema != config.OutputSchemaMinimal {
|
|
t.Fatalf("output schema = %q, want %q", transcript.Metadata.OutputSchema, config.OutputSchemaMinimal)
|
|
}
|
|
if len(transcript.Segments) != 1 || transcript.Segments[0].ID != 1 {
|
|
t.Fatalf("unexpected minimal output: %#v", transcript.Segments)
|
|
}
|
|
bytes, readErr := os.ReadFile(output)
|
|
if readErr != nil {
|
|
t.Fatalf("read output: %v", readErr)
|
|
}
|
|
if strings.Contains(string(bytes), "categories") {
|
|
t.Fatalf("minimal output unexpectedly contains categories:\n%s", string(bytes))
|
|
}
|
|
}
|
|
|
|
func TestNormalizeFullSchemaOutputValidatesAndHasProvenanceFallback(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `[
|
|
{"start": 1, "end": 2, "speaker": "Alice", "text": "first"},
|
|
{"start": 3, "end": 4, "speaker": "Bob", "text": "second", "source":"custom.json", "source_segment_index": 7}
|
|
]`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--output-schema", config.OutputSchemaFull,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var transcript schema.Transcript
|
|
readJSON(t, output, &transcript)
|
|
if err := schema.ValidateTranscript(transcript); err != nil {
|
|
t.Fatalf("full output should validate: %v", err)
|
|
}
|
|
if len(transcript.Segments) != 2 {
|
|
t.Fatalf("segment count = %d, want 2", len(transcript.Segments))
|
|
}
|
|
if transcript.Segments[0].Source != filepath.Base(input) {
|
|
t.Fatalf("source fallback = %q, want %q", transcript.Segments[0].Source, filepath.Base(input))
|
|
}
|
|
if transcript.Segments[0].SourceSegmentIndex == nil || *transcript.Segments[0].SourceSegmentIndex != 0 {
|
|
t.Fatalf("source_segment_index fallback = %v, want 0", transcript.Segments[0].SourceSegmentIndex)
|
|
}
|
|
if transcript.Segments[1].Source != "custom.json" {
|
|
t.Fatalf("explicit source preserved = %q, want custom.json", transcript.Segments[1].Source)
|
|
}
|
|
if transcript.Segments[1].SourceSegmentIndex == nil || *transcript.Segments[1].SourceSegmentIndex != 7 {
|
|
t.Fatalf("explicit source_segment_index preserved = %v, want 7", transcript.Segments[1].SourceSegmentIndex)
|
|
}
|
|
if transcript.OverlapGroups == nil || len(transcript.OverlapGroups) != 0 {
|
|
t.Fatalf("overlap_groups = %#v, want empty array", transcript.OverlapGroups)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeEmptySegmentsArrayProducesValidOutput(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var transcript schema.IntermediateTranscript
|
|
readJSON(t, output, &transcript)
|
|
if len(transcript.Segments) != 0 {
|
|
t.Fatalf("segment count = %d, want 0", len(transcript.Segments))
|
|
}
|
|
if err := schema.ValidateIntermediateTranscript(transcript); err != nil {
|
|
t.Fatalf("intermediate output should validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSelectedOutputSchemaIsHonored(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[{"start":1,"end":2,"speaker":"A","text":"one"}]}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--output-schema", config.OutputSchemaMinimal,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var transcript schema.MinimalTranscript
|
|
readJSON(t, output, &transcript)
|
|
if transcript.Metadata.OutputSchema != config.OutputSchemaMinimal {
|
|
t.Fatalf("output schema = %q, want %q", transcript.Metadata.OutputSchema, config.OutputSchemaMinimal)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeReportFileWrittenAndContainsObjectInputShape(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[{"start":1,"end":2,"speaker":"A","text":"one"}]}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
reportPath := filepath.Join(dir, "report.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--report-file", reportPath,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var rpt report.Report
|
|
readJSON(t, reportPath, &rpt)
|
|
audit := extractNormalizeAudit(t, rpt)
|
|
if audit.InputShape != "object_with_segments" {
|
|
t.Fatalf("input shape = %q, want object_with_segments", audit.InputShape)
|
|
}
|
|
if audit.InputSegmentCount != 1 {
|
|
t.Fatalf("input segment count = %d, want 1", audit.InputSegmentCount)
|
|
}
|
|
if audit.OutputSchema != config.OutputSchemaIntermediate {
|
|
t.Fatalf("output schema = %q, want %q", audit.OutputSchema, config.OutputSchemaIntermediate)
|
|
}
|
|
if len(audit.OutputModules) != 1 || audit.OutputModules[0] != "json" {
|
|
t.Fatalf("output modules = %v, want [json]", audit.OutputModules)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeReportIncludesBareArrayShape(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `[{"start":1,"end":2,"speaker":"A","text":"one"}]`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
reportPath := filepath.Join(dir, "report.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--report-file", reportPath,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var rpt report.Report
|
|
readJSON(t, reportPath, &rpt)
|
|
audit := extractNormalizeAudit(t, rpt)
|
|
if audit.InputShape != "bare_segments_array" {
|
|
t.Fatalf("input shape = %q, want bare_segments_array", audit.InputShape)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeReportDoesNotIncludeTranscriptText(t *testing.T) {
|
|
dir := t.TempDir()
|
|
const segmentText = "normalize-report-secret-text"
|
|
input := writeJSONFile(t, dir, "input.json", `[{"start":1,"end":2,"speaker":"A","text":"`+segmentText+`"}]`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
reportPath := filepath.Join(dir, "report.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--report-file", reportPath,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var rpt report.Report
|
|
readJSON(t, reportPath, &rpt)
|
|
for _, event := range rpt.Events {
|
|
if strings.Contains(event.Message, segmentText) {
|
|
t.Fatalf("report unexpectedly contained transcript text in event %#v", event)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeReportEmptyInputEmitsWarning(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
reportPath := filepath.Join(dir, "report.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--report-file", reportPath,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("normalize failed: %v", err)
|
|
}
|
|
|
|
var rpt report.Report
|
|
readJSON(t, reportPath, &rpt)
|
|
found := false
|
|
for _, event := range rpt.Events {
|
|
if event.Stage == "normalize" && event.Module == "normalize" && event.Severity == report.SeverityWarning &&
|
|
strings.Contains(event.Message, "zero segments") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected empty transcript warning event, got %#v", rpt.Events)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeReportWriteFailureReturnsClearError(t *testing.T) {
|
|
dir := t.TempDir()
|
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[{"start":1,"end":2,"speaker":"A","text":"one"}]}`)
|
|
output := filepath.Join(dir, "normalized.json")
|
|
|
|
err := executeNormalize(
|
|
"--input-file", input,
|
|
"--output-file", output,
|
|
"--report-file", dir,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected report write failure")
|
|
}
|
|
if !strings.Contains(err.Error(), "write --report-file") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func executeNormalize(args ...string) error {
|
|
cmd := NewRootCommand()
|
|
cmd.SetArgs(append([]string{"normalize"}, args...))
|
|
return cmd.Execute()
|
|
}
|
|
|
|
type normalizeAudit struct {
|
|
Command string `json:"command"`
|
|
InputFile string `json:"input_file"`
|
|
OutputFile string `json:"output_file"`
|
|
InputShape string `json:"input_shape"`
|
|
InputSegmentCount int `json:"input_segment_count"`
|
|
OutputSchema string `json:"output_schema"`
|
|
OutputModules []string `json:"output_modules"`
|
|
IDsReassigned bool `json:"ids_reassigned"`
|
|
SortingChangedInput bool `json:"sorting_changed_input_order"`
|
|
SegmentsWithCategories int `json:"segments_with_categories"`
|
|
}
|
|
|
|
func extractNormalizeAudit(t *testing.T, rpt report.Report) normalizeAudit {
|
|
t.Helper()
|
|
for _, event := range rpt.Events {
|
|
if event.Stage == "normalize" && event.Module == "normalize-audit" {
|
|
var audit normalizeAudit
|
|
if err := json.Unmarshal([]byte(event.Message), &audit); err != nil {
|
|
t.Fatalf("decode normalize audit: %v", err)
|
|
}
|
|
return audit
|
|
}
|
|
}
|
|
t.Fatalf("missing normalize-audit event: %#v", rpt.Events)
|
|
return normalizeAudit{}
|
|
}
|