feat: toggle managed instance start and stop

This commit is contained in:
QiuSW
2026-07-25 17:10:20 +08:00
parent 3a3c92c875
commit b4e16fa708
7 changed files with 390 additions and 16 deletions
+98
View File
@@ -2,6 +2,7 @@ package ui
import (
"context"
"errors"
"path/filepath"
"strings"
"sync/atomic"
@@ -191,6 +192,103 @@ func TestShellDoesNotStartTheSameInstanceTwiceWhilePending(t *testing.T) {
}
}
func TestShellStopsManagedInstanceAsynchronouslyAndRestoresStartAction(t *testing.T) {
shell := NewShell(material.NewTheme())
target := shell.rows[0]
stopped := make(chan InstanceRow, 1)
shell.OnStopInstance(func(_ context.Context, row InstanceRow) error {
stopped <- row
return nil
}, nil)
shell.requestInstanceAction(target.ID)
if row, ok := shell.instanceRow(target.ID); !ok || row.Status != "停止中" {
t.Fatalf("stop status = %#v, want 停止中", row)
}
select {
case got := <-stopped:
if got.ID != target.ID || got.Status != "运行中" {
t.Fatalf("stopped row = %#v", got)
}
case <-time.After(time.Second):
t.Fatal("stop callback was not invoked")
}
var result instanceStopResult
select {
case result = <-shell.stopResults:
case <-time.After(time.Second):
t.Fatal("stop result was not produced")
}
shell.stopResults <- result
shell.consumeStopResults()
row, ok := shell.instanceRow(target.ID)
if !ok || row.Status != "已退出" || row.PID != 0 || row.RemoteDebugPort != 0 || instanceActionFor(row).stop {
t.Fatalf("completed stop row = %#v", row)
}
}
func TestShellDoesNotStopTheSameInstanceTwiceOrExternalInstance(t *testing.T) {
shell := NewShell(material.NewTheme())
target := shell.rows[0]
entered := make(chan struct{}, 1)
release := make(chan struct{})
var calls atomic.Int32
shell.OnStopInstance(func(context.Context, InstanceRow) error {
calls.Add(1)
entered <- struct{}{}
<-release
return nil
}, nil)
shell.requestInstanceAction(target.ID)
shell.requestInstanceAction(target.ID)
select {
case <-entered:
case <-time.After(time.Second):
t.Fatal("stop callback was not invoked")
}
if calls.Load() != 1 {
t.Fatalf("stop calls = %d, want 1", calls.Load())
}
close(release)
select {
case result := <-shell.stopResults:
shell.stopResults <- result
shell.consumeStopResults()
case <-time.After(time.Second):
t.Fatal("stop result was not produced")
}
external := shell.rows[2]
var externalStops atomic.Int32
shell.OnStopInstance(func(context.Context, InstanceRow) error { externalStops.Add(1); return nil }, nil)
shell.OnStartInstance(func(context.Context, InstanceRow, SettingsState) (InstanceStartOutcome, error) {
return InstanceStartOutcome{External: true, Source: "browser_message_window"}, nil
}, nil)
shell.requestInstanceAction(external.ID)
if externalStops.Load() != 0 {
t.Fatal("external association used stop callback")
}
}
func TestShellRestoresManagedStatusAfterStopFailure(t *testing.T) {
shell := NewShell(material.NewTheme())
target := shell.rows[0]
shell.OnStopInstance(func(context.Context, InstanceRow) error { return errors.New("permission denied") }, nil)
shell.requestInstanceAction(target.ID)
var result instanceStopResult
select {
case result = <-shell.stopResults:
case <-time.After(time.Second):
t.Fatal("stop result was not produced")
}
shell.stopResults <- result
shell.consumeStopResults()
row, ok := shell.instanceRow(target.ID)
if !ok || row.Status != "运行中" || !strings.Contains(shell.instanceFeedback, "失败") {
t.Fatalf("failed stop row = %#v, feedback=%q", row, shell.instanceFeedback)
}
}
func TestShellCancelsOnlyTheActivePathSearch(t *testing.T) {
shell := NewShell(material.NewTheme())
started := make(chan struct{}, 1)