Unify pre-execution stage decisions
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -11,23 +12,185 @@ import (
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/stage"
|
||||
)
|
||||
|
||||
func TestDecideStageAction(t *testing.T) {
|
||||
stages := BuildFullPlan()[:2]
|
||||
m := manifest.New("2026-05-03", time.Now().UTC())
|
||||
m.MarkStageSucceeded("prepare", time.Now().UTC(), nil)
|
||||
|
||||
if got := decideStageAction(stages[0], m, false); got != stageActionSkip {
|
||||
t.Fatalf("prepare action = %q, want %q", got, stageActionSkip)
|
||||
func TestDecideStageExecution(t *testing.T) {
|
||||
statuses := []manifest.StageStatus{"", manifest.StatusSucceeded, manifest.StatusFailed, manifest.StatusStale, manifest.StatusSkipped}
|
||||
validations := []struct {
|
||||
name string
|
||||
value *stage.ResumeValidation
|
||||
}{
|
||||
{name: "none"},
|
||||
{name: "resumable", value: resumeValidation(stage.Resumable())},
|
||||
{name: "not resumable", value: resumeValidation(stage.NonResumable("output changed"))},
|
||||
}
|
||||
if got := decideStageAction(stages[1], m, false); got != stageActionRun {
|
||||
t.Fatalf("transcribe action = %q, want %q", got, stageActionRun)
|
||||
for _, status := range statuses {
|
||||
for _, force := range []bool{false, true} {
|
||||
for _, validation := range validations {
|
||||
name := fmt.Sprintf("status=%s/force=%t/resume=%s", status, force, validation.name)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := decideStageExecution(status, force, validation.value)
|
||||
want := preExecutionDecision{action: stageActionRun}
|
||||
switch {
|
||||
case force:
|
||||
want.invalidationReason = staleReasonForcedReplacement
|
||||
want.invalidateWhenStarted = true
|
||||
case status == manifest.StatusSucceeded && validation.value != nil && !validation.value.Resumable:
|
||||
want.markSelectedStale = true
|
||||
want.selectedStaleReason = "output changed"
|
||||
want.invalidationReason = staleReasonNotResumable
|
||||
case status == manifest.StatusSucceeded:
|
||||
want.action = stageActionSkip
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("decision = %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resumeValidation(value stage.ResumeValidation) *stage.ResumeValidation { return &value }
|
||||
|
||||
func TestPlanningAndExecutionTransitionParity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
seed func(*manifest.Manifest, time.Time)
|
||||
force bool
|
||||
resume *stage.ResumeValidation
|
||||
result *stage.StageResult
|
||||
wantAction stageAction
|
||||
wantStatus manifest.StageStatus
|
||||
}{
|
||||
{
|
||||
name: "ordinary skip",
|
||||
seed: func(m *manifest.Manifest, at time.Time) {
|
||||
m.MarkStageSucceeded("prepare", at, nil)
|
||||
m.MarkStageSucceeded("transcribe", at, nil)
|
||||
},
|
||||
wantAction: stageActionSkip,
|
||||
wantStatus: manifest.StatusSucceeded,
|
||||
},
|
||||
{
|
||||
name: "non-resumable success",
|
||||
seed: func(m *manifest.Manifest, at time.Time) {
|
||||
m.MarkStageSucceeded("prepare", at, nil)
|
||||
m.MarkStageSucceeded("transcribe", at, nil)
|
||||
},
|
||||
resume: resumeValidation(stage.NonResumable("output changed")),
|
||||
result: &stage.StageResult{},
|
||||
wantAction: stageActionRun,
|
||||
wantStatus: manifest.StatusStale,
|
||||
},
|
||||
{
|
||||
name: "forced replacement",
|
||||
seed: func(m *manifest.Manifest, at time.Time) {
|
||||
m.MarkStageSucceeded("prepare", at, nil)
|
||||
m.MarkStageSucceeded("transcribe", at, nil)
|
||||
},
|
||||
force: true,
|
||||
result: &stage.StageResult{},
|
||||
wantAction: stageActionRun,
|
||||
wantStatus: manifest.StatusStale,
|
||||
},
|
||||
{
|
||||
name: "unchanged self-skip",
|
||||
seed: func(m *manifest.Manifest, at time.Time) {
|
||||
m.MarkStageSkipped("prepare", at, "disabled")
|
||||
m.MarkStageSucceeded("transcribe", at, nil)
|
||||
},
|
||||
result: &stage.StageResult{
|
||||
Disposition: stage.StageDispositionSkipped,
|
||||
SkipReason: "disabled",
|
||||
},
|
||||
wantAction: stageActionRun,
|
||||
wantStatus: manifest.StatusSucceeded,
|
||||
},
|
||||
{
|
||||
name: "changed self-skip",
|
||||
seed: func(m *manifest.Manifest, at time.Time) {
|
||||
m.MarkStageSkipped("prepare", at, "old reason")
|
||||
m.MarkStageSucceeded("transcribe", at, nil)
|
||||
},
|
||||
result: &stage.StageResult{
|
||||
Disposition: stage.StageDispositionSkipped,
|
||||
SkipReason: "new reason",
|
||||
},
|
||||
wantAction: stageActionRun,
|
||||
wantStatus: manifest.StatusStale,
|
||||
},
|
||||
{
|
||||
name: "success replacing non-success",
|
||||
seed: func(m *manifest.Manifest, at time.Time) {
|
||||
m.MarkStageFailed("prepare", at, "failed")
|
||||
m.MarkStageSucceeded("transcribe", at, nil)
|
||||
},
|
||||
result: &stage.StageResult{},
|
||||
wantAction: stageActionRun,
|
||||
wantStatus: manifest.StatusStale,
|
||||
},
|
||||
}
|
||||
|
||||
if got := decideStageAction(stages[0], m, true); got != stageActionRun {
|
||||
t.Fatalf("forced prepare action = %q, want %q", got, stageActionRun)
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
at := time.Date(2026, time.May, 3, 12, 0, 0, 0, time.UTC)
|
||||
planned := manifest.New("2026-05-03", at)
|
||||
executed := manifest.New("2026-05-03", at)
|
||||
test.seed(planned, at)
|
||||
test.seed(executed, at)
|
||||
|
||||
plannedAction := applyParityTransition(t, planned, test.force, test.resume, test.result, false, at.Add(time.Second))
|
||||
executedAction := applyParityTransition(t, executed, test.force, test.resume, test.result, true, at.Add(time.Second))
|
||||
if plannedAction != test.wantAction || executedAction != test.wantAction {
|
||||
t.Fatalf("actions = planned %q, executed %q; want %q", plannedAction, executedAction, test.wantAction)
|
||||
}
|
||||
if got := planned.Stages["transcribe"].Status; got != test.wantStatus {
|
||||
t.Fatalf("planned dependent status = %q, want %q", got, test.wantStatus)
|
||||
}
|
||||
if got := executed.Stages["transcribe"].Status; got != test.wantStatus {
|
||||
t.Fatalf("executed dependent status = %q, want %q", got, test.wantStatus)
|
||||
}
|
||||
plannedError := planned.Stages["transcribe"].Error
|
||||
executedError := executed.Stages["transcribe"].Error
|
||||
if !reflect.DeepEqual(plannedError, executedError) {
|
||||
t.Fatalf("dependent errors differ: planned %#v, executed %#v", plannedError, executedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func applyParityTransition(
|
||||
t *testing.T,
|
||||
m *manifest.Manifest,
|
||||
force bool,
|
||||
resume *stage.ResumeValidation,
|
||||
result *stage.StageResult,
|
||||
actual bool,
|
||||
at time.Time,
|
||||
) stageAction {
|
||||
t.Helper()
|
||||
decision := decideStageExecution(recordedStageStatus(m, "prepare"), force, resume)
|
||||
if _, err := applyPreExecutionDecision(m, "prepare", decision, at); err != nil {
|
||||
t.Fatalf("apply pre-execution decision: %v", err)
|
||||
}
|
||||
if decision.action == stageActionSkip {
|
||||
return decision.action
|
||||
}
|
||||
prior := capturePriorStageOutcome(m, "prepare")
|
||||
if _, err := beginStageAttempt(m, "prepare", decision, at); err != nil {
|
||||
t.Fatalf("begin attempt: %v", err)
|
||||
}
|
||||
if result != nil {
|
||||
modeled := *result
|
||||
if actual {
|
||||
modeled.Logs = []string{"run.log"}
|
||||
}
|
||||
if _, err := applyCompletedStageOutcome(m, "prepare", &modeled, nil, nil, prior, at); err != nil {
|
||||
t.Fatalf("apply completed outcome: %v", err)
|
||||
}
|
||||
}
|
||||
return decision.action
|
||||
}
|
||||
|
||||
func TestInvalidationDependents(t *testing.T) {
|
||||
tests := []struct {
|
||||
stage string
|
||||
|
||||
Reference in New Issue
Block a user