@@ -12,16 +12,36 @@ import { codaDeleteRowsTool } from '@/tools/coda/delete_rows'
1212import { codaListDocsTool } from '@/tools/coda/list_docs'
1313import { codaListRowsTool } from '@/tools/coda/list_rows'
1414import { codaPublishDocTool } from '@/tools/coda/publish_doc'
15+ import { codaResolveBrowserLinkTool } from '@/tools/coda/resolve_browser_link'
1516import { codaUpdateAclSettingsTool } from '@/tools/coda/update_acl_settings'
1617import { codaUpdatePageTool } from '@/tools/coda/update_page'
1718import { codaUpdateRowTool } from '@/tools/coda/update_row'
1819import { codaUpsertRowsTool } from '@/tools/coda/upsert_rows'
1920import { buildCodaUrl , CODA_FIELD_UPDATE_RETRY , CODA_RETRY } from '@/tools/coda/utils'
2021import { codaWhoamiTool } from '@/tools/coda/whoami'
2122import { ErrorExtractorId , extractErrorMessageWithId } from '@/tools/error-extractors'
23+ import type { OutputProperty } from '@/tools/types'
2224
2325const table = { accessToken : 'token' , docId : 'AbCDeFGH' , tableId : 'grid-pqRst-U' }
2426
27+ /** Lists output paths a tool returned as null whose schema does not declare `nullable`. */
28+ function findUndeclaredNulls (
29+ value : unknown ,
30+ properties : Record < string , OutputProperty > | undefined ,
31+ path : string
32+ ) : string [ ] {
33+ if ( ! properties || value === null || typeof value !== 'object' ) return [ ]
34+ return Object . entries ( properties ) . flatMap ( ( [ key , schema ] ) => {
35+ const child = ( value as Record < string , unknown > ) [ key ]
36+ const childPath = `${ path } .${ key } `
37+ if ( child === null ) return schema . nullable ? [ ] : [ childPath ]
38+ if ( Array . isArray ( child ) ) {
39+ return child . flatMap ( ( item ) => findUndeclaredNulls ( item , schema . items ?. properties , childPath ) )
40+ }
41+ return findUndeclaredNulls ( child , schema . properties , childPath )
42+ } )
43+ }
44+
2545function resolveUrl < P > ( url : string | ( ( params : P ) => string ) , params : P ) : string {
2646 return typeof url === 'function' ? url ( params ) : url
2747}
@@ -46,6 +66,12 @@ describe('Coda request URLs', () => {
4666 )
4767 } )
4868
69+ it ( 'rejects a blank browser link instead of sending no url' , ( ) => {
70+ expect ( ( ) =>
71+ resolveUrl ( codaResolveBrowserLinkTool . request . url , { accessToken : 'token' , url : ' ' } )
72+ ) . toThrow ( 'url is required' )
73+ } )
74+
4975 it ( 'builds list docs URLs without a doc path' , ( ) => {
5076 expect (
5177 resolveUrl ( codaListDocsTool . request . url , {
@@ -84,6 +110,26 @@ describe('Coda row bodies', () => {
84110 } )
85111 } )
86112
113+ it ( 'maps a column named cells instead of reading it as the cells wrapper' , ( ) => {
114+ expect (
115+ codaUpdateRowTool . request . body ! ( {
116+ ...table ,
117+ rowId : 'i-1' ,
118+ cells : { cells : 'x' , Status : 'Done' } ,
119+ } )
120+ ) . toEqual ( {
121+ row : {
122+ cells : [
123+ { column : 'cells' , value : 'x' } ,
124+ { column : 'Status' , value : 'Done' } ,
125+ ] ,
126+ } ,
127+ } )
128+ expect (
129+ codaUpdateRowTool . request . body ! ( { ...table , rowId : 'i-1' , cells : { cells : 'x' } } )
130+ ) . toEqual ( { row : { cells : [ { column : 'cells' , value : 'x' } ] } } )
131+ } )
132+
87133 it ( 'rejects an empty upsert' , ( ) => {
88134 expect ( ( ) => codaUpsertRowsTool . request . body ! ( { ...table , rows : '[]' } ) ) . toThrow (
89135 'at least one row'
@@ -339,6 +385,27 @@ describe('Coda tool registration', () => {
339385 expect ( allTools ) . toHaveLength ( 60 )
340386 } )
341387
388+ it . each ( allTools ) (
389+ '%s declares every output it can return as null as nullable' ,
390+ async ( _ , tool ) => {
391+ const config = tool as {
392+ outputs : Record < string , OutputProperty >
393+ transformResponse : ( response : Response , params : object ) => Promise < { output : unknown } >
394+ }
395+ const sparseBody = {
396+ items : [ { doc : { } , page : { } , metrics : [ { } ] } ] ,
397+ customDocDomains : [ { } ] ,
398+ resource : { } ,
399+ id : 'x' ,
400+ }
401+ const { output } = await config . transformResponse (
402+ new Response ( JSON . stringify ( sparseBody ) ) ,
403+ table
404+ )
405+ expect ( findUndeclaredNulls ( output , config . outputs , '' ) ) . toEqual ( [ ] )
406+ }
407+ )
408+
342409 it . each ( allTools ) (
343410 '%s retries safely repeatable calls and authenticates with the Coda credential' ,
344411 ( _ , tool ) => {
0 commit comments