package manifest import ( "testing" "time" ) func TestStageMarkHelpers(t *testing.T) { m := New("2026-05-03", time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC)) runningAt := time.Date(2026, 5, 3, 12, 1, 0, 0, time.UTC) m.MarkStageRunning("transcribe", runningAt) stage := m.Stages["transcribe"] if stage == nil { t.Fatal("stage not created") } if stage.Status != StatusRunning { t.Fatalf("status = %q, want %q", stage.Status, StatusRunning) } if stage.StartedAt == nil || !stage.StartedAt.Equal(runningAt) { t.Fatalf("started_at = %v, want %v", stage.StartedAt, runningAt) } succeededAt := runningAt.Add(2 * time.Minute) outputs := []ArtifactRecord{{Kind: "transcript_polished", LocalPath: "transcripts/polished.json"}} m.MarkStageSucceeded("transcribe", succeededAt, outputs) if stage.Status != StatusSucceeded { t.Fatalf("status = %q, want %q", stage.Status, StatusSucceeded) } if stage.CompletedAt == nil || !stage.CompletedAt.Equal(succeededAt) { t.Fatalf("completed_at = %v, want %v", stage.CompletedAt, succeededAt) } if len(stage.Outputs) != 1 { t.Fatalf("outputs len = %d, want 1", len(stage.Outputs)) } outputs[0].LocalPath = "mutated.json" if stage.Outputs[0].LocalPath != "transcripts/polished.json" { t.Fatalf("stage outputs should be copied, got %#v", stage.Outputs) } failedAt := succeededAt.Add(1 * time.Minute) m.MarkStageFailed("analyze", failedAt, "analyzer crashed") failed := m.Stages["analyze"] if failed == nil { t.Fatal("failed stage missing") } if failed.Status != StatusFailed { t.Fatalf("status = %q, want %q", failed.Status, StatusFailed) } if failed.Error == nil || failed.Error.Message != "analyzer crashed" { t.Fatalf("error = %#v, want message", failed.Error) } skippedAt := failedAt.Add(1 * time.Minute) m.MarkStageSkipped("notify", skippedAt, "notifications disabled") skipped := m.Stages["notify"] if skipped == nil { t.Fatal("skipped stage missing") } if skipped.Status != StatusSkipped { t.Fatalf("status = %q, want %q", skipped.Status, StatusSkipped) } if skipped.Error == nil || skipped.Error.Message != "notifications disabled" { t.Fatalf("error = %#v, want skip reason", skipped.Error) } if skipped.Error.Code != "skipped" { t.Fatalf("error code = %q, want %q", skipped.Error.Code, "skipped") } } func TestMarkStageRunningClearsPriorCompletionErrorAndResultDetails(t *testing.T) { m := New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) failedAt := time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC) m.MarkStageFailed("merge", failedAt, "boom") setStageResultDetails(m.Stages["merge"]) runAt := failedAt.Add(1 * time.Minute) m.MarkStageRunning("merge", runAt) stage := m.Stages["merge"] if stage == nil { t.Fatal("missing stage record") } if stage.Status != StatusRunning { t.Fatalf("status = %q, want %q", stage.Status, StatusRunning) } if stage.CompletedAt != nil { t.Fatalf("completed_at = %v, want nil while running", stage.CompletedAt) } if stage.Error != nil { t.Fatalf("error = %#v, want nil while running", stage.Error) } requireStageResultDetailsCleared(t, stage) } func TestMarkStageSucceededClearsError(t *testing.T) { m := New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) m.MarkStageFailed("analyze", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), "failed once") m.MarkStageSucceeded("analyze", time.Date(2026, 5, 3, 10, 2, 0, 0, time.UTC), nil) stage := m.Stages["analyze"] if stage == nil { t.Fatal("missing stage record") } if stage.Status != StatusSucceeded { t.Fatalf("status = %q, want %q", stage.Status, StatusSucceeded) } if stage.Error != nil { t.Fatalf("error = %#v, want nil on success", stage.Error) } } func TestMarkStageFailedClearsEarlierResultDetails(t *testing.T) { m := New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) m.MarkStageSucceeded("extract", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) setStageResultDetails(m.Stages["extract"]) m.MarkStageFailed("extract", time.Date(2026, 5, 3, 10, 2, 0, 0, time.UTC), "replacement failed") stage := m.Stages["extract"] if stage == nil || stage.Status != StatusFailed { t.Fatalf("stage = %#v, want failed", stage) } requireStageResultDetailsCleared(t, stage) } func TestMarkStageSkippedClearsEarlierResultDetails(t *testing.T) { m := New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) m.MarkStageSucceeded("extract", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) setStageResultDetails(m.Stages["extract"]) m.MarkStageSkipped("extract", time.Date(2026, 5, 3, 10, 2, 0, 0, time.UTC), "integration_disabled") stage := m.Stages["extract"] if stage == nil { t.Fatal("missing stage record") } if stage.Status != StatusSkipped { t.Fatalf("status = %q, want %q", stage.Status, StatusSkipped) } requireStageResultDetailsCleared(t, stage) } func TestMarkStageStalePreservesResultDetails(t *testing.T) { m := New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) m.MarkStageSucceeded("extract", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) stage := m.Stages["extract"] setStageResultDetails(stage) m.MarkStageStale("extract", time.Date(2026, 5, 3, 10, 2, 0, 0, time.UTC), "result is not resumable") if stage.Status != StatusStale { t.Fatalf("status = %q, want %q", stage.Status, StatusStale) } if len(stage.Outputs) != 1 || len(stage.Logs) != 1 || len(stage.GeneratedConfigs) != 1 || len(stage.Metadata) != 1 { t.Fatalf("result details were not preserved: %#v", stage) } } func setStageResultDetails(stage *StageRecord) { stage.Outputs = []ArtifactRecord{{ Kind: "structured_data", SourceID: "narratio.example.characters", LocalPath: "artifacts/characters.json", }} stage.Logs = []string{"logs/extract.log"} stage.GeneratedConfigs = []string{"generated/extract.yaml"} stage.Metadata = map[string]any{"bundle_path": "extract/results/run-1"} } func requireStageResultDetailsCleared(t *testing.T, stage *StageRecord) { t.Helper() if len(stage.Outputs) != 0 || len(stage.Logs) != 0 || len(stage.GeneratedConfigs) != 0 || len(stage.Metadata) != 0 { t.Fatalf("result details were not cleared: %#v", stage) } }