2026-05-02 10:37:38 -05:00
package app
import (
"bytes"
2026-05-02 10:51:23 -05:00
"context"
2026-05-02 10:43:45 -05:00
"os"
"path/filepath"
2026-05-02 10:37:38 -05:00
"strings"
"testing"
2026-05-02 10:51:23 -05:00
"time"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
2026-05-02 10:37:38 -05:00
)
func TestExecuteValidCommands ( t * testing . T ) {
2026-05-02 10:56:42 -05:00
workspaceRoot := t . TempDir ( )
pipelinePath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
2026-05-02 10:51:23 -05:00
manifestPath := writeManifestPathForExecute ( t )
2026-05-02 10:43:45 -05:00
2026-05-02 10:37:38 -05:00
cases := [ ] struct {
name string
args [ ] string
wantOut string
} {
2026-05-02 11:38:42 -05:00
{ name : "run" , args : [ ] string { "run" , "--config" , pipelinePath , "--session" , sessionPath } , wantOut : "narratio run: session 2026-05-03; executed=8 skipped=0; manifest=" } ,
{ name : "plan" , args : [ ] string { "plan" , "--config" , pipelinePath , "--session" , sessionPath } , wantOut : "prepare: skip\ntranscribe: skip\nnormalize: skip\nmerge: skip\npolish: skip\nanalyze: skip\narchive: skip\nnotify: skip" } ,
2026-05-02 10:51:23 -05:00
{ name : "status" , args : [ ] string { "status" , "--manifest" , manifestPath } , wantOut : "session_id: 2026-05-03" } ,
2026-05-02 11:38:42 -05:00
{ name : "resume" , args : [ ] string { "resume" , "--config" , pipelinePath , "--session" , sessionPath } , wantOut : "narratio resume: session 2026-05-03 has no remaining stages" } ,
{ name : "run-stage" , args : [ ] string { "run-stage" , "--config" , pipelinePath , "--session" , sessionPath , "polish" } , wantOut : "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest=" } ,
2026-05-02 10:37:38 -05:00
}
for _ , tc := range cases {
t . Run ( tc . name , func ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( tc . args , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "exit code = %d, want 0" , code )
}
if stderr . Len ( ) != 0 {
t . Fatalf ( "stderr = %q, want empty" , stderr . String ( ) )
}
if ! strings . Contains ( stdout . String ( ) , tc . wantOut ) {
t . Fatalf ( "stdout = %q, want to contain %q" , stdout . String ( ) , tc . wantOut )
}
} )
}
}
2026-05-02 10:51:23 -05:00
func TestExecuteMissingRequiredFlags ( t * testing . T ) {
2026-05-02 10:43:45 -05:00
cases := [ ] struct {
name string
args [ ] string
want string
} {
{ name : "run missing flags" , args : [ ] string { "run" } , want : "run: --config and --session are required" } ,
{ name : "plan missing flags" , args : [ ] string { "plan" } , want : "plan: --config and --session are required" } ,
2026-05-02 10:51:23 -05:00
{ name : "status missing flags" , args : [ ] string { "status" } , want : "status: --manifest is required" } ,
2026-05-02 11:38:42 -05:00
{ name : "resume missing flags" , args : [ ] string { "resume" } , want : "resume: --config and --session are required" } ,
2026-05-02 11:05:28 -05:00
{ name : "run-stage missing name" , args : [ ] string { "run-stage" , "--config" , "a" , "--session" , "b" } , want : "run-stage: expected exactly one stage name" } ,
{ name : "run-stage missing config flags" , args : [ ] string { "run-stage" , "polish" } , want : "run-stage: --config and --session are required" } ,
2026-05-02 10:43:45 -05:00
}
for _ , tc := range cases {
t . Run ( tc . name , func ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( tc . args , & stdout , & stderr )
if code == 0 {
t . Fatalf ( "exit code = 0, want non-zero" )
}
if stdout . Len ( ) != 0 {
t . Fatalf ( "stdout = %q, want empty" , stdout . String ( ) )
}
if ! strings . Contains ( stderr . String ( ) , tc . want ) {
t . Fatalf ( "stderr = %q, want to contain %q" , stderr . String ( ) , tc . want )
}
} )
}
}
2026-05-02 11:05:28 -05:00
func TestExecuteRunStageUnknownFails ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string { "run-stage" , "--config" , pipelinePath , "--session" , sessionPath , "unknown" } , & stdout , & stderr )
if code == 0 {
t . Fatal ( "exit code = 0, want non-zero" )
}
if ! strings . Contains ( stderr . String ( ) , "unknown stage" ) {
t . Fatalf ( "stderr = %q, want unknown stage error" , stderr . String ( ) )
}
}
2026-05-02 10:37:38 -05:00
func TestExecuteInvalidCommand ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string { "bogus" } , & stdout , & stderr )
if code == 0 {
t . Fatalf ( "exit code = 0, want non-zero" )
}
if stdout . Len ( ) != 0 {
t . Fatalf ( "stdout = %q, want empty" , stdout . String ( ) )
}
out := stderr . String ( )
if ! strings . Contains ( out , "unknown command" ) {
t . Fatalf ( "stderr = %q, want unknown command message" , out )
}
if ! strings . Contains ( out , "Usage: narratio" ) {
t . Fatalf ( "stderr = %q, want usage message" , out )
}
}
func TestExecuteMissingCommand ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( nil , & stdout , & stderr )
if code == 0 {
t . Fatalf ( "exit code = 0, want non-zero" )
}
if stdout . Len ( ) != 0 {
t . Fatalf ( "stdout = %q, want empty" , stdout . String ( ) )
}
if ! strings . Contains ( stderr . String ( ) , "Usage: narratio" ) {
t . Fatalf ( "stderr = %q, want usage message" , stderr . String ( ) )
}
}
2026-05-02 10:43:45 -05:00
2026-05-02 10:56:42 -05:00
func writeValidConfigFiles ( t * testing . T , workspaceRoot string ) ( string , string ) {
2026-05-02 10:43:45 -05:00
t . Helper ( )
dir := t . TempDir ( )
pipelinePath := filepath . Join ( dir , "pipeline.yml" )
sessionPath := filepath . Join ( dir , "session.yml" )
pipelineYAML := ` workspace :
2026-05-02 10:56:42 -05:00
root : ` + workspaceRoot + `
2026-05-02 10:43:45 -05:00
storage :
backend : s3
whisperx :
timeout : 15 m
seriatim :
timeout : 30 s
audita :
timeout : 1 h
analyzer :
timeout : 20 m
artifacts :
output_dir : artifacts
notification :
timeout : 10 s
`
sessionYAML := ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
`
if err := os . WriteFile ( pipelinePath , [ ] byte ( pipelineYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write pipeline config: %v" , err )
}
if err := os . WriteFile ( sessionPath , [ ] byte ( sessionYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write session config: %v" , err )
}
2026-05-02 11:26:43 -05:00
mustWriteTestFile ( t , filepath . Join ( dir , "speakers.yml" ) , "alice: alice.flac\n" )
mustWriteTestFile ( t , filepath . Join ( dir , "autocorrect.yml" ) , "[]\n" )
mustWriteTestFile ( t , filepath . Join ( dir , "glossary.yml" ) , "[]\n" )
mustWriteTestFile ( t , filepath . Join ( dir , "audio" , "alice.flac" ) , "audio-bytes" )
2026-05-02 10:43:45 -05:00
return pipelinePath , sessionPath
}
2026-05-02 10:51:23 -05:00
func writeManifestPathForExecute ( t * testing . T ) string {
t . Helper ( )
store := & manifest . LocalStore { }
m := manifest . New ( "2026-05-03" , time . Date ( 2026 , 5 , 3 , 10 , 0 , 0 , 0 , time . UTC ) )
m . MarkStageSucceeded ( "prepare" , time . Date ( 2026 , 5 , 3 , 10 , 1 , 0 , 0 , time . UTC ) , nil )
path := filepath . Join ( t . TempDir ( ) , "manifest.json" )
if err := store . Save ( context . Background ( ) , path , m ) ; err != nil {
t . Fatalf ( "save manifest: %v" , err )
}
return path
}
2026-05-02 11:26:43 -05:00
func mustWriteTestFile ( t * testing . T , path , contents string ) {
t . Helper ( )
if err := os . MkdirAll ( filepath . Dir ( path ) , 0 o755 ) ; err != nil {
t . Fatalf ( "mkdir %q: %v" , path , err )
}
if err := os . WriteFile ( path , [ ] byte ( contents ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write %q: %v" , path , err )
}
}