Clear superseded session stage result details
This commit is contained in:
@@ -34,7 +34,7 @@ implementation sequence.
|
||||
| Stage 13 | Complete |
|
||||
| Stage 14 | Complete |
|
||||
| Stage 15 | Complete |
|
||||
| Stage 16 | Pending |
|
||||
| Stage 16 | Complete |
|
||||
| Stage 17 | Pending |
|
||||
| Stage 18 | Pending |
|
||||
|
||||
|
||||
@@ -171,10 +171,20 @@ func TestExtractLifecycleForcedSelfSkipInvalidatesDownstream(t *testing.T) {
|
||||
|
||||
func TestExtractLifecycleForcedFailureInvalidatesDownstream(t *testing.T) {
|
||||
cfg, env, runner := extractionLifecycleFixture(t, true)
|
||||
runner.failuresRemaining = 1
|
||||
markLifecycleStageSucceeded(t, cfg, "analyze")
|
||||
plan, _ := BuildSingleStagePlan("extract")
|
||||
|
||||
first, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env})
|
||||
if err != nil {
|
||||
t.Fatalf("initial executeStages() error = %v", err)
|
||||
}
|
||||
succeeded := loadLifecycleManifest(t, cfg).Stages["extract"]
|
||||
if succeeded == nil || succeeded.Status != manifest.StatusSucceeded || len(succeeded.Outputs) == 0 || len(succeeded.Logs) == 0 || len(succeeded.Metadata) == 0 {
|
||||
t.Fatalf("initial extraction result = %#v, want succeeded result details", succeeded)
|
||||
}
|
||||
|
||||
markLifecycleStageSucceeded(t, cfg, "analyze")
|
||||
runner.failuresRemaining = 1
|
||||
|
||||
if _, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env, Force: true}); err == nil {
|
||||
t.Fatal("executeStages() error = nil, want forced extraction failure")
|
||||
}
|
||||
@@ -185,6 +195,21 @@ func TestExtractLifecycleForcedFailureInvalidatesDownstream(t *testing.T) {
|
||||
if loaded.Stages["analyze"].Error == nil || loaded.Stages["analyze"].Error.Message != staleReasonForcedReplacement {
|
||||
t.Fatalf("analyze stale reason = %#v, want %q", loaded.Stages["analyze"].Error, staleReasonForcedReplacement)
|
||||
}
|
||||
failed := loaded.Stages["extract"]
|
||||
if len(failed.Outputs) != 0 || len(failed.Logs) != 0 || len(failed.GeneratedConfigs) != 0 || len(failed.Metadata) != 0 {
|
||||
t.Fatalf("failed replacement inherited extraction result details: %#v", failed)
|
||||
}
|
||||
historical, err := (&manifest.LocalStore{}).LoadRun(context.Background(), first.RunManifestPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRun(initial) error = %v", err)
|
||||
}
|
||||
historicalExtract := historical.Stages["extract"]
|
||||
if historicalExtract == nil || historicalExtract.Status != manifest.StatusSucceeded || len(historicalExtract.Outputs) == 0 || len(historicalExtract.Logs) == 0 || len(historicalExtract.Metadata) == 0 {
|
||||
t.Fatalf("historical extraction result = %#v, want preserved succeeded details", historicalExtract)
|
||||
}
|
||||
if _, err := os.Stat(succeeded.Outputs[0].LocalPath); err != nil {
|
||||
t.Fatalf("durable extraction output was not preserved: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractLifecycleRepeatedSelfSkipPreservesSucceededDownstream(t *testing.T) {
|
||||
|
||||
@@ -248,7 +248,6 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
|
||||
skipped = append(skipped, s.Name())
|
||||
skippedAt := nowUTC()
|
||||
m.MarkStageSkipped(s.Name(), skippedAt, result.SkipReason)
|
||||
clearStageResultDetails(m.Stages[s.Name()])
|
||||
applyStageResultToManifest(m, s.Name(), result)
|
||||
if !priorOutcome.isSameSelfSkip(result.SkipReason) {
|
||||
invalidateDownstreamSucceededStagesWithReason(m, s.Name(), skippedAt, staleReasonSelfSkip)
|
||||
@@ -562,15 +561,6 @@ func applyStageResultToManifest(m *manifest.Manifest, stageName string, result *
|
||||
}
|
||||
}
|
||||
|
||||
func clearStageResultDetails(sr *manifest.StageRecord) {
|
||||
if sr == nil {
|
||||
return
|
||||
}
|
||||
sr.Logs = nil
|
||||
sr.GeneratedConfigs = nil
|
||||
sr.Metadata = nil
|
||||
}
|
||||
|
||||
func ensureManifestIdentity(cfg *config.Config, m *manifest.Manifest, runID string) (bool, error) {
|
||||
if cfg == nil || cfg.Pipeline == nil || cfg.Session == nil || m == nil {
|
||||
return false, nil
|
||||
|
||||
@@ -742,6 +742,41 @@ func TestExecuteStagesForceRerunsSucceeded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteStagesSuccessfulReplacementDoesNotInheritResultDetails(t *testing.T) {
|
||||
cfg := testConfig(t)
|
||||
manifestPath := manifestPathFor(cfg)
|
||||
store := &manifest.LocalStore{}
|
||||
|
||||
existing := manifest.New(cfg.Session.SessionID, time.Date(2026, 5, 3, 1, 0, 0, 0, time.UTC))
|
||||
existing.MarkStageSucceeded("transcribe", time.Date(2026, 5, 3, 1, 1, 0, 0, time.UTC), []manifest.ArtifactRecord{{
|
||||
Kind: "transcript_raw", LocalPath: "transcripts/old.json",
|
||||
}})
|
||||
existingStage := existing.Stages["transcribe"]
|
||||
existingStage.Logs = []string{"logs/old.log"}
|
||||
existingStage.GeneratedConfigs = []string{"generated/old.yaml"}
|
||||
existingStage.Metadata = map[string]any{"old_result": true}
|
||||
if err := store.Save(context.Background(), manifestPath, existing); err != nil {
|
||||
t.Fatalf("Save manifest error = %v", err)
|
||||
}
|
||||
|
||||
runs := 0
|
||||
replacement := resultStage{name: "transcribe", result: &stage.StageResult{}, runs: &runs}
|
||||
if _, err := executeStages(context.Background(), cfg, []stage.Stage{replacement}, RunOptions{Force: true}); err != nil {
|
||||
t.Fatalf("executeStages() error = %v", err)
|
||||
}
|
||||
loaded, err := store.Load(context.Background(), manifestPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load manifest error = %v", err)
|
||||
}
|
||||
record := loaded.Stages["transcribe"]
|
||||
if runs != 1 || record == nil || record.Status != manifest.StatusSucceeded {
|
||||
t.Fatalf("runs = %d, record = %#v, want one successful replacement", runs, record)
|
||||
}
|
||||
if len(record.Outputs) != 0 || len(record.Logs) != 0 || len(record.GeneratedConfigs) != 0 || len(record.Metadata) != 0 {
|
||||
t.Fatalf("replacement inherited result details: %#v", record)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteStagesForceSuccessInvalidatesDownstreamSucceededStages(t *testing.T) {
|
||||
cfg := testConfig(t)
|
||||
manifestPath := manifestPathFor(cfg)
|
||||
|
||||
@@ -88,6 +88,7 @@ func New(sessionID string, now time.Time) *Manifest {
|
||||
// MarkStageRunning marks a stage as running and updates timestamps.
|
||||
func (m *Manifest) MarkStageRunning(name string, at time.Time) {
|
||||
s := m.ensureStage(name, at)
|
||||
s.clearResultDetails()
|
||||
s.Status = StatusRunning
|
||||
s.StartedAt = timePtr(at)
|
||||
s.CompletedAt = nil
|
||||
@@ -110,6 +111,7 @@ func (m *Manifest) MarkStageSucceeded(name string, at time.Time, outputs []Artif
|
||||
// MarkStageFailed marks a stage as failed and records error metadata.
|
||||
func (m *Manifest) MarkStageFailed(name string, at time.Time, message string) {
|
||||
s := m.ensureStage(name, at)
|
||||
s.clearResultDetails()
|
||||
s.Status = StatusFailed
|
||||
s.CompletedAt = timePtr(at)
|
||||
s.Error = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)}
|
||||
@@ -121,10 +123,10 @@ func (m *Manifest) MarkStageFailed(name string, at time.Time, message string) {
|
||||
// MarkStageSkipped marks a stage as skipped and records the skip reason.
|
||||
func (m *Manifest) MarkStageSkipped(name string, at time.Time, reason string) {
|
||||
s := m.ensureStage(name, at)
|
||||
s.clearResultDetails()
|
||||
s.Status = StatusSkipped
|
||||
s.CompletedAt = timePtr(at)
|
||||
s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)}
|
||||
s.Outputs = nil
|
||||
s.UpdatedAt = at
|
||||
m.UpdatedAt = at
|
||||
}
|
||||
@@ -138,6 +140,13 @@ func (m *Manifest) MarkStageStale(name string, at time.Time, reason string) {
|
||||
m.UpdatedAt = at
|
||||
}
|
||||
|
||||
func (s *StageRecord) clearResultDetails() {
|
||||
s.Outputs = nil
|
||||
s.Logs = nil
|
||||
s.GeneratedConfigs = nil
|
||||
s.Metadata = nil
|
||||
}
|
||||
|
||||
func (m *Manifest) ensureStage(name string, at time.Time) *StageRecord {
|
||||
if m.Stages == nil {
|
||||
m.Stages = map[string]*StageRecord{}
|
||||
|
||||
@@ -69,11 +69,12 @@ func TestStageMarkHelpers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkStageRunningClearsPriorCompletionAndError(t *testing.T) {
|
||||
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)
|
||||
@@ -91,6 +92,7 @@ func TestMarkStageRunningClearsPriorCompletionAndError(t *testing.T) {
|
||||
if stage.Error != nil {
|
||||
t.Fatalf("error = %#v, want nil while running", stage.Error)
|
||||
}
|
||||
requireStageResultDetailsCleared(t, stage)
|
||||
}
|
||||
|
||||
func TestMarkStageSucceededClearsError(t *testing.T) {
|
||||
@@ -111,11 +113,24 @@ func TestMarkStageSucceededClearsError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkStageSkippedClearsEarlierOutputs(t *testing.T) {
|
||||
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), []ArtifactRecord{
|
||||
{Kind: "structured_data", SourceID: "narratio.example.characters", LocalPath: "artifacts/characters.json"},
|
||||
})
|
||||
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")
|
||||
|
||||
@@ -126,7 +141,39 @@ func TestMarkStageSkippedClearsEarlierOutputs(t *testing.T) {
|
||||
if stage.Status != StatusSkipped {
|
||||
t.Fatalf("status = %q, want %q", stage.Status, StatusSkipped)
|
||||
}
|
||||
if len(stage.Outputs) != 0 {
|
||||
t.Fatalf("outputs = %#v, want cleared", stage.Outputs)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user