mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
* feat(squads): show member working status on squad detail page
Add a new GET /api/squads/{id}/members/status endpoint that returns each
member's derived working/idle/offline/unstable status, the issues each
agent is currently running, and the last observed activity timestamp.
The Squad detail page's Members tab consumes this snapshot to render a
status pill and an active-issue link next to each agent, with live
refresh wired through the existing task/agent/daemon WS events.
Human members are returned with status=null so the UI can keep them in
the same list without implying a presence signal. Archived agents stay
in the response and surface as offline rather than being filtered out.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): address review feedback on member status endpoint
- i18n the "blocked" issue-status pill in squad members tab (was a
bare literal that failed `i18next/no-literal-string` lint).
- Treat any dispatched/running task as working, even when its
`agent_task_queue.issue_id` is NULL (chat / quick-create tasks).
The agent slot is occupied regardless of whether we can render an
issue link.
- Force `offline` for archived agents so they appear in the list
but never look like they're still on duty, matching the RFC
decision in MUL-2319.
- Include `workspaceKeys.squads` in the post-reconnect /
workspace-switch bulk invalidation so members-status recovers
after a disconnect during which task/runtime events were missed.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
53 lines
1.9 KiB
Go
53 lines
1.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func TestDeriveSquadMemberStatus(t *testing.T) {
|
|
now := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
|
|
online := pgtype.Text{String: "online", Valid: true}
|
|
offline := pgtype.Text{String: "offline", Valid: true}
|
|
missing := pgtype.Text{}
|
|
|
|
tsAgo := func(d time.Duration) pgtype.Timestamptz {
|
|
return pgtype.Timestamptz{Time: now.Add(-d), Valid: true}
|
|
}
|
|
tsNone := pgtype.Timestamptz{}
|
|
|
|
cases := []struct {
|
|
name string
|
|
archived bool
|
|
runtimeStatus pgtype.Text
|
|
lastSeen pgtype.Timestamptz
|
|
hasActiveTask bool
|
|
want string
|
|
}{
|
|
{"active wins over offline runtime", false, offline, tsAgo(time.Hour), true, "working"},
|
|
{"active wins over missing runtime", false, missing, tsNone, true, "working"},
|
|
{"online runtime, no task", false, online, tsAgo(2 * time.Second), false, "idle"},
|
|
{"offline runtime, recent heartbeat", false, offline, tsAgo(2 * time.Minute), false, "unstable"},
|
|
{"offline runtime, stale heartbeat", false, offline, tsAgo(2 * time.Hour), false, "offline"},
|
|
{"offline runtime, no heartbeat", false, offline, tsNone, false, "offline"},
|
|
{"no runtime row", false, missing, tsNone, false, "offline"},
|
|
// Archived agents always report offline regardless of any leftover
|
|
// runtime row or task — they should appear in the squad listing
|
|
// but never look like they're still working.
|
|
{"archived agent with active task", true, online, tsAgo(time.Second), true, "offline"},
|
|
{"archived agent with online runtime", true, online, tsAgo(time.Second), false, "offline"},
|
|
{"archived agent already offline", true, offline, tsAgo(time.Hour), false, "offline"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := deriveSquadMemberStatus(tc.archived, tc.runtimeStatus, tc.lastSeen, tc.hasActiveTask, now)
|
|
if got != tc.want {
|
|
t.Fatalf("deriveSquadMemberStatus = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|