@@ -59,31 +59,192 @@ describe("ToolOutput", () => {
5959 { type : "text" , text : "one\ntwo" } ,
6060 {
6161 type : "text" ,
62- text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 2 o f 3 ; f u l l o u t p u t c o u l d n o t b e s a v e d : E [ A - Z ] + : [ ^ \n ] + \] $ / ) ,
62+ text : expect . stringMatching (
63+ / ^ \[ s h o w i n g l i n e s 1 - 2 o f 3 ; f u l l o u t p u t c o u l d n o t b e s a v e d : E [ A - Z ] + : [ ^ \n ] + \] $ / ,
64+ ) ,
6365 } ,
6466 ] )
6567 } ) ,
6668 { maxLines : 2 , maxBytes : 1_000 } ,
6769 ) ,
6870 )
6971
70- it . live ( "reports lines shown under the byte limit" , ( ) =>
72+ it . live ( "cuts at the byte limit, inside a line when it falls there " , ( ) =>
7173 withStore (
7274 ( output ) =>
7375 Effect . gen ( function * ( ) {
7476 const result = yield * output . truncate ( { content : [ { type : "text" , text : "one\ntwo" } ] } )
7577 expect ( result . content ) . toEqual ( [
76- { type : "text" , text : "one" } ,
78+ { type : "text" , text : "one\nt " } ,
7779 {
7880 type : "text" ,
79- text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 1 o f 2 ; f u l l o u t p u t s a v e d t o .+ \] $ / ) ,
81+ text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 2 o f 2 \( l a s t l i n e c u t \) ; f u l l o u t p u t s a v e d t o .+ \] $ / ) ,
8082 } ,
8183 ] )
8284 } ) ,
8385 { maxLines : 100 , maxBytes : 5 } ,
8486 ) ,
8587 )
8688
89+ it . live ( "shows the start of a single line longer than the byte limit" , ( ) =>
90+ withStore (
91+ ( output ) =>
92+ Effect . gen ( function * ( ) {
93+ const text = JSON . stringify ( { items : Array . from ( { length : 50 } , ( _ , index ) => index ) } )
94+ const result = yield * output . truncate ( { content : [ { type : "text" , text } ] } )
95+ expect ( result . content ) . toEqual ( [
96+ { type : "text" , text : text . slice ( 0 , 20 ) } ,
97+ {
98+ type : "text" ,
99+ text : expect . stringMatching (
100+ new RegExp ( "^\\[showing lines 1-1 of 1 \\(last line cut\\); full output saved to .+\\]$" ) ,
101+ ) ,
102+ } ,
103+ ] )
104+ } ) ,
105+ { maxLines : 100 , maxBytes : 20 } ,
106+ ) ,
107+ )
108+
109+ it . live ( "never splits a character or an emoji at the cut" , ( ) =>
110+ withStore (
111+ ( output ) =>
112+ Effect . gen ( function * ( ) {
113+ const family = "👨👩👧👦"
114+ const cases = [
115+ { text : "ab✓✓✓" , shown : "ab✓" } ,
116+ { text : `ab${ family } cd` , shown : "ab" } ,
117+ { text : "ab👍🏽cd" , shown : "ab" } ,
118+ { text : "ab🇯🇵cd" , shown : "ab" } ,
119+ { text : "ab𝒳𝒳" , shown : "ab𝒳" } ,
120+ ]
121+ for ( const item of cases ) {
122+ const result = yield * output . truncate ( { content : [ { type : "text" , text : item . text } ] } )
123+ const first = result . content [ 0 ]
124+ expect ( first ) . toEqual ( { type : "text" , text : item . shown } )
125+ expect ( first ?. type === "text" && first . text . isWellFormed ( ) ) . toBe ( true )
126+ }
127+ } ) ,
128+ { maxLines : 100 , maxBytes : 7 } ,
129+ ) ,
130+ )
131+
132+ it . live ( "keeps flags and long clusters whole however long the text before them" , ( ) =>
133+ withStore (
134+ ( output ) =>
135+ Effect . gen ( function * ( ) {
136+ const flags = yield * output . truncate ( { content : [ { type : "text" , text : "a" + "🇯🇵" . repeat ( 40 ) } ] } )
137+ expect ( flags . content [ 0 ] ) . toEqual ( { type : "text" , text : "a" + "🇯🇵" . repeat ( 20 ) } )
138+ const cluster = "👍" + "🏽\u0301" . repeat ( 60 )
139+ const long = yield * output . truncate ( { content : [ { type : "text" , text : `ab${ cluster } z` } ] } )
140+ expect ( long . content [ 0 ] ) . toEqual ( { type : "text" , text : "ab" } )
141+ } ) ,
142+ { maxLines : 100 , maxBytes : 165 } ,
143+ ) ,
144+ )
145+
146+ it . live ( "cuts inside a later text part and leaves out the parts after it" , ( ) =>
147+ withStore (
148+ ( output ) =>
149+ Effect . gen ( function * ( ) {
150+ const inside = yield * output . truncate ( {
151+ content : [
152+ { type : "text" , text : "" } ,
153+ { type : "text" , text : "abc" } ,
154+ { type : "text" , text : "defgh" } ,
155+ { type : "text" , text : "" } ,
156+ ] ,
157+ } )
158+ expect ( inside . content ) . toEqual ( [
159+ { type : "text" , text : "" } ,
160+ { type : "text" , text : "abc" } ,
161+ { type : "text" , text : "de" } ,
162+ {
163+ type : "text" ,
164+ text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 2 o f 2 \( l a s t l i n e c u t \) ; / ) ,
165+ } ,
166+ ] )
167+ const between = yield * output . truncate ( {
168+ content : [
169+ { type : "text" , text : "abcde" } ,
170+ { type : "text" , text : "fgh" } ,
171+ ] ,
172+ } )
173+ expect ( between . content ) . toEqual ( [
174+ { type : "text" , text : "abcde" } ,
175+ { type : "text" , text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 1 o f 2 ; / ) } ,
176+ ] )
177+ } ) ,
178+ { maxLines : 100 , maxBytes : 6 } ,
179+ ) ,
180+ )
181+
182+ it . live ( "ends a line cut before its line break" , ( ) =>
183+ withStore (
184+ ( output ) =>
185+ Effect . gen ( function * ( ) {
186+ const crlf = yield * output . truncate ( { content : [ { type : "text" , text : "one\r\ntwo\r\nthree" } ] } )
187+ expect ( crlf . content ) . toEqual ( [
188+ { type : "text" , text : "one" } ,
189+ { type : "text" , text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 1 o f 3 ; / ) } ,
190+ ] )
191+ const blank = yield * output . truncate ( { content : [ { type : "text" , text : "\nfoo\nbar" } ] } )
192+ expect ( blank . content ) . toEqual ( [ { type : "text" , text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 1 o f 3 ; / ) } ] )
193+ } ) ,
194+ { maxLines : 1 , maxBytes : 1_000 } ,
195+ ) ,
196+ )
197+
198+ it . live ( "counts a blank last line of the preview" , ( ) =>
199+ withStore (
200+ ( output ) =>
201+ Effect . gen ( function * ( ) {
202+ const result = yield * output . truncate ( { content : [ { type : "text" , text : "a\n\n\nb" } ] } )
203+ expect ( result . content ) . toEqual ( [
204+ { type : "text" , text : "a\n" } ,
205+ { type : "text" , text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 2 o f 4 ; / ) } ,
206+ ] )
207+ } ) ,
208+ { maxLines : 2 , maxBytes : 1_000 } ,
209+ ) ,
210+ )
211+
212+ it . live ( "shows nothing under a zero line limit" , ( ) =>
213+ withStore (
214+ ( output ) =>
215+ Effect . gen ( function * ( ) {
216+ const result = yield * output . truncate ( { content : [ { type : "text" , text : "secret" } ] } )
217+ expect ( result . content ) . toEqual ( [ { type : "text" , text : expect . stringMatching ( / ^ \[ s h o w i n g 0 l i n e s o f 1 ; / ) } ] )
218+ } ) ,
219+ { maxLines : 0 , maxBytes : 1_000 } ,
220+ ) ,
221+ )
222+
223+ it . live ( "joins text parts with a line break only where one is missing" , ( ) =>
224+ withStore (
225+ ( output , fs ) =>
226+ Effect . gen ( function * ( ) {
227+ const result = yield * output . truncate ( {
228+ content : [
229+ { type : "text" , text : "one\n" } ,
230+ { type : "text" , text : "two" } ,
231+ { type : "text" , text : "three" } ,
232+ ] ,
233+ } )
234+ const outputPath = result . metadata ?. outputPath
235+ expect ( typeof outputPath ) . toBe ( "string" )
236+ if ( typeof outputPath !== "string" ) return
237+ expect ( yield * fs . readFileString ( outputPath ) ) . toBe ( "one\ntwo\nthree" )
238+ expect ( result . content ) . toEqual ( [
239+ { type : "text" , text : "one\n" } ,
240+ { type : "text" , text : "two" } ,
241+ { type : "text" , text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 2 o f 3 ; / ) } ,
242+ ] )
243+ } ) ,
244+ { maxLines : 2 , maxBytes : 1_000 } ,
245+ ) ,
246+ )
247+
87248 it . live ( "preserves mixed content ordering" , ( ) =>
88249 withStore (
89250 ( output ) =>
@@ -144,7 +305,10 @@ describe("ToolOutput", () => {
144305 const result = yield * output . truncate ( { content : [ { type : "text" , text : "one\n" } ] } )
145306 expect ( result . content ) . toEqual ( [
146307 { type : "text" , text : "one" } ,
147- { type : "text" , text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 1 o f 1 ; f u l l o u t p u t s a v e d t o / ) } ,
308+ {
309+ type : "text" ,
310+ text : expect . stringMatching ( / ^ \[ s h o w i n g l i n e s 1 - 1 o f 1 ; f u l l o u t p u t s a v e d t o / ) ,
311+ } ,
148312 ] )
149313 } ) ,
150314 { maxLines : 2 , maxBytes : 3 } ,
0 commit comments