@@ -2922,3 +2922,121 @@ func TestTodosListAggregateSortGuard(t *testing.T) {
29222922 errA := executeTodosCommand (NewTodosCmd (), appA , "list" , "--in" , "123" , "--assignee" , "Alice" , "--sort" , "title" )
29232923 require .NoError (t , errA )
29242924}
2925+
2926+ // populatedTodoTransport serves a single fully-populated todo from the GET
2927+ // endpoint so that todos show exercises every field #449 reported as missing.
2928+ // The description carries no bc-attachment markup, so no download path fires;
2929+ // description_attachments rides along as SDK metadata independent of the
2930+ // rich-text body.
2931+ type populatedTodoTransport struct {}
2932+
2933+ func (populatedTodoTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
2934+ // Fail closed: this transport only knows how to answer the todo GET. Any
2935+ // other route or method means `todos show` took an unexpected path, which
2936+ // should fail the test rather than silently pass on the canned body.
2937+ if req .Method != http .MethodGet || ! strings .Contains (req .URL .Path , "/todos/999" ) {
2938+ return nil , fmt .Errorf ("unexpected request: %s %s" , req .Method , req .URL .Path )
2939+ }
2940+
2941+ header := make (http.Header )
2942+ header .Set ("Content-Type" , "application/json" )
2943+
2944+ body := `{
2945+ "id": 999,
2946+ "status": "active",
2947+ "title": "Populated todo",
2948+ "type": "Todo",
2949+ "content": "Populated todo",
2950+ "description": "<div>Plain description, no attachments</div>",
2951+ "completed": false,
2952+ "position": 1,
2953+ "starts_on": "2026-08-01",
2954+ "due_on": "2026-08-15",
2955+ "comments_count": 4,
2956+ "boosts_count": 3,
2957+ "completion_subscribers": [
2958+ {"id": 5551, "name": "Reviewer One"},
2959+ {"id": 5552, "name": "Reviewer Two"}
2960+ ],
2961+ "description_attachments": [
2962+ {
2963+ "id": 8801,
2964+ "sgid": "sgid-abc",
2965+ "filename": "spec.pdf",
2966+ "content_type": "application/pdf",
2967+ "byte_size": 2048,
2968+ "download_url": "https://example.com/spec.pdf",
2969+ "previewable": false,
2970+ "preview_url": "",
2971+ "thumbnail_url": ""
2972+ }
2973+ ]
2974+ }`
2975+
2976+ return & http.Response {
2977+ StatusCode : 200 ,
2978+ Body : io .NopCloser (strings .NewReader (body )),
2979+ Header : header ,
2980+ }, nil
2981+ }
2982+
2983+ // TestTodosShowJSONSurfacesSDKFields is a regression guard for #449: the
2984+ // fields that report (due_on, starts_on, comments_count, boosts_count,
2985+ // completion_subscribers, description_attachments) must all surface in the
2986+ // --json output of todos show. The fix landed on main via the SDK model
2987+ // (SDK #375/#400) absorbed by CLI #552/#554; this test keeps the omitempty
2988+ // date/count fields exercised with nonzero values so a future SDK bump can't
2989+ // silently drop them.
2990+ func TestTodosShowJSONSurfacesSDKFields (t * testing.T ) {
2991+ app , buf := setupGroupTodoApp (t , populatedTodoTransport {})
2992+
2993+ err := executeTodosCommand (NewTodosCmd (), app , "show" , "999" )
2994+ require .NoError (t , err )
2995+
2996+ var resp struct {
2997+ Data map [string ]json.RawMessage `json:"data"`
2998+ }
2999+ require .NoError (t , json .Unmarshal (buf .Bytes (), & resp ))
3000+
3001+ // Every reported field must be present as a key in the emitted JSON.
3002+ for _ , key := range []string {
3003+ "due_on" , "starts_on" , "comments_count" , "boosts_count" ,
3004+ "completion_subscribers" , "description_attachments" ,
3005+ } {
3006+ require .Contains (t , resp .Data , key , "todos show --json must surface %q (#449)" , key )
3007+ }
3008+
3009+ var dueOn , startsOn string
3010+ require .NoError (t , json .Unmarshal (resp .Data ["due_on" ], & dueOn ))
3011+ require .NoError (t , json .Unmarshal (resp .Data ["starts_on" ], & startsOn ))
3012+ assert .Equal (t , "2026-08-15" , dueOn )
3013+ assert .Equal (t , "2026-08-01" , startsOn )
3014+
3015+ var commentsCount , boostsCount int
3016+ require .NoError (t , json .Unmarshal (resp .Data ["comments_count" ], & commentsCount ))
3017+ require .NoError (t , json .Unmarshal (resp .Data ["boosts_count" ], & boostsCount ))
3018+ assert .Equal (t , 4 , commentsCount )
3019+ assert .Equal (t , 3 , boostsCount )
3020+
3021+ var subscribers []struct {
3022+ ID int64 `json:"id"`
3023+ Name string `json:"name"`
3024+ }
3025+ require .NoError (t , json .Unmarshal (resp .Data ["completion_subscribers" ], & subscribers ))
3026+ require .Len (t , subscribers , 2 )
3027+ assert .Equal (t , int64 (5551 ), subscribers [0 ].ID )
3028+ assert .Equal (t , "Reviewer One" , subscribers [0 ].Name )
3029+ assert .Equal (t , int64 (5552 ), subscribers [1 ].ID )
3030+ assert .Equal (t , "Reviewer Two" , subscribers [1 ].Name )
3031+
3032+ var attachments []struct {
3033+ ID int64 `json:"id"`
3034+ Filename string `json:"filename"`
3035+ DownloadURL string `json:"download_url"`
3036+ }
3037+ require .NoError (t , json .Unmarshal (resp .Data ["description_attachments" ], & attachments ))
3038+ require .Len (t , attachments , 1 )
3039+ assert .Equal (t , int64 (8801 ), attachments [0 ].ID )
3040+ assert .Equal (t , "spec.pdf" , attachments [0 ].Filename )
3041+ assert .Equal (t , "https://example.com/spec.pdf" , attachments [0 ].DownloadURL )
3042+ }
0 commit comments