Files
pad/internal/models/comment_test.go
T
xarmian 0f1069e9df feat(comments): agents can edit and delete comments from the CLI and every MCP transport (TASK-2695) (#1607)
Web users could edit and delete comments since PLAN-1662. Agents, who write
most comments, could only append corrections.

- Server: new item-scoped routes, PATCH/DELETE
  /workspaces/{ws}/items/{ref}/comments/{id}. They answer 404 unless the
  comment is on {ref}, then run the existing handlers unchanged (edit is
  author-only, delete needs item-edit). GET /server/capabilities advertises
  them as item_scoped_comment_writes.
- CLI: `pad item comment-edit <ref> <comment-id> [message|--stdin]` and
  `pad item comment-delete <ref> <comment-id>`. Both refuse to send to a
  server without the capability, whose bare 404 would otherwise read as
  "comment not found". `pad item comments` now prints each comment's id,
  plus "edited" once its body has changed.
- models.Comment serialises a derived `edited` bool, true when
  updated_at > created_at (the web timeline's rule).
- MCP 0.57: pad_item edit-comment / delete-comment and a comment_id param,
  on stdio, remote /mcp and WebMCP. All three use the item-scoped routes,
  so the check that the comment is on ref happens on the server for every
  transport. Remote packages a delete's 204 as the CLI's JSON.

Found along the way, and out of scope: deleting a comment that has replies
answers 500, because parent_id has no ON DELETE. Filed as BUG-3252.

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9
2026-09-27 07:59:11 -04:00

48 lines
1.3 KiB
Go

package models
import (
"encoding/json"
"strings"
"testing"
"time"
)
// TASK-2695: `edited` is derived from the timestamps and emitted on every
// serialised comment, nested replies included.
func TestCommentEditedMarker(t *testing.T) {
created := time.Date(2026, 9, 27, 12, 0, 0, 0, time.UTC)
fresh := Comment{ID: "c1", CreatedAt: created, UpdatedAt: created}
edited := Comment{ID: "c2", CreatedAt: created, UpdatedAt: created.Add(time.Second)}
if fresh.IsEdited() {
t.Error("a comment whose timestamps match is not edited")
}
if !edited.IsEdited() {
t.Error("a comment updated after creation is edited")
}
if (Comment{}).IsEdited() {
t.Error("a zero comment is not edited")
}
fresh.Replies = []Comment{edited}
b, err := json.Marshal(fresh)
if err != nil {
t.Fatal(err)
}
got := string(b)
if !strings.Contains(got, `"edited":false`) || !strings.Contains(got, `"edited":true`) {
t.Fatalf("want edited on the comment and on its reply, got %s", got)
}
if !strings.Contains(got, `"id":"c1"`) || !strings.Contains(got, `"replies":[`) {
t.Fatalf("the ordinary fields must still serialise: %s", got)
}
var back Comment
if err := json.Unmarshal(b, &back); err != nil {
t.Fatalf("a serialised comment must still decode: %v", err)
}
if back.ID != "c1" || len(back.Replies) != 1 {
t.Fatalf("round trip lost fields: %+v", back)
}
}