@@ -192,6 +192,151 @@ describe('procedure call mutation round-trip', () => {
192192 } )
193193} )
194194
195+ // ---------------------------------------------------------------------------
196+ // Legacy project XML tolerance (scratchfoundation/scratch-editor#532)
197+ //
198+ // Older saved projects may omit mutation attributes that the current serializer
199+ // always writes. Treating every attribute as required breaks loading of
200+ // projects that predate (or selectively elide) those attributes. `proccode`
201+ // and the argument arrays (`argumentids`, `argumentnames`, `argumentdefaults`)
202+ // stay required: `proccode` is the block's identity, and the argument arrays'
203+ // lengths must match the proccode token count and each other, so silently
204+ // defaulting any of them to `[]` would turn an N-arg procedure into a 0-arg
205+ // block. `warp` is the one attribute recoverable with a sane default (`false`).
206+ // ---------------------------------------------------------------------------
207+
208+ describe ( 'legacy mutation tolerance' , ( ) => {
209+ it ( 'procedures_prototype mutation without warp loads with warp=false' , ( ) => {
210+ expect ( ( ) =>
211+ loadXml ( `
212+ <xml>
213+ <block type="procedures_definition">
214+ <statement name="custom_block">
215+ <block type="procedures_prototype">
216+ <mutation
217+ proccode="test %s"
218+ argumentids='["arg1"]'
219+ argumentnames='["x"]'
220+ argumentdefaults='[""]'>
221+ </mutation>
222+ <value name="arg1">
223+ <block type="argument_reporter_string_number">
224+ <field name="VALUE">x</field>
225+ </block>
226+ </value>
227+ </block>
228+ </statement>
229+ </block>
230+ </xml>
231+ ` ) ,
232+ ) . not . toThrow ( )
233+
234+ const proto = workspace . getAllBlocks ( false ) . find ( ( b ) => b . type === 'procedures_prototype' )
235+ assert ( proto , 'Expected procedures_prototype block' )
236+ expect ( ( proto as any ) . warp_ ) . toBe ( false )
237+ } )
238+
239+ it ( 'procedures_call mutation without warp loads with warp=false' , ( ) => {
240+ expect ( ( ) =>
241+ loadXml ( `
242+ <xml>
243+ <block type="procedures_call">
244+ <mutation
245+ proccode="test %s"
246+ argumentids='["arg1"]'>
247+ </mutation>
248+ </block>
249+ </xml>
250+ ` ) ,
251+ ) . not . toThrow ( )
252+
253+ const callBlock = workspace . getAllBlocks ( false ) . find ( ( b ) => b . type === 'procedures_call' )
254+ assert ( callBlock , 'Expected procedures_call block' )
255+ expect ( ( callBlock as any ) . warp_ ) . toBe ( false )
256+ } )
257+
258+ it ( 'procedures_prototype mutation without proccode still throws' , ( ) => {
259+ // proccode is the block's identity and cannot be defaulted.
260+ expect ( ( ) =>
261+ loadXml ( `
262+ <xml>
263+ <block type="procedures_definition">
264+ <statement name="custom_block">
265+ <block type="procedures_prototype">
266+ <mutation warp="false"></mutation>
267+ </block>
268+ </statement>
269+ </block>
270+ </xml>
271+ ` ) ,
272+ ) . toThrow ( / p r o c c o d e / )
273+ } )
274+
275+ // argumentids/argumentnames/argumentdefaults have a structural invariant:
276+ // their lengths must equal the %s/%n/%b token count in proccode. Silently
277+ // defaulting any of them to [] would turn an N-arg procedure into a 0-arg
278+ // block, so keep them required and surface the problem loudly.
279+ it ( 'procedures_prototype mutation without argumentids still throws' , ( ) => {
280+ expect ( ( ) =>
281+ loadXml ( `
282+ <xml>
283+ <block type="procedures_definition">
284+ <statement name="custom_block">
285+ <block type="procedures_prototype">
286+ <mutation proccode="test %s" argumentnames='["x"]' argumentdefaults='[""]' warp="false"></mutation>
287+ </block>
288+ </statement>
289+ </block>
290+ </xml>
291+ ` ) ,
292+ ) . toThrow ( / a r g u m e n t i d s / )
293+ } )
294+
295+ it ( 'procedures_prototype mutation without argumentnames still throws' , ( ) => {
296+ expect ( ( ) =>
297+ loadXml ( `
298+ <xml>
299+ <block type="procedures_definition">
300+ <statement name="custom_block">
301+ <block type="procedures_prototype">
302+ <mutation proccode="test %s" argumentids='["arg1"]' argumentdefaults='[""]' warp="false"></mutation>
303+ </block>
304+ </statement>
305+ </block>
306+ </xml>
307+ ` ) ,
308+ ) . toThrow ( / a r g u m e n t n a m e s / )
309+ } )
310+
311+ it ( 'procedures_prototype mutation without argumentdefaults still throws' , ( ) => {
312+ expect ( ( ) =>
313+ loadXml ( `
314+ <xml>
315+ <block type="procedures_definition">
316+ <statement name="custom_block">
317+ <block type="procedures_prototype">
318+ <mutation proccode="test %s" argumentids='["arg1"]' argumentnames='["x"]' warp="false"></mutation>
319+ </block>
320+ </statement>
321+ </block>
322+ </xml>
323+ ` ) ,
324+ ) . toThrow ( / a r g u m e n t d e f a u l t s / )
325+ } )
326+
327+ it ( 'procedures_call mutation without argumentids still throws' , ( ) => {
328+ expect ( ( ) =>
329+ loadXml ( `
330+ <xml>
331+ <block type="procedures_call">
332+ <mutation proccode="test %s" warp="false"></mutation>
333+ </block>
334+ </xml>
335+ ` ) ,
336+ ) . toThrow ( / a r g u m e n t i d s / )
337+ } )
338+ } )
339+
195340// ---------------------------------------------------------------------------
196341// PR #3492: context menu delegation
197342// ---------------------------------------------------------------------------
0 commit comments