| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "reasonix/internal/control" |
| 7 | "reasonix/internal/jobs" |
| 8 | "reasonix/internal/workspacelease" |
| 9 | ) |
| 10 | |
| 11 | type backgroundRuntimeController struct { |
| 12 | control.SessionAPI |
| 13 | status control.RuntimeStatus |
| 14 | jobs []jobs.View |
| 15 | lease workspacelease.State |
| 16 | cancelled []string |
| 17 | turnCancel int |
| 18 | } |
| 19 | |
| 20 | func (c *backgroundRuntimeController) RuntimeStatus() control.RuntimeStatus { return c.status } |
| 21 | func (c *backgroundRuntimeController) Jobs() []jobs.View { |
| 22 | return append([]jobs.View(nil), c.jobs...) |
| 23 | } |
| 24 | func (c *backgroundRuntimeController) CancelJob(id string) bool { |
| 25 | for _, job := range c.jobs { |
| 26 | if job.ID == id { |
| 27 | c.cancelled = append(c.cancelled, id) |
| 28 | return true |
| 29 | } |
| 30 | } |
| 31 | return false |
| 32 | } |
| 33 | func (c *backgroundRuntimeController) Cancel() { c.turnCancel++ } |
| 34 | func (c *backgroundRuntimeController) WorkspaceLeaseState() workspacelease.State { |
| 35 | return c.lease |
| 36 | } |
| 37 | |
| 38 | func TestBackgroundRuntimeAPIsKeepDetachedJobsActionable(t *testing.T) { |
| 39 | targetCtrl := &backgroundRuntimeController{ |
| 40 | status: control.RuntimeStatus{BackgroundJobs: 1}, |
| 41 | jobs: []jobs.View{{ID: "job-target", Kind: "bash", Label: "target tests", Status: "running", StartedAt: 1}}, |
| 42 | } |
| 43 | focusedCtrl := &backgroundRuntimeController{ |
| 44 | status: control.RuntimeStatus{BackgroundJobs: 1}, |
| 45 | jobs: []jobs.View{{ID: "job-focused", Kind: "bash", Label: "focused tests", Status: "running", StartedAt: 2}}, |
| 46 | } |
| 47 | target := &WorkspaceTab{ID: "target", TopicTitle: "Detached task", Ctrl: targetCtrl} |
| 48 | focused := &WorkspaceTab{ID: "focused", TopicTitle: "Focused task", Ctrl: focusedCtrl} |
| 49 | app := &App{ |
| 50 | tabs: map[string]*WorkspaceTab{"focused": focused}, |
| 51 | tabOrder: []string{"focused"}, |
| 52 | activeTabID: "focused", |
| 53 | detachedSessions: map[string]*WorkspaceTab{"session-target": target}, |
| 54 | } |
| 55 | |
| 56 | work := app.ActiveWorkForTab("target") |
| 57 | if len(work.Jobs) != 1 || work.Jobs[0].ID != "job-target" { |
| 58 | t.Fatalf("ActiveWorkForTab(target) = %+v, want target job", work) |
| 59 | } |
| 60 | result, err := app.CancelJobsForTab("target", []string{"job-target", "job-target", "missing"}) |
| 61 | if err != nil { |
| 62 | t.Fatalf("CancelJobsForTab: %v", err) |
| 63 | } |
| 64 | if len(result.Cancelled) != 1 || result.Cancelled[0] != "job-target" || len(result.NotRunning) != 1 || result.NotRunning[0] != "missing" { |
| 65 | t.Fatalf("CancelJobsForTab result = %+v", result) |
| 66 | } |
| 67 | if len(targetCtrl.cancelled) != 1 || len(focusedCtrl.cancelled) != 0 { |
| 68 | t.Fatalf("cancellation routing target=%v focused=%v", targetCtrl.cancelled, focusedCtrl.cancelled) |
| 69 | } |
| 70 | |
| 71 | runtimes := app.BackgroundRuntimes() |
| 72 | if len(runtimes) != 2 { |
| 73 | t.Fatalf("BackgroundRuntimes = %+v, want visible and detached", runtimes) |
| 74 | } |
| 75 | var foundDetached bool |
| 76 | for _, runtime := range runtimes { |
| 77 | if runtime.TabID == "target" { |
| 78 | foundDetached = runtime.Detached && runtime.Title == "Detached task" && len(runtime.Jobs) == 1 |
| 79 | } |
| 80 | } |
| 81 | if !foundDetached { |
| 82 | t.Fatalf("detached runtime missing from %+v", runtimes) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func TestCancelJobForLocalRuntimeByExplicitTab(t *testing.T) { |
| 87 | localCtrl := &backgroundRuntimeController{ |
| 88 | status: control.RuntimeStatus{BackgroundJobs: 1}, |
| 89 | jobs: []jobs.View{{ID: "job-local", Kind: "bash", Status: "running"}}, |
| 90 | } |
| 91 | app := &App{ |
| 92 | tabs: map[string]*WorkspaceTab{"local": {ID: "local", Ctrl: localCtrl}}, |
| 93 | tabOrder: []string{"local"}, |
| 94 | activeTabID: "local", |
| 95 | } |
| 96 | |
| 97 | listed := app.JobsForTab("local") |
| 98 | if len(listed) != 1 || listed[0].ID != "job-local" { |
| 99 | t.Fatalf("JobsForTab(local) = %+v, want local job", listed) |
| 100 | } |
| 101 | cancelled, err := app.CancelJobForTab("local", "job-local") |
| 102 | if err != nil { |
| 103 | t.Fatalf("CancelJobForTab(local): %v", err) |
| 104 | } |
| 105 | if !cancelled || len(localCtrl.cancelled) != 1 || localCtrl.cancelled[0] != "job-local" { |
| 106 | t.Fatalf("local cancellation = %t, calls=%v; want local job cancelled", cancelled, localCtrl.cancelled) |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | func TestWorkspaceConflictIdentifiesExactLocalWriterWithoutIdentity(t *testing.T) { |
| 111 | root := t.TempDir() |
| 112 | waiterCtrl := &backgroundRuntimeController{ |
| 113 | status: control.RuntimeStatus{Running: true}, |
| 114 | lease: workspacelease.State{Waiting: true}, |
| 115 | } |
| 116 | ownerCtrl := &backgroundRuntimeController{ |
| 117 | status: control.RuntimeStatus{BackgroundJobs: 1}, |
| 118 | jobs: []jobs.View{{ID: "owner-job", Kind: "bash", Status: "running"}}, |
| 119 | lease: workspacelease.State{Acquired: true}, |
| 120 | } |
| 121 | app := &App{ |
| 122 | tabs: map[string]*WorkspaceTab{ |
| 123 | "waiter": {ID: "waiter", WorkspaceRoot: root, Ctrl: waiterCtrl}, |
| 124 | "owner": {ID: "owner", TopicTitle: "Writing task", WorkspaceRoot: root, Ctrl: ownerCtrl}, |
| 125 | }, |
| 126 | tabOrder: []string{"waiter", "owner"}, |
| 127 | activeTabID: "waiter", |
| 128 | } |
| 129 | |
| 130 | conflict := app.WorkspaceConflictForTab("waiter") |
| 131 | if conflict.State != "local" || conflict.OwnerTabID != "owner" || conflict.OwnerTitle != "Writing task" || !conflict.CanReveal { |
| 132 | t.Fatalf("WorkspaceConflictForTab = %+v, want exact local owner", conflict) |
| 133 | } |
| 134 | if len(conflict.OwnerWork.Jobs) != 1 || conflict.OwnerWork.Jobs[0].ID != "owner-job" { |
| 135 | t.Fatalf("owner work = %+v, want sanitized active work", conflict.OwnerWork) |
| 136 | } |
| 137 | } |
| 138 |