| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "testing" |
| 6 | |
| 7 | "reasonix/desktop/internal/sessionui" |
| 8 | "reasonix/internal/session" |
| 9 | ) |
| 10 | |
| 11 | func newManualSessionTestApp(t *testing.T) *App { |
| 12 | t.Helper() |
| 13 | isolateDesktopUserDirs(t) |
| 14 | a := NewApp() |
| 15 | a.ctx = t.Context() |
| 16 | installNoopRuntimeEvents(a) |
| 17 | t.Cleanup(func() { |
| 18 | if err := a.stopManualCreations(); err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | for _, tab := range a.tabs { |
| 22 | if tab.Ctrl != nil { |
| 23 | tab.Ctrl.Close() |
| 24 | if closed, ok := tab.Ctrl.(interface{ Closed() <-chan struct{} }); ok { |
| 25 | <-closed.Closed() |
| 26 | } |
| 27 | } |
| 28 | } |
| 29 | a.closeSessionServices() |
| 30 | _ = a.sessionUI.Close() |
| 31 | _ = a.desktopDrafts.Close() |
| 32 | }) |
| 33 | return a |
| 34 | } |
| 35 | |
| 36 | func TestManualCreationUniqueIdempotentAndVisibleBeforeFirstSend(t *testing.T) { |
| 37 | a := newManualSessionTestApp(t) |
| 38 | for _, id := range []string{"manual-test-one", "manual-test-two", "manual-test-three"} { |
| 39 | first, err := a.BeginManualSessionCreation(ManualSessionCreationRequest{OperationID: id, Scope: "global"}) |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | retry, err := a.BeginManualSessionCreation(ManualSessionCreationRequest{OperationID: id, Scope: "global"}) |
| 44 | if err != nil || retry.Ref != first.Ref { |
| 45 | t.Fatalf("replayed identity=%+v %v", retry, err) |
| 46 | } |
| 47 | } |
| 48 | a.manualCreationTasks.Wait() |
| 49 | for _, id := range []string{"manual-test-one", "manual-test-two", "manual-test-three"} { |
| 50 | view, err := a.GetManualSessionCreation(id) |
| 51 | if err != nil || view.Phase != "ready" { |
| 52 | t.Fatalf("runtime did not start: %+v %v", view, err) |
| 53 | } |
| 54 | } |
| 55 | state, err := a.workspaceRegistry().Load(t.Context()) |
| 56 | if err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | if len(state.Workspaces["global"].SessionIDs) != 3 { |
| 60 | t.Fatalf("session ids=%v", state.Workspaces["global"].SessionIDs) |
| 61 | } |
| 62 | for _, id := range state.Workspaces["global"].SessionIDs { |
| 63 | snapshot, err := a.desktopSessionService("").Query().Snapshot(t.Context(), session.SessionRef{HostID: "local", SessionID: id}) |
| 64 | if err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | for _, message := range snapshot.Projection.Messages { |
| 68 | if message.Role != "system" { |
| 69 | t.Fatal("creation submitted a non-system message") |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | if drafts, err := a.ListSessionDraftSummaries(); err != nil || len(drafts) != 0 { |
| 74 | t.Fatalf("new path created drafts: %+v %v", drafts, err) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func TestComposerRestartConflictAndUnknownSubmission(t *testing.T) { |
| 79 | a := newManualSessionTestApp(t) |
| 80 | op, err := a.BeginManualSessionCreation(ManualSessionCreationRequest{OperationID: "composer-test-create", Scope: "global"}) |
| 81 | if err != nil { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | a.manualCreationTasks.Wait() |
| 85 | input, err := a.GetSessionComposerState(op.Ref) |
| 86 | if err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | req := SessionComposerSaveRequest{Ref: op.Ref, ExpectedRevision: input.Revision, ContentVersion: 1, ContentJSON: `{"text":"keep me","attachments":[],"future":{"keep":true}}`} |
| 90 | saved, err := a.SaveSessionComposerState(req) |
| 91 | if err != nil || saved.Conflict { |
| 92 | t.Fatalf("save=%+v %v", saved, err) |
| 93 | } |
| 94 | conflict, err := a.SaveSessionComposerState(req) |
| 95 | if err != nil || !conflict.Conflict { |
| 96 | t.Fatalf("CAS=%+v %v", conflict, err) |
| 97 | } |
| 98 | path := a.sessionUI.Path() |
| 99 | _ = a.sessionUI.Close() |
| 100 | a.sessionUI = sessionui.New(path) |
| 101 | restored, err := a.GetSessionComposerState(op.Ref) |
| 102 | if err != nil || restored.ContentJSON != req.ContentJSON { |
| 103 | t.Fatalf("restored=%+v %v", restored, err) |
| 104 | } |
| 105 | pending, err := a.BeginSessionComposerSubmission(op.Ref, saved.Revision, "composer-pending", `{"input":"hello"}`) |
| 106 | if err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | unknown, err := a.GetSessionComposerState(op.Ref) |
| 110 | if err != nil || unknown.SubmissionPhase != "unknown" || unknown.ContentJSON != req.ContentJSON { |
| 111 | t.Fatalf("unknown=%+v %v", unknown, err) |
| 112 | } |
| 113 | req.ExpectedRevision = pending.Revision |
| 114 | if _, err := a.SaveSessionComposerState(req); err == nil { |
| 115 | t.Fatal("pending input was overwritten") |
| 116 | } |
| 117 | accepted, err := a.CompleteSessionComposerSubmission(op.Ref, "composer-pending", "accepted") |
| 118 | if err != nil || accepted.ContentJSON != "{}" { |
| 119 | t.Fatalf("accepted=%+v %v", accepted, err) |
| 120 | } |
| 121 | if !json.Valid([]byte(accepted.ContentJSON)) { |
| 122 | t.Fatal("invalid saved content") |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | func TestPreviousDraftAPIWillNotCreateNewRecords(t *testing.T) { |
| 127 | a := newManualSessionTestApp(t) |
| 128 | if _, err := a.OpenSessionDraftForTarget("global", ""); err == nil { |
| 129 | t.Fatal("legacy API created a draft") |
| 130 | } |
| 131 | rows, err := a.ListSessionDraftSummaries() |
| 132 | if err != nil || len(rows) != 0 { |
| 133 | t.Fatalf("legacy rows=%+v %v", rows, err) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | func TestManualCreationCancelledBuildRetainsIdentityForRetry(t *testing.T) { |
| 138 | a := newManualSessionTestApp(t) |
| 139 | started, release := make(chan struct{}), make(chan struct{}) |
| 140 | a.tabBuildStartHook = func(string) { close(started); <-release } |
| 141 | operation, err := a.BeginManualSessionCreation(ManualSessionCreationRequest{OperationID: "cancelled-build-operation", Scope: "global"}) |
| 142 | if err != nil { |
| 143 | t.Fatal(err) |
| 144 | } |
| 145 | <-started |
| 146 | a.shuttingDown.Store(true) |
| 147 | a.cancelAllTabBuilds() |
| 148 | close(release) |
| 149 | a.manualCreationTasks.Wait() |
| 150 | failed, err := a.GetManualSessionCreation(operation.OperationID) |
| 151 | if err != nil || failed.Phase != "starting" || failed.Ref != operation.Ref { |
| 152 | t.Fatalf("cancelled creation=%+v %v", failed, err) |
| 153 | } |
| 154 | a.shuttingDown.Store(false) |
| 155 | a.tabBuildStartHook = nil |
| 156 | if _, err := a.RetryManualSessionCreation(operation.OperationID); err != nil { |
| 157 | t.Fatal(err) |
| 158 | } |
| 159 | a.manualCreationTasks.Wait() |
| 160 | retried, err := a.GetManualSessionCreation(operation.OperationID) |
| 161 | if err != nil || retried.Phase != "ready" || retried.Ref != operation.Ref { |
| 162 | t.Fatalf("retry=%+v %v", retried, err) |
| 163 | } |
| 164 | state, err := a.workspaceRegistry().Load(t.Context()) |
| 165 | if err != nil || len(state.Workspaces["global"].SessionIDs) != 1 { |
| 166 | t.Fatalf("retry created replacement: %+v %v", state, err) |
| 167 | } |
| 168 | } |
| 169 |