@@ -59,129 +59,126 @@ describe('tool-call semaphore helpers', () => {
5959import { AirtableClient } from '../src/client.js' ;
6060
6161describe ( 'AirtableClient.deleteFields' , ( ) => {
62- it ( 'processes all fields and reports succeeded/failed counts' , async ( ) => {
63- // Schema contains all three fields so resolveField succeeds for each.
64- // The cache is invalidated after each successful delete, causing a re-fetch
65- // that always returns the full schema. postForm fails only for fldBAD.
66- const SCHEMA = {
67- data : {
68- tableSchemas : [ {
69- id : 'tblAAA' ,
70- columns : [
71- { id : 'fld001' , name : 'Field A' , type : 'text' , typeOptions : { } } ,
72- { id : 'fldBAD' , name : 'Bad Field' , type : 'text' , typeOptions : { } } ,
73- { id : 'fld003' , name : 'Field C' , type : 'text' , typeOptions : { } } ,
74- ] ,
75- views : [ ] ,
76- } ] ,
77- } ,
78- } ;
62+ it ( 'batch-deletes all fields with no dependencies in one API call' , async ( ) => {
63+ const SCHEMA = { data : { tableSchemas : [ { id : 'tblAAA' , columns : [
64+ { id : 'fld001' , name : 'Field A' , type : 'text' , typeOptions : { } } ,
65+ { id : 'fld002' , name : 'Field B' , type : 'text' , typeOptions : { } } ,
66+ ] , views : [ ] } ] } } ;
7967 const auth = createMockAuth ( {
80- get ( ) {
81- return { ok : true , status : 200 , json : async ( ) => SCHEMA , text : async ( ) => '{}' } ;
68+ get ( ) { return { ok : true , status : 200 , json : async ( ) => SCHEMA , text : async ( ) => '{}' } ; } ,
69+ postForm ( url ) {
70+ if ( url . includes ( 'destroyMultipleColumns' ) ) {
71+ return { ok : true , status : 200 , json : async ( ) => ( { } ) , text : async ( ) => '{}' } ;
72+ }
73+ return { ok : false , status : 500 , json : async ( ) => ( { } ) , text : async ( ) => '' } ;
8274 } ,
75+ } ) ;
76+ const client = new AirtableClient ( auth ) ;
77+ const result = await client . deleteFields ( 'appTEST' , [
78+ { fieldId : 'fld001' , expectedName : 'Field A' } ,
79+ { fieldId : 'fld002' , expectedName : 'Field B' } ,
80+ ] ) ;
81+ assert . equal ( result . succeeded . length , 2 ) ;
82+ assert . equal ( result . failed . length , 0 ) ;
83+ const batchCalls = auth . calls . filter ( c => c . url . includes ( 'destroyMultipleColumns' ) ) ;
84+ assert . equal ( batchCalls . length , 1 , 'should make exactly one batch call when no deps' ) ;
85+ } ) ;
86+
87+ it ( 'makes two batch calls when force=true and deps exist' , async ( ) => {
88+ const SCHEMA = { data : { tableSchemas : [ { id : 'tblAAA' , columns : [
89+ { id : 'fld001' , name : 'HasDeps' , type : 'text' , typeOptions : { } } ,
90+ ] , views : [ ] } ] } } ;
91+ let batchCallCount = 0 ;
92+ const auth = createMockAuth ( {
93+ get ( ) { return { ok : true , status : 200 , json : async ( ) => SCHEMA , text : async ( ) => '{}' } ; } ,
8394 postForm ( url ) {
84- // fldBAD destroy call fails with a non-dependency error → throws in deleteField
85- if ( url . includes ( 'fldBAD' ) ) {
86- return { ok : false , status : 422 , json : async ( ) => ( { error : { type : 'NOT_FOUND' } } ) , text : async ( ) => 'not found' } ;
95+ if ( ! url . includes ( 'destroyMultipleColumns' ) ) return { ok : true , status : 200 , json : async ( ) => ( { } ) , text : async ( ) => '{}' } ;
96+ batchCallCount ++ ;
97+ if ( batchCallCount === 1 ) {
98+ return { ok : false , status : 422 , json : async ( ) => ( { error : { type : 'SCHEMA_DEPENDENCIES_VALIDATION_FAILED' } } ) , text : async ( ) => '' } ;
8799 }
88100 return { ok : true , status : 200 , json : async ( ) => ( { } ) , text : async ( ) => '{}' } ;
89101 } ,
90102 } ) ;
91103 const client = new AirtableClient ( auth ) ;
92- const fields = [
93- { fieldId : 'fld001' , expectedName : 'Field A' } ,
94- { fieldId : 'fldBAD' , expectedName : 'Bad Field' } ,
95- { fieldId : 'fld003' , expectedName : 'Field C' } ,
96- ] ;
97- const result = await client . deleteFields ( 'appTEST' , fields , { force : true } ) ;
98- assert . equal ( result . succeeded . length , 2 , 'two fields should succeed' ) ;
99- assert . equal ( result . failed . length , 1 , 'one field should fail' ) ;
100- assert . equal ( result . failed [ 0 ] . fieldId , 'fldBAD' ) ;
101- assert . ok ( typeof result . failed [ 0 ] . error === 'string' , 'error must be a string' ) ;
104+ const result = await client . deleteFields ( 'appTEST' , [ { fieldId : 'fld001' , expectedName : 'HasDeps' } ] , { force : true } ) ;
105+ assert . equal ( result . succeeded . length , 1 ) ;
106+ assert . equal ( result . failed . length , 0 ) ;
107+ assert . equal ( result . succeeded [ 0 ] . forced , true ) ;
108+ assert . equal ( batchCallCount , 2 , 'should make two batch calls for force=true + deps' ) ;
102109 } ) ;
103110
104- it ( 'calls onProgress once per field' , async ( ) => {
111+ it ( 'routes to failed when deps exist and force=false' , async ( ) => {
112+ const SCHEMA = { data : { tableSchemas : [ { id : 'tblAAA' , columns : [
113+ { id : 'fld001' , name : 'HasDeps' , type : 'text' , typeOptions : { } } ,
114+ ] , views : [ ] } ] } } ;
105115 const auth = createMockAuth ( {
106- get ( ) {
107- return {
108- ok : true , status : 200 ,
109- json : async ( ) => ( {
110- data : { tableSchemas : [ { id : 'tbl1' , columns : [ { id : 'fld001' , name : 'A' , type : 'text' , typeOptions : { } } ] , views : [ ] } ] } ,
111- } ) ,
112- text : async ( ) => '{}' ,
113- } ;
116+ get ( ) { return { ok : true , status : 200 , json : async ( ) => SCHEMA , text : async ( ) => '{}' } ; } ,
117+ postForm ( ) {
118+ return { ok : false , status : 422 , json : async ( ) => ( { error : { type : 'SCHEMA_DEPENDENCIES_VALIDATION_FAILED' } } ) , text : async ( ) => '' } ;
114119 } ,
115120 } ) ;
116121 const client = new AirtableClient ( auth ) ;
117- const progressLog = [ ] ;
118- await client . deleteFields ( 'appTEST' , [ { fieldId : 'fld001' , expectedName : 'A' } ] , {
119- onProgress : ( info ) => progressLog . push ( info ) ,
122+ const result = await client . deleteFields ( 'appTEST' , [ { fieldId : 'fld001' , expectedName : 'HasDeps' } ] , { force : false } ) ;
123+ assert . equal ( result . succeeded . length , 0 ) ;
124+ assert . equal ( result . failed . length , 1 ) ;
125+ assert . ok ( result . failed [ 0 ] . error . includes ( 'dependencies' ) , `expected dep error, got: "${ result . failed [ 0 ] . error } "` ) ;
126+ const batchCalls = auth . calls . filter ( c => c . url . includes ( 'destroyMultipleColumns' ) ) ;
127+ assert . equal ( batchCalls . length , 1 , 'should stop after first call when force=false' ) ;
128+ } ) ;
129+
130+ it ( 'routes to failed when expectedName does not match' , async ( ) => {
131+ const SCHEMA = { data : { tableSchemas : [ { id : 'tblAAA' , columns : [
132+ { id : 'fld001' , name : 'Actual Name' , type : 'text' , typeOptions : { } } ,
133+ ] , views : [ ] } ] } } ;
134+ const auth = createMockAuth ( {
135+ get ( ) { return { ok : true , status : 200 , json : async ( ) => SCHEMA , text : async ( ) => '{}' } ; } ,
120136 } ) ;
121- assert . equal ( progressLog . length , 1 ) ;
122- assert . equal ( progressLog [ 0 ] . index , 0 ) ;
123- assert . equal ( progressLog [ 0 ] . total , 1 ) ;
137+ const client = new AirtableClient ( auth ) ;
138+ const result = await client . deleteFields ( 'appTEST' , [ { fieldId : 'fld001' , expectedName : 'Wrong Name' } ] ) ;
139+ assert . equal ( result . succeeded . length , 0 ) ;
140+ assert . equal ( result . failed . length , 1 ) ;
141+ assert . ok ( result . failed [ 0 ] . error . includes ( 'Safety check' ) , `expected safety check error, got: "${ result . failed [ 0 ] . error } "` ) ;
142+ assert . equal ( auth . calls . filter ( c => c . url . includes ( 'destroyMultipleColumns' ) ) . length , 0 , 'no batch call if safety check fails' ) ;
124143 } ) ;
125144
126- it ( 'continues processing after a per-field failure' , async ( ) => {
127- // Schema contains both fields. fld001 postForm fails (non-dep error → throws),
128- // fld002 succeeds. Cache is invalidated only on success, but the schema mock
129- // always returns both fields so both resolveField calls succeed regardless.
130- const SCHEMA2 = {
131- data : { tableSchemas : [ { id : 'tbl1' , columns : [
132- { id : 'fld001' , name : 'A' , type : 'text' , typeOptions : { } } ,
133- { id : 'fld002' , name : 'B' , type : 'text' , typeOptions : { } } ,
134- ] , views : [ ] } ] } ,
135- } ;
145+ it ( 'processes fields from different tables independently' , async ( ) => {
146+ const SCHEMA = { data : { tableSchemas : [
147+ { id : 'tbl1' , columns : [ { id : 'fld001' , name : 'A' , type : 'text' , typeOptions : { } } ] , views : [ ] } ,
148+ { id : 'tbl2' , columns : [ { id : 'fld002' , name : 'B' , type : 'text' , typeOptions : { } } ] , views : [ ] } ,
149+ ] } } ;
136150 const auth = createMockAuth ( {
137- get ( ) {
138- return { ok : true , status : 200 , json : async ( ) => SCHEMA2 , text : async ( ) => '{}' } ;
139- } ,
151+ get ( ) { return { ok : true , status : 200 , json : async ( ) => SCHEMA , text : async ( ) => '{}' } ; } ,
140152 postForm ( url ) {
141- if ( url . includes ( 'fld001 ' ) ) return { ok : false , status : 422 , json : async ( ) => ( { error : { type : 'ERR' } } ) , text : async ( ) => 'err ' } ;
142- return { ok : true , status : 200 , json : async ( ) => ( { } ) , text : async ( ) => '{} ' } ;
153+ if ( url . includes ( 'tbl1 ' ) ) return { ok : true , status : 200 , json : async ( ) => ( { } ) , text : async ( ) => '{} ' } ;
154+ return { ok : false , status : 500 , json : async ( ) => ( { error : { type : 'INTERNAL' } } ) , text : async ( ) => 'error ' } ;
143155 } ,
144156 } ) ;
145157 const client = new AirtableClient ( auth ) ;
146158 const result = await client . deleteFields ( 'appTEST' , [
147159 { fieldId : 'fld001' , expectedName : 'A' } ,
148160 { fieldId : 'fld002' , expectedName : 'B' } ,
149- ] , { force : true } ) ;
161+ ] ) ;
150162 assert . equal ( result . succeeded . length , 1 ) ;
151163 assert . equal ( result . failed . length , 1 ) ;
164+ assert . equal ( result . succeeded [ 0 ] . fieldId , 'fld001' ) ;
165+ assert . equal ( result . failed [ 0 ] . fieldId , 'fld002' ) ;
152166 } ) ;
153167
154- it ( 'routes deleted:false (dependency-blocked) to failed[] when force=false' , async ( ) => {
155- const DEPENDENCY_RESPONSE = {
156- ok : false ,
157- status : 400 ,
158- json : async ( ) => ( {
159- error : {
160- type : 'SCHEMA_DEPENDENCIES_VALIDATION_FAILED' ,
161- details : { dependentColumns : [ { id : 'fldDEP' , type : 'formula' } ] } ,
162- } ,
163- } ) ,
164- text : async ( ) => JSON . stringify ( { error : { type : 'SCHEMA_DEPENDENCIES_VALIDATION_FAILED' } } ) ,
165- } ;
168+ it ( 'calls onProgress once at end with final counts' , async ( ) => {
169+ const SCHEMA = { data : { tableSchemas : [ { id : 'tbl1' , columns : [
170+ { id : 'fld001' , name : 'A' , type : 'text' , typeOptions : { } } ,
171+ ] , views : [ ] } ] } } ;
166172 const auth = createMockAuth ( {
167- get ( ) {
168- return {
169- ok : true , status : 200 ,
170- json : async ( ) => ( {
171- data : { tableSchemas : [ { id : 'tbl1' , columns : [ { id : 'fld001' , name : 'HasDeps' , type : 'formula' , typeOptions : { } } ] , views : [ ] } ] } ,
172- } ) ,
173- text : async ( ) => '{}' ,
174- } ;
175- } ,
176- postForm ( ) {
177- return DEPENDENCY_RESPONSE ;
178- } ,
173+ get ( ) { return { ok : true , status : 200 , json : async ( ) => SCHEMA , text : async ( ) => '{}' } ; } ,
179174 } ) ;
180175 const client = new AirtableClient ( auth ) ;
181- const result = await client . deleteFields ( 'appTEST' , [ { fieldId : 'fld001' , expectedName : 'HasDeps' } ] , { force : false } ) ;
182- assert . equal ( result . succeeded . length , 0 , 'no fields should be in succeeded' ) ;
183- assert . equal ( result . failed . length , 1 , 'dependency-blocked field should be in failed' ) ;
184- assert . ok ( result . failed [ 0 ] . error . includes ( 'dependencies' ) || result . failed [ 0 ] . error . includes ( 'not deleted' ) ,
185- `error message should mention dependencies or not deleted, got: "${ result . failed [ 0 ] . error } "` ) ;
176+ const progressLog = [ ] ;
177+ await client . deleteFields ( 'appTEST' , [ { fieldId : 'fld001' , expectedName : 'A' } ] , {
178+ onProgress : ( info ) => progressLog . push ( info ) ,
179+ } ) ;
180+ assert . equal ( progressLog . length , 1 ) ;
181+ assert . equal ( progressLog [ 0 ] . index , 0 ) ;
182+ assert . equal ( progressLog [ 0 ] . total , 1 ) ;
186183 } ) ;
187184} ) ;
0 commit comments