Skip to content

Commit 22ef9d3

Browse files
committed
fix: resolve type conflicts, wire team modules, remove stubs
- Remove team_scaling.go (stub returning errors, no real implementation) - Fix type conflicts: use types.TeamEvent, types.MailboxMessage, types.TaskClaim, types.TeamTask everywhere instead of local duplicates - Add CLI-specific response structs (cliEvent, cliMailboxMessage, cliTask) for JSON decoding, with conversion methods to canonical types - Fix team_restart.go: remove stub error, use correct field names (WorkerID instead of Owner, TaskInProgress status check) - Wire health/stall checks into teamcli.go monitorProcess() with periodic checkTeamHealthAndStall that detects and restarts dead workers - Wire graceful shutdown into Stop()/Close() via gracefulShutdownWorkers - Add heartbeat loop in coordinator executeTask() for stale detection - Add stale worker monitoring goroutine in coordinator runExecPhase() - Update all tests to use canonical types
1 parent 368fe22 commit 22ef9d3

10 files changed

Lines changed: 611 additions & 469 deletions

internal/agent/team_events.go

Lines changed: 98 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@ import (
44
"context"
55
"fmt"
66
"time"
7+
8+
"github.com/junhoyeo/contrabass/internal/types"
79
)
810

9-
// TeamEvent represents a team activity event
10-
type TeamEvent struct {
11+
// EventFilter defines filters for querying events.
12+
type EventFilter struct {
13+
AfterEventID string
14+
Type string
15+
Worker string
16+
TaskID string
17+
WakeableOnly bool
18+
}
19+
20+
// cliEvent is the JSON shape returned by the team CLI event API.
21+
type cliEvent struct {
1122
EventID string `json:"event_id"`
1223
Team string `json:"team"`
1324
Type string `json:"type"`
@@ -21,25 +32,75 @@ type TeamEvent struct {
2132
CreatedAt time.Time `json:"created_at"`
2233
}
2334

24-
// EventFilter defines filters for querying events
25-
type EventFilter struct {
26-
AfterEventID string
27-
Type string
28-
Worker string
29-
TaskID string
30-
WakeableOnly bool
35+
func (e *cliEvent) toTeamEvent() types.TeamEvent {
36+
data := make(map[string]interface{})
37+
if e.Worker != "" {
38+
data["worker"] = e.Worker
39+
}
40+
if e.TaskID != "" {
41+
data["task_id"] = e.TaskID
42+
}
43+
if e.MessageID != "" {
44+
data["message_id"] = e.MessageID
45+
}
46+
if e.Reason != "" {
47+
data["reason"] = e.Reason
48+
}
49+
if e.State != "" {
50+
data["state"] = e.State
51+
}
52+
if e.PrevState != "" {
53+
data["prev_state"] = e.PrevState
54+
}
55+
if e.EventID != "" {
56+
data["event_id"] = e.EventID
57+
}
58+
for k, v := range e.Metadata {
59+
data[k] = v
60+
}
61+
return types.TeamEvent{
62+
Type: e.Type,
63+
TeamName: e.Team,
64+
Data: data,
65+
Timestamp: e.CreatedAt,
66+
}
67+
}
68+
69+
func teamEventToCLI(teamName string, event *types.TeamEvent) map[string]interface{} {
70+
input := map[string]interface{}{
71+
"team_name": teamName,
72+
"type": event.Type,
73+
}
74+
if w, ok := event.Data["worker"].(string); ok && w != "" {
75+
input["worker"] = w
76+
}
77+
if t, ok := event.Data["task_id"].(string); ok && t != "" {
78+
input["task_id"] = t
79+
}
80+
if m, ok := event.Data["message_id"].(string); ok && m != "" {
81+
input["message_id"] = m
82+
}
83+
if r, ok := event.Data["reason"].(string); ok && r != "" {
84+
input["reason"] = r
85+
}
86+
if s, ok := event.Data["state"].(string); ok && s != "" {
87+
input["state"] = s
88+
}
89+
if p, ok := event.Data["prev_state"].(string); ok && p != "" {
90+
input["prev_state"] = p
91+
}
92+
return input
3193
}
3294

33-
// ReadEvents retrieves events from the team event log
34-
func (r *teamCLIRunner) ReadEvents(ctx context.Context, workspace, teamName string, filter *EventFilter) ([]TeamEvent, error) {
95+
// ReadEvents retrieves events from the team event log.
96+
func (r *teamCLIRunner) ReadEvents(ctx context.Context, workspace, teamName string, filter *EventFilter) ([]types.TeamEvent, error) {
3597
if filter == nil {
3698
filter = &EventFilter{}
3799
}
38100

39101
input := map[string]interface{}{
40102
"team_name": teamName,
41103
}
42-
43104
if filter.AfterEventID != "" {
44105
input["after_event_id"] = filter.AfterEventID
45106
}
@@ -57,20 +118,24 @@ func (r *teamCLIRunner) ReadEvents(ctx context.Context, workspace, teamName stri
57118
}
58119

59120
var resp struct {
60-
Count int `json:"count"`
61-
Cursor string `json:"cursor"`
62-
Events []TeamEvent `json:"events"`
121+
Count int `json:"count"`
122+
Cursor string `json:"cursor"`
123+
Events []cliEvent `json:"events"`
63124
}
64125

65126
if err := r.runTeamAPI(ctx, workspace, "read-events", input, &resp); err != nil {
66127
return nil, fmt.Errorf("read team events: %w", err)
67128
}
68129

69-
return resp.Events, nil
130+
events := make([]types.TeamEvent, len(resp.Events))
131+
for i, e := range resp.Events {
132+
events[i] = e.toTeamEvent()
133+
}
134+
return events, nil
70135
}
71136

72-
// AwaitEvent waits for a specific event to occur
73-
func (r *teamCLIRunner) AwaitEvent(ctx context.Context, workspace, teamName string, filter *EventFilter, timeout time.Duration) (*TeamEvent, error) {
137+
// AwaitEvent waits for a specific event to occur.
138+
func (r *teamCLIRunner) AwaitEvent(ctx context.Context, workspace, teamName string, filter *EventFilter, timeout time.Duration) (*types.TeamEvent, error) {
74139
if filter == nil {
75140
filter = &EventFilter{}
76141
}
@@ -79,7 +144,6 @@ func (r *teamCLIRunner) AwaitEvent(ctx context.Context, workspace, teamName stri
79144
"team_name": teamName,
80145
"timeout_ms": timeout.Milliseconds(),
81146
}
82-
83147
if filter.AfterEventID != "" {
84148
input["after_event_id"] = filter.AfterEventID
85149
}
@@ -97,9 +161,9 @@ func (r *teamCLIRunner) AwaitEvent(ctx context.Context, workspace, teamName stri
97161
}
98162

99163
var resp struct {
100-
Status string `json:"status"`
101-
Cursor string `json:"cursor"`
102-
Event *TeamEvent `json:"event"`
164+
Status string `json:"status"`
165+
Cursor string `json:"cursor"`
166+
Event *cliEvent `json:"event"`
103167
}
104168

105169
if err := r.runTeamAPI(ctx, workspace, "await-event", input, &resp); err != nil {
@@ -109,50 +173,31 @@ func (r *teamCLIRunner) AwaitEvent(ctx context.Context, workspace, teamName stri
109173
if resp.Status == "timeout" {
110174
return nil, fmt.Errorf("timeout waiting for event")
111175
}
112-
113176
if resp.Event == nil {
114177
return nil, fmt.Errorf("no event received")
115178
}
116179

117-
return resp.Event, nil
180+
event := resp.Event.toTeamEvent()
181+
return &event, nil
118182
}
119183

120-
// AppendEvent adds an event to the team event log
121-
func (r *teamCLIRunner) AppendEvent(ctx context.Context, workspace, teamName string, event *TeamEvent) (*TeamEvent, error) {
122-
input := map[string]interface{}{
123-
"team_name": teamName,
124-
"type": event.Type,
125-
"worker": event.Worker,
126-
}
127-
128-
if event.TaskID != "" {
129-
input["task_id"] = event.TaskID
130-
}
131-
if event.MessageID != "" {
132-
input["message_id"] = event.MessageID
133-
}
134-
if event.Reason != "" {
135-
input["reason"] = event.Reason
136-
}
137-
if event.State != "" {
138-
input["state"] = event.State
139-
}
140-
if event.PrevState != "" {
141-
input["prev_state"] = event.PrevState
142-
}
184+
// AppendEvent adds an event to the team event log.
185+
func (r *teamCLIRunner) AppendEvent(ctx context.Context, workspace, teamName string, event *types.TeamEvent) (*types.TeamEvent, error) {
186+
input := teamEventToCLI(teamName, event)
143187

144188
var resp struct {
145-
Event TeamEvent `json:"event"`
189+
Event cliEvent `json:"event"`
146190
}
147191

148192
if err := r.runTeamAPI(ctx, workspace, "append-event", input, &resp); err != nil {
149193
return nil, fmt.Errorf("append team event: %w", err)
150194
}
151195

152-
return &resp.Event, nil
196+
result := resp.Event.toTeamEvent()
197+
return &result, nil
153198
}
154199

155-
// IdleState represents the idle state of the team
200+
// IdleState represents the idle state of the team.
156201
type IdleState struct {
157202
TeamName string `json:"team_name"`
158203
WorkerCount int `json:"worker_count"`
@@ -167,20 +212,18 @@ type IdleState struct {
167212
} `json:"last_all_workers_idle_event,omitempty"`
168213
}
169214

170-
// ReadIdleState retrieves the current idle state of the team
215+
// ReadIdleState retrieves the current idle state of the team.
171216
func (r *teamCLIRunner) ReadIdleState(ctx context.Context, workspace, teamName string) (*IdleState, error) {
172217
var state IdleState
173-
174218
if err := r.runTeamAPI(ctx, workspace, "read-idle-state", map[string]string{
175219
"team_name": teamName,
176220
}, &state); err != nil {
177221
return nil, fmt.Errorf("read idle state: %w", err)
178222
}
179-
180223
return &state, nil
181224
}
182225

183-
// StallState represents whether the team is stalled
226+
// StallState represents whether the team is stalled.
184227
type StallState struct {
185228
TeamName string `json:"team_name"`
186229
TeamStalled bool `json:"team_stalled"`
@@ -193,15 +236,13 @@ type StallState struct {
193236
Reasons []string `json:"reasons"`
194237
}
195238

196-
// ReadStallState retrieves the current stall state of the team
239+
// ReadStallState retrieves the current stall state of the team.
197240
func (r *teamCLIRunner) ReadStallState(ctx context.Context, workspace, teamName string) (*StallState, error) {
198241
var state StallState
199-
200242
if err := r.runTeamAPI(ctx, workspace, "read-stall-state", map[string]string{
201243
"team_name": teamName,
202244
}, &state); err != nil {
203245
return nil, fmt.Errorf("read stall state: %w", err)
204246
}
205-
206247
return &state, nil
207248
}

internal/agent/team_events_test.go

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"encoding/json"
55
"testing"
66
"time"
7+
8+
"github.com/junhoyeo/contrabass/internal/types"
79
)
810

9-
func TestTeamEventJSON(t *testing.T) {
10-
event := &TeamEvent{
11+
func TestCLIEventToTeamEvent(t *testing.T) {
12+
event := &cliEvent{
1113
EventID: "evt_123",
1214
Team: "test-team",
1315
Type: "task_completed",
@@ -16,39 +18,59 @@ func TestTeamEventJSON(t *testing.T) {
1618
State: "completed",
1719
PrevState: "in_progress",
1820
Metadata: map[string]interface{}{
19-
"duration_ms": 1500,
21+
"duration_ms": float64(1500),
2022
},
2123
CreatedAt: time.Now(),
2224
}
2325

26+
teamEvent := event.toTeamEvent()
27+
28+
if teamEvent.Type != event.Type {
29+
t.Errorf("Type mismatch: got %s, want %s", teamEvent.Type, event.Type)
30+
}
31+
if teamEvent.TeamName != event.Team {
32+
t.Errorf("TeamName mismatch: got %s, want %s", teamEvent.TeamName, event.Team)
33+
}
34+
if teamEvent.Data["worker"] != event.Worker {
35+
t.Errorf("Worker mismatch: got %v, want %s", teamEvent.Data["worker"], event.Worker)
36+
}
37+
if teamEvent.Data["state"] != event.State {
38+
t.Errorf("State mismatch: got %v, want %s", teamEvent.Data["state"], event.State)
39+
}
40+
if teamEvent.Data["prev_state"] != event.PrevState {
41+
t.Errorf("PrevState mismatch: got %v, want %s", teamEvent.Data["prev_state"], event.PrevState)
42+
}
43+
if teamEvent.Data["event_id"] != event.EventID {
44+
t.Errorf("EventID mismatch: got %v, want %s", teamEvent.Data["event_id"], event.EventID)
45+
}
46+
}
47+
48+
func TestTeamEventJSON(t *testing.T) {
49+
event := types.TeamEvent{
50+
Type: "task_completed",
51+
TeamName: "test-team",
52+
Data: map[string]interface{}{
53+
"worker": "worker-1",
54+
"task_id": "task-1",
55+
},
56+
Timestamp: time.Now(),
57+
}
58+
2459
data, err := json.Marshal(event)
2560
if err != nil {
2661
t.Fatalf("Failed to marshal event: %v", err)
2762
}
2863

29-
var decoded TeamEvent
64+
var decoded types.TeamEvent
3065
if err := json.Unmarshal(data, &decoded); err != nil {
3166
t.Fatalf("Failed to unmarshal event: %v", err)
3267
}
3368

34-
if decoded.EventID != event.EventID {
35-
t.Errorf("EventID mismatch: got %s, want %s", decoded.EventID, event.EventID)
36-
}
37-
3869
if decoded.Type != event.Type {
3970
t.Errorf("Type mismatch: got %s, want %s", decoded.Type, event.Type)
4071
}
41-
42-
if decoded.Worker != event.Worker {
43-
t.Errorf("Worker mismatch: got %s, want %s", decoded.Worker, event.Worker)
44-
}
45-
46-
if decoded.State != event.State {
47-
t.Errorf("State mismatch: got %s, want %s", decoded.State, event.State)
48-
}
49-
50-
if decoded.PrevState != event.PrevState {
51-
t.Errorf("PrevState mismatch: got %s, want %s", decoded.PrevState, event.PrevState)
72+
if decoded.TeamName != event.TeamName {
73+
t.Errorf("TeamName mismatch: got %s, want %s", decoded.TeamName, event.TeamName)
5274
}
5375
}
5476

@@ -63,11 +85,9 @@ func TestEventFilter(t *testing.T) {
6385
if filter.AfterEventID != "evt_100" {
6486
t.Errorf("AfterEventID mismatch: got %s, want evt_100", filter.AfterEventID)
6587
}
66-
6788
if filter.Type != "worker_state_changed" {
6889
t.Errorf("Type mismatch: got %s, want worker_state_changed", filter.Type)
6990
}
70-
7191
if !filter.WakeableOnly {
7292
t.Error("WakeableOnly should be true")
7393
}
@@ -96,11 +116,9 @@ func TestIdleStateJSON(t *testing.T) {
96116
if decoded.TeamName != state.TeamName {
97117
t.Errorf("TeamName mismatch: got %s, want %s", decoded.TeamName, state.TeamName)
98118
}
99-
100119
if decoded.IdleWorkerCount != state.IdleWorkerCount {
101120
t.Errorf("IdleWorkerCount mismatch: got %d, want %d", decoded.IdleWorkerCount, state.IdleWorkerCount)
102121
}
103-
104122
if decoded.AllWorkersIdle != state.AllWorkersIdle {
105123
t.Errorf("AllWorkersIdle mismatch: got %v, want %v", decoded.AllWorkersIdle, state.AllWorkersIdle)
106124
}
@@ -132,11 +150,9 @@ func TestStallStateJSON(t *testing.T) {
132150
if decoded.TeamStalled != state.TeamStalled {
133151
t.Errorf("TeamStalled mismatch: got %v, want %v", decoded.TeamStalled, state.TeamStalled)
134152
}
135-
136153
if len(decoded.Reasons) != len(state.Reasons) {
137154
t.Errorf("Reasons length mismatch: got %d, want %d", len(decoded.Reasons), len(state.Reasons))
138155
}
139-
140156
if decoded.PendingTaskCount != state.PendingTaskCount {
141157
t.Errorf("PendingTaskCount mismatch: got %d, want %d", decoded.PendingTaskCount, state.PendingTaskCount)
142158
}

0 commit comments

Comments
 (0)