feat: wire gui instance start and delete confirmation

This commit is contained in:
QiuSW
2026-07-22 17:02:59 +08:00
parent 46578c8743
commit 176e3cdad2
12 changed files with 600 additions and 59 deletions
+93
View File
@@ -2,6 +2,7 @@ package ui
import (
"context"
"sync/atomic"
"testing"
"time"
@@ -90,6 +91,98 @@ func TestShellDeletesOnlyTheRequestedInstance(t *testing.T) {
}
}
func TestShellDeletesOnlyAfterConfirmation(t *testing.T) {
shell := NewShell(material.NewTheme())
target := shell.rows[1]
changes := 0
shell.OnInstancesChanged(func([]InstanceRow) { changes++ })
shell.requestDelete(target.ID)
if shell.pendingDeleteID != target.ID {
t.Fatalf("pending delete = %q, want %q", shell.pendingDeleteID, target.ID)
}
if len(shell.rows) != 4 {
t.Fatalf("instance count changed before confirmation: %d", len(shell.rows))
}
shell.cancelDelete()
if shell.pendingDeleteID != "" || len(shell.rows) != 4 || changes != 0 {
t.Fatalf("cancel delete state = pending %q, rows %d, changes %d", shell.pendingDeleteID, len(shell.rows), changes)
}
shell.requestDelete(target.ID)
shell.confirmDelete()
if shell.pendingDeleteID != "" || len(shell.rows) != 3 || changes != 1 {
t.Fatalf("confirm delete state = pending %q, rows %d, changes %d", shell.pendingDeleteID, len(shell.rows), changes)
}
}
func TestShellStartsInstanceAsynchronouslyAndAppliesResult(t *testing.T) {
shell := NewShell(material.NewTheme())
target := shell.rows[0]
started := make(chan InstanceRow, 1)
shell.OnStartInstance(func(_ context.Context, row InstanceRow, _ SettingsState) (int, error) {
started <- row
return 4242, nil
}, nil)
shell.requestStart(target.ID)
if row, ok := shell.instanceRow(target.ID); !ok || row.Status != "启动中" {
t.Fatalf("start status = %#v, want 启动中", row)
}
select {
case got := <-started:
if got.ID != target.ID {
t.Fatalf("started instance = %q, want %q", got.ID, target.ID)
}
case <-time.After(time.Second):
t.Fatal("start callback was not invoked")
}
var result instanceStartResult
select {
case result = <-shell.startResults:
case <-time.After(time.Second):
t.Fatal("start result was not produced")
}
shell.startResults <- result
shell.consumeStartResults()
if row, ok := shell.instanceRow(target.ID); !ok || row.Status != "运行中" {
t.Fatalf("completed start status = %#v, want 运行中", row)
}
}
func TestShellDoesNotStartTheSameInstanceTwiceWhilePending(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.OnStartInstance(func(_ context.Context, _ InstanceRow, _ SettingsState) (int, error) {
calls.Add(1)
entered <- struct{}{}
<-release
return 4242, nil
}, nil)
shell.requestStart(target.ID)
shell.requestStart(target.ID)
select {
case <-entered:
case <-time.After(time.Second):
t.Fatal("start callback was not invoked")
}
if calls.Load() != 1 {
t.Fatalf("start calls = %d, want 1", calls.Load())
}
close(release)
select {
case result := <-shell.startResults:
shell.startResults <- result
shell.consumeStartResults()
case <-time.After(time.Second):
t.Fatal("start result was not produced")
}
}
func TestShellCancelsOnlyTheActivePathSearch(t *testing.T) {
shell := NewShell(material.NewTheme())
started := make(chan struct{}, 1)