Skip to content

Commit 067fb97

Browse files
leo-aa88cursoragent
andcommitted
fix(inspect): P3 polish from third PR review (#136)
- Omit empty workflow in /api/runs list JSON (omitempty + test) - Print BoundAddr after listener is ready in inspect --web - Validate --port range (1-65535 or 0) with exit 2 - Typed list/state decodes in server_test Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2a2691e commit 067fb97

7 files changed

Lines changed: 107 additions & 13 deletions

File tree

internal/cli/inspect_web.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import (
77
"os/signal"
88
"strings"
99
"syscall"
10+
"time"
1011

1112
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/inspect"
1213
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/state/sqlite"
1314
"github.com/spf13/cobra"
1415
)
1516

1617
func runInspectWeb(cmd *cobra.Command, port int, traceUIBase string) error {
18+
if err := inspect.ValidateInspectPort(port); err != nil {
19+
return NewExitError(ExitValidationError, err)
20+
}
1721
g := Globals()
1822
graph, root, err := prepareProjectGraph(g.ProjectRoot, g)
1923
if err != nil {
@@ -55,15 +59,27 @@ func runInspectWeb(cmd *cobra.Command, port int, traceUIBase string) error {
5559
return fmt.Errorf("inspect: %w", err)
5660
}
5761

58-
addr := srv.ListenAddr()
62+
runCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
63+
defer stop()
64+
65+
errCh := make(chan error, 1)
66+
go func() { errCh <- srv.ListenAndServe(runCtx) }()
67+
68+
deadline := time.Now().Add(3 * time.Second)
69+
for !srv.ListenReady() {
70+
if time.Now().After(deadline) {
71+
return fmt.Errorf("inspect: server did not start listening")
72+
}
73+
time.Sleep(10 * time.Millisecond)
74+
}
75+
76+
addr := srv.BoundAddr()
5977
url := "http://" + addr + "/"
6078
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "Inspector listening on %s (read-only)\nOpen %s\nPress Ctrl+C to stop.\n", addr, url); err != nil {
6179
return err
6280
}
6381

64-
runCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
65-
defer stop()
66-
if err := srv.ListenAndServe(runCtx); err != nil && runCtx.Err() == nil {
82+
if err := <-errCh; err != nil && runCtx.Err() == nil {
6783
return fmt.Errorf("inspect: server: %w", err)
6884
}
6985
return nil

internal/cli/inspect_web_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,24 @@ func TestInspect_web_invalidTraceUI_exit2(t *testing.T) {
117117
}
118118
}
119119

120+
func TestInspect_web_invalidPort_exit2(t *testing.T) {
121+
root := runProjRoot(t)
122+
db := filepath.Join(t.TempDir(), "port.db")
123+
124+
ResetGlobalsForTest()
125+
cmd := NewRootCmd()
126+
cmd.SetOut(io.Discard)
127+
cmd.SetErr(io.Discard)
128+
cmd.SetArgs([]string{"inspect", "--web", "--project", root, "--state", db, "--port", "99999"})
129+
err := cmd.Execute()
130+
if err == nil {
131+
t.Fatal("expected error")
132+
}
133+
if ExitCodeOf(err) != ExitValidationError {
134+
t.Fatalf("exit=%d err=%v", ExitCodeOf(err), err)
135+
}
136+
}
137+
120138
func TestInspect_web_missingDB_exit2(t *testing.T) {
121139
db := filepath.Join(t.TempDir(), "no.db")
122140
root := runProjRoot(t)

internal/inspect/port.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package inspect
2+
3+
import "fmt"
4+
5+
// ValidateInspectPort returns an error when port is not 0 (ephemeral) or in 1..65535.
6+
func ValidateInspectPort(port int) error {
7+
if port == 0 {
8+
return nil
9+
}
10+
if port < 1 || port > 65535 {
11+
return fmt.Errorf("inspect: port %d out of range (use 1-65535, or 0 for an ephemeral test port)", port)
12+
}
13+
return nil
14+
}

internal/inspect/port_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package inspect
2+
3+
import "testing"
4+
5+
func TestValidateInspectPort(t *testing.T) {
6+
tests := []struct {
7+
port int
8+
isErr bool
9+
}{
10+
{0, false},
11+
{8787, false},
12+
{1, false},
13+
{65535, false},
14+
{-1, true},
15+
{65536, true},
16+
}
17+
for _, tc := range tests {
18+
err := ValidateInspectPort(tc.port)
19+
if tc.isErr && err == nil {
20+
t.Fatalf("port %d: want error", tc.port)
21+
}
22+
if !tc.isErr && err != nil {
23+
t.Fatalf("port %d: %v", tc.port, err)
24+
}
25+
}
26+
}

internal/inspect/responses.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
// ListRunsResponse is GET /api/runs JSON.
1010
type ListRunsResponse struct {
1111
StatePath string `json:"statePath"`
12-
Workflow string `json:"workflow"`
12+
Workflow string `json:"workflow,omitempty"`
1313
Runs []statejson.RunRecord `json:"runs"`
1414
}
1515

internal/inspect/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ func (s *Server) Handler() http.Handler {
8383
return securityHeaders(RejectMutation(s.mux))
8484
}
8585

86+
// ListenReady reports whether [Server.ListenAndServe] has bound a TCP listener.
87+
func (s *Server) ListenReady() bool {
88+
s.mu.RLock()
89+
ok := s.boundAddr != ""
90+
s.mu.RUnlock()
91+
return ok
92+
}
93+
8694
// BoundAddr returns the address the server is listening on after [Server.ListenAndServe] starts.
8795
// When Port is 0, this is the kernel-assigned port (127.0.0.1:NNNN).
8896
func (s *Server) BoundAddr() string {

internal/inspect/server_test.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,26 @@ func TestServer_API_readOnly(t *testing.T) {
9999
if res.StatusCode != http.StatusOK {
100100
t.Fatalf("status=%d", res.StatusCode)
101101
}
102-
var body map[string]any
102+
var body ListRunsResponse
103103
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
104104
t.Fatal(err)
105105
}
106-
runs, ok := body["runs"].([]any)
107-
if !ok || len(runs) != 1 {
108-
t.Fatalf("runs=%v", body["runs"])
106+
if body.Workflow != "" {
107+
t.Fatalf("workflow=%q want empty when unfiltered", body.Workflow)
108+
}
109+
if len(body.Runs) != 1 {
110+
t.Fatalf("runs=%v", body.Runs)
111+
}
112+
raw, err := json.Marshal(body)
113+
if err != nil {
114+
t.Fatal(err)
115+
}
116+
var top map[string]json.RawMessage
117+
if err := json.Unmarshal(raw, &top); err != nil {
118+
t.Fatal(err)
119+
}
120+
if _, ok := top["workflow"]; ok {
121+
t.Fatalf("top-level workflow key in list response: %s", raw)
109122
}
110123
})
111124

@@ -148,13 +161,12 @@ func TestServer_API_readOnly(t *testing.T) {
148161
t.Fatal(err)
149162
}
150163
defer res.Body.Close()
151-
var body map[string]any
164+
var body StateResponse
152165
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
153166
t.Fatal(err)
154167
}
155-
resources, _ := body["resources"].([]any)
156-
if len(resources) != 1 {
157-
t.Fatalf("resources=%v", body["resources"])
168+
if len(body.Resources) != 1 {
169+
t.Fatalf("resources=%v", body.Resources)
158170
}
159171
})
160172

0 commit comments

Comments
 (0)