mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* fix(comments): leave system replies for denied mentions (MUL-8203) Co-authored-by: multica-agent <[email protected]> * fix(comments): address refusal notice review nits (MUL-8203) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Yushen <[email protected]> Co-authored-by: multica-agent <[email protected]>
215 lines
10 KiB
Go
215 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/multica-ai/multica/server/internal/events"
|
|
"github.com/multica-ai/multica/server/internal/testutil"
|
|
"github.com/multica-ai/multica/server/internal/util"
|
|
"github.com/multica-ai/multica/server/pkg/protocol"
|
|
)
|
|
|
|
func TestCommentInvocationDeniedNoticePersistsWithoutDisclosure(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
privateID, _, readerID := privateAgentTestFixture(t)
|
|
allowedID := createHandlerTestAgent(t, "Allowed notice target", nil)
|
|
squadID := createCommentTriggerPreviewSquad(t, "Denied notice squad", privateID)
|
|
foreignWorkspace := dbfx.Workspace(t, "Other notice workspace", "notice-"+uuid.NewString())
|
|
foreignID := dbfx.Agent(t, "Other workspace secret", handlerTestRuntimeID(t), testutil.Cols{"workspace_id": foreignWorkspace})
|
|
missingID := uuid.NewString()
|
|
issueID := dbfx.Issue(t, "Persist denied mentions")
|
|
dbfx.Cleanup(t, `DELETE FROM comment WHERE issue_id = $1`, issueID)
|
|
dbfx.Cleanup(t, `DELETE FROM agent_task_queue WHERE issue_id = $1`, issueID)
|
|
content := fmt.Sprintf("[@Private alias](mention://agent/%s) [@Missing alias](mention://agent/%s) [@Foreign alias](mention://agent/%s) [@Team alias](mention://squad/%s) [@Allowed alias](mention://agent/%s)", privateID, missingID, foreignID, squadID, allowedID)
|
|
preview := previewCommentTriggersForTest(t, issueID, map[string]any{"content": content})
|
|
if len(preview.Blocked) != 4 {
|
|
t.Fatalf("blocked preview = %+v, want four targets", preview.Blocked)
|
|
}
|
|
var count int
|
|
dbfx.QueryRow(t, `SELECT count(*) FROM comment WHERE issue_id = $1`, issueID).Scan(&count)
|
|
if count != 0 {
|
|
t.Fatal("preview wrote a comment")
|
|
}
|
|
h := *testHandler
|
|
h.Bus = events.New()
|
|
var notices []CommentResponse
|
|
h.Bus.Subscribe(protocol.EventCommentCreated, func(event events.Event) {
|
|
if payload, ok := event.Payload.(map[string]any); ok {
|
|
if comment, ok := payload["comment"].(CommentResponse); ok && comment.AuthorType == "system" {
|
|
notices = append(notices, comment)
|
|
}
|
|
}
|
|
})
|
|
var posted CommentResponse
|
|
testutil.Call(t, h.CreateComment, withURLParam(newRequest(http.MethodPost, "/api/issues/"+issueID+"/comments", map[string]any{"content": content}), "id", issueID)).Want(http.StatusCreated).JSON(&posted)
|
|
if len(posted.TriggerOutcomes) != 5 || findCommentOutcome(t, posted.TriggerOutcomes, allowedID).Status != DispatchQueued {
|
|
t.Fatalf("partial submission outcomes = %+v", posted.TriggerOutcomes)
|
|
}
|
|
if len(notices) != 1 {
|
|
t.Fatalf("broadcast system notices = %d, want one for this submission", len(notices))
|
|
}
|
|
notice := notices[0]
|
|
if notice.ParentID == nil || *notice.ParentID != posted.ID || notice.Type != "system" {
|
|
t.Fatalf("notice is not a system reply to the submitted comment: %+v", notice)
|
|
}
|
|
for _, alias := range []string{"Private alias", "Missing alias", "Foreign alias", "Team alias"} {
|
|
if !strings.Contains(notice.Content, alias) {
|
|
t.Errorf("notice omitted %q: %s", alias, notice.Content)
|
|
}
|
|
}
|
|
if strings.Count(notice.Content, "Target not found or permission not granted") != 4 {
|
|
t.Errorf("denied targets do not share the generic reason: %s", notice.Content)
|
|
}
|
|
for _, secret := range []string{"private-access-test-agent", "Other workspace secret", "[email protected]", "Allowed alias"} {
|
|
if strings.Contains(notice.Content, secret) {
|
|
t.Errorf("notice exposed an unrelated or private value %q", secret)
|
|
}
|
|
}
|
|
if len(util.ParseMentions(notice.Content)) != 0 {
|
|
t.Fatalf("notice contains actionable mentions: %s", notice.Content)
|
|
}
|
|
dbfx.QueryRow(t, `SELECT count(*) FROM agent_task_queue WHERE issue_id = $1`, issueID).Scan(&count)
|
|
if count != 1 || countQueuedCommentTriggerTasks(t, issueID, allowedID) != 1 {
|
|
t.Fatal("denied mentions or the system notice created an extra task")
|
|
}
|
|
var comments []CommentResponse
|
|
testutil.Call(t, h.ListComments, withURLParam(newRequestAs(readerID, http.MethodGet, "/api/issues/"+issueID+"/comments", nil), "id", issueID)).Want(http.StatusOK).JSON(&comments)
|
|
if len(comments) != 2 || comments[1].ID != notice.ID || comments[1].Content != notice.Content {
|
|
t.Fatalf("another reader did not receive the saved notice: %+v", comments)
|
|
}
|
|
var timeline []TimelineEntry
|
|
testutil.Call(t, h.ListTimeline, withURLParam(newRequestAs(readerID, http.MethodGet, "/api/issues/"+issueID+"/timeline", nil), "id", issueID)).Want(http.StatusOK).JSON(&timeline)
|
|
for _, entry := range timeline {
|
|
if entry.ID == notice.ID && entry.Content != nil && *entry.Content == notice.Content {
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("saved notice is absent from the timeline")
|
|
}
|
|
|
|
func TestCommentInvocationDeniedNoticeForAgentEditsKeepsHistory(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
privateID, _, _ := privateAgentTestFixture(t)
|
|
leaderID := createHandlerTestAgent(t, "Denied handoff leader", nil)
|
|
issueID := dbfx.Issue(t, "Denied agent handoff", testutil.Cols{"status": "in_progress"})
|
|
dbfx.Cleanup(t, `DELETE FROM comment WHERE issue_id = $1`, issueID)
|
|
sourceTaskID := dbfx.Task(t, leaderID, testutil.Cols{"issue_id": issueID, "runtime_id": handlerTestRuntimeID(t), "status": "running", "originator_user_id": testUserID, "accountable_user_id": testUserID})
|
|
content := fmt.Sprintf("[@Worker alias](mention://agent/%s) take over", privateID)
|
|
request := func(method, path string, body any) *http.Request {
|
|
req := newRequest(method, path, body)
|
|
req.Header.Set("X-Agent-ID", leaderID)
|
|
req.Header.Set("X-Task-ID", sourceTaskID)
|
|
return req
|
|
}
|
|
var posted CommentResponse
|
|
testutil.Call(t, testHandler.CreateComment, withURLParam(request(http.MethodPost, "/api/issues/"+issueID+"/comments", map[string]any{"content": content}), "id", issueID)).Want(http.StatusCreated).JSON(&posted)
|
|
if posted.AuthorType != "agent" || findCommentOutcome(t, posted.TriggerOutcomes, privateID).Status != DispatchBlocked {
|
|
t.Fatalf("agent handoff = %+v", posted)
|
|
}
|
|
var firstNoticeID, firstNotice string
|
|
dbfx.QueryRow(t, `SELECT id, content FROM comment WHERE parent_id = $1 AND author_type = 'system'`, posted.ID).Scan(&firstNoticeID, &firstNotice)
|
|
submittedAt, err := time.Parse(time.RFC3339Nano, posted.UpdatedAt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(firstNotice, submittedAt.UTC().Format("Jan 2, 2006 at 15:04:05 MST")) || strings.Contains(firstNotice, "revision") {
|
|
t.Fatalf("notice should identify the submission time without internal version details: %s", firstNotice)
|
|
}
|
|
edit := func(text string) CommentResponse {
|
|
var updated CommentResponse
|
|
testutil.Call(t, testHandler.UpdateComment, withURLParam(request(http.MethodPut, "/api/comments/"+posted.ID, map[string]any{"content": text}), "commentId", posted.ID)).Want(http.StatusOK).JSON(&updated)
|
|
return updated
|
|
}
|
|
edit(content)
|
|
edit("Handoff withdrawn")
|
|
updated := edit(content)
|
|
var notices []CommentResponse
|
|
_, notices = listComments(t, issueID, "?thread="+posted.ID)
|
|
if len(notices) != 3 || notices[1].ID != firstNoticeID || notices[1].Content != firstNotice {
|
|
t.Fatalf("edits changed the old receipt or created an extra receipt: %+v", notices)
|
|
}
|
|
editedAt, err := time.Parse(time.RFC3339Nano, updated.UpdatedAt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if notices[2].ID == firstNoticeID || !strings.Contains(notices[2].Content, editedAt.UTC().Format("Jan 2, 2006 at 15:04:05 MST")) || strings.Contains(notices[2].Content, "revision") {
|
|
t.Fatalf("edited-back submission did not get its own historical receipt: %+v", notices[2])
|
|
}
|
|
var tasks int
|
|
dbfx.QueryRow(t, `SELECT count(*) FROM agent_task_queue WHERE issue_id = $1`, issueID).Scan(&tasks)
|
|
if tasks != 1 {
|
|
t.Fatalf("handoff or notices created extra runs: %d tasks", tasks)
|
|
}
|
|
stored, err := testHandler.Queries.GetComment(context.Background(), parseUUID(firstNoticeID))
|
|
if err != nil || stored.AuthorType != "system" || stored.Content != firstNotice {
|
|
t.Fatalf("historical notice changed: %+v, %v", stored, err)
|
|
}
|
|
}
|
|
|
|
func TestCommentInvocationDeniedNoticeSkipsOtherSubmissions(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
privateID, _, _ := privateAgentTestFixture(t)
|
|
allowedID := createHandlerTestAgent(t, "Successful notice target", nil)
|
|
for _, tc := range []struct{ name, content string }{
|
|
{"successful", fmt.Sprintf("[@Allowed](mention://agent/%s) proceed", allowedID)},
|
|
{"note", fmt.Sprintf("/note [@Private](mention://agent/%s) reference only", privateID)},
|
|
{"code", fmt.Sprintf("`[@Private](mention://agent/%s)` is an example", privateID)},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
issueID := dbfx.Issue(t, "No permission notice for "+tc.name)
|
|
dbfx.Cleanup(t, `DELETE FROM comment WHERE issue_id = $1`, issueID)
|
|
dbfx.Cleanup(t, `DELETE FROM agent_task_queue WHERE issue_id = $1`, issueID)
|
|
testutil.Call(t, testHandler.CreateComment, withURLParam(newRequest(http.MethodPost, "/api/issues/"+issueID+"/comments", map[string]any{"content": tc.content}), "id", issueID)).Want(http.StatusCreated)
|
|
var comments int
|
|
dbfx.QueryRow(t, `SELECT count(*) FROM comment WHERE issue_id = $1`, issueID).Scan(&comments)
|
|
if comments != 1 {
|
|
t.Fatalf("submission added a refusal notice: %d comments", comments)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommentInvocationDeniedNoticeSkipsDeletedParent(t *testing.T) {
|
|
if testHandler == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
privateID, _, _ := privateAgentTestFixture(t)
|
|
issueID := dbfx.Issue(t, "Delete before permission notice")
|
|
h := *testHandler
|
|
h.Bus = events.New()
|
|
h.Bus.Subscribe(protocol.EventCommentCreated, func(event events.Event) {
|
|
payload, ok := event.Payload.(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
comment, ok := payload["comment"].(CommentResponse)
|
|
if ok && comment.AuthorType == "member" {
|
|
dbfx.Exec(t, `DELETE FROM comment WHERE id = $1 AND workspace_id = $2`, comment.ID, testWorkspaceID)
|
|
}
|
|
})
|
|
var posted CommentResponse
|
|
testutil.Call(t, h.CreateComment, withURLParam(newRequest(http.MethodPost, "/api/issues/"+issueID+"/comments", map[string]any{
|
|
"content": fmt.Sprintf("[@Private](mention://agent/%s)", privateID),
|
|
}), "id", issueID)).Want(http.StatusCreated).JSON(&posted)
|
|
if findCommentOutcome(t, posted.TriggerOutcomes, privateID).Status != DispatchBlocked {
|
|
t.Fatal("notice failure changed the admission result")
|
|
}
|
|
var comments int
|
|
dbfx.QueryRow(t, `SELECT count(*) FROM comment WHERE issue_id = $1`, issueID).Scan(&comments)
|
|
if comments != 0 {
|
|
t.Fatalf("notice left an orphan after deletion: %d comments", comments)
|
|
}
|
|
}
|