Enforce continuous validation
This commit is contained in:
@@ -3,259 +3,16 @@ package stage
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/audita"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/notify"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestStagesReturnExpectedMetadata(t *testing.T) {
|
||||
stages := All()
|
||||
if len(stages) == 0 {
|
||||
t.Fatal("expected non-empty stage list")
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
store := artifacts.NewLocalStore(root)
|
||||
cfgDir := t.TempDir()
|
||||
sessionPath := filepath.Join(cfgDir, "session.yml")
|
||||
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||
writeStageTestFile(t, sessionPath, "session_id: 2026-05-03\n")
|
||||
writeStageTestFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n")
|
||||
writeStageTestFile(t, pipelinePath, "workspace:\n root: "+root+"\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "audio", "alice.flac"), "a")
|
||||
|
||||
wf := &whisperx.FakeClient{}
|
||||
sf := &seriatim.FakeRunner{}
|
||||
af := &audita.FakeRunner{}
|
||||
sc := &scriptorium.FakeRunner{}
|
||||
st := &storage.FakeBackend{}
|
||||
nf := ¬ify.FakeSender{}
|
||||
|
||||
env := &Env{
|
||||
Config: &config.Config{
|
||||
SessionPath: sessionPath,
|
||||
CampaignPath: campaignPath,
|
||||
PipelinePath: pipelinePath,
|
||||
Campaign: &config.CampaignConfig{CampaignID: "sample-campaign"},
|
||||
Pipeline: &config.PipelineConfig{
|
||||
Workspace: config.WorkspaceConfig{Root: root},
|
||||
Storage: config.StorageConfig{
|
||||
S3: &config.StorageS3Config{
|
||||
Bucket: "my-dnd-archive",
|
||||
RootPrefix: "dnd",
|
||||
},
|
||||
},
|
||||
Publish: &config.PublishConfig{
|
||||
Enabled: boolPtr(true),
|
||||
UploadRun: boolPtr(true),
|
||||
},
|
||||
},
|
||||
StableInputs: config.ResolvedStableInputs{
|
||||
SpeakersFile: config.ResolvedInputFile{
|
||||
Path: "./speakers.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
AutocorrectFile: config.ResolvedInputFile{
|
||||
Path: "./autocorrect.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
GlossaryFile: config.ResolvedInputFile{
|
||||
Path: "./glossary.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PlayersFile: config.ResolvedInputFile{
|
||||
Path: "./players.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PartyFile: config.ResolvedInputFile{
|
||||
Path: "./party.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
},
|
||||
Session: &config.SessionConfig{
|
||||
SessionID: "2026-05-03",
|
||||
Campaign: "sample-campaign",
|
||||
Inputs: config.SessionInputsConfig{
|
||||
AudioDir: "./audio",
|
||||
},
|
||||
},
|
||||
},
|
||||
ArtifactStore: store,
|
||||
WhisperX: wf,
|
||||
Seriatim: sf,
|
||||
Audita: af,
|
||||
Scriptorium: sc,
|
||||
ObjectStore: st,
|
||||
Notifier: nf,
|
||||
}
|
||||
|
||||
m := manifest.New("2026-05-03", time.Now().UTC())
|
||||
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "extract", "render", "analyze"} {
|
||||
m.MarkStageSucceeded(name, time.Now().UTC(), nil)
|
||||
}
|
||||
m.RunID = "20260516T000000Z-abcdef12"
|
||||
m.Campaign = "sample-campaign"
|
||||
m.LocalWorkDir = artifacts.SessionRunRootForCampaign(root, "sample-campaign", "2026-05-03", m.RunID)
|
||||
m.S3RunPrefix = "dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/" + m.RunID + "/"
|
||||
m.S3Bucket = "my-dnd-archive"
|
||||
if err := os.MkdirAll(filepath.Join(m.LocalWorkDir, "inputs"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir workdir inputs: %v", err)
|
||||
}
|
||||
writeStageTestFile(t, filepath.Join(m.LocalWorkDir, "inputs", "session.yml"), "session_id: 2026-05-03\n")
|
||||
writePublishRunManifest(t, m.LocalWorkDir, m.SessionID, m.Campaign, m.RunID, nil)
|
||||
for _, s := range stages {
|
||||
result, err := s.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("stage %q returned unexpected error: %v", s.Name(), err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatalf("stage %q returned nil result", s.Name())
|
||||
}
|
||||
if s.Name() == "prepare" {
|
||||
if result.Metadata["prepared"] != true {
|
||||
t.Fatalf("prepare metadata = %#v, want prepared=true", result.Metadata)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "transcribe" {
|
||||
if result.Metadata["stage"] != "transcribe" {
|
||||
t.Fatalf("transcribe metadata = %#v, want stage=transcribe", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) == 0 {
|
||||
t.Fatalf("transcribe outputs = %#v, want non-empty", result.Outputs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "merge" {
|
||||
if result.Metadata["stage"] != "merge" {
|
||||
t.Fatalf("merge metadata = %#v, want stage=merge", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) == 0 {
|
||||
t.Fatalf("merge outputs = %#v, want non-empty", result.Outputs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "polish" {
|
||||
if result.Metadata["stage"] != "polish" {
|
||||
t.Fatalf("polish metadata = %#v, want stage=polish", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) == 0 {
|
||||
t.Fatalf("polish outputs = %#v, want non-empty", result.Outputs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "normalize" {
|
||||
if result.Metadata["stage"] != "normalize" {
|
||||
t.Fatalf("normalize metadata = %#v, want stage=normalize", result.Metadata)
|
||||
}
|
||||
if result.Metadata["output_schema"] == nil {
|
||||
t.Fatalf("normalize metadata = %#v, want output_schema metadata", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final" {
|
||||
t.Fatalf("normalize outputs = %#v, want transcript_final output", result.Outputs)
|
||||
}
|
||||
if len(result.Logs) != 2 {
|
||||
t.Fatalf("normalize logs = %#v, want stdout+stderr", result.Logs)
|
||||
}
|
||||
if len(result.GeneratedConfigs) != 1 {
|
||||
t.Fatalf("normalize generated configs = %#v, want one path", result.GeneratedConfigs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "analyze" {
|
||||
if result.Metadata["stage"] != "analyze" {
|
||||
t.Fatalf("analyze metadata = %#v, want stage=analyze", result.Metadata)
|
||||
}
|
||||
if result.Metadata["skipped"] != true {
|
||||
t.Fatalf("analyze metadata = %#v, want skipped=true when scriptorium is unconfigured", result.Metadata)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "trim" {
|
||||
if result.Metadata["stage"] != "trim" {
|
||||
t.Fatalf("trim metadata = %#v, want stage=trim", result.Metadata)
|
||||
}
|
||||
if result.Metadata["trim_action"] != "copy_disabled" {
|
||||
t.Fatalf("trim metadata = %#v, want trim_action=copy_disabled", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final_trimmed" {
|
||||
t.Fatalf("trim outputs = %#v, want transcript_final_trimmed output", result.Outputs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "extract" {
|
||||
if result.Disposition != StageDispositionSkipped || result.SkipReason != extractSkipReason {
|
||||
t.Fatalf("extract result = %#v, want disabled self-skip", result)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "render" {
|
||||
if result.Metadata["stage"] != "render" {
|
||||
t.Fatalf("render metadata = %#v, want stage=render", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) != 2 {
|
||||
t.Fatalf("render outputs = %#v, want 2 markdown outputs", result.Outputs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.Name() == "publish" {
|
||||
if result.Metadata["stage"] != "publish" {
|
||||
t.Fatalf("publish metadata = %#v, want stage=publish", result.Metadata)
|
||||
}
|
||||
if result.Metadata["uploaded"] != true {
|
||||
t.Fatalf("publish metadata = %#v, want uploaded=true", result.Metadata)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if result.Metadata["placeholder"] != true {
|
||||
t.Fatalf("stage %q missing placeholder metadata", s.Name())
|
||||
}
|
||||
}
|
||||
|
||||
whisperXRequests := wf.RequestsSnapshot()
|
||||
if len(whisperXRequests) != 1 {
|
||||
t.Fatalf("whisperx calls = %d, want 1", len(whisperXRequests))
|
||||
}
|
||||
if len(sf.Requests) != 1 {
|
||||
t.Fatalf("seriatim calls = %d, want 1", len(sf.Requests))
|
||||
}
|
||||
if len(af.Requests) != 1 {
|
||||
t.Fatalf("audita calls = %d, want 1", len(af.Requests))
|
||||
}
|
||||
if len(sc.RunRequests) != 0 {
|
||||
t.Fatalf("scriptorium run calls = %d, want 0 when scriptorium config is absent", len(sc.RunRequests))
|
||||
}
|
||||
if _, ok := st.Objects["dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/"+m.RunID+"/manifest.json"]; !ok {
|
||||
t.Fatalf("publish upload missing manifest key in fake object store")
|
||||
}
|
||||
if len(nf.Requests) != 1 {
|
||||
t.Fatalf("notify calls = %d, want 1", len(nf.Requests))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceholderAdapterErrorPropagation(t *testing.T) {
|
||||
cases := []struct {
|
||||
stageName string
|
||||
@@ -292,13 +49,3 @@ func TestPlaceholderAdapterErrorPropagation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeStageTestFile(t *testing.T, path, contents string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(%q): %v", path, err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(%q): %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1203,3 +1203,13 @@ func boolPtr(v bool) *bool {
|
||||
p := v
|
||||
return &p
|
||||
}
|
||||
|
||||
func writeStageTestFile(t *testing.T, path, contents string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(%q): %v", path, err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(%q): %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user