|
|
|
@@ -273,7 +273,7 @@ func TestListDelegationsFromActivityLogs_SingleDelegateRow(t *testing.T) {
|
|
|
|
|
"summary", "status", "error_detail",
|
|
|
|
|
"response_preview", "delegation_id", "created_at",
|
|
|
|
|
}).AddRow(
|
|
|
|
|
"act-1", "delegate",
|
|
|
|
|
"act-1", "delegation",
|
|
|
|
|
"ws-1", "ws-2",
|
|
|
|
|
"analyse Q1 numbers",
|
|
|
|
|
"in_progress",
|
|
|
|
@@ -296,8 +296,8 @@ func TestListDelegationsFromActivityLogs_SingleDelegateRow(t *testing.T) {
|
|
|
|
|
if e["id"] != "act-1" {
|
|
|
|
|
t.Errorf("id: got %v, want act-1", e["id"])
|
|
|
|
|
}
|
|
|
|
|
if e["type"] != "delegate" {
|
|
|
|
|
t.Errorf("type: got %v, want delegate", e["type"])
|
|
|
|
|
if e["type"] != "delegation" {
|
|
|
|
|
t.Errorf("type: got %v, want delegation", e["type"])
|
|
|
|
|
}
|
|
|
|
|
if e["source_id"] != "ws-1" {
|
|
|
|
|
t.Errorf("source_id: got %v, want ws-1", e["source_id"])
|
|
|
|
@@ -331,9 +331,9 @@ func TestListDelegationsFromActivityLogs_DelegateResultWithError(t *testing.T) {
|
|
|
|
|
"summary", "status", "error_detail",
|
|
|
|
|
"response_preview", "delegation_id", "created_at",
|
|
|
|
|
}).AddRow(
|
|
|
|
|
"act-2", "delegate_result",
|
|
|
|
|
"act-2", "delegation",
|
|
|
|
|
"ws-1", "ws-2",
|
|
|
|
|
"result summary",
|
|
|
|
|
"Delegation failed",
|
|
|
|
|
"failed",
|
|
|
|
|
"Callee workspace not reachable",
|
|
|
|
|
`{"text":"the result body text"}`,
|
|
|
|
@@ -353,8 +353,8 @@ func TestListDelegationsFromActivityLogs_DelegateResultWithError(t *testing.T) {
|
|
|
|
|
t.Fatalf("expected 1 entry, got %d", len(got))
|
|
|
|
|
}
|
|
|
|
|
e := got[0]
|
|
|
|
|
if e["type"] != "delegate_result" {
|
|
|
|
|
t.Errorf("type: got %v", e["type"])
|
|
|
|
|
if e["type"] != "delegation" {
|
|
|
|
|
t.Errorf("type: got %v, want delegation", e["type"])
|
|
|
|
|
}
|
|
|
|
|
if e["error"] != "Callee workspace not reachable" {
|
|
|
|
|
t.Errorf("error: got %v", e["error"])
|
|
|
|
@@ -417,8 +417,8 @@ func TestListDelegationsFromActivityLogs_RowsErr(t *testing.T) {
|
|
|
|
|
"summary", "status", "error_detail",
|
|
|
|
|
"response_preview", "delegation_id", "created_at",
|
|
|
|
|
}).
|
|
|
|
|
AddRow("act-1", "delegate", "ws-1", "ws-2", "task", "queued", "", "", "", now).
|
|
|
|
|
AddRow("act-2", "delegate", "ws-1", "ws-3", "another task", "queued", "", "", "", now).
|
|
|
|
|
AddRow("act-1", "delegation", "ws-1", "ws-2", "task", "queued", "", "", "", now).
|
|
|
|
|
AddRow("act-2", "delegation", "ws-1", "ws-3", "another task", "queued", "", "", "", now).
|
|
|
|
|
RowError(1, context.DeadlineExceeded)
|
|
|
|
|
mock.ExpectQuery("SELECT .+ FROM activity_logs").
|
|
|
|
|
WithArgs("ws-1").
|
|
|
|
@@ -445,3 +445,168 @@ func TestListDelegationsFromActivityLogs_RowsErr(t *testing.T) {
|
|
|
|
|
// sqlmock.NewRows([]string{}).AddRow(...) to panic in test SETUP. The handler
|
|
|
|
|
// has no recover(), so a scan panic would crash the process — the correct
|
|
|
|
|
// behaviour. Real-DB integration tests cover this path.
|
|
|
|
|
|
|
|
|
|
// ---------- Deduplication by delegation_id ----------
|
|
|
|
|
|
|
|
|
|
// TestListDelegationsFromActivityLogs_DeduplicationKeepsNewest verifies that when
|
|
|
|
|
// both the initial 'delegate' row and the terminal 'delegate_result' row exist for
|
|
|
|
|
// the same delegation_id, only one entry (the newest) is returned. This is the
|
|
|
|
|
// fix for the double-row artifact where the agent's _refresh_queued_from_platform
|
|
|
|
|
// would scan the stale initial row (status=in_progress) before reaching the
|
|
|
|
|
// terminal row (status=completed/failed).
|
|
|
|
|
func TestListDelegationsFromActivityLogs_DeduplicationKeepsNewest(t *testing.T) {
|
|
|
|
|
mockDB, mock, err := sqlmock.New()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
|
|
|
}
|
|
|
|
|
prevDB := db.DB
|
|
|
|
|
db.DB = mockDB
|
|
|
|
|
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
|
|
|
|
|
|
|
|
|
t0 := time.Now().Add(-2 * time.Hour)
|
|
|
|
|
t1 := time.Now().Add(-1 * time.Hour)
|
|
|
|
|
// Rows returned in created_at DESC order. The query uses DISTINCT ON
|
|
|
|
|
// (delegation_id) ORDER BY delegation_id, created_at DESC, so the newest
|
|
|
|
|
// row per delegation_id is returned.
|
|
|
|
|
rows := sqlmock.NewRows([]string{
|
|
|
|
|
"id", "activity_type", "source_id", "target_id",
|
|
|
|
|
"summary", "status", "error_detail",
|
|
|
|
|
"response_preview", "delegation_id", "created_at",
|
|
|
|
|
}).
|
|
|
|
|
// delegate_result row (newest for del-abc) — should be kept
|
|
|
|
|
AddRow("act-2", "delegation", "ws-1", "ws-2",
|
|
|
|
|
"Delegation completed", "completed", "",
|
|
|
|
|
`{"text":"the answer is 42"}`, "del-abc", t1).
|
|
|
|
|
// delegate row (oldest for del-abc) — should be dropped by DISTINCT ON
|
|
|
|
|
AddRow("act-1", "delegation", "ws-1", "ws-2",
|
|
|
|
|
"Delegating to ws-2", "in_progress", "",
|
|
|
|
|
"", "del-abc", t0)
|
|
|
|
|
mock.ExpectQuery("SELECT .+ FROM activity_logs").
|
|
|
|
|
WithArgs("ws-1").
|
|
|
|
|
WillReturnRows(rows)
|
|
|
|
|
|
|
|
|
|
broadcaster := newTestBroadcaster()
|
|
|
|
|
wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
|
|
|
|
dh := NewDelegationHandler(wh, broadcaster)
|
|
|
|
|
|
|
|
|
|
got := dh.listDelegationsFromActivityLogs(context.Background(), "ws-1")
|
|
|
|
|
if len(got) != 1 {
|
|
|
|
|
t.Fatalf("expected 1 entry after deduplication, got %d: %v", len(got), got)
|
|
|
|
|
}
|
|
|
|
|
e := got[0]
|
|
|
|
|
if e["delegation_id"] != "del-abc" {
|
|
|
|
|
t.Errorf("delegation_id: got %v, want del-abc", e["delegation_id"])
|
|
|
|
|
}
|
|
|
|
|
if e["status"] != "completed" {
|
|
|
|
|
t.Errorf("status: got %v, want completed (newest row kept)", e["status"])
|
|
|
|
|
}
|
|
|
|
|
if e["response_preview"] != `{"text":"the answer is 42"}` {
|
|
|
|
|
t.Errorf("response_preview: got %v", e["response_preview"])
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("sqlmock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestListDelegationsFromActivityLogs_DeduplicationDistinctDelegations verifies that
|
|
|
|
|
// rows with different delegation_ids are all returned (deduplication is per-id, not global).
|
|
|
|
|
func TestListDelegationsFromActivityLogs_DeduplicationDistinctDelegations(t *testing.T) {
|
|
|
|
|
mockDB, mock, err := sqlmock.New()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
|
|
|
}
|
|
|
|
|
prevDB := db.DB
|
|
|
|
|
db.DB = mockDB
|
|
|
|
|
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
rows := sqlmock.NewRows([]string{
|
|
|
|
|
"id", "activity_type", "source_id", "target_id",
|
|
|
|
|
"summary", "status", "error_detail",
|
|
|
|
|
"response_preview", "delegation_id", "created_at",
|
|
|
|
|
}).
|
|
|
|
|
AddRow("act-a", "delegation", "ws-1", "ws-2", "task a", "completed", "", "", "del-A", now).
|
|
|
|
|
AddRow("act-b", "delegation", "ws-1", "ws-3", "task b", "failed", "timeout", "", "del-B", now).
|
|
|
|
|
AddRow("act-c", "delegation", "ws-1", "ws-4", "task c", "in_progress", "", "", "del-C", now)
|
|
|
|
|
mock.ExpectQuery("SELECT .+ FROM activity_logs").
|
|
|
|
|
WithArgs("ws-1").
|
|
|
|
|
WillReturnRows(rows)
|
|
|
|
|
|
|
|
|
|
broadcaster := newTestBroadcaster()
|
|
|
|
|
wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
|
|
|
|
dh := NewDelegationHandler(wh, broadcaster)
|
|
|
|
|
|
|
|
|
|
got := dh.listDelegationsFromActivityLogs(context.Background(), "ws-1")
|
|
|
|
|
if len(got) != 3 {
|
|
|
|
|
t.Fatalf("expected 3 entries (all distinct delegation_ids), got %d", len(got))
|
|
|
|
|
}
|
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
|
for _, e := range got {
|
|
|
|
|
id := e["delegation_id"].(string)
|
|
|
|
|
if seen[id] {
|
|
|
|
|
t.Errorf("duplicate delegation_id %q in result", id)
|
|
|
|
|
}
|
|
|
|
|
seen[id] = true
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("sqlmock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestListDelegationsFromActivityLogs_DeduplicationMixedTerminalStatuses verifies that
|
|
|
|
|
// a failed delegate_result row (newest) is kept over the initial in_progress row,
|
|
|
|
|
// and a completed delegate_result is kept over the initial pending row.
|
|
|
|
|
func TestListDelegationsFromActivityLogs_DeduplicationMixedTerminalStatuses(t *testing.T) {
|
|
|
|
|
mockDB, mock, err := sqlmock.New()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
|
|
|
}
|
|
|
|
|
prevDB := db.DB
|
|
|
|
|
db.DB = mockDB
|
|
|
|
|
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
|
|
|
|
|
|
|
|
|
tOld := time.Now().Add(-3 * time.Hour)
|
|
|
|
|
tMid := time.Now().Add(-2 * time.Hour)
|
|
|
|
|
tNew := time.Now().Add(-1 * time.Hour)
|
|
|
|
|
rows := sqlmock.NewRows([]string{
|
|
|
|
|
"id", "activity_type", "source_id", "target_id",
|
|
|
|
|
"summary", "status", "error_detail",
|
|
|
|
|
"response_preview", "delegation_id", "created_at",
|
|
|
|
|
}).
|
|
|
|
|
// del-X: newest is completed → keep completed
|
|
|
|
|
AddRow("act-x2", "delegation", "ws-1", "ws-2", "task X done", "completed", "", `{"text":"X result"}`, "del-X", tNew).
|
|
|
|
|
AddRow("act-x1", "delegation", "ws-1", "ws-2", "Delegating to ws-2", "pending", "", "", "del-X", tOld).
|
|
|
|
|
// del-Y: newest is failed → keep failed
|
|
|
|
|
AddRow("act-y2", "delegation", "ws-1", "ws-3", "task Y done", "failed", "network error", "", "del-Y", tMid).
|
|
|
|
|
AddRow("act-y1", "delegation", "ws-1", "ws-3", "Delegating to ws-3", "dispatched", "", "", "del-Y", tOld)
|
|
|
|
|
mock.ExpectQuery("SELECT .+ FROM activity_logs").
|
|
|
|
|
WithArgs("ws-1").
|
|
|
|
|
WillReturnRows(rows)
|
|
|
|
|
|
|
|
|
|
broadcaster := newTestBroadcaster()
|
|
|
|
|
wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
|
|
|
|
dh := NewDelegationHandler(wh, broadcaster)
|
|
|
|
|
|
|
|
|
|
got := dh.listDelegationsFromActivityLogs(context.Background(), "ws-1")
|
|
|
|
|
if len(got) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 entries after deduplication, got %d: %v", len(got), got)
|
|
|
|
|
}
|
|
|
|
|
byID := make(map[string]map[string]interface{})
|
|
|
|
|
for _, e := range got {
|
|
|
|
|
byID[e["delegation_id"].(string)] = e
|
|
|
|
|
}
|
|
|
|
|
x := byID["del-X"]
|
|
|
|
|
if x["status"] != "completed" {
|
|
|
|
|
t.Errorf("del-X: got status %v, want completed", x["status"])
|
|
|
|
|
}
|
|
|
|
|
y := byID["del-Y"]
|
|
|
|
|
if y["status"] != "failed" {
|
|
|
|
|
t.Errorf("del-Y: got status %v, want failed", y["status"])
|
|
|
|
|
}
|
|
|
|
|
if y["error"] != "network error" {
|
|
|
|
|
t.Errorf("del-Y: got error %v, want 'network error'", y["error"])
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("sqlmock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|