mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
Before: The feed gave no sense of how far an agent had got. Agents could
not post a progress report that the feed understands: `comment add` had no
way to set a comment type, and the run brief told agents not to post
progress at all.
After: Each feed message shows a compact checklist under its thread bar
(done/total, a small bar, the first 5 items, "+N more"), and the issue peek
shows the same block under its title. Finished issues (done or closed
category) fold to a single "done/total" line with a chevron to expand. A
progress report is a `progress_update` comment by an agent whose body is a
Markdown task list; the newest one per issue wins.
How: The thread-summaries endpoint gains `latest_progress`
({comment_id, content, created_at} | null) from one batched DISTINCT ON
query, so there is no new endpoint. Core adds the type, a tolerant zod
schema, `parseProgressChecklist`, and a realtime patch for new reports.
Views add `FeedProgressCard`, mounted from the thread bar and the peek, with
strings in all five locales. `multica issue comment add --type
progress_update` lets agents post a report, and the run brief documents it as
the one exception to the single-comment rule.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
130 lines
4.5 KiB
Go
130 lines
4.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/multica-ai/multica/server/internal/testutil"
|
|
)
|
|
|
|
const progressChecklist = "Plan\n\n- [x] read the code\n- [ ] write the fix\n"
|
|
|
|
func progressCols(at time.Time, extra ...testutil.Cols) testutil.Cols {
|
|
cols := testutil.Cols{
|
|
"type": "progress_update",
|
|
"author_type": "agent",
|
|
"author_id": uuid.NewString(),
|
|
"created_at": at,
|
|
}
|
|
for _, e := range extra {
|
|
for k, v := range e {
|
|
cols[k] = v
|
|
}
|
|
}
|
|
return cols
|
|
}
|
|
|
|
// latestProgressFor returns the single summary row's latest_progress. The
|
|
// issue always carries a plain member comment so a row exists even when no
|
|
// progress report qualifies.
|
|
func latestProgressFor(t *testing.T, issueID string) *LatestProgressResponse {
|
|
t.Helper()
|
|
got := listThreadSummaries(t, issueID)
|
|
if len(got.Summaries) != 1 {
|
|
t.Fatalf("summaries = %+v, want exactly one row", got.Summaries)
|
|
}
|
|
return got.Summaries[0].LatestProgress
|
|
}
|
|
|
|
func TestListIssueThreadSummaries_LatestProgress_NewestWins(t *testing.T) {
|
|
issueID := dbfx.Issue(t, "progress newest")
|
|
at := agentQuestionClock()
|
|
dbfx.Comment(t, issueID, "- [ ] old item", progressCols(at(1)))
|
|
newest := dbfx.Comment(t, issueID, progressChecklist, progressCols(at(3)))
|
|
dbfx.Comment(t, issueID, "- [x] middle item", progressCols(at(2)))
|
|
|
|
got := latestProgressFor(t, issueID)
|
|
if got == nil {
|
|
t.Fatal("latest_progress = nil, want the newest report")
|
|
}
|
|
if got.CommentID != newest || got.Content != progressChecklist {
|
|
t.Errorf("latest_progress = %+v, want comment %s with its full body", got, newest)
|
|
}
|
|
created, err := time.Parse(time.RFC3339Nano, got.CreatedAt)
|
|
if err != nil || !created.Equal(at(3)) {
|
|
t.Errorf("created_at = %q (err %v), want %s", got.CreatedAt, err, at(3).Format(time.RFC3339Nano))
|
|
}
|
|
}
|
|
|
|
func TestListIssueThreadSummaries_LatestProgress_NullWhenNone(t *testing.T) {
|
|
issueID := dbfx.Issue(t, "progress none")
|
|
dbfx.Comment(t, issueID, "just a note")
|
|
if got := latestProgressFor(t, issueID); got != nil {
|
|
t.Fatalf("latest_progress = %+v, want null", got)
|
|
}
|
|
}
|
|
|
|
func TestListIssueThreadSummaries_LatestProgress_MemberAuthoredIgnored(t *testing.T) {
|
|
issueID := dbfx.Issue(t, "progress member")
|
|
dbfx.Comment(t, issueID, progressChecklist, testutil.Cols{"type": "progress_update"})
|
|
if got := latestProgressFor(t, issueID); got != nil {
|
|
t.Fatalf("latest_progress = %+v, want null for a member-authored report", got)
|
|
}
|
|
}
|
|
|
|
func TestListIssueThreadSummaries_LatestProgress_DeletedIgnored(t *testing.T) {
|
|
issueID := dbfx.Issue(t, "progress deleted")
|
|
at := agentQuestionClock()
|
|
older := dbfx.Comment(t, issueID, "- [ ] still open", progressCols(at(1)))
|
|
dbfx.Comment(t, issueID, progressChecklist, progressCols(at(2), testutil.Cols{"deleted_at": at(3)}))
|
|
|
|
got := latestProgressFor(t, issueID)
|
|
if got == nil || got.CommentID != older {
|
|
t.Fatalf("latest_progress = %+v, want the older report %s once the newest is deleted", got, older)
|
|
}
|
|
}
|
|
|
|
func TestListIssueThreadSummaries_LatestProgress_PlainCommentTypeIgnored(t *testing.T) {
|
|
issueID := dbfx.Issue(t, "progress plain type")
|
|
dbfx.Comment(t, issueID, progressChecklist, testutil.Cols{
|
|
"author_type": "agent", "author_id": uuid.NewString(),
|
|
})
|
|
if got := latestProgressFor(t, issueID); got != nil {
|
|
t.Fatalf("latest_progress = %+v, want null for a type=comment checklist", got)
|
|
}
|
|
}
|
|
|
|
func TestListIssueThreadSummaries_LatestProgress_RequiresTaskList(t *testing.T) {
|
|
issueID := dbfx.Issue(t, "progress task list")
|
|
at := agentQuestionClock()
|
|
checklist := dbfx.Comment(t, issueID, " * [X] nested and starred", progressCols(at(1)))
|
|
dbfx.Comment(t, issueID, "Working on it, no checklist yet.\n- plain bullet", progressCols(at(2)))
|
|
|
|
got := latestProgressFor(t, issueID)
|
|
if got == nil || got.CommentID != checklist {
|
|
t.Fatalf("latest_progress = %+v, want the checklist %s; a newer report without a task list must not shadow it", got, checklist)
|
|
}
|
|
}
|
|
|
|
func TestListIssueThreadSummaries_LatestProgress_PerIssue(t *testing.T) {
|
|
a := dbfx.Issue(t, "progress a")
|
|
b := dbfx.Issue(t, "progress b")
|
|
at := agentQuestionClock()
|
|
progressA := dbfx.Comment(t, a, "- [ ] a", progressCols(at(1)))
|
|
dbfx.Comment(t, b, "plain")
|
|
|
|
got := listThreadSummaries(t, a, b)
|
|
byIssue := map[string]*LatestProgressResponse{}
|
|
for _, s := range got.Summaries {
|
|
byIssue[s.IssueID] = s.LatestProgress
|
|
}
|
|
if byIssue[a] == nil || byIssue[a].CommentID != progressA {
|
|
t.Errorf("issue a latest_progress = %+v, want %s", byIssue[a], progressA)
|
|
}
|
|
if byIssue[b] != nil {
|
|
t.Errorf("issue b latest_progress = %+v, want null", byIssue[b])
|
|
}
|
|
}
|