790 lines
27 KiB
Go
790 lines
27 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
)
|
|
|
|
func TestExecuteValidCommands(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":"commands-test","segments":[{"speaker":"alice"}]}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
|
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
wantOut string
|
|
}{
|
|
{name: "run", args: []string{"run", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=11 skipped=1; manifest="},
|
|
{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\nextract: run\nrender: skip\nanalyze: skip\npublish: skip\nnotify: skip"},
|
|
{name: "session status", args: []string{"session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "Session: 2026-05-03"},
|
|
{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="},
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteMissingRequiredFlags(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{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 removed", args: []string{"resume"}, want: `unknown command: "resume"`},
|
|
{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:"},
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteRunStageUnknownFails(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", "unknown", "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") {
|
|
t.Fatalf("stderr = %q, want unknown stage error", stderr.String())
|
|
}
|
|
}
|
|
|
|
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"`) {
|
|
t.Fatalf("stderr = %q, want unknown stage alias error", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
|
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1}]}`)
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
code := Execute([]string{"run-stage", "normalize", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "stage=normalize executed=1 skipped=0") {
|
|
t.Fatalf("stdout = %q, want normalize stage execution", stdout.String())
|
|
}
|
|
}
|
|
|
|
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()
|
|
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
code := Execute([]string{"run-stage", "prepare", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("prepare exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
|
|
code = Execute([]string{"run-stage", "transcribe", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
|
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")
|
|
}
|
|
|
|
outPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "transcripts", "raw", "alice.json")
|
|
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)
|
|
}
|
|
}
|
|
|
|
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, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(%q): %v", secretsDir, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(secretsDir, "OPENROUTER_API_KEY"), []byte("from-secret-file\n"), 0o600); 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")
|
|
campaignPath := writeAppTestCampaignConfig(t, configDir)
|
|
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: 10s
|
|
`
|
|
sessionYAML := `session_id: ` + sessionID + `
|
|
campaign: sample-campaign
|
|
inputs:
|
|
audio_dir: ./audio
|
|
speakers_file: ./speakers.yml
|
|
autocorrect_file: ./autocorrect.yml
|
|
glossary_file: ./glossary.yml
|
|
players_file: ./players.yml
|
|
party_file: ./party.yml
|
|
`
|
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
|
t.Fatalf("write pipeline.yml: %v", err)
|
|
}
|
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); 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)
|
|
})
|
|
|
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", sessionID)
|
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"schema":"seriatim-intermediate","segments":[]}`)
|
|
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "[]\n")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"run-stage", "polish", sessionID, "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
|
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")
|
|
campaignPath := writeAppTestCampaignConfig(t, configDir)
|
|
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: 10s
|
|
`
|
|
sessionYAML := `session_id: 2026-05-03
|
|
campaign: sample-campaign
|
|
inputs:
|
|
audio_dir: ./audio
|
|
speakers_file: ./speakers.yml
|
|
autocorrect_file: ./autocorrect.yml
|
|
glossary_file: ./glossary.yml
|
|
players_file: ./players.yml
|
|
party_file: ./party.yml
|
|
`
|
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
|
t.Fatalf("write pipeline.yml: %v", err)
|
|
}
|
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
|
t.Fatalf("write session.yml: %v", err)
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"run", "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(), "read secrets env_dir") {
|
|
t.Fatalf("stderr = %q, want secrets read-dir error context", stderr.String())
|
|
}
|
|
}
|
|
|
|
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()
|
|
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
|
originalDefaults := append([]string(nil), config.DefaultPipelineConfigSearchPaths...)
|
|
config.DefaultPipelineConfigSearchPaths = []string{pipelinePath}
|
|
defer func() {
|
|
config.DefaultPipelineConfigSearchPaths = originalDefaults
|
|
}()
|
|
_ = campaignPath
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"run", "2026-05-03", "--session", sessionPath}, &stdout, &stderr)
|
|
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=11 skipped=1; manifest=") {
|
|
t.Fatalf("stdout = %q, want successful run output", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteMissingCampaignConfigReportsRegistryPath(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
if err := os.Remove(campaignPath); err != nil {
|
|
t.Fatalf("remove campaign config: %v", err)
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"run", "2026-05-03", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
|
if code == 0 {
|
|
t.Fatal("exit code = 0, want non-zero")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "load campaign config") {
|
|
t.Fatalf("stderr = %q, want campaign discovery failure", stderr.String())
|
|
}
|
|
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
|
|
players_file: ./players.yml
|
|
party_file: ./party.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")
|
|
mustWriteTestFile(t, filepath.Join(otherDir, "players.yml"), "[]\n")
|
|
mustWriteTestFile(t, filepath.Join(otherDir, "party.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())
|
|
}
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
func writeValidConfigFiles(t *testing.T, workspaceRoot string, transcribeURL ...string) (string, string, string) {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
|
campaignRoot := filepath.Join(dir, "campaigns")
|
|
campaignDir := filepath.Join(campaignRoot, "sample-campaign")
|
|
campaignPath := filepath.Join(campaignDir, "campaign.yml")
|
|
sessionPath := filepath.Join(dir, "session.yml")
|
|
url := "https://example.com/transcribe"
|
|
if len(transcribeURL) > 0 && strings.TrimSpace(transcribeURL[0]) != "" {
|
|
url = transcribeURL[0]
|
|
}
|
|
seriatimBinary := writeSeriatimAppTestWrapper(t)
|
|
scriptoriumBinary := writeScriptoriumAppTestWrapper(t)
|
|
auditaBinary := writeAuditaAppTestWrapper(t)
|
|
t.Setenv("GO_WANT_APP_SERIATIM_HELPER", "1")
|
|
t.Setenv("GO_WANT_APP_SCRIPTORIUM_HELPER", "1")
|
|
t.Setenv("GO_WANT_APP_AUDITA_HELPER", "1")
|
|
t.Setenv("AUDITA_LLM_API_KEY", "test-audita-key")
|
|
t.Setenv("PATH", filepath.Dir(scriptoriumBinary)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
pipelineYAML := `workspace:
|
|
root: ` + workspaceRoot + `
|
|
campaigns:
|
|
root: ` + campaignRoot + `
|
|
default_campaign_id: sample-campaign
|
|
cache:
|
|
root: ` + filepath.Join(workspaceRoot, "cache") + `
|
|
spool:
|
|
root: ` + filepath.Join(workspaceRoot, "spool") + `
|
|
storage:
|
|
backend: s3
|
|
s3:
|
|
bucket: test-bucket
|
|
publish:
|
|
enabled: true
|
|
upload_run: false
|
|
whisperx:
|
|
transcribe_url: ` + url + `
|
|
timeout: 2s
|
|
retries: 0
|
|
retry_delay: 1ms
|
|
concurrency: 1
|
|
seriatim:
|
|
binary: ` + seriatimBinary + `
|
|
timeout: 10m
|
|
output_schema: seriatim-intermediate
|
|
coalesce_gap: 3.0
|
|
report: true
|
|
audita:
|
|
binary: ` + auditaBinary + `
|
|
notification:
|
|
timeout: 10s
|
|
`
|
|
|
|
sessionYAML := `session_id: 2026-05-03
|
|
inputs:
|
|
audio_dir: ./audio
|
|
`
|
|
campaignYAML := `campaign_id: sample-campaign
|
|
inputs:
|
|
speakers_file: ./speakers.yml
|
|
autocorrect_file: ./autocorrect.yml
|
|
glossary_file: ./glossary.yml
|
|
players_file: ./players.yml
|
|
party_file: ./party.yml
|
|
`
|
|
|
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
|
t.Fatalf("write pipeline config: %v", err)
|
|
}
|
|
if err := os.MkdirAll(campaignDir, 0o755); err != nil {
|
|
t.Fatalf("create campaign dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
|
t.Fatalf("write campaign config: %v", err)
|
|
}
|
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
|
t.Fatalf("write session config: %v", err)
|
|
}
|
|
|
|
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")
|
|
mustWriteTestFile(t, filepath.Join(campaignDir, "players.yml"), "[]\n")
|
|
mustWriteTestFile(t, filepath.Join(campaignDir, "party.yml"), "[]\n")
|
|
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
|
|
|
return pipelinePath, campaignPath, sessionPath
|
|
}
|
|
|
|
func writeAppTestCampaignConfig(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
campaignPath := filepath.Join(dir, "campaign.yml")
|
|
campaignYAML := `campaign_id: sample-campaign
|
|
inputs:
|
|
speakers_file: ./speakers.yml
|
|
autocorrect_file: ./autocorrect.yml
|
|
glossary_file: ./glossary.yml
|
|
players_file: ./players.yml
|
|
party_file: ./party.yml
|
|
`
|
|
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
|
t.Fatalf("write campaign.yml: %v", err)
|
|
}
|
|
return campaignPath
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func mustWriteTestFile(t *testing.T, path, contents string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir %q: %v", path, err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
|
t.Fatalf("write %q: %v", path, err)
|
|
}
|
|
}
|
|
|
|
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), 0o755); err != nil {
|
|
t.Fatalf("WriteFile(%q): %v", path, err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func writeScriptoriumAppTestWrapper(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(), "scriptorium")
|
|
content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestScriptoriumAppHelper -- \"$@\"\n"
|
|
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
|
t.Fatalf("WriteFile(%q): %v", path, err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestScriptoriumAppHelper(t *testing.T) {
|
|
if os.Getenv("GO_WANT_APP_SCRIPTORIUM_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)
|
|
}
|
|
runArgs := args[start:]
|
|
|
|
outputPath := appSeriatimFlagValue(runArgs, "--out")
|
|
if strings.TrimSpace(outputPath) == "" {
|
|
outputPath = appSeriatimFlagValue(runArgs, "--output")
|
|
}
|
|
if strings.TrimSpace(outputPath) == "" {
|
|
_, _ = os.Stderr.WriteString("missing output flag\n")
|
|
os.Exit(2)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir output dir: %v\n", err))
|
|
os.Exit(2)
|
|
}
|
|
if err := os.WriteFile(outputPath, []byte(`{"trim_action":"copy","warnings":[]}`), 0o644); err != nil {
|
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err))
|
|
os.Exit(2)
|
|
}
|
|
_, _ = os.Stdout.WriteString("scriptorium helper stdout\n")
|
|
_, _ = os.Stderr.WriteString("scriptorium helper stderr\n")
|
|
os.Exit(0)
|
|
}
|
|
|
|
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), 0o755); 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":[]}`), 0o644); 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), 0o755); err != nil {
|
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir report dir: %v\n", err))
|
|
os.Exit(2)
|
|
}
|
|
if err := os.WriteFile(reportPath, []byte(`{"report":true}`), 0o644); 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)
|
|
}
|
|
|
|
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), 0o755); 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), 0o755); 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":[]}`), 0o644); 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), 0o755); err != nil {
|
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir report dir: %v\n", err))
|
|
os.Exit(2)
|
|
}
|
|
if err := os.WriteFile(reportPath, []byte(`{"report":true}`), 0o644); err != nil {
|
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("write report: %v\n", err))
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
if strings.TrimSpace(workDir) != "" {
|
|
if err := os.MkdirAll(workDir, 0o755); 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)
|
|
}
|
|
|
|
func appSeriatimFlagValue(args []string, name string) string {
|
|
for i := 0; i < len(args)-1; i++ {
|
|
if args[i] == name {
|
|
return args[i+1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|