Enforce continuous validation

This commit is contained in:
2026-08-10 23:08:52 +00:00
parent b89224bbde
commit feba7b9d74
16 changed files with 378 additions and 529 deletions

View File

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

View File

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