mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 22:10:25 +00:00
0f1069e9df
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
48 lines
1.3 KiB
Go
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)
|
|
}
|
|
}
|