| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "strings" |
| 6 | "sync" |
| 7 | "testing" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | func TestRemoteResumeFailurePublicationOrdersRetirement(t *testing.T) { |
| 12 | for _, kind := range []string{"reconnect", "retire", "suspend", "park", "close", "state"} { |
| 13 | t.Run(kind, func(t *testing.T) { |
| 14 | isolateDesktopUserDirs(t) |
| 15 | client := &http.Client{} |
| 16 | tab := &remoteTab{id: "remote-1", state: "ready", client: client, gen: 7, selectionRevision: 9, |
| 17 | ref: RemoteTabRef{HostID: "box", Workspace: "app"}, |
| 18 | session: remoteTabSessionState{name: "target", path: "/target"}, topicTitle: "Target", |
| 19 | routing: remoteTabSessionRouting{currentPath: "/target", pathRevision: 11, running: map[string]bool{}}, |
| 20 | } |
| 21 | a := &App{remoteTabs: map[string]*remoteTab{tab.id: tab}} |
| 22 | previous := &remoteTabOpenSelection{session: remoteTabSessionState{name: "old", path: "/old"}, topicTitle: "Old", currentPath: "/old", revision: 9} |
| 23 | route := a.beginRemoteTabProvisionalResume(tab.id, tab, client, 7, "/target") |
| 24 | route.previousSelection = previous |
| 25 | entered, release := make(chan struct{}), make(chan struct{}) |
| 26 | var releaseOnce sync.Once |
| 27 | unblock := func() { releaseOnce.Do(func() { close(release) }) } |
| 28 | t.Cleanup(unblock) |
| 29 | events := &eventLog{} |
| 30 | a.remoteEventHook = func(name string, payload any) { |
| 31 | if name == "remote-tab:updated" { |
| 32 | close(entered) |
| 33 | <-release |
| 34 | } |
| 35 | events.add(name, payload) |
| 36 | } |
| 37 | finished := make(chan struct{}) |
| 38 | go func() { a.completeRemoteTabResumeFailure(tab.id, tab, client, 7, route, "rejected"); close(finished) }() |
| 39 | select { |
| 40 | case <-entered: |
| 41 | case <-time.After(3 * time.Second): |
| 42 | t.Fatal("failure did not reach metadata publication") |
| 43 | } |
| 44 | attempted, retired := make(chan struct{}), make(chan struct{}) |
| 45 | go func() { |
| 46 | close(attempted) |
| 47 | switch kind { |
| 48 | case "reconnect": |
| 49 | a.reconnectRemoteTabGeneration(tab.id, 7) |
| 50 | case "retire": |
| 51 | a.retireRemoteTabGeneration(tab.id, 7) |
| 52 | case "suspend": |
| 53 | a.suspendRemoteTabPumps("box", "reconnecting", "") |
| 54 | case "park": |
| 55 | a.parkRemoteTabsForServer("box", "app", "serve_down", "") |
| 56 | case "close": |
| 57 | _ = a.closeRemoteTabRegistration(tab.id, true) |
| 58 | case "state": |
| 59 | a.emitRemoteTabStateForGeneration(tab.id, 7, "error", "stream ended") |
| 60 | } |
| 61 | close(retired) |
| 62 | }() |
| 63 | <-attempted |
| 64 | select { |
| 65 | case <-retired: |
| 66 | t.Fatal("retirement overtook an in-flight failure publication") |
| 67 | case <-time.After(30 * time.Millisecond): |
| 68 | } |
| 69 | a.remoteTabMu.Lock() |
| 70 | intact := a.remoteTabs[tab.id] == tab && tab.gen == 7 && tab.state == "ready" && tab.err == "rejected" && tab.session.path == "/old" |
| 71 | a.remoteTabMu.Unlock() |
| 72 | if !intact { |
| 73 | t.Fatal("retirement mutated identity before prior publication completed") |
| 74 | } |
| 75 | unblock() |
| 76 | select { |
| 77 | case <-finished: |
| 78 | case <-time.After(3 * time.Second): |
| 79 | t.Fatal("failure did not finish") |
| 80 | } |
| 81 | select { |
| 82 | case <-retired: |
| 83 | case <-time.After(3 * time.Second): |
| 84 | t.Fatal("retirement did not finish") |
| 85 | } |
| 86 | records := events.recorded() |
| 87 | if len(records) < 2 { |
| 88 | t.Fatalf("missing ordered failure events: %v", records) |
| 89 | } |
| 90 | // The terminal failure follows its metadata; any retirement state follows both. |
| 91 | if !strings.Contains(records[0], "remote-tab:updated") || !strings.Contains(records[1], "rejected") { |
| 92 | t.Fatalf("publication order = %v", records) |
| 93 | } |
| 94 | }) |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func TestRemoteResumeFailureRejectsLostOwnership(t *testing.T) { |
| 99 | for _, kind := range []string{"generation", "selection", "route-revision", "path", "client", "replacement"} { |
| 100 | t.Run(kind, func(t *testing.T) { |
| 101 | client := &http.Client{} |
| 102 | tab := &remoteTab{id: "remote-1", state: "ready", client: client, gen: 7, selectionRevision: 9, |
| 103 | session: remoteTabSessionState{path: "/old"}, routing: remoteTabSessionRouting{currentPath: "/old", pathRevision: 11, running: map[string]bool{}}, |
| 104 | } |
| 105 | log := &eventLog{} |
| 106 | a := &App{remoteTabs: map[string]*remoteTab{tab.id: tab}, remoteEventHook: log.add} |
| 107 | route := a.beginRemoteTabProvisionalResume(tab.id, tab, client, 7, "/target") |
| 108 | switch kind { |
| 109 | case "generation": |
| 110 | tab.gen++ |
| 111 | case "selection": |
| 112 | tab.selectionRevision++ |
| 113 | case "route-revision": |
| 114 | tab.routing.pathRevision++ |
| 115 | case "path": |
| 116 | tab.routing.currentPath = "/newer" |
| 117 | case "client": |
| 118 | tab.client = &http.Client{} |
| 119 | case "replacement": |
| 120 | a.remoteTabs[tab.id] = &remoteTab{id: tab.id, state: "ready", gen: 7, client: client} |
| 121 | } |
| 122 | beforeRoute := tab.routing.currentPath |
| 123 | if !a.completeRemoteTabResumeFailure(tab.id, tab, client, 7, route, "obsolete") { |
| 124 | t.Fatal("stale failure claimed completion") |
| 125 | } |
| 126 | if tab.err != "" || tab.routing.currentPath != beforeRoute || len(log.recorded()) != 0 { |
| 127 | t.Fatalf("stale failure mutated or published: error=%q route=%q events=%v", tab.err, tab.routing.currentPath, log.recorded()) |
| 128 | } |
| 129 | }) |
| 130 | } |
| 131 | } |
| 132 |