2026-05-02 10:37:38 -05:00
package app
import (
"bytes"
2026-05-02 10:51:23 -05:00
"context"
2026-05-03 22:17:17 -05:00
"fmt"
2026-05-03 17:49:26 -05:00
"net/http"
"net/http/httptest"
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"
2026-05-14 21:02:06 -05:00
"gitea.maximumdirect.net/eric/narratio/internal/config"
2026-05-02 10:51:23 -05:00
"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 ( )
2026-05-03 17:49:26 -05:00
srv := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "source":"commands-test","segments":[ { "speaker":"alice"}]} ` ) )
} ) )
defer srv . Close ( )
2026-05-20 20:41:28 -05:00
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot , srv . URL )
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-22 23:01:27 -05:00
{ name : "run" , args : [ ] string { "run" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , wantOut : "narratio run: session 2026-05-03; executed=9 skipped=0; manifest=" } ,
2026-05-23 04:30:45 +00:00
{ name : "session plan" , args : [ ] string { "session" , "plan" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , wantOut : "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\nnormalize: skip\ntrim: skip\nanalyze: skip\npublish: skip\nnotify: skip" } ,
2026-05-22 23:01:27 -05:00
{ name : "session status" , args : [ ] string { "session" , "status" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , wantOut : "Session: 2026-05-03" } ,
{ name : "resume" , args : [ ] string { "resume" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , wantOut : "narratio resume: session 2026-05-03 has no remaining stages" } ,
{ name : "run-stage" , args : [ ] string { "run-stage" , "polish" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , 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
} {
2026-05-22 22:09:17 -05:00
{ name : "run missing session" , args : [ ] string { "run" } , want : "run: session_id is required" } ,
{ name : "plan old top-level removed" , args : [ ] string { "plan" } , want : ` unknown command: "plan" ` } ,
{ name : "status old top-level removed" , args : [ ] string { "status" } , want : ` unknown command: "status" ` } ,
{ name : "resume missing session" , args : [ ] string { "resume" } , want : "resume: session_id is required" } ,
{ name : "run-stage missing name" , args : [ ] string { "run-stage" , "--config" , "a" , "--session" , "b" } , want : "run-stage: expected stage name and session_id" } ,
{ name : "run-stage missing session" , args : [ ] string { "run-stage" , "polish" } , want : "run-stage: expected stage name and session_id" } ,
{ name : "run missing config uses defaults" , args : [ ] string { "run" , "2026-05-03" , "--session" , "session.yml" } , want : "run: no pipeline config path provided and no default pipeline config found; searched:" } ,
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 ( )
2026-05-20 20:41:28 -05:00
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot , "https://example.com/transcribe" )
2026-05-02 11:05:28 -05:00
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-22 23:01:27 -05:00
code := Execute ( [ ] string { "run-stage" , "unknown" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , & stdout , & stderr )
2026-05-02 11:05:28 -05:00
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-23 04:30:45 +00:00
func TestExecuteRunStageArchiveAliasFails ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot , "https://example.com/transcribe" )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string { "run-stage" , "archive" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , & stdout , & stderr )
if code == 0 {
t . Fatal ( "exit code = 0, want non-zero" )
}
if ! strings . Contains ( stderr . String ( ) , ` unknown stage "archive" ` ) {
2026-05-23 12:43:50 +00:00
t . Fatalf ( "stderr = %q, want unknown stage alias error" , stderr . String ( ) )
2026-05-23 04:30:45 +00:00
}
}
2026-05-10 21:26:53 +00:00
func TestExecuteRunStageNormalizeIsAccepted ( t * testing . T ) {
2026-05-08 16:51:01 +00:00
workspaceRoot := t . TempDir ( )
2026-05-20 20:41:28 -05:00
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot , "https://example.com/transcribe" )
2026-05-17 20:57:27 +00:00
workRoot := filepath . Join ( workspaceRoot , "work" , "sample-campaign" , "2026-05-03" )
2026-05-22 19:05:23 -05:00
mustWriteTestFile ( t , filepath . Join ( workRoot , "transcripts" , "polished.json" ) , ` { "segments":[ { "id":1}]} ` )
2026-05-08 16:51:01 +00:00
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-22 23:01:27 -05:00
code := Execute ( [ ] string { "run-stage" , "normalize" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , & stdout , & stderr )
2026-05-10 21:26:53 +00:00
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
2026-05-08 16:51:01 +00:00
}
2026-05-10 21:26:53 +00:00
if ! strings . Contains ( stdout . String ( ) , "stage=normalize executed=1 skipped=0" ) {
t . Fatalf ( "stdout = %q, want normalize stage execution" , stdout . String ( ) )
2026-05-08 16:51:01 +00:00
}
}
2026-05-03 17:49:26 -05:00
func TestExecuteRunStageTranscribeUsesConfiguredWhisperXServer ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
var serverCalls int
srv := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
serverCalls ++
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "source":"run-stage-test","segments":[ { "speaker":"alice"}]} ` ) )
} ) )
defer srv . Close ( )
2026-05-20 20:41:28 -05:00
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot , srv . URL )
2026-05-03 17:49:26 -05:00
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-22 23:01:27 -05:00
code := Execute ( [ ] string { "run-stage" , "prepare" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , & stdout , & stderr )
2026-05-03 17:49:26 -05:00
if code != 0 {
t . Fatalf ( "prepare exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
stdout . Reset ( )
stderr . Reset ( )
2026-05-22 23:01:27 -05:00
code = Execute ( [ ] string { "run-stage" , "transcribe" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath , "--force" } , & stdout , & stderr )
2026-05-03 17:49:26 -05:00
if code != 0 {
t . Fatalf ( "transcribe exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
if serverCalls == 0 {
t . Fatal ( "expected whisperx server to be called at least once" )
}
2026-05-17 20:57:27 +00:00
outPath := filepath . Join ( workspaceRoot , "work" , "sample-campaign" , "2026-05-03" , "transcripts" , "raw" , "alice.json" )
2026-05-03 17:49:26 -05:00
data , err := os . ReadFile ( outPath )
if err != nil {
t . Fatalf ( "ReadFile(%q): %v" , outPath , err )
}
got := strings . TrimSpace ( string ( data ) )
if got == ` { "schema":"speaker_transcript.v1","segments":[]} ` {
t . Fatalf ( "got noop transcript output: %q" , got )
}
if ! strings . Contains ( got , ` "source":"run-stage-test" ` ) {
t . Fatalf ( "output = %q, want run-stage server json" , got )
}
}
2026-05-16 07:56:22 -05:00
func TestExecuteRunStagePolishLoadsCredentialFromSecretsDir ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
configDir := t . TempDir ( )
sessionID := "2026-05-03"
secretsDir := filepath . Join ( configDir , "secrets" )
if err := os . MkdirAll ( secretsDir , 0 o755 ) ; err != nil {
t . Fatalf ( "MkdirAll(%q): %v" , secretsDir , err )
}
if err := os . WriteFile ( filepath . Join ( secretsDir , "OPENROUTER_API_KEY" ) , [ ] byte ( "from-secret-file\n" ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write OPENROUTER_API_KEY secret file: %v" , err )
}
seriatimBinary := writeSeriatimAppTestWrapper ( t )
auditaBinary := writeAuditaAppTestWrapper ( t )
t . Setenv ( "GO_WANT_APP_SERIATIM_HELPER" , "1" )
t . Setenv ( "GO_WANT_APP_AUDITA_HELPER" , "1" )
pipelinePath := filepath . Join ( configDir , "pipeline.yml" )
2026-05-20 20:41:28 -05:00
campaignPath := writeAppTestCampaignConfig ( t , configDir )
2026-05-16 07:56:22 -05:00
sessionPath := filepath . Join ( configDir , "session.yml" )
pipelineYAML := ` workspace :
root : ` + workspaceRoot + `
storage :
backend : local
secrets :
env_dir : . / secrets
whisperx :
transcribe_url : https : //example.com/transcribe
seriatim :
binary : ` + seriatimBinary + `
audita :
binary : ` + auditaBinary + `
llm_api_key_env : OPENROUTER_API_KEY
notification :
timeout : 10 s
`
sessionYAML := ` session_id: ` + sessionID + `
2026-05-16 14:11:59 +00:00
campaign : sample - campaign
2026-05-16 07:56:22 -05:00
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.yml: %v" , err )
}
if err := os . WriteFile ( sessionPath , [ ] byte ( sessionYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write session.yml: %v" , err )
}
originalWD , err := os . Getwd ( )
if err != nil {
t . Fatalf ( "Getwd(): %v" , err )
}
if err := os . Chdir ( configDir ) ; err != nil {
t . Fatalf ( "Chdir(%q): %v" , configDir , err )
}
t . Cleanup ( func ( ) {
_ = os . Chdir ( originalWD )
} )
2026-05-17 20:57:27 +00:00
workRoot := filepath . Join ( workspaceRoot , "work" , "sample-campaign" , sessionID )
2026-05-22 19:05:23 -05:00
mustWriteTestFile ( t , filepath . Join ( workRoot , "transcripts" , "base.json" ) , ` { "schema":"seriatim-intermediate","segments":[]} ` )
2026-05-16 07:56:22 -05:00
mustWriteTestFile ( t , filepath . Join ( workRoot , "inputs" , "glossary.yml" ) , "[]\n" )
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-22 23:01:27 -05:00
code := Execute ( [ ] string { "run-stage" , "polish" , sessionID , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath , "--force" } , & stdout , & stderr )
2026-05-16 07:56:22 -05:00
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
if ! strings . Contains ( stdout . String ( ) , "stage=polish executed=1 skipped=0" ) {
t . Fatalf ( "stdout = %q, want polish execution" , stdout . String ( ) )
}
}
func TestExecuteRunFailsWhenConfiguredSecretsDirMissing ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
configDir := t . TempDir ( )
pipelinePath := filepath . Join ( configDir , "pipeline.yml" )
2026-05-20 20:41:28 -05:00
campaignPath := writeAppTestCampaignConfig ( t , configDir )
2026-05-16 07:56:22 -05:00
sessionPath := filepath . Join ( configDir , "session.yml" )
pipelineYAML := ` workspace :
root : ` + workspaceRoot + `
storage :
backend : local
secrets :
env_dir : . / missing - secrets
whisperx :
transcribe_url : https : //example.com/transcribe
seriatim :
binary : seriatim
audita :
binary : audita
notification :
timeout : 10 s
`
sessionYAML := ` session_id : 2026 - 05 - 03
2026-05-16 14:11:59 +00:00
campaign : sample - campaign
2026-05-16 07:56:22 -05:00
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.yml: %v" , err )
}
if err := os . WriteFile ( sessionPath , [ ] byte ( sessionYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write session.yml: %v" , err )
}
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-22 23:01:27 -05:00
code := Execute ( [ ] string { "run" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , & stdout , & stderr )
2026-05-16 07:56:22 -05:00
if code == 0 {
t . Fatal ( "exit code = 0, want non-zero" )
}
if ! strings . Contains ( stderr . String ( ) , "read secrets env_dir" ) {
t . Fatalf ( "stderr = %q, want secrets read-dir error context" , stderr . String ( ) )
}
}
2026-05-14 21:02:06 -05:00
func TestExecuteUsesDefaultPipelineConfigPathWhenConfigFlagOmitted ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
srv := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
_ , _ = w . Write ( [ ] byte ( ` { "source":"default-config-test","segments":[ { "speaker":"alice"}]} ` ) )
} ) )
defer srv . Close ( )
2026-05-20 20:41:28 -05:00
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot , srv . URL )
2026-05-14 21:02:06 -05:00
originalDefaults := append ( [ ] string ( nil ) , config . DefaultPipelineConfigSearchPaths ... )
config . DefaultPipelineConfigSearchPaths = [ ] string { pipelinePath }
defer func ( ) {
config . DefaultPipelineConfigSearchPaths = originalDefaults
} ( )
2026-05-22 23:01:27 -05:00
_ = campaignPath
2026-05-14 21:02:06 -05:00
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-22 22:09:17 -05:00
code := Execute ( [ ] string { "run" , "2026-05-03" , "--session" , sessionPath } , & stdout , & stderr )
2026-05-14 21:02:06 -05:00
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
if ! strings . Contains ( stdout . String ( ) , "narratio run: session 2026-05-03; executed=9 skipped=0; manifest=" ) {
t . Fatalf ( "stdout = %q, want successful run output" , stdout . String ( ) )
}
}
2026-05-22 23:01:27 -05:00
func TestExecuteMissingCampaignConfigReportsRegistryPath ( t * testing . T ) {
2026-05-20 20:41:28 -05:00
workspaceRoot := t . TempDir ( )
2026-05-22 23:01:27 -05:00
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
if err := os . Remove ( campaignPath ) ; err != nil {
t . Fatalf ( "remove campaign config: %v" , err )
}
2026-05-20 20:41:28 -05:00
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-22 22:09:17 -05:00
code := Execute ( [ ] string { "run" , "2026-05-03" , "--config" , pipelinePath , "--session" , sessionPath } , & stdout , & stderr )
2026-05-20 20:41:28 -05:00
if code == 0 {
t . Fatal ( "exit code = 0, want non-zero" )
}
if stdout . Len ( ) != 0 {
t . Fatalf ( "stdout = %q, want empty" , stdout . String ( ) )
}
2026-05-22 23:01:27 -05:00
if ! strings . Contains ( stderr . String ( ) , "load campaign config" ) {
2026-05-20 20:41:28 -05:00
t . Fatalf ( "stderr = %q, want campaign discovery failure" , stderr . String ( ) )
}
2026-05-22 23:01:27 -05:00
if ! strings . Contains ( stderr . String ( ) , filepath . ToSlash ( filepath . Join ( "campaigns" , "sample-campaign" , "campaign.yml" ) ) ) {
t . Fatalf ( "stderr = %q, want campaign registry path" , stderr . String ( ) )
}
}
func TestExecuteUsesPipelineDefaultCampaignID ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , _ , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string { "session" , "status" , "2026-05-03" , "--config" , pipelinePath , "--session" , sessionPath } , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
if ! strings . Contains ( stdout . String ( ) , "Campaign: sample-campaign" ) {
t . Fatalf ( "stdout = %q, want default campaign" , stdout . String ( ) )
}
}
func TestExecuteCampaignIDSelectsRegistryCampaign ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
campaignRoot := filepath . Dir ( filepath . Dir ( campaignPath ) )
otherDir := filepath . Join ( campaignRoot , "icewind" )
mustWriteTestFile ( t , filepath . Join ( otherDir , "campaign.yml" ) , ` campaign_id : icewind
inputs :
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
` )
mustWriteTestFile ( t , filepath . Join ( otherDir , "speakers.yml" ) , "match:\n - speaker: Alice\n match: [\"alice\"]\n" )
mustWriteTestFile ( t , filepath . Join ( otherDir , "autocorrect.yml" ) , "[]\n" )
mustWriteTestFile ( t , filepath . Join ( otherDir , "glossary.yml" ) , "[]\n" )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string { "session" , "status" , "2026-05-03" , "--config" , pipelinePath , "--campaign" , "icewind" , "--session" , sessionPath } , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
if ! strings . Contains ( stdout . String ( ) , "Campaign: icewind" ) {
t . Fatalf ( "stdout = %q, want selected campaign" , stdout . String ( ) )
}
}
func TestExecuteRejectsCampaignIDAndCampaignFile ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string { "session" , "status" , "2026-05-03" , "--config" , pipelinePath , "--campaign" , "sample-campaign" , "--campaign-file" , campaignPath , "--session" , sessionPath } , & stdout , & stderr )
if code == 0 {
t . Fatal ( "exit code = 0, want non-zero" )
}
if ! strings . Contains ( stderr . String ( ) , "mutually exclusive" ) {
t . Fatalf ( "stderr = %q, want mutually exclusive error" , stderr . String ( ) )
2026-05-20 20:41:28 -05:00
}
}
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-20 20:41:28 -05:00
func writeValidConfigFiles ( t * testing . T , workspaceRoot string , transcribeURL ... string ) ( string , string , string ) {
2026-05-02 10:43:45 -05:00
t . Helper ( )
dir := t . TempDir ( )
pipelinePath := filepath . Join ( dir , "pipeline.yml" )
2026-05-22 23:01:27 -05:00
campaignRoot := filepath . Join ( dir , "campaigns" )
campaignDir := filepath . Join ( campaignRoot , "sample-campaign" )
campaignPath := filepath . Join ( campaignDir , "campaign.yml" )
2026-05-02 10:43:45 -05:00
sessionPath := filepath . Join ( dir , "session.yml" )
2026-05-03 17:49:26 -05:00
url := "https://example.com/transcribe"
if len ( transcribeURL ) > 0 && strings . TrimSpace ( transcribeURL [ 0 ] ) != "" {
url = transcribeURL [ 0 ]
}
2026-05-03 22:17:17 -05:00
seriatimBinary := writeSeriatimAppTestWrapper ( t )
2026-05-04 08:05:27 -05:00
auditaBinary := writeAuditaAppTestWrapper ( t )
2026-05-03 22:17:17 -05:00
t . Setenv ( "GO_WANT_APP_SERIATIM_HELPER" , "1" )
2026-05-04 08:05:27 -05:00
t . Setenv ( "GO_WANT_APP_AUDITA_HELPER" , "1" )
t . Setenv ( "AUDITA_LLM_API_KEY" , "test-audita-key" )
2026-05-02 10:43:45 -05:00
pipelineYAML := ` workspace :
2026-05-02 10:56:42 -05:00
root : ` + workspaceRoot + `
2026-05-22 23:01:27 -05:00
campaigns :
root : ` + campaignRoot + `
default_campaign_id : sample - campaign
2026-05-21 22:22:08 -05:00
cache :
root : ` + filepath.Join(workspaceRoot, "cache") + `
spool :
root : ` + filepath.Join(workspaceRoot, "spool") + `
2026-05-02 10:43:45 -05:00
storage :
backend : s3
2026-05-16 14:11:59 +00:00
s3 :
bucket : test - bucket
2026-05-23 04:30:45 +00:00
publish :
2026-05-16 14:43:29 +00:00
enabled : true
upload_run : false
2026-05-02 10:43:45 -05:00
whisperx :
2026-05-03 17:49:26 -05:00
transcribe_url : ` + url + `
timeout : 2 s
retries : 0
retry_delay : 1 ms
concurrency : 1
2026-05-02 10:43:45 -05:00
seriatim :
2026-05-03 22:17:17 -05:00
binary : ` + seriatimBinary + `
2026-05-03 19:13:50 -05:00
timeout : 10 m
output_schema : seriatim - intermediate
coalesce_gap : 3.0
report : true
2026-05-02 10:43:45 -05:00
audita :
2026-05-04 08:05:27 -05:00
binary : ` + auditaBinary + `
2026-05-02 10:43:45 -05:00
notification :
timeout : 10 s
`
sessionYAML := ` session_id : 2026 - 05 - 03
inputs :
audio_dir : . / audio
2026-05-20 20:41:28 -05:00
`
2026-05-22 23:01:27 -05:00
campaignYAML := ` campaign_id : sample - campaign
2026-05-20 20:41:28 -05:00
inputs :
2026-05-02 10:43:45 -05:00
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 )
}
2026-05-22 23:01:27 -05:00
if err := os . MkdirAll ( campaignDir , 0 o755 ) ; err != nil {
t . Fatalf ( "create campaign dir: %v" , err )
}
2026-05-20 20:41:28 -05:00
if err := os . WriteFile ( campaignPath , [ ] byte ( campaignYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write campaign config: %v" , err )
}
2026-05-02 10:43:45 -05:00
if err := os . WriteFile ( sessionPath , [ ] byte ( sessionYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write session config: %v" , err )
}
2026-05-22 23:01:27 -05:00
mustWriteTestFile ( t , filepath . Join ( campaignDir , "speakers.yml" ) , "match:\n - speaker: Alice\n match: [\"alice\"]\n" )
mustWriteTestFile ( t , filepath . Join ( campaignDir , "autocorrect.yml" ) , "[]\n" )
mustWriteTestFile ( t , filepath . Join ( campaignDir , "glossary.yml" ) , "[]\n" )
2026-05-02 11:26:43 -05:00
mustWriteTestFile ( t , filepath . Join ( dir , "audio" , "alice.flac" ) , "audio-bytes" )
2026-05-20 20:41:28 -05:00
return pipelinePath , campaignPath , sessionPath
}
func writeAppTestCampaignConfig ( t * testing . T , dir string ) string {
t . Helper ( )
campaignPath := filepath . Join ( dir , "campaign.yml" )
2026-05-22 23:01:27 -05:00
campaignYAML := ` campaign_id : sample - campaign
2026-05-20 20:41:28 -05:00
inputs :
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
`
if err := os . WriteFile ( campaignPath , [ ] byte ( campaignYAML ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write campaign.yml: %v" , err )
}
return campaignPath
2026-05-02 10:43:45 -05:00
}
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 )
}
}
2026-05-03 22:17:17 -05:00
func writeSeriatimAppTestWrapper ( t * testing . T ) string {
t . Helper ( )
exe , err := os . Executable ( )
if err != nil {
t . Fatalf ( "os.Executable() error = %v" , err )
}
path := filepath . Join ( t . TempDir ( ) , "seriatim-helper-wrapper.sh" )
content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestSeriatimAppHelper -- \"$@\"\n"
if err := os . WriteFile ( path , [ ] byte ( content ) , 0 o755 ) ; err != nil {
t . Fatalf ( "WriteFile(%q): %v" , path , err )
}
return path
}
func TestSeriatimAppHelper ( t * testing . T ) {
if os . Getenv ( "GO_WANT_APP_SERIATIM_HELPER" ) != "1" {
return
}
args := os . Args
start := - 1
for i := range args {
if args [ i ] == "--" {
start = i + 1
break
}
}
if start < 0 || start >= len ( args ) {
_ , _ = os . Stderr . WriteString ( "missing -- args separator\n" )
os . Exit ( 2 )
}
mergeArgs := args [ start : ]
outputPath := appSeriatimFlagValue ( mergeArgs , "--output-file" )
reportPath := appSeriatimFlagValue ( mergeArgs , "--report-file" )
if strings . TrimSpace ( outputPath ) == "" {
_ , _ = os . Stderr . WriteString ( "missing --output-file\n" )
os . Exit ( 2 )
}
if err := os . MkdirAll ( filepath . Dir ( outputPath ) , 0 o755 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "mkdir output dir: %v\n" , err ) )
os . Exit ( 2 )
}
if err := os . WriteFile ( outputPath , [ ] byte ( ` { "schema":"seriatim-intermediate","segments":[]} ` ) , 0 o644 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "write output: %v\n" , err ) )
os . Exit ( 2 )
}
if strings . TrimSpace ( reportPath ) != "" {
if err := os . MkdirAll ( filepath . Dir ( reportPath ) , 0 o755 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "mkdir report dir: %v\n" , err ) )
os . Exit ( 2 )
}
if err := os . WriteFile ( reportPath , [ ] byte ( ` { "report":true} ` ) , 0 o644 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "write report: %v\n" , err ) )
os . Exit ( 2 )
}
}
_ , _ = os . Stdout . WriteString ( "seriatim helper stdout\n" )
_ , _ = os . Stderr . WriteString ( "seriatim helper stderr\n" )
os . Exit ( 0 )
}
2026-05-04 08:05:27 -05:00
func writeAuditaAppTestWrapper ( t * testing . T ) string {
t . Helper ( )
exe , err := os . Executable ( )
if err != nil {
t . Fatalf ( "os.Executable() error = %v" , err )
}
path := filepath . Join ( t . TempDir ( ) , "audita-helper-wrapper.sh" )
content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestAuditaAppHelper -- \"$@\"\n"
if err := os . WriteFile ( path , [ ] byte ( content ) , 0 o755 ) ; err != nil {
t . Fatalf ( "WriteFile(%q): %v" , path , err )
}
return path
}
func TestAuditaAppHelper ( t * testing . T ) {
if os . Getenv ( "GO_WANT_APP_AUDITA_HELPER" ) != "1" {
return
}
args := os . Args
start := - 1
for i := range args {
if args [ i ] == "--" {
start = i + 1
break
}
}
if start < 0 || start >= len ( args ) {
_ , _ = os . Stderr . WriteString ( "missing -- args separator\n" )
os . Exit ( 2 )
}
processArgs := args [ start : ]
outputPath := appSeriatimFlagValue ( processArgs , "--output" )
reportPath := appSeriatimFlagValue ( processArgs , "--report-json" )
workDir := appSeriatimFlagValue ( processArgs , "--work-dir" )
if strings . TrimSpace ( outputPath ) == "" {
_ , _ = os . Stderr . WriteString ( "missing --output\n" )
os . Exit ( 2 )
}
if err := os . MkdirAll ( filepath . Dir ( outputPath ) , 0 o755 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "mkdir output dir: %v\n" , err ) )
os . Exit ( 2 )
}
if err := os . WriteFile ( outputPath , [ ] byte ( ` { "schema":"audita.processed.v1","segments":[]} ` ) , 0 o644 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "write output: %v\n" , err ) )
os . Exit ( 2 )
}
if strings . TrimSpace ( reportPath ) != "" {
if err := os . MkdirAll ( filepath . Dir ( reportPath ) , 0 o755 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "mkdir report dir: %v\n" , err ) )
os . Exit ( 2 )
}
if err := os . WriteFile ( reportPath , [ ] byte ( ` { "report":true} ` ) , 0 o644 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "write report: %v\n" , err ) )
os . Exit ( 2 )
}
}
if strings . TrimSpace ( workDir ) != "" {
if err := os . MkdirAll ( workDir , 0 o755 ) ; err != nil {
_ , _ = os . Stderr . WriteString ( fmt . Sprintf ( "mkdir work dir: %v\n" , err ) )
os . Exit ( 2 )
}
}
_ , _ = os . Stdout . WriteString ( "audita helper stdout\n" )
_ , _ = os . Stderr . WriteString ( "audita helper stderr\n" )
os . Exit ( 0 )
}
2026-05-03 22:17:17 -05:00
func appSeriatimFlagValue ( args [ ] string , name string ) string {
for i := 0 ; i < len ( args ) - 1 ; i ++ {
if args [ i ] == name {
return args [ i + 1 ]
}
}
return ""
}