feat(process): add wait task surface

Co-authored-by: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 03:33:29 +00:00
parent 79e2ffa6ed
commit 85cd6dd7c8
5 changed files with 126 additions and 0 deletions

View file

@ -89,6 +89,16 @@ type TaskProcessGet struct {
ID string
}
// TaskProcessWait waits for a managed process to finish through Core.PERFORM.
//
// Example:
//
// c.PERFORM(process.TaskProcessWait{ID: "proc-1"})
type TaskProcessWait struct {
// ID identifies a managed process started by this service.
ID string
}
// TaskProcessOutput requests the captured output of a managed process through Core.PERFORM.
//
// Example:

View file

@ -273,6 +273,26 @@ func TestGlobal_Output(t *testing.T) {
assert.Contains(t, output, "global-output")
}
func TestGlobal_Wait(t *testing.T) {
svc, _ := newTestService(t)
old := defaultService.Swap(svc)
defer func() {
if old != nil {
defaultService.Store(old)
}
}()
proc, err := Start(context.Background(), "echo", "global-wait")
require.NoError(t, err)
info, err := Wait(proc.ID)
require.NoError(t, err)
assert.Equal(t, proc.ID, info.ID)
assert.Equal(t, StatusExited, info.Status)
assert.Equal(t, 0, info.ExitCode)
}
func TestGlobal_Signal(t *testing.T) {
svc, _ := newTestService(t)

View file

@ -114,6 +114,19 @@ func Output(id string) (string, error) {
return svc.Output(id)
}
// Wait blocks until a managed process exits and returns its final snapshot.
//
// Example:
//
// info, err := process.Wait("proc-1")
func Wait(id string) (Info, error) {
svc := Default()
if svc == nil {
return Info{}, ErrServiceNotInitialized
}
return svc.Wait(id)
}
// List returns all processes from the default service.
//
// Example:

View file

@ -529,6 +529,24 @@ func (s *Service) Output(id string) (string, error) {
return proc.Output(), nil
}
// Wait blocks until a managed process exits and returns its final snapshot.
//
// Example:
//
// info, err := svc.Wait("proc-1")
func (s *Service) Wait(id string) (Info, error) {
proc, err := s.Get(id)
if err != nil {
return Info{}, err
}
if err := proc.Wait(); err != nil {
return proc.Info(), err
}
return proc.Info(), nil
}
// findByPID locates a managed process by operating-system PID.
func (s *Service) findByPID(pid int) *Process {
s.mu.RLock()
@ -672,6 +690,17 @@ func (s *Service) handleTask(c *core.Core, task core.Task) core.Result {
}
return core.Result{Value: proc.Info(), OK: true}
case TaskProcessWait:
if m.ID == "" {
return core.Result{Value: coreerr.E("Service.handleTask", "task process wait requires an id", nil), OK: false}
}
info, err := s.Wait(m.ID)
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: info, OK: true}
case TaskProcessOutput:
if m.ID == "" {
return core.Result{Value: coreerr.E("Service.handleTask", "task process output requires an id", nil), OK: false}

View file

@ -597,6 +597,41 @@ func TestService_Output(t *testing.T) {
})
}
func TestService_Wait(t *testing.T) {
t.Run("returns final info on success", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "waited")
require.NoError(t, err)
info, err := svc.Wait(proc.ID)
require.NoError(t, err)
assert.Equal(t, proc.ID, info.ID)
assert.Equal(t, StatusExited, info.Status)
assert.Equal(t, 0, info.ExitCode)
})
t.Run("returns error on unknown id", func(t *testing.T) {
svc, _ := newTestService(t)
_, err := svc.Wait("nonexistent")
assert.ErrorIs(t, err, ErrProcessNotFound)
})
t.Run("returns info alongside failure", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "sh", "-c", "exit 7")
require.NoError(t, err)
info, err := svc.Wait(proc.ID)
require.Error(t, err)
assert.Equal(t, proc.ID, info.ID)
assert.Equal(t, StatusExited, info.Status)
assert.Equal(t, 7, info.ExitCode)
})
}
func TestService_OnShutdown(t *testing.T) {
t.Run("kills all running processes", func(t *testing.T) {
svc, _ := newTestService(t)
@ -770,6 +805,25 @@ func TestService_OnStartup(t *testing.T) {
assert.Equal(t, StatusKilled, proc.Status)
})
t.Run("registers process.wait task", func(t *testing.T) {
svc, c := newTestService(t)
err := svc.OnStartup(context.Background())
require.NoError(t, err)
proc, err := svc.Start(context.Background(), "echo", "action-wait")
require.NoError(t, err)
result := c.PERFORM(TaskProcessWait{ID: proc.ID})
require.True(t, result.OK)
info, ok := result.Value.(Info)
require.True(t, ok)
assert.Equal(t, proc.ID, info.ID)
assert.Equal(t, StatusExited, info.Status)
assert.Equal(t, 0, info.ExitCode)
})
t.Run("registers process.list task", func(t *testing.T) {
svc, c := newTestService(t)