Files
multica/server/internal/handler/comment_reply_authz_handler_test.go
Bohan Jiang 9d453fac1e fix(comments): clarify 409 for top-level comment from a comment-triggered task (MUL-4417) (#5292)
* fix(comments): clarify 409 when a comment-triggered task posts a top-level comment (MUL-4417)

A comment-triggered task that posted a parentless top-level comment on its
own issue got a 409 whose message named the required parent id but never
said top-level comments are disallowed. Agents misread it as the issue being
locked and deleted good replies trying to reset. Keep the guard (agents must
reply under their trigger comment), but make the error self-explanatory and
document the constraint in the CLI --parent help. Add handler-level tests
pinning the rejected top-level case and the allowed reply-under-trigger case.

Refs GH #5266.

Co-authored-by: multica-agent <github@multica.ai>

* fix(comments): tighten 409 wording and assert the fix hint (MUL-4417)

Review nits on #5292: drop the inaccurate "while it is active" phrasing and
the redundancy from the 409 message so it matches the actual allow-set
(trigger or coalesced comment); collapse the incident narration to one line;
and assert the actionable parent_id (--parent) hint in the regression test so
the guidance can't be dropped silently.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J (Multica agent) <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-12 14:23:30 +08:00

85 lines
3.0 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestCreateComment_TriggeredTaskRejectsTopLevelComment exercises the full
// CreateComment handler path (not just taskCoversReplyParent) for the trap
// reported in MUL-4417 / GH #5266: a comment-triggered task that posts a
// parentless, top-level comment on its own issue is rejected with a 409 whose
// message names the trigger comment and states that top-level comments are not
// allowed. Pinning the message here keeps it from silently drifting away from
// the behavior the CLI help now documents.
func TestCreateComment_TriggeredTaskRejectsTopLevelComment(t *testing.T) {
if testHandler == nil || testPool == nil {
t.Skip("database not available")
}
fx := newRunningSquadLeaderTaskFixture(t)
w := httptest.NewRecorder()
r := newRequest("POST", "/api/issues/"+fx.IssueID+"/comments", map[string]any{
"content": "dispatching a squad from this task",
})
r = withURLParam(r, "id", fx.IssueID)
r.Header.Set("X-Agent-ID", fx.LeaderID)
r.Header.Set("X-Task-ID", fx.TaskID)
testHandler.CreateComment(w, r)
if w.Code != http.StatusConflict {
t.Fatalf("CreateComment top-level: expected 409, got %d: %s", w.Code, w.Body.String())
}
if got := countAgentCommentsForIssue(t, fx.IssueID, fx.LeaderID); got != 0 {
t.Fatalf("expected rejected top-level comment not to be stored, got %d", got)
}
var body map[string]any
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
t.Fatalf("decode error response: %v", err)
}
msg, _ := body["error"].(string)
// Pin the three semantic pieces without locking the exact wording: why it
// was rejected, the comment to reply under, and the actionable fix. The last
// one guards against the guidance being dropped in a future edit.
for _, want := range []string{
"top-level comments", // reason
fx.TriggerCommentID, // the comment to reply under
"parent_id (--parent)", // actionable fix
} {
if !strings.Contains(msg, want) {
t.Fatalf("409 message should contain %q, got %q", want, msg)
}
}
}
// TestCreateComment_TriggeredTaskAllowsReplyUnderTrigger is the positive half:
// the same task replying under its trigger comment succeeds, proving the guard
// rejects only the top-level case and does not lock the whole issue for
// comments (MUL-4417 / GH #5266).
func TestCreateComment_TriggeredTaskAllowsReplyUnderTrigger(t *testing.T) {
if testHandler == nil || testPool == nil {
t.Skip("database not available")
}
fx := newRunningSquadLeaderTaskFixture(t)
w := httptest.NewRecorder()
r := newRequest("POST", "/api/issues/"+fx.IssueID+"/comments", map[string]any{
"content": "replying under the trigger comment",
"parent_id": fx.TriggerCommentID,
})
r = withURLParam(r, "id", fx.IssueID)
r.Header.Set("X-Agent-ID", fx.LeaderID)
r.Header.Set("X-Task-ID", fx.TaskID)
testHandler.CreateComment(w, r)
if w.Code != http.StatusCreated {
t.Fatalf("CreateComment reply-under-trigger: expected 201, got %d: %s", w.Code, w.Body.String())
}
}