202 lines
6.5 KiB
Go
202 lines
6.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/analyzer"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/audita"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/notify"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/seriatim"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/whisperx"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/stage"
|
|
)
|
|
|
|
type failingStage struct {
|
|
name string
|
|
err error
|
|
}
|
|
|
|
func (s failingStage) Name() string { return s.name }
|
|
func (s failingStage) Declares() stage.IODecl { return stage.IODecl{} }
|
|
func (s failingStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) {
|
|
return nil, s.err
|
|
}
|
|
|
|
func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
|
|
summary, err := executeStages(context.Background(), cfg, BuildFullPlan(), RunOptions{})
|
|
if err != nil {
|
|
t.Fatalf("executeStages() error = %v", err)
|
|
}
|
|
if len(summary.StageNames) != 8 {
|
|
t.Fatalf("stage count = %d, want 8", len(summary.StageNames))
|
|
}
|
|
|
|
store := &manifest.LocalStore{}
|
|
m, err := store.Load(context.Background(), summary.ManifestPath)
|
|
if err != nil {
|
|
t.Fatalf("Load manifest error = %v", err)
|
|
}
|
|
|
|
for _, name := range []string{"prepare", "transcribe", "normalize", "merge", "polish", "analyze", "archive", "notify"} {
|
|
sr := m.Stages[name]
|
|
if sr == nil {
|
|
t.Fatalf("missing stage record %q", name)
|
|
}
|
|
if sr.Status != manifest.StatusSucceeded {
|
|
t.Fatalf("stage %q status = %q, want %q", name, sr.Status, manifest.StatusSucceeded)
|
|
}
|
|
if sr.Metadata == nil || sr.Metadata["placeholder"] != true {
|
|
t.Fatalf("stage %q missing placeholder metadata", name)
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(summary.ManifestPath); err != nil {
|
|
t.Fatalf("manifest file missing at %q: %v", summary.ManifestPath, err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteStagesFailureUpdatesManifest(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
|
|
stages := []stage.Stage{
|
|
BuildFullPlan()[0],
|
|
failingStage{name: "transcribe", err: errors.New("boom")},
|
|
BuildFullPlan()[2],
|
|
}
|
|
|
|
summary, err := executeStages(context.Background(), cfg, stages, RunOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if summary != nil {
|
|
t.Fatalf("summary = %#v, want nil on failure", summary)
|
|
}
|
|
if !strings.Contains(err.Error(), "stage \"transcribe\" failed") {
|
|
t.Fatalf("error = %q, want stage failure", err.Error())
|
|
}
|
|
|
|
manifestPath := manifestPathFor(cfg)
|
|
store := &manifest.LocalStore{}
|
|
m, loadErr := store.Load(context.Background(), manifestPath)
|
|
if loadErr != nil {
|
|
t.Fatalf("Load manifest error = %v", loadErr)
|
|
}
|
|
|
|
if got := m.Stages["prepare"]; got == nil || got.Status != manifest.StatusSucceeded {
|
|
t.Fatalf("prepare status = %#v, want succeeded", got)
|
|
}
|
|
if got := m.Stages["transcribe"]; got == nil || got.Status != manifest.StatusFailed {
|
|
t.Fatalf("transcribe status = %#v, want failed", got)
|
|
}
|
|
if got := m.Stages["normalize"]; got != nil {
|
|
t.Fatalf("normalize should not run, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestExecuteStagesLoadsExistingManifest(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
manifestPath := manifestPathFor(cfg)
|
|
store := &manifest.LocalStore{}
|
|
|
|
existing := manifest.New(cfg.Session.SessionID, time.Date(2026, 5, 3, 1, 0, 0, 0, time.UTC))
|
|
existing.MarkStageSucceeded("prepare", time.Date(2026, 5, 3, 1, 1, 0, 0, time.UTC), nil)
|
|
if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll() error = %v", err)
|
|
}
|
|
if err := store.Save(context.Background(), manifestPath, existing); err != nil {
|
|
t.Fatalf("Save manifest error = %v", err)
|
|
}
|
|
|
|
_, err := executeStages(context.Background(), cfg, []stage.Stage{BuildFullPlan()[1]}, RunOptions{})
|
|
if err != nil {
|
|
t.Fatalf("executeStages() error = %v", err)
|
|
}
|
|
|
|
loaded, err := store.Load(context.Background(), manifestPath)
|
|
if err != nil {
|
|
t.Fatalf("Load manifest error = %v", err)
|
|
}
|
|
if loaded.Stages["prepare"] == nil || loaded.Stages["prepare"].Status != manifest.StatusSucceeded {
|
|
t.Fatalf("existing stage prepare should remain succeeded")
|
|
}
|
|
if loaded.Stages["transcribe"] == nil || loaded.Stages["transcribe"].Status != manifest.StatusSucceeded {
|
|
t.Fatalf("transcribe should be succeeded after run")
|
|
}
|
|
}
|
|
|
|
func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
env *Env
|
|
}{
|
|
{name: "transcribe", env: &Env{WhisperX: &whisperx.FakeClient{Err: errors.New("transcribe fail")}}},
|
|
{name: "merge", env: &Env{Seriatim: &seriatim.FakeRunner{Err: errors.New("merge fail")}}},
|
|
{name: "polish", env: &Env{Audita: &audita.FakeRunner{Err: errors.New("polish fail")}}},
|
|
{name: "analyze", env: &Env{Analyzer: &analyzer.FakeRunner{Err: errors.New("analyze fail")}}},
|
|
{name: "archive", env: &Env{Storage: &storage.FakeBackend{Err: errors.New("archive fail")}}},
|
|
{name: "notify", env: &Env{Notifier: ¬ify.FakeSender{Err: errors.New("notify fail")}}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
selected, err := stage.Select(tc.name)
|
|
if err != nil {
|
|
t.Fatalf("Select() error = %v", err)
|
|
}
|
|
|
|
artifactStore := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root)
|
|
tc.env.Config = cfg
|
|
tc.env.ArtifactStore = artifactStore
|
|
tc.env.ManifestStore = &manifest.LocalStore{}
|
|
|
|
_, runErr := executeStages(context.Background(), cfg, []stage.Stage{selected}, RunOptions{Env: tc.env})
|
|
if runErr == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
m, loadErr := tc.env.ManifestStore.Load(context.Background(), manifestPathFor(cfg))
|
|
if loadErr != nil {
|
|
t.Fatalf("load manifest error = %v", loadErr)
|
|
}
|
|
sr := m.Stages[tc.name]
|
|
if sr == nil {
|
|
t.Fatalf("missing stage record %q", tc.name)
|
|
}
|
|
if sr.Status != manifest.StatusFailed {
|
|
t.Fatalf("status = %q, want %q", sr.Status, manifest.StatusFailed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testConfig(t *testing.T) *config.Config {
|
|
t.Helper()
|
|
|
|
workspace := t.TempDir()
|
|
return &config.Config{
|
|
Pipeline: &config.PipelineConfig{Workspace: config.WorkspaceConfig{Root: workspace}},
|
|
Session: &config.SessionConfig{
|
|
SessionID: "2026-05-03",
|
|
Inputs: config.SessionInputsConfig{
|
|
AudioDir: "./audio",
|
|
SpeakersFile: "./speakers.yml",
|
|
AutocorrectFile: "./autocorrect.yml",
|
|
GlossaryFile: "./glossary.yml",
|
|
},
|
|
},
|
|
}
|
|
}
|