From 98fe6e006852ddea4bd4efd9233007b590de3339 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 25 Jul 2026 00:36:46 -0700 Subject: [PATCH] test(todos): guard todos show --json surfaces SDK fields (Refs #449) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add TestTodosShowJSONSurfacesSDKFields, a regression guard for #449: the fields that report (due_on, starts_on, comments_count, boosts_count, completion_subscribers, description_attachments) must all surface in the --json output of `todos show`. The implementation already landed on main via the SDK model — SDK #375 added completion_subscribers/comments_count, SDK #400 added description_attachments, absorbed by CLI #552/#554's SDK bumps; due_on, starts_on and boosts_count already existed with omitempty. `todos show` emits the SDK Todo object directly, so the fields surface today. This is coverage, not a fix (hence Refs, not Fixes). The test serves a fully-populated todo and asserts both the presence and the values of all six fields, keeping the omitempty date/count fields exercised with nonzero values so a future SDK bump can't silently drop them. --- internal/commands/todos_test.go | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/internal/commands/todos_test.go b/internal/commands/todos_test.go index bfcd5b17b..ffe2f1b3c 100644 --- a/internal/commands/todos_test.go +++ b/internal/commands/todos_test.go @@ -2922,3 +2922,121 @@ func TestTodosListAggregateSortGuard(t *testing.T) { errA := executeTodosCommand(NewTodosCmd(), appA, "list", "--in", "123", "--assignee", "Alice", "--sort", "title") require.NoError(t, errA) } + +// populatedTodoTransport serves a single fully-populated todo from the GET +// endpoint so that todos show exercises every field #449 reported as missing. +// The description carries no bc-attachment markup, so no download path fires; +// description_attachments rides along as SDK metadata independent of the +// rich-text body. +type populatedTodoTransport struct{} + +func (populatedTodoTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Fail closed: this transport only knows how to answer the todo GET. Any + // other route or method means `todos show` took an unexpected path, which + // should fail the test rather than silently pass on the canned body. + if req.Method != http.MethodGet || !strings.Contains(req.URL.Path, "/todos/999") { + return nil, fmt.Errorf("unexpected request: %s %s", req.Method, req.URL.Path) + } + + header := make(http.Header) + header.Set("Content-Type", "application/json") + + body := `{ + "id": 999, + "status": "active", + "title": "Populated todo", + "type": "Todo", + "content": "Populated todo", + "description": "
Plain description, no attachments
", + "completed": false, + "position": 1, + "starts_on": "2026-08-01", + "due_on": "2026-08-15", + "comments_count": 4, + "boosts_count": 3, + "completion_subscribers": [ + {"id": 5551, "name": "Reviewer One"}, + {"id": 5552, "name": "Reviewer Two"} + ], + "description_attachments": [ + { + "id": 8801, + "sgid": "sgid-abc", + "filename": "spec.pdf", + "content_type": "application/pdf", + "byte_size": 2048, + "download_url": "https://example.com/spec.pdf", + "previewable": false, + "preview_url": "", + "thumbnail_url": "" + } + ] + }` + + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(body)), + Header: header, + }, nil +} + +// TestTodosShowJSONSurfacesSDKFields is a regression guard for #449: the +// fields that report (due_on, starts_on, comments_count, boosts_count, +// completion_subscribers, description_attachments) must all surface in the +// --json output of todos show. The fix landed on main via the SDK model +// (SDK #375/#400) absorbed by CLI #552/#554; this test keeps the omitempty +// date/count fields exercised with nonzero values so a future SDK bump can't +// silently drop them. +func TestTodosShowJSONSurfacesSDKFields(t *testing.T) { + app, buf := setupGroupTodoApp(t, populatedTodoTransport{}) + + err := executeTodosCommand(NewTodosCmd(), app, "show", "999") + require.NoError(t, err) + + var resp struct { + Data map[string]json.RawMessage `json:"data"` + } + require.NoError(t, json.Unmarshal(buf.Bytes(), &resp)) + + // Every reported field must be present as a key in the emitted JSON. + for _, key := range []string{ + "due_on", "starts_on", "comments_count", "boosts_count", + "completion_subscribers", "description_attachments", + } { + require.Contains(t, resp.Data, key, "todos show --json must surface %q (#449)", key) + } + + var dueOn, startsOn string + require.NoError(t, json.Unmarshal(resp.Data["due_on"], &dueOn)) + require.NoError(t, json.Unmarshal(resp.Data["starts_on"], &startsOn)) + assert.Equal(t, "2026-08-15", dueOn) + assert.Equal(t, "2026-08-01", startsOn) + + var commentsCount, boostsCount int + require.NoError(t, json.Unmarshal(resp.Data["comments_count"], &commentsCount)) + require.NoError(t, json.Unmarshal(resp.Data["boosts_count"], &boostsCount)) + assert.Equal(t, 4, commentsCount) + assert.Equal(t, 3, boostsCount) + + var subscribers []struct { + ID int64 `json:"id"` + Name string `json:"name"` + } + require.NoError(t, json.Unmarshal(resp.Data["completion_subscribers"], &subscribers)) + require.Len(t, subscribers, 2) + assert.Equal(t, int64(5551), subscribers[0].ID) + assert.Equal(t, "Reviewer One", subscribers[0].Name) + assert.Equal(t, int64(5552), subscribers[1].ID) + assert.Equal(t, "Reviewer Two", subscribers[1].Name) + + var attachments []struct { + ID int64 `json:"id"` + Filename string `json:"filename"` + DownloadURL string `json:"download_url"` + } + require.NoError(t, json.Unmarshal(resp.Data["description_attachments"], &attachments)) + require.Len(t, attachments, 1) + assert.Equal(t, int64(8801), attachments[0].ID) + assert.Equal(t, "spec.pdf", attachments[0].Filename) + assert.Equal(t, "https://example.com/spec.pdf", attachments[0].DownloadURL) +}