169 lines
6.0 KiB
Go
169 lines
6.0 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/stage"
|
|
)
|
|
|
|
type terminalSessionStore struct {
|
|
events []string
|
|
saveErr error
|
|
saved *manifest.Manifest
|
|
}
|
|
|
|
type successfulStageSaveFailingStore struct {
|
|
delegate manifest.Store
|
|
stageName string
|
|
dependent string
|
|
failed bool
|
|
}
|
|
|
|
func (s *successfulStageSaveFailingStore) Create(ctx context.Context, sessionID string) (*manifest.Manifest, error) {
|
|
return s.delegate.Create(ctx, sessionID)
|
|
}
|
|
|
|
func (s *successfulStageSaveFailingStore) Load(ctx context.Context, path string) (*manifest.Manifest, error) {
|
|
return s.delegate.Load(ctx, path)
|
|
}
|
|
|
|
func (s *successfulStageSaveFailingStore) Save(ctx context.Context, path string, m *manifest.Manifest) error {
|
|
record := m.Stages[s.stageName]
|
|
if !s.failed && record != nil && record.Status == manifest.StatusSucceeded {
|
|
s.failed = true
|
|
m.MarkStageSucceeded(s.dependent, time.Now().UTC(), nil)
|
|
return errors.New("injected successful-stage save failure")
|
|
}
|
|
return s.delegate.Save(ctx, path, m)
|
|
}
|
|
|
|
func (s *terminalSessionStore) Create(_ context.Context, sessionID string) (*manifest.Manifest, error) {
|
|
return manifest.New(sessionID, time.Now().UTC()), nil
|
|
}
|
|
|
|
func (s *terminalSessionStore) Load(context.Context, string) (*manifest.Manifest, error) {
|
|
return nil, errors.New("unexpected manifest load")
|
|
}
|
|
|
|
func (s *terminalSessionStore) Save(_ context.Context, _ string, m *manifest.Manifest) error {
|
|
s.events = append(s.events, "session")
|
|
s.saved = m
|
|
return s.saveErr
|
|
}
|
|
|
|
func TestPersistSessionTerminalFailurePreservesCauseAndSessionAuthority(t *testing.T) {
|
|
cause := errors.New("stage failed")
|
|
sessionFailure := errors.New("session unavailable")
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
sessionErr error
|
|
cause error
|
|
alreadyTerminal bool
|
|
}{
|
|
{name: "session saved", cause: cause},
|
|
{name: "session save fails", sessionErr: sessionFailure, cause: cause},
|
|
{name: "cancellation", cause: context.Canceled},
|
|
{name: "already terminal session", cause: cause, alreadyTerminal: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
sessionStore := &terminalSessionStore{saveErr: tc.sessionErr}
|
|
sessionManifest := manifest.New("2026-05-03", time.Now().UTC())
|
|
if tc.alreadyTerminal {
|
|
sessionManifest.RecordFailure(time.Now().UTC(), "earlier failure")
|
|
}
|
|
|
|
err := persistSessionTerminalFailure(
|
|
context.Background(), sessionStore, "session.json", sessionManifest, tc.cause,
|
|
time.Now().UTC(),
|
|
)
|
|
if !errors.Is(err, tc.cause) {
|
|
t.Fatalf("error = %v, want original cause %v", err, tc.cause)
|
|
}
|
|
if tc.sessionErr != nil && !errors.Is(err, tc.sessionErr) {
|
|
t.Fatalf("error = %v, want session failure %v", err, tc.sessionErr)
|
|
}
|
|
if got := sessionStore.events; len(got) != 1 || got[0] != "session" {
|
|
t.Fatalf("save calls = %v, want [session]", got)
|
|
}
|
|
if sessionStore.saved == nil || sessionStore.saved.LastError == nil || sessionStore.saved.LastError.Message != tc.cause.Error() {
|
|
t.Fatalf("session terminal error = %#v, want %q", sessionStore.saved, tc.cause)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteStagesTerminalizesCancelledStage(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
|
|
_, err := executeStages(context.Background(), cfg, []stage.Stage{
|
|
failingStage{name: "prepare", err: context.Canceled},
|
|
}, RunOptions{})
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("executeStages() error = %v, want context cancellation", err)
|
|
}
|
|
store := &manifest.LocalStore{}
|
|
sessionManifest, loadErr := store.Load(context.Background(), manifestPathFor(cfg))
|
|
if loadErr != nil {
|
|
t.Fatalf("load session manifest: %v", loadErr)
|
|
}
|
|
if sessionManifest.Stages["prepare"] == nil || sessionManifest.Stages["prepare"].Status != manifest.StatusFailed || sessionManifest.LastError == nil {
|
|
t.Fatalf("session terminal record = %#v, want failed prepare stage and last error", sessionManifest)
|
|
}
|
|
}
|
|
|
|
func TestExecuteStagesResumeValidationFailureRecordsSessionFailure(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
store := &manifest.LocalStore{}
|
|
seed := manifest.New(cfg.Session.SessionID, time.Now().UTC())
|
|
seed.MarkStageSucceeded("checked", time.Now().UTC(), nil)
|
|
if err := store.Save(context.Background(), manifestPathFor(cfg), seed); err != nil {
|
|
t.Fatalf("save session manifest: %v", err)
|
|
}
|
|
_, err := executeStages(context.Background(), cfg, []stage.Stage{
|
|
resumeCheckingStage{name: "checked", validateErr: errors.New("inspection unavailable"), runs: new(int)},
|
|
}, RunOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "inspection unavailable") {
|
|
t.Fatalf("executeStages() error = %v, want resume validation failure", err)
|
|
}
|
|
persisted, loadErr := store.Load(context.Background(), manifestPathFor(cfg))
|
|
if loadErr != nil {
|
|
t.Fatalf("load session manifest: %v", loadErr)
|
|
}
|
|
if persisted.Stages["checked"].Status != manifest.StatusSucceeded || persisted.LastError == nil {
|
|
t.Fatalf("session manifest = %#v, want preserved success with terminal error", persisted)
|
|
}
|
|
}
|
|
|
|
func TestExecuteStagesSuccessfulSaveFailureMarksStageFailedAndInvalidatesDependents(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
store := &successfulStageSaveFailingStore{
|
|
delegate: &manifest.LocalStore{},
|
|
stageName: "polish",
|
|
dependent: "normalize",
|
|
}
|
|
|
|
_, err := executeStages(context.Background(), cfg, []stage.Stage{
|
|
countingStage{name: "polish", runs: new(int)},
|
|
}, RunOptions{Env: &Env{ManifestStore: store}})
|
|
if err == nil || !strings.Contains(err.Error(), "save manifest after stage \"polish\"") {
|
|
t.Fatalf("executeStages() error = %v, want successful-stage save failure", err)
|
|
}
|
|
|
|
persisted, loadErr := store.delegate.Load(context.Background(), manifestPathFor(cfg))
|
|
if loadErr != nil {
|
|
t.Fatalf("load session manifest: %v", loadErr)
|
|
}
|
|
if got := persisted.Stages["polish"]; got == nil || got.Status != manifest.StatusFailed {
|
|
t.Fatalf("polish stage = %#v, want failed", got)
|
|
}
|
|
if got := persisted.Stages["normalize"]; got == nil || got.Status != manifest.StatusStale {
|
|
t.Fatalf("normalize stage = %#v, want stale", got)
|
|
}
|
|
}
|