Define adapter interfaces and fake implementations

This commit is contained in:
2026-05-02 11:16:27 -05:00
parent 78e7e4f41b
commit 2854da48c2
24 changed files with 876 additions and 73 deletions

View File

@@ -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,
}
}

View File

@@ -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 = &notify.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)

View File

@@ -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: &notify.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",
},
},
}
}