|
|
|
|
@@ -12,11 +12,8 @@ import (
|
|
|
|
|
"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/artifactmodel"
|
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
|
|
|
@@ -502,102 +499,6 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) {
|
|
|
|
|
if sr.Status != manifest.StatusSucceeded {
|
|
|
|
|
t.Fatalf("stage %q status = %q, want %q", name, sr.Status, manifest.StatusSucceeded)
|
|
|
|
|
}
|
|
|
|
|
if name == "prepare" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["prepared"] != true {
|
|
|
|
|
t.Fatalf("prepare metadata missing prepared=true: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "transcribe" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "transcribe" {
|
|
|
|
|
t.Fatalf("transcribe metadata missing stage=transcribe: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Outputs) == 0 {
|
|
|
|
|
t.Fatalf("transcribe outputs missing")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "merge" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "merge" {
|
|
|
|
|
t.Fatalf("merge metadata missing stage=merge: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Outputs) == 0 {
|
|
|
|
|
t.Fatalf("merge outputs missing")
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Logs) == 0 {
|
|
|
|
|
t.Fatalf("merge logs missing")
|
|
|
|
|
}
|
|
|
|
|
if len(sr.GeneratedConfigs) == 0 {
|
|
|
|
|
t.Fatalf("merge generated configs missing")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "polish" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "polish" {
|
|
|
|
|
t.Fatalf("polish metadata missing stage=polish: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Outputs) == 0 {
|
|
|
|
|
t.Fatalf("polish outputs missing")
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Logs) == 0 {
|
|
|
|
|
t.Fatalf("polish logs missing")
|
|
|
|
|
}
|
|
|
|
|
if len(sr.GeneratedConfigs) == 0 {
|
|
|
|
|
t.Fatalf("polish generated configs missing")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "normalize" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "normalize" {
|
|
|
|
|
t.Fatalf("normalize metadata missing stage=normalize: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Outputs) == 0 {
|
|
|
|
|
t.Fatalf("normalize outputs missing")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "analyze" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "analyze" {
|
|
|
|
|
t.Fatalf("analyze metadata missing stage=analyze: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if sr.Metadata["skipped"] != true {
|
|
|
|
|
t.Fatalf("analyze metadata missing skipped=true when scriptorium is unconfigured: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "trim" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "trim" {
|
|
|
|
|
t.Fatalf("trim metadata missing stage=trim: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if sr.Metadata["trim_action"] != "copy_disabled" {
|
|
|
|
|
t.Fatalf("trim metadata missing trim_action=copy_disabled: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Outputs) == 0 {
|
|
|
|
|
t.Fatalf("trim outputs missing")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "render" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "render" {
|
|
|
|
|
t.Fatalf("render metadata missing stage=render: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if len(sr.Outputs) == 0 {
|
|
|
|
|
t.Fatalf("render outputs missing")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name == "publish" {
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["stage"] != "publish" {
|
|
|
|
|
t.Fatalf("publish metadata missing stage=publish: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
if sr.Metadata["skipped"] != true {
|
|
|
|
|
t.Fatalf("publish metadata missing skipped=true for test config without publish section: %#v", sr.Metadata)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if sr.Metadata == nil || sr.Metadata["placeholder"] != true {
|
|
|
|
|
t.Fatalf("stage %q missing placeholder metadata", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(m.Inputs) == 0 {
|
|
|
|
|
t.Fatalf("manifest inputs should be recorded by prepare")
|
|
|
|
|
@@ -946,14 +847,6 @@ func TestExecuteStagesCreatesRunManifestPerInvocation(t *testing.T) {
|
|
|
|
|
t.Fatalf("first executeStages() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
store := &manifest.LocalStore{}
|
|
|
|
|
sessionManifest, err := store.Load(context.Background(), run1.ManifestPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load first session manifest error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
sessionManifest.Inputs = append(sessionManifest.Inputs, manifest.InputRecord{Kind: "audio", Path: "audio/alice.flac"})
|
|
|
|
|
if err := store.Save(context.Background(), run1.ManifestPath, sessionManifest); err != nil {
|
|
|
|
|
t.Fatalf("Save session history error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
run2, err := executeStages(context.Background(), cfg, []stage.Stage{BuildFullPlan()[0]}, RunOptions{Force: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("second executeStages() error = %v", err)
|
|
|
|
|
@@ -977,15 +870,15 @@ func TestExecuteStagesCreatesRunManifestPerInvocation(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sessionManifest, err = store.Load(context.Background(), run2.ManifestPath)
|
|
|
|
|
sessionManifest, err := store.Load(context.Background(), run2.ManifestPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load session manifest error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if sessionManifest.RunID != run2.RunID {
|
|
|
|
|
t.Fatalf("session manifest run_id = %q, want latest run id %q", sessionManifest.RunID, run2.RunID)
|
|
|
|
|
}
|
|
|
|
|
if len(sessionManifest.Inputs) != 1 || sessionManifest.Inputs[0].Path != "audio/alice.flac" {
|
|
|
|
|
t.Fatalf("session history inputs = %#v, want preserved input", sessionManifest.Inputs)
|
|
|
|
|
if len(sessionManifest.Inputs) == 0 {
|
|
|
|
|
t.Fatal("session manifest inputs are empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, run := range []*RunSummary{run1, run2} {
|
|
|
|
|
@@ -1367,153 +1260,46 @@ func TestExecuteStagesSkippedStagePreservesExistingOutputsProvenance(t *testing.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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{Scriptorium: &scriptorium.FakeRunner{RunErr: errors.New("analyze fail")}}},
|
|
|
|
|
{name: "publish", env: &Env{ObjectStore: &storage.FakeBackend{UploadErr: errors.New("publish fail")}}},
|
|
|
|
|
{name: "notify", env: &Env{Notifier: ¬ify.FakeSender{Err: errors.New("notify fail")}}},
|
|
|
|
|
func TestAdapterFailureMarksManifestFailed(t *testing.T) {
|
|
|
|
|
cfg := testConfig(t)
|
|
|
|
|
selected, err := stage.Select("polish")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Select() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
env := &Env{
|
|
|
|
|
Audita: &audita.FakeRunner{Err: errors.New("polish fail")},
|
|
|
|
|
Config: cfg,
|
|
|
|
|
ArtifactStore: artifactStore,
|
|
|
|
|
ManifestStore: &manifest.LocalStore{},
|
|
|
|
|
}
|
|
|
|
|
paths, err := artifactStore.EnsureLayoutFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("EnsureLayout() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "base.json"), []byte(`{"segments":[]}`), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write merged transcript: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(paths.InputsDir, "glossary.yml"), []byte("terms: []\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write glossary: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifactStore := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root)
|
|
|
|
|
tc.env.Config = cfg
|
|
|
|
|
tc.env.ArtifactStore = artifactStore
|
|
|
|
|
tc.env.ManifestStore = &manifest.LocalStore{}
|
|
|
|
|
if tc.name == "transcribe" {
|
|
|
|
|
paths, ensureErr := artifactStore.EnsureLayoutFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
|
|
|
|
if ensureErr != nil {
|
|
|
|
|
t.Fatalf("EnsureLayout() error = %v", ensureErr)
|
|
|
|
|
}
|
|
|
|
|
audioPath := filepath.Join(paths.AudioDir, "alice.flac")
|
|
|
|
|
if err := os.WriteFile(audioPath, []byte("audio"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write transcribe fixture audio: %v", err)
|
|
|
|
|
}
|
|
|
|
|
m := manifest.New(cfg.Session.SessionID, time.Now().UTC())
|
|
|
|
|
m.Inputs = append(m.Inputs, manifest.InputRecord{Kind: "audio", Path: audioPath})
|
|
|
|
|
if err := tc.env.ManifestStore.Save(context.Background(), manifestPathFor(cfg), m); err != nil {
|
|
|
|
|
t.Fatalf("seed manifest: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if tc.name == "merge" {
|
|
|
|
|
paths, ensureErr := artifactStore.EnsureLayoutFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
|
|
|
|
if ensureErr != nil {
|
|
|
|
|
t.Fatalf("EnsureLayout() error = %v", ensureErr)
|
|
|
|
|
}
|
|
|
|
|
rawPath := filepath.Join(paths.TranscriptsRawDir, "alice.json")
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(rawPath), 0o755); err != nil {
|
|
|
|
|
t.Fatalf("mkdir raw dir: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(rawPath, []byte(`{"segments":[]}`), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write raw transcript: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(paths.InputsDir, "speakers.yml"), []byte("match: []\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write speakers: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(paths.InputsDir, "autocorrect.yml"), []byte("rules: []\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write autocorrect: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if tc.name == "polish" {
|
|
|
|
|
paths, ensureErr := artifactStore.EnsureLayoutFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
|
|
|
|
if ensureErr != nil {
|
|
|
|
|
t.Fatalf("EnsureLayout() error = %v", ensureErr)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "base.json"), []byte(`{"segments":[]}`), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write merged transcript: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(paths.InputsDir, "glossary.yml"), []byte("terms: []\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write glossary: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if tc.name == "analyze" {
|
|
|
|
|
paths, ensureErr := artifactStore.EnsureLayoutFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
|
|
|
|
if ensureErr != nil {
|
|
|
|
|
t.Fatalf("EnsureLayout() error = %v", ensureErr)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "polished.json"), []byte(`{"segments":[]}`), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write processed transcript: %v", err)
|
|
|
|
|
}
|
|
|
|
|
cfg.Pipeline.Scriptorium = &config.ScriptoriumConfig{
|
|
|
|
|
Binary: "scriptorium",
|
|
|
|
|
Timeout: "10m",
|
|
|
|
|
Artifacts: map[string]config.ScriptoriumArtifactConfig{
|
|
|
|
|
"session_recap": {
|
|
|
|
|
Enabled: true,
|
|
|
|
|
PromptID: "dnd.session_recap",
|
|
|
|
|
OutputPath: "artifacts/session_recap.md",
|
|
|
|
|
Inputs: map[string]config.ScriptoriumInputConfig{
|
|
|
|
|
"transcript": {Source: "narratio.transcript.polished", Required: true},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if tc.name == "publish" {
|
|
|
|
|
cfg.Pipeline.Publish = &config.PublishConfig{
|
|
|
|
|
Enabled: boolPtr(true),
|
|
|
|
|
UploadRun: boolPtr(true),
|
|
|
|
|
}
|
|
|
|
|
cfg.Pipeline.Storage.S3 = &config.StorageS3Config{
|
|
|
|
|
Bucket: "my-dnd-archive",
|
|
|
|
|
RootPrefix: "dnd",
|
|
|
|
|
}
|
|
|
|
|
runID := "20260516T010203Z-0a1b2c3d"
|
|
|
|
|
runWorkDir := filepath.Join(cfg.Pipeline.Workspace.Root, "work", cfg.Session.Campaign, cfg.Session.SessionID, runID)
|
|
|
|
|
if err := os.MkdirAll(filepath.Join(runWorkDir, "inputs"), 0o755); err != nil {
|
|
|
|
|
t.Fatalf("mkdir publish inputs dir: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(runWorkDir, "inputs", "session.yml"), []byte("session_id: 2026-05-03\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write publish fixture session.yml: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(runWorkDir, "manifest.json"), []byte("{}\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write publish fixture manifest.json: %v", err)
|
|
|
|
|
}
|
|
|
|
|
_, runErr := executeStages(context.Background(), cfg, []stage.Stage{selected}, RunOptions{Env: env})
|
|
|
|
|
if runErr == nil {
|
|
|
|
|
t.Fatal("expected error, got nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seed := manifest.New(cfg.Session.SessionID, time.Now().UTC())
|
|
|
|
|
seed.Campaign = cfg.Session.Campaign
|
|
|
|
|
seed.RunID = runID
|
|
|
|
|
seed.LocalWorkDir = runWorkDir
|
|
|
|
|
seed.S3Bucket = "my-dnd-archive"
|
|
|
|
|
seed.S3SessionPrefix = "dnd/campaigns/" + cfg.Session.Campaign + "/sessions/" + cfg.Session.SessionID + "/"
|
|
|
|
|
seed.S3RunPrefix = seed.S3SessionPrefix + "runs/" + runID + "/"
|
|
|
|
|
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "extract", "render", "analyze"} {
|
|
|
|
|
seed.MarkStageSucceeded(name, time.Now().UTC(), nil)
|
|
|
|
|
}
|
|
|
|
|
if err := tc.env.ManifestStore.Save(context.Background(), manifestPathFor(cfg), seed); err != nil {
|
|
|
|
|
t.Fatalf("seed publish manifest: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, 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)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
m, err := env.ManifestStore.Load(context.Background(), manifestPathFor(cfg))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("load manifest error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
record := m.Stages["polish"]
|
|
|
|
|
if record == nil {
|
|
|
|
|
t.Fatal("missing polish stage record")
|
|
|
|
|
}
|
|
|
|
|
if record.Status != manifest.StatusFailed {
|
|
|
|
|
t.Fatalf("status = %q, want %q", record.Status, manifest.StatusFailed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|