package run import ( "context" "runtime" "sync" "testing" "time" "github.com/google/uuid" ) // fakeRepo 仅实现 supervisor 实际用到的 UpdateRunProfileExit;其余满足接口即可。 type fakeRepo struct { mu sync.Mutex exitID uuid.UUID exitCode *int32 lastErr string called bool } func (f *fakeRepo) CreateRunProfile(context.Context, *RunProfile) (*RunProfile, error) { return nil, nil } func (f *fakeRepo) GetRunProfileByID(context.Context, uuid.UUID) (*RunProfile, error) { return nil, nil } func (f *fakeRepo) ListRunProfilesByWorkspace(context.Context, uuid.UUID) ([]*RunProfile, error) { return nil, nil } func (f *fakeRepo) UpdateRunProfile(context.Context, *RunProfile) (*RunProfile, error) { return nil, nil } func (f *fakeRepo) DeleteRunProfile(context.Context, uuid.UUID) error { return nil } func (f *fakeRepo) MarkRunProfileStarted(context.Context, uuid.UUID) (*RunProfile, error) { return nil, nil } func (f *fakeRepo) UpdateRunProfileExit(_ context.Context, id uuid.UUID, exitCode *int32, lastErr string) error { f.mu.Lock() defer f.mu.Unlock() f.called = true f.exitID = id f.exitCode = exitCode f.lastErr = lastErr return nil } func testCfg() SupervisorConfig { return SupervisorConfig{ KillGrace: 2 * time.Second, OutBufferLines: 100, OutTailBytes: 2000, WSSendBuffer: 16, ShutdownGrace: 5 * time.Second, } } func quickExit() SpawnParams { p := SpawnParams{ProfileID: uuid.New()} if runtime.GOOS == "windows" { p.Command, p.Args = "cmd", []string{"/c", "exit", "0"} } else { p.Command, p.Args = "sh", []string{"-c", "exit 0"} } return p } func sleeper() SpawnParams { p := SpawnParams{ProfileID: uuid.New()} if runtime.GOOS == "windows" { p.Command, p.Args = "cmd", []string{"/c", "timeout", "/t", "30", "/nobreak"} } else { p.Command, p.Args = "sh", []string{"-c", "sleep 30"} } return p } // TestSpawn_SelfExit_RecordsCrash 验证:未被我方终止而自行退出 → crashed, // 且 onExit 把退出码落库(monitor 在 close(done) 前已完成 onExit)。 func TestSpawn_SelfExit_RecordsCrash(t *testing.T) { repo := &fakeRepo{} sup := NewSupervisor(repo, testCfg(), nil) params := quickExit() proc, err := sup.Spawn(params) if err != nil { t.Fatalf("spawn: %v", err) } select { case <-proc.done: case <-time.After(10 * time.Second): t.Fatal("process did not exit") } if got := sup.StatusOf(params.ProfileID); got != StatusCrashed { t.Fatalf("status = %q, want crashed (self-exit not initiated by us)", got) } repo.mu.Lock() defer repo.mu.Unlock() if !repo.called { t.Fatal("UpdateRunProfileExit not called on exit") } if repo.exitID != params.ProfileID { t.Fatalf("exit recorded for wrong profile: %v", repo.exitID) } if repo.exitCode == nil || *repo.exitCode != 0 { t.Fatalf("exit code = %v, want 0", repo.exitCode) } } // TestStop_TerminatesAndMarksStopped 验证 Stop 整树终止并判定为 stopped。 func TestStop_TerminatesAndMarksStopped(t *testing.T) { repo := &fakeRepo{} sup := NewSupervisor(repo, testCfg(), nil) params := sleeper() proc, err := sup.Spawn(params) if err != nil { t.Fatalf("spawn: %v", err) } if got := sup.StatusOf(params.ProfileID); got != StatusRunning { t.Fatalf("status = %q, want running", got) } if err := sup.Stop(context.Background(), params.ProfileID, testCfg().KillGrace); err != nil { t.Fatalf("stop: %v", err) } // Stop 已等 done;onExit 在 close(done) 前完成。 select { case <-proc.done: default: t.Fatal("done not closed after Stop") } if got := sup.StatusOf(params.ProfileID); got != StatusStopped { t.Fatalf("status = %q, want stopped", got) } if _, ok := sup.Get(params.ProfileID); ok { t.Fatal("proc still registered after stop") } } // TestSpawn_AlreadyRunning 验证同一 profile 不能并发拉起两个进程。 func TestSpawn_AlreadyRunning(t *testing.T) { sup := NewSupervisor(&fakeRepo{}, testCfg(), nil) params := sleeper() if _, err := sup.Spawn(params); err != nil { t.Fatalf("first spawn: %v", err) } defer func() { _ = sup.Stop(context.Background(), params.ProfileID, testCfg().KillGrace) }() if _, err := sup.Spawn(params); err != ErrAlreadyRunning { t.Fatalf("second spawn err = %v, want ErrAlreadyRunning", err) } } // TestStop_NotRunning 验证停止未运行的 profile 返回 ErrNotRunning。 func TestStop_NotRunning(t *testing.T) { sup := NewSupervisor(&fakeRepo{}, testCfg(), nil) if err := sup.Stop(context.Background(), uuid.New(), testCfg().KillGrace); err != ErrNotRunning { t.Fatalf("err = %v, want ErrNotRunning", err) } }