feat(process): add wait API endpoint

This commit is contained in:
Virgil 2026-04-04 07:41:05 +00:00
parent 720104babc
commit cf9291d095
2 changed files with 101 additions and 0 deletions

View file

@ -98,6 +98,7 @@ func (p *ProcessProvider) RegisterRoutes(rg *gin.RouterGroup) {
rg.GET("/processes", p.listProcesses)
rg.GET("/processes/:id", p.getProcess)
rg.GET("/processes/:id/output", p.getProcessOutput)
rg.POST("/processes/:id/wait", p.waitProcess)
rg.POST("/processes/:id/input", p.inputProcess)
rg.POST("/processes/:id/close-stdin", p.closeProcessStdin)
rg.POST("/processes/:id/kill", p.killProcess)
@ -231,6 +232,28 @@ func (p *ProcessProvider) Describe() []api.RouteDescription {
"type": "string",
},
},
{
Method: "POST",
Path: "/processes/:id/wait",
Summary: "Wait for a managed process",
Description: "Blocks until the process exits and returns the final process snapshot. Non-zero exits include the snapshot in the error details payload.",
Tags: []string{"process"},
Response: map[string]any{
"type": "object",
"properties": map[string]any{
"id": map[string]any{"type": "string"},
"command": map[string]any{"type": "string"},
"args": map[string]any{"type": "array"},
"dir": map[string]any{"type": "string"},
"startedAt": map[string]any{"type": "string", "format": "date-time"},
"running": map[string]any{"type": "boolean"},
"status": map[string]any{"type": "string"},
"exitCode": map[string]any{"type": "integer"},
"duration": map[string]any{"type": "integer"},
"pid": map[string]any{"type": "integer"},
},
},
},
{
Method: "POST",
Path: "/processes/:id/input",
@ -456,6 +479,28 @@ func (p *ProcessProvider) getProcessOutput(c *gin.Context) {
c.JSON(http.StatusOK, api.OK(output))
}
func (p *ProcessProvider) waitProcess(c *gin.Context) {
if p.service == nil {
c.JSON(http.StatusServiceUnavailable, api.Fail("service_unavailable", "process service is not configured"))
return
}
info, err := p.service.Wait(c.Param("id"))
if err != nil {
status := http.StatusInternalServerError
switch {
case err == process.ErrProcessNotFound:
status = http.StatusNotFound
case info.Status == process.StatusExited || info.Status == process.StatusKilled:
status = http.StatusConflict
}
c.JSON(status, api.FailWithDetails("wait_failed", err.Error(), info))
return
}
c.JSON(http.StatusOK, api.OK(info))
}
type processInputRequest struct {
Input string `json:"input"`
}

View file

@ -320,6 +320,60 @@ func TestProcessProvider_GetProcessOutput_Good(t *testing.T) {
assert.Contains(t, resp.Data, "output-check")
}
func TestProcessProvider_WaitProcess_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "echo", "wait-check")
require.NoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/wait", nil)
require.NoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp goapi.Response[process.Info]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, proc.ID, resp.Data.ID)
assert.Equal(t, process.StatusExited, resp.Data.Status)
assert.Equal(t, 0, resp.Data.ExitCode)
}
func TestProcessProvider_WaitProcess_NonZeroExit_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "sh", "-c", "exit 7")
require.NoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/wait", nil)
require.NoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusConflict, w.Code)
var resp goapi.Response[any]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.False(t, resp.Success)
require.NotNil(t, resp.Error)
assert.Equal(t, "wait_failed", resp.Error.Code)
assert.Contains(t, resp.Error.Message, "process exited with code 7")
details, ok := resp.Error.Details.(map[string]any)
require.True(t, ok)
assert.Equal(t, "exited", details["status"])
assert.Equal(t, float64(7), details["exitCode"])
assert.Equal(t, proc.ID, details["id"])
}
func TestProcessProvider_InputAndCloseStdin_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "cat")
@ -484,6 +538,7 @@ func TestProcessProvider_ProcessRoutes_Unavailable(t *testing.T) {
"/api/process/processes",
"/api/process/processes/anything",
"/api/process/processes/anything/output",
"/api/process/processes/anything/wait",
"/api/process/processes/anything/input",
"/api/process/processes/anything/close-stdin",
"/api/process/processes/anything/kill",
@ -494,6 +549,7 @@ func TestProcessProvider_ProcessRoutes_Unavailable(t *testing.T) {
method := "GET"
switch {
case strings.HasSuffix(path, "/kill"),
strings.HasSuffix(path, "/wait"),
strings.HasSuffix(path, "/input"),
strings.HasSuffix(path, "/close-stdin"):
method = "POST"