Define adapter interfaces and fake implementations
This commit is contained in:
@@ -7,9 +7,11 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -17,12 +19,14 @@ import (
|
||||
type Env struct {
|
||||
Config *config.Config
|
||||
ArtifactStore artifacts.Store
|
||||
ManifestStore manifest.Store
|
||||
Logger *slog.Logger
|
||||
|
||||
WhisperX whisperx.Client
|
||||
Seriatim seriatim.Runner
|
||||
Audita audita.Runner
|
||||
Analyzer analyzer.Runner
|
||||
Storage storage.Backend
|
||||
Notifier notify.Sender
|
||||
}
|
||||
|
||||
@@ -34,11 +38,13 @@ func toStageEnv(env *Env) *stage.Env {
|
||||
return &stage.Env{
|
||||
Config: env.Config,
|
||||
ArtifactStore: env.ArtifactStore,
|
||||
ManifestStore: env.ManifestStore,
|
||||
Logger: env.Logger,
|
||||
WhisperX: env.WhisperX,
|
||||
Seriatim: env.Seriatim,
|
||||
Audita: env.Audita,
|
||||
Analyzer: env.Analyzer,
|
||||
Storage: env.Storage,
|
||||
Notifier: env.Notifier,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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/logging"
|
||||
@@ -16,6 +22,7 @@ import (
|
||||
|
||||
type RunOptions struct {
|
||||
Force bool
|
||||
Env *Env
|
||||
}
|
||||
|
||||
type RunSummary struct {
|
||||
@@ -25,36 +32,64 @@ type RunSummary struct {
|
||||
}
|
||||
|
||||
func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage, opts RunOptions) (*RunSummary, error) {
|
||||
store := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root)
|
||||
paths, err := store.EnsureLayout(cfg.Session.SessionID)
|
||||
env := opts.Env
|
||||
if env == nil {
|
||||
env = &Env{}
|
||||
}
|
||||
if env.Config == nil {
|
||||
env.Config = cfg
|
||||
}
|
||||
if env.ArtifactStore == nil {
|
||||
env.ArtifactStore = artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root)
|
||||
}
|
||||
if env.ManifestStore == nil {
|
||||
env.ManifestStore = &manifest.LocalStore{}
|
||||
}
|
||||
if env.Logger == nil {
|
||||
env.Logger = logging.NewLogger(os.Stderr, slog.LevelInfo)
|
||||
}
|
||||
if env.WhisperX == nil {
|
||||
env.WhisperX = &whisperx.NoopClient{}
|
||||
}
|
||||
if env.Seriatim == nil {
|
||||
env.Seriatim = &seriatim.NoopRunner{}
|
||||
}
|
||||
if env.Audita == nil {
|
||||
env.Audita = &audita.NoopRunner{}
|
||||
}
|
||||
if env.Analyzer == nil {
|
||||
env.Analyzer = &analyzer.NoopRunner{}
|
||||
}
|
||||
if env.Storage == nil {
|
||||
env.Storage = &storage.NoopBackend{}
|
||||
}
|
||||
if env.Notifier == nil {
|
||||
env.Notifier = ¬ify.NoopSender{}
|
||||
}
|
||||
|
||||
artifactStore := env.ArtifactStore
|
||||
paths, err := artifactStore.EnsureLayout(cfg.Session.SessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare workdir: %w", err)
|
||||
}
|
||||
|
||||
lock, err := store.AcquireSessionLock(cfg.Session.SessionID)
|
||||
lock, err := artifactStore.AcquireSessionLock(cfg.Session.SessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acquire session lock: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = store.ReleaseSessionLock(lock)
|
||||
_ = artifactStore.ReleaseSessionLock(lock)
|
||||
}()
|
||||
|
||||
manifestStore := &manifest.LocalStore{}
|
||||
manifestPath := paths.ManifestPath
|
||||
|
||||
m, err := loadOrCreateManifest(ctx, manifestStore, manifestPath, cfg.Session.SessionID)
|
||||
m, err := loadOrCreateManifest(ctx, env.ManifestStore, manifestPath, cfg.Session.SessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
env := &Env{
|
||||
Config: cfg,
|
||||
ArtifactStore: store,
|
||||
Logger: logging.NewLogger(os.Stderr, slog.LevelInfo),
|
||||
}
|
||||
stageEnv := toStageEnv(env)
|
||||
|
||||
_ = opts // TODO: use --force behavior in future skip/stale logic.
|
||||
_ = opts.Force // TODO: use --force behavior in future skip/stale logic.
|
||||
|
||||
runNames := make([]string, 0, len(stages))
|
||||
for _, s := range stages {
|
||||
@@ -62,14 +97,14 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
|
||||
|
||||
now := nowUTC()
|
||||
m.MarkStageRunning(s.Name(), now)
|
||||
if err := manifestStore.Save(ctx, manifestPath, m); err != nil {
|
||||
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
|
||||
return nil, fmt.Errorf("save manifest before stage %q: %w", s.Name(), err)
|
||||
}
|
||||
|
||||
result, err := s.Run(ctx, stageEnv, m)
|
||||
if err != nil {
|
||||
m.MarkStageFailed(s.Name(), nowUTC(), err.Error())
|
||||
if saveErr := manifestStore.Save(ctx, manifestPath, m); saveErr != nil {
|
||||
if saveErr := env.ManifestStore.Save(ctx, manifestPath, m); saveErr != nil {
|
||||
return nil, fmt.Errorf("stage %q failed (%v) and manifest save failed (%v)", s.Name(), err, saveErr)
|
||||
}
|
||||
return nil, fmt.Errorf("stage %q failed: %w", s.Name(), err)
|
||||
@@ -79,7 +114,7 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
|
||||
m.MarkStageSucceeded(s.Name(), nowUTC(), outputs)
|
||||
applyStageResultToManifest(m, s.Name(), result)
|
||||
|
||||
if err := manifestStore.Save(ctx, manifestPath, m); err != nil {
|
||||
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
|
||||
return nil, fmt.Errorf("save manifest after stage %q: %w", s.Name(), err)
|
||||
}
|
||||
}
|
||||
@@ -91,7 +126,7 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadOrCreateManifest(ctx context.Context, store *manifest.LocalStore, path, sessionID string) (*manifest.Manifest, error) {
|
||||
func loadOrCreateManifest(ctx context.Context, store manifest.Store, path, sessionID string) (*manifest.Manifest, error) {
|
||||
exists, err := fileExists(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check manifest existence %q: %w", path, err)
|
||||
|
||||
@@ -9,6 +9,13 @@ import (
|
||||
"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"
|
||||
@@ -98,24 +105,6 @@ func TestExecuteStagesFailureUpdatesManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteStagesLoadsExistingManifest(t *testing.T) {
|
||||
cfg := testConfig(t)
|
||||
manifestPath := manifestPathFor(cfg)
|
||||
@@ -146,3 +135,67 @@ func TestExecuteStagesLoadsExistingManifest(t *testing.T) {
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user