| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "context" |
| 6 | "fmt" |
| 7 | "os" |
| 8 | "os/exec" |
| 9 | "sync/atomic" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "reasonix/desktop/internal/sessionui" |
| 14 | ) |
| 15 | |
| 16 | func TestManualCreationManagerProcess(t *testing.T) { |
| 17 | path := os.Getenv("REASONIX_TEST_CREATION_STORE") |
| 18 | if path == "" { |
| 19 | return |
| 20 | } |
| 21 | a := NewApp() |
| 22 | a.ctx = t.Context() |
| 23 | installNoopRuntimeEvents(a) |
| 24 | a.sessionUI = sessionui.New(path) |
| 25 | m := a.creationManager() |
| 26 | m.execute = func(context.Context, ManualSessionCreationView, func(string)) error { |
| 27 | fmt.Println("executing") |
| 28 | _, _ = bufio.NewReader(os.Stdin).ReadString('\n') |
| 29 | return nil |
| 30 | } |
| 31 | m.Ensure("two-process-creation", "recovery", "") |
| 32 | a.manualCreationTasks.Wait() |
| 33 | if err := a.stopManualCreations(); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | _ = a.sessionUI.Close() |
| 37 | } |
| 38 | |
| 39 | func TestManualCreationTwoManagersRespectCommittedResult(t *testing.T) { |
| 40 | a := newManualSessionTestApp(t) |
| 41 | v, _ := seedManualCreation(t, a, "two-process-creation", "starting") |
| 42 | cmd := exec.Command(os.Args[0], "-test.run=^TestManualCreationManagerProcess$") |
| 43 | cmd.Env = append(os.Environ(), "REASONIX_TEST_CREATION_STORE="+a.sessionUIStore().Path()) |
| 44 | output, err := cmd.StdoutPipe() |
| 45 | if err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | input, err := cmd.StdinPipe() |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | cmd.Stderr = os.Stderr |
| 53 | if err = cmd.Start(); err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | t.Cleanup(func() { |
| 57 | _ = input.Close() |
| 58 | if cmd.ProcessState == nil { |
| 59 | _ = cmd.Process.Kill() |
| 60 | _ = cmd.Wait() |
| 61 | } |
| 62 | }) |
| 63 | line, err := bufio.NewReader(output).ReadString('\n') |
| 64 | if err != nil || line != "executing\n" { |
| 65 | t.Fatalf("owner handshake: %q %v", line, err) |
| 66 | } |
| 67 | m := a.creationManager() |
| 68 | var executions atomic.Int32 |
| 69 | m.execute = func(context.Context, ManualSessionCreationView, func(string)) error { executions.Add(1); return nil } |
| 70 | m.Ensure(v.OperationID, "recovery", "") |
| 71 | waitFor(t, "second manager waits", func() bool { p := m.Snapshot(v.OperationID); return p != nil && p.Status == "waiting_lock" }) |
| 72 | _ = input.Close() |
| 73 | if err = cmd.Wait(); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | a.manualCreationTasks.Wait() |
| 77 | got, err := a.GetManualSessionCreation(v.OperationID) |
| 78 | if err != nil || got.Phase != "ready" || executions.Load() != 0 { |
| 79 | t.Fatalf("competing execution: %+v count=%d err=%v", got, executions.Load(), err) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | func TestManualCreationStaleRetryCannotRestartNewerFailure(t *testing.T) { |
| 84 | a := newManualSessionTestApp(t) |
| 85 | v, record := seedManualCreation(t, a, "stale-retry-creation", "failed") |
| 86 | if _, err := a.sessionUIStore().Save(t.Context(), "creation", record.Key, record.Revision, record.Payload); err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | m := a.creationManager() |
| 90 | var executions atomic.Int32 |
| 91 | m.execute = func(context.Context, ManualSessionCreationView, func(string)) error { executions.Add(1); return nil } |
| 92 | m.Ensure(v.OperationID, "retry", record.Revision) |
| 93 | a.manualCreationTasks.Wait() |
| 94 | if executions.Load() != 0 { |
| 95 | t.Fatal("stale request restarted a newer failure") |
| 96 | } |
| 97 | if _, err := a.RetryManualSessionCreation(v.OperationID); err != nil { |
| 98 | t.Fatal(err) |
| 99 | } |
| 100 | a.manualCreationTasks.Wait() |
| 101 | if executions.Load() != 1 { |
| 102 | t.Fatal("fresh explicit retry did not execute") |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | func TestManualCreationSlowProgressIsObservationOnly(t *testing.T) { |
| 107 | a := &App{} |
| 108 | m := &manualCreationManager{a: a, tasks: make(map[string]*manualCreationTask), now: func() time.Time { return time.Unix(100, 0) }} |
| 109 | task := &manualCreationTask{id: "slow-operation", progress: ManualCreationProgress{Status: "running", Stage: "building_runtime", StageStartedAt: 1}} |
| 110 | m.tasks[task.id] = task |
| 111 | p := m.Snapshot(task.id) |
| 112 | if !p.Slow || p.Status != "running" || len(m.tasks) != 1 { |
| 113 | t.Fatalf("slow status triggered a transition: %+v", p) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestManualCreationLateStageCannotReplaceNewerGeneration(t *testing.T) { |
| 118 | m := &manualCreationManager{tasks: make(map[string]*manualCreationTask), now: time.Now} |
| 119 | task := &manualCreationTask{id: "late-stage", running: true} |
| 120 | m.tasks[task.id] = task |
| 121 | m.setStageForGeneration(task, "running", "building_runtime", 2) |
| 122 | m.setStageForGeneration(task, "running", "publishing_controller", 1) |
| 123 | if got := m.Snapshot(task.id); got.Stage != "building_runtime" { |
| 124 | t.Fatalf("late stage replaced generation 2: %+v", got) |
| 125 | } |
| 126 | } |
| 127 |