@@ -386,26 +386,67 @@ func smartChipDocsContent() []any {
386386
387387func newSmartChipDocsTestServer (t * testing.T ) (* docs.Service , func ()) {
388388 t .Helper ()
389+ return newDocsContentTestServer (t , "Smart chip doc" , "Chips" , smartChipDocsContent )
390+ }
391+
392+ func linkedTextDocsContent () []any {
393+ return []any {
394+ map [string ]any {"paragraph" : map [string ]any {"elements" : []any {
395+ map [string ]any {"startIndex" : 1 , "endIndex" : 7 , "textRun" : map [string ]any {"content" : "Intro " }},
396+ map [string ]any {"startIndex" : 7 , "endIndex" : 21 , "textRun" : map [string ]any {
397+ "content" : "Ticket [draft]" ,
398+ "textStyle" : map [string ]any {"link" : map [string ]any {"url" : "https://tracker.example.com/a_(draft)" }},
399+ }},
400+ map [string ]any {"startIndex" : 21 , "endIndex" : 26 , "textRun" : map [string ]any {"content" : " and " }},
401+ map [string ]any {"startIndex" : 26 , "endIndex" : 45 , "textRun" : map [string ]any {
402+ "content" : "https://example.com" ,
403+ "textStyle" : map [string ]any {"link" : map [string ]any {"url" : "https://example.com" }},
404+ }},
405+ map [string ]any {"startIndex" : 45 , "endIndex" : 52 , "textRun" : map [string ]any {"content" : " owner " }},
406+ map [string ]any {"startIndex" : 52 , "endIndex" : 53 , "person" : map [string ]any {
407+ "personProperties" : map [string ]any {"name" : "Sample Person" , "email" : "sample@example.com" },
408+ }},
409+ map [string ]any {"startIndex" : 53 , "endIndex" : 54 , "textRun" : map [string ]any {"content" : "\n " }},
410+ }}},
411+ map [string ]any {"table" : map [string ]any {"tableRows" : []any {
412+ map [string ]any {"tableCells" : []any {
413+ map [string ]any {"content" : []any {map [string ]any {"paragraph" : map [string ]any {"elements" : []any {
414+ map [string ]any {"startIndex" : 55 , "endIndex" : 65 , "textRun" : map [string ]any {
415+ "content" : "Table link" ,
416+ "textStyle" : map [string ]any {"link" : map [string ]any {"url" : "https://example.com/table" }},
417+ }},
418+ map [string ]any {"startIndex" : 65 , "endIndex" : 66 , "textRun" : map [string ]any {"content" : "\n " }},
419+ }}}}},
420+ map [string ]any {"content" : []any {map [string ]any {"paragraph" : map [string ]any {"elements" : []any {
421+ map [string ]any {"startIndex" : 67 , "endIndex" : 73 , "textRun" : map [string ]any {"content" : "Plain\n " }},
422+ }}}}},
423+ }},
424+ }}},
425+ }
426+ }
427+
428+ func newLinkedTextDocsTestServer (t * testing.T ) (* docs.Service , func ()) {
429+ t .Helper ()
430+ return newDocsContentTestServer (t , "Linked text doc" , "Links" , linkedTextDocsContent )
431+ }
432+
433+ func newDocsContentTestServer (t * testing.T , title , tabTitle string , content func () []any ) (* docs.Service , func ()) {
434+ t .Helper ()
389435
390436 srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
391437 if ! strings .HasPrefix (r .URL .Path , "/v1/documents/" ) || r .Method != http .MethodGet {
392438 http .NotFound (w , r )
393439 return
394440 }
395441
396- response := map [string ]any {
397- "documentId" : "doc1" ,
398- "title" : "Smart chip doc" ,
399- }
442+ response := map [string ]any {"documentId" : "doc1" , "title" : title }
400443 if r .URL .Query ().Get ("includeTabsContent" ) == "true" {
401444 response ["tabs" ] = []any {map [string ]any {
402- "tabProperties" : map [string ]any {"tabId" : "tab-1" , "title" : "Chips" , "index" : 0 },
403- "documentTab" : map [string ]any {
404- "body" : map [string ]any {"content" : smartChipDocsContent ()},
405- },
445+ "tabProperties" : map [string ]any {"tabId" : "tab-1" , "title" : tabTitle , "index" : 0 },
446+ "documentTab" : map [string ]any {"body" : map [string ]any {"content" : content ()}},
406447 }}
407448 } else {
408- response ["body" ] = map [string ]any {"content" : smartChipDocsContent ()}
449+ response ["body" ] = map [string ]any {"content" : content ()}
409450 }
410451
411452 w .Header ().Set ("Content-Type" , "application/json" )
@@ -724,6 +765,87 @@ func TestDocsCat_SmartChipsAreOptInForTextOutput(t *testing.T) {
724765 }
725766}
726767
768+ func TestDocsCat_LinkedTextUsesRenderedPathAndJSONSidecar (t * testing.T ) {
769+ t .Parallel ()
770+
771+ docSvc , cleanup := newLinkedTextDocsTestServer (t )
772+ defer cleanup ()
773+
774+ plain := "Intro Ticket [draft] and https://example.com owner \n Table link\n \t Plain\n "
775+ rendered := "Intro [Ticket \\ [draft\\ ]](https://tracker.example.com/a_\\ (draft\\ )) and https://example.com owner @Sample Person <sample@example.com>\n [Table link](https://example.com/table)\n \t Plain\n "
776+
777+ result := runDocsCatCommand (t , docSvc , []string {"doc1" }, false )
778+ if result .err != nil {
779+ t .Fatalf ("cat: %v" , result .err )
780+ }
781+ if result .stdout != plain {
782+ t .Fatalf ("default text changed\n got: %q\n want: %q" , result .stdout , plain )
783+ }
784+
785+ result = runDocsCatCommand (t , docSvc , []string {"doc1" , "--chips" }, false )
786+ if result .err != nil {
787+ t .Fatalf ("cat --chips: %v" , result .err )
788+ }
789+ if result .stdout != rendered {
790+ t .Fatalf ("unexpected rendered linked text\n got: %q\n want: %q" , result .stdout , rendered )
791+ }
792+
793+ result = runDocsCatCommand (t , docSvc , []string {"doc1" , "--tab" , "Links" , "--chips" }, false )
794+ if result .err != nil {
795+ t .Fatalf ("cat --tab Links --chips: %v" , result .err )
796+ }
797+ if result .stdout != rendered {
798+ t .Fatalf ("unexpected tab linked text\n got: %q\n want: %q" , result .stdout , rendered )
799+ }
800+
801+ result = runDocsCatCommand (t , docSvc , []string {"doc1" }, true )
802+ if result .err != nil {
803+ t .Fatalf ("cat --json: %v" , result .err )
804+ }
805+ var out struct {
806+ Text string `json:"text"`
807+ RenderedText string `json:"renderedText"`
808+ Chips []docsSmartChip `json:"chips"`
809+ Links []docsTextLink `json:"links"`
810+ }
811+ if err := json .Unmarshal ([]byte (result .stdout ), & out ); err != nil {
812+ t .Fatalf ("JSON parse: %v\n raw: %q" , err , result .stdout )
813+ }
814+ if out .Text != plain || out .RenderedText != rendered {
815+ t .Fatalf ("unexpected JSON text: %#v" , out )
816+ }
817+ if len (out .Chips ) != 1 || out .Chips [0 ].Type != "person" {
818+ t .Fatalf ("unexpected mixed smart chips: %#v" , out .Chips )
819+ }
820+ if len (out .Links ) != 3 {
821+ t .Fatalf ("links length = %d, want 3: %#v" , len (out .Links ), out .Links )
822+ }
823+ if out .Links [0 ].Text != "Ticket [draft]" || out .Links [0 ].URL != "https://tracker.example.com/a_(draft)" || out .Links [0 ].StartIndex != 7 || out .Links [0 ].EndIndex != 21 {
824+ t .Fatalf ("unexpected first link: %#v" , out .Links [0 ])
825+ }
826+ if out .Links [1 ].Text != "https://example.com" || out .Links [1 ].URL != out .Links [1 ].Text {
827+ t .Fatalf ("unexpected visible-URL link: %#v" , out .Links [1 ])
828+ }
829+ if out .Links [2 ].Text != "Table link" || out .Links [2 ].URL != "https://example.com/table" {
830+ t .Fatalf ("unexpected table-cell link: %#v" , out .Links [2 ])
831+ }
832+ }
833+
834+ func TestDocsCat_LinkedTextRespectsMaxBytesAtomically (t * testing.T ) {
835+ t .Parallel ()
836+
837+ docSvc , cleanup := newLinkedTextDocsTestServer (t )
838+ defer cleanup ()
839+
840+ result := runDocsCatCommand (t , docSvc , []string {"doc1" , "--chips" , "--max-bytes" , "20" }, false )
841+ if result .err != nil {
842+ t .Fatalf ("cat --chips --max-bytes: %v" , result .err )
843+ }
844+ if got , want := result .stdout , "Intro " ; got != want {
845+ t .Fatalf ("link markdown should not be partially rendered\n got: %q\n want: %q" , got , want )
846+ }
847+ }
848+
727849func TestDocsCat_JSONAddsSmartChipDataWithoutChangingText (t * testing.T ) {
728850 t .Parallel ()
729851
0 commit comments