@lark-apaas/coding-steering 0.1.32-dev.942e73f → 0.1.32-dev.b12eab4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,34 @@
1
1
  #!/usr/bin/env python3
2
- """Regression tests for template logo placement in generated deck HTML."""
2
+ """Regression tests for template asset placement in generated deck HTML."""
3
3
  import os
4
+ import shutil
5
+ import subprocess
4
6
  import sys
5
7
  import tempfile
6
8
  import unittest
7
9
 
8
10
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
11
 
10
- from verify_logo_scope import validate_logo_scope
12
+ from verify_layout_assets import validate_layout_assets
11
13
 
12
14
 
13
- class LogoScopeTest(unittest.TestCase):
15
+ class LayoutAssetContractTest(unittest.TestCase):
16
+ def write_asset(self, root, relative_path, contents, copied_prefix):
17
+ source = os.path.join(root, 'assets', relative_path)
18
+ copied = os.path.join(root, copied_prefix, relative_path)
19
+ os.makedirs(os.path.dirname(source), exist_ok=True)
20
+ os.makedirs(os.path.dirname(copied), exist_ok=True)
21
+ with open(source, 'wb') as stream:
22
+ stream.write(contents)
23
+ shutil.copyfile(source, copied)
24
+
14
25
  def write_pack(self, root):
26
+ self.write_asset(
27
+ root,
28
+ 'logos/1.png',
29
+ b'logo',
30
+ 'assets/pptx-volcengine',
31
+ )
15
32
  with open(os.path.join(root, 'design.md'), 'w', encoding='utf-8') as stream:
16
33
  stream.write(
17
34
  '---\n'
@@ -48,6 +65,94 @@ class LogoScopeTest(unittest.TestCase):
48
65
  stream.write(content)
49
66
  return path
50
67
 
68
+ def write_texture_pack(self, root):
69
+ self.write_asset(
70
+ root,
71
+ 'textures/1.webp',
72
+ b'texture',
73
+ 'assets/pptx-claude',
74
+ )
75
+ with open(os.path.join(root, 'design.md'), 'w', encoding='utf-8') as stream:
76
+ stream.write(
77
+ '---\n'
78
+ 'assets:\n'
79
+ ' texture-1:\n'
80
+ ' path: assets/textures/1.webp\n'
81
+ ' kind: texture\n'
82
+ 'layouts: layouts.md\n'
83
+ '---\n'
84
+ )
85
+ with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
86
+ stream.write(
87
+ '---\n'
88
+ 'canvas: 1920x1080\n'
89
+ 'layouts:\n'
90
+ ' cover:\n'
91
+ ' role: cover\n'
92
+ ' slots:\n'
93
+ ' - {role: texture, box: [1142, 0, 778, 1080], type: pic, asset: texture-1}\n'
94
+ ' content:\n'
95
+ ' role: content\n'
96
+ ' slots:\n'
97
+ ' - {role: title, box: [100, 100, 800, 100], type: title}\n'
98
+ '---\n'
99
+ )
100
+
101
+ def write_background_pack(self, root):
102
+ self.write_asset(
103
+ root,
104
+ 'backgrounds/content.webp',
105
+ b'background',
106
+ 'assets/pptx-claude',
107
+ )
108
+ with open(os.path.join(root, 'design.md'), 'w', encoding='utf-8') as stream:
109
+ stream.write(
110
+ '---\n'
111
+ 'assets:\n'
112
+ ' bg-content-1:\n'
113
+ ' path: assets/backgrounds/content.webp\n'
114
+ ' kind: background\n'
115
+ 'layouts: layouts.md\n'
116
+ '---\n'
117
+ )
118
+ with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
119
+ stream.write(
120
+ '---\n'
121
+ 'canvas: 1920x1080\n'
122
+ 'layouts:\n'
123
+ ' content:\n'
124
+ ' role: content\n'
125
+ ' background: bg-content-1\n'
126
+ ' slots:\n'
127
+ ' - {role: title, box: [100, 100, 800, 100], type: title}\n'
128
+ '---\n'
129
+ )
130
+
131
+ def write_theme_pack(self, root):
132
+ with open(os.path.join(root, 'design.md'), 'w', encoding='utf-8') as stream:
133
+ stream.write(
134
+ '---\n'
135
+ 'themes: [dark, light]\n'
136
+ 'default-theme: dark\n'
137
+ 'layouts: layouts.md\n'
138
+ '---\n'
139
+ )
140
+ with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
141
+ stream.write(
142
+ '---\n'
143
+ 'canvas: 1920x1080\n'
144
+ 'layouts:\n'
145
+ ' dark-content:\n'
146
+ ' role: content\n'
147
+ ' themes: [dark]\n'
148
+ ' slots: []\n'
149
+ ' light-content:\n'
150
+ ' role: content\n'
151
+ ' themes: [light]\n'
152
+ ' slots: []\n'
153
+ '---\n'
154
+ )
155
+
51
156
  def test_logo_is_limited_to_every_archetype_with_its_slot(self):
52
157
  with tempfile.TemporaryDirectory() as root:
53
158
  self.write_pack(root)
@@ -55,16 +160,20 @@ class LogoScopeTest(unittest.TestCase):
55
160
  root,
56
161
  '<deck-stage>'
57
162
  '<section data-pptx-layout="cover">'
58
- '<img src="assets/pptx-volcengine/logos/1.png"></section>'
163
+ '<img data-pptx-asset="logo-1" '
164
+ 'style="position:absolute;left:64px;top:64px;width:224px;height:48px" '
165
+ 'src="assets/pptx-volcengine/logos/1.png"></section>'
59
166
  '<section data-pptx-layout="content"><h1>正文</h1></section>'
60
167
  '<section data-pptx-layout="closing">'
61
- '<img src="assets/pptx-volcengine/logos/1.png"></section>'
168
+ '<img data-pptx-asset="logo-1" '
169
+ 'style="position:absolute;left:64px;top:64px;width:224px;height:48px" '
170
+ 'src="assets/pptx-volcengine/logos/1.png"></section>'
62
171
  '</deck-stage>',
63
172
  )
64
173
 
65
174
  self.assertEqual(
66
175
  [],
67
- validate_logo_scope(root, html, 'assets/pptx-volcengine'),
176
+ validate_layout_assets(root, html, 'assets/pptx-volcengine'),
68
177
  )
69
178
 
70
179
  def test_logo_on_an_unowned_archetype_is_rejected(self):
@@ -74,11 +183,13 @@ class LogoScopeTest(unittest.TestCase):
74
183
  root,
75
184
  '<deck-stage>'
76
185
  '<section data-pptx-layout="content">'
77
- '<img src="assets/pptx-volcengine/logos/1.png"></section>'
186
+ '<img data-pptx-asset="logo-1" '
187
+ 'style="position:absolute;left:64px;top:64px;width:224px;height:48px" '
188
+ 'src="assets/pptx-volcengine/logos/1.png"></section>'
78
189
  '</deck-stage>',
79
190
  )
80
191
 
81
- problems = validate_logo_scope(root, html, 'assets/pptx-volcengine')
192
+ problems = validate_layout_assets(root, html, 'assets/pptx-volcengine')
82
193
 
83
194
  self.assertEqual(1, len(problems))
84
195
  self.assertIn('content', problems[0])
@@ -90,13 +201,15 @@ class LogoScopeTest(unittest.TestCase):
90
201
  html = self.write_html(
91
202
  root,
92
203
  '<deck-stage><section data-pptx-layout="cover">'
93
- '<section class="content"><img src="assets/pptx-volcengine/logos/1.png">'
204
+ '<section class="content"><img data-pptx-asset="logo-1" '
205
+ 'style="position:absolute;left:64px;top:64px;width:224px;height:48px" '
206
+ 'src="assets/pptx-volcengine/logos/1.png">'
94
207
  '</section></section></deck-stage>',
95
208
  )
96
209
 
97
210
  self.assertEqual(
98
211
  [],
99
- validate_logo_scope(root, html, 'assets/pptx-volcengine'),
212
+ validate_layout_assets(root, html, 'assets/pptx-volcengine'),
100
213
  )
101
214
 
102
215
  def test_each_slide_declares_its_template_archetype(self):
@@ -108,8 +221,104 @@ class LogoScopeTest(unittest.TestCase):
108
221
  )
109
222
 
110
223
  self.assertEqual(
111
- ['第 1 页缺少 data-pptx-layout,无法核验模板 logo 归属'],
112
- validate_logo_scope(root, html, 'assets/pptx-volcengine'),
224
+ ['第 1 页缺少 data-pptx-layout,无法核验模板资产归属'],
225
+ validate_layout_assets(root, html, 'assets/pptx-volcengine'),
226
+ )
227
+
228
+ def test_missing_deck_slides_is_rejected_even_without_fixed_assets(self):
229
+ with tempfile.TemporaryDirectory() as root:
230
+ self.write_theme_pack(root)
231
+ html = self.write_html(root, '<main><section>普通页面</section></main>')
232
+
233
+ self.assertEqual(
234
+ ['没有识别到 deck-stage 的直属 slide section,无法核验模板页型'],
235
+ validate_layout_assets(root, html, 'assets/pptx-theme'),
236
+ )
237
+
238
+ def test_layout_outside_default_theme_requires_explicit_theme(self):
239
+ with tempfile.TemporaryDirectory() as root:
240
+ self.write_theme_pack(root)
241
+ html = self.write_html(
242
+ root,
243
+ '<deck-stage>'
244
+ '<section data-pptx-layout="light-content">正文</section>'
245
+ '</deck-stage>',
246
+ )
247
+
248
+ self.assertEqual(
249
+ ['第 1 页页型 light-content 不支持当前主题 dark;'
250
+ '允许主题: light。若确需切换,显式声明 data-pptx-theme'],
251
+ validate_layout_assets(root, html, 'assets/pptx-theme'),
252
+ )
253
+
254
+ def test_explicit_non_default_theme_must_match_layout(self):
255
+ with tempfile.TemporaryDirectory() as root:
256
+ self.write_theme_pack(root)
257
+ html = self.write_html(
258
+ root,
259
+ '<deck-stage>'
260
+ '<section data-pptx-layout="light-content" '
261
+ 'data-pptx-theme="light">正文</section>'
262
+ '</deck-stage>',
263
+ )
264
+
265
+ self.assertEqual(
266
+ [],
267
+ validate_layout_assets(root, html, 'assets/pptx-theme'),
268
+ )
269
+
270
+ def test_unknown_explicit_theme_is_rejected(self):
271
+ with tempfile.TemporaryDirectory() as root:
272
+ self.write_theme_pack(root)
273
+ html = self.write_html(
274
+ root,
275
+ '<deck-stage>'
276
+ '<section data-pptx-layout="dark-content" '
277
+ 'data-pptx-theme="brand-new">正文</section>'
278
+ '</deck-stage>',
279
+ )
280
+
281
+ self.assertEqual(
282
+ ['第 1 页声明了不存在的模板主题: brand-new'],
283
+ validate_layout_assets(root, html, 'assets/pptx-theme'),
284
+ )
285
+
286
+ def test_pack_without_fixed_assets_still_accepts_valid_layout(self):
287
+ with tempfile.TemporaryDirectory() as root:
288
+ self.write_theme_pack(root)
289
+ html = self.write_html(
290
+ root,
291
+ '<deck-stage>'
292
+ '<section data-pptx-layout="dark-content">正文</section>'
293
+ '</deck-stage>',
294
+ )
295
+
296
+ self.assertEqual(
297
+ [],
298
+ validate_layout_assets(root, html, 'assets/pptx-theme'),
299
+ )
300
+
301
+ def test_non_mapping_layout_entry_does_not_crash_theme_validation(self):
302
+ with tempfile.TemporaryDirectory() as root:
303
+ self.write_theme_pack(root)
304
+ with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
305
+ stream.write(
306
+ '---\n'
307
+ 'canvas: 1920x1080\n'
308
+ 'layouts:\n'
309
+ ' malformed: unavailable\n'
310
+ '---\n'
311
+ )
312
+ html = self.write_html(
313
+ root,
314
+ '<deck-stage>'
315
+ '<section data-pptx-layout="malformed">正文</section>'
316
+ '</deck-stage>',
317
+ )
318
+
319
+ self.assertEqual(
320
+ [],
321
+ validate_layout_assets(root, html, 'assets/pptx-theme'),
113
322
  )
114
323
 
115
324
  def test_logo_in_global_css_is_rejected(self):
@@ -123,8 +332,8 @@ class LogoScopeTest(unittest.TestCase):
123
332
  )
124
333
 
125
334
  self.assertEqual(
126
- ['模板 logo logo-1 出现在 slide section 外,无法核验页型归属'],
127
- validate_logo_scope(root, html, 'assets/pptx-volcengine'),
335
+ ['模板资产 logo-1 出现在 slide section 外,无法核验页型归属'],
336
+ validate_layout_assets(root, html, 'assets/pptx-volcengine'),
128
337
  )
129
338
 
130
339
  def test_logo_in_a_slide_style_block_is_rejected(self):
@@ -139,8 +348,251 @@ class LogoScopeTest(unittest.TestCase):
139
348
  )
140
349
 
141
350
  self.assertEqual(
142
- ['模板 logo logo-1 出现在 slide section 外,无法核验页型归属'],
143
- validate_logo_scope(root, html, 'assets/pptx-volcengine'),
351
+ ['模板资产 logo-1 出现在 slide section 外,无法核验页型归属'],
352
+ validate_layout_assets(root, html, 'assets/pptx-volcengine'),
353
+ )
354
+
355
+ def test_layout_must_use_its_bound_texture(self):
356
+ with tempfile.TemporaryDirectory() as root:
357
+ self.write_texture_pack(root)
358
+ html = self.write_html(
359
+ root,
360
+ '<deck-stage><section data-pptx-layout="cover">'
361
+ '<img src="assets/generated/replacement.webp"></section></deck-stage>',
362
+ )
363
+
364
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
365
+
366
+ self.assertEqual(1, len(problems))
367
+ self.assertIn('cover', problems[0])
368
+ self.assertIn('texture-1', problems[0])
369
+ self.assertIn('缺少固定实例', problems[0])
370
+
371
+ def test_layout_bound_texture_on_an_unowned_layout_is_rejected(self):
372
+ with tempfile.TemporaryDirectory() as root:
373
+ self.write_texture_pack(root)
374
+ html = self.write_html(
375
+ root,
376
+ '<deck-stage><section data-pptx-layout="content">'
377
+ '<img data-pptx-asset="texture-1" '
378
+ 'style="position:absolute;left:1142px;top:0;width:778px;height:1080px" '
379
+ 'src="assets/pptx-claude/textures/1.webp"></section></deck-stage>',
380
+ )
381
+
382
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
383
+
384
+ self.assertEqual(1, len(problems))
385
+ self.assertIn('content', problems[0])
386
+ self.assertIn('texture-1', problems[0])
387
+ self.assertIn('不得使用', problems[0])
388
+
389
+ def test_layout_must_use_its_bound_background_with_query_and_fragment(self):
390
+ with tempfile.TemporaryDirectory() as root:
391
+ self.write_background_pack(root)
392
+ html = self.write_html(
393
+ root,
394
+ '<deck-stage><section data-pptx-layout="content" '
395
+ 'data-pptx-asset="bg-content-1" '
396
+ 'style="background-image:url('
397
+ '\'assets/pptx-claude/backgrounds/content.webp?v=2#slide\')">'
398
+ '正文</section></deck-stage>',
399
+ )
400
+
401
+ self.assertEqual(
402
+ [],
403
+ validate_layout_assets(root, html, 'assets/pptx-claude'),
404
+ )
405
+
406
+ def test_repeated_asset_slots_require_repeated_placements(self):
407
+ with tempfile.TemporaryDirectory() as root:
408
+ self.write_texture_pack(root)
409
+ with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
410
+ stream.write(
411
+ '---\n'
412
+ 'canvas: 1920x1080\n'
413
+ 'layouts:\n'
414
+ ' cover:\n'
415
+ ' role: cover\n'
416
+ ' slots:\n'
417
+ ' - {role: texture, box: [0, 0, 100, 100], type: pic, asset: texture-1}\n'
418
+ ' - {role: texture, box: [1800, 980, 100, 100], type: pic, asset: texture-1}\n'
419
+ '---\n'
420
+ )
421
+ html = self.write_html(
422
+ root,
423
+ '<deck-stage><section data-pptx-layout="cover">'
424
+ '<img data-pptx-asset="texture-1" '
425
+ 'style="position:absolute;left:0;top:0;width:100px;height:100px" '
426
+ 'src="assets/pptx-claude/textures/1.webp"></section></deck-stage>',
427
+ )
428
+
429
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
430
+
431
+ self.assertEqual(1, len(problems))
432
+ self.assertIn('位置尺寸应为 [1800, 980, 100, 100]', problems[0])
433
+
434
+ def test_legacy_logo_scope_entrypoint_keeps_failure_exit_code(self):
435
+ with tempfile.TemporaryDirectory() as root:
436
+ self.write_texture_pack(root)
437
+ html = self.write_html(
438
+ root,
439
+ '<deck-stage><section data-pptx-layout="cover">'
440
+ '<img src="assets/generated/replacement.webp"></section></deck-stage>',
441
+ )
442
+
443
+ result = subprocess.run(
444
+ [
445
+ sys.executable,
446
+ os.path.join(os.path.dirname(__file__), 'verify_logo_scope.py'),
447
+ root,
448
+ html,
449
+ '--asset-prefix',
450
+ 'assets/pptx-claude',
451
+ ],
452
+ capture_output=True,
453
+ check=False,
454
+ text=True,
455
+ )
456
+
457
+ self.assertEqual(1, result.returncode)
458
+ self.assertIn('PPTX_LAYOUT_ASSETS: FAIL', result.stdout)
459
+
460
+ def test_copied_asset_must_keep_the_source_pptx_bytes(self):
461
+ with tempfile.TemporaryDirectory() as root:
462
+ self.write_texture_pack(root)
463
+ copied = os.path.join(
464
+ root, 'assets', 'pptx-claude', 'textures', '1.webp')
465
+ with open(copied, 'wb') as stream:
466
+ stream.write(b'generated replacement')
467
+ html = self.write_html(
468
+ root,
469
+ '<deck-stage><section data-pptx-layout="cover">'
470
+ '<img data-pptx-asset="texture-1" '
471
+ 'style="position:absolute;left:1142px;top:0;width:778px;height:1080px" '
472
+ 'src="assets/pptx-claude/textures/1.webp"></section></deck-stage>',
473
+ )
474
+
475
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
476
+
477
+ self.assertEqual(1, len(problems))
478
+ self.assertIn('已被替换或改写', problems[0])
479
+
480
+ def test_fixed_asset_must_keep_its_layout_box(self):
481
+ with tempfile.TemporaryDirectory() as root:
482
+ self.write_texture_pack(root)
483
+ html = self.write_html(
484
+ root,
485
+ '<deck-stage><section data-pptx-layout="cover">'
486
+ '<img data-pptx-asset="texture-1" '
487
+ 'style="position:absolute;left:0;top:0;width:778px;height:1080px" '
488
+ 'src="assets/pptx-claude/textures/1.webp"></section></deck-stage>',
489
+ )
490
+
491
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
492
+
493
+ self.assertEqual(1, len(problems))
494
+ self.assertIn('位置尺寸必须为 [1142, 0, 778, 1080]', problems[0])
495
+
496
+ def test_fixed_asset_cannot_be_hidden(self):
497
+ with tempfile.TemporaryDirectory() as root:
498
+ self.write_texture_pack(root)
499
+ html = self.write_html(
500
+ root,
501
+ '<deck-stage><section data-pptx-layout="cover">'
502
+ '<div hidden><img data-pptx-asset="texture-1" '
503
+ 'style="position:absolute;left:1142px;top:0;width:778px;height:1080px" '
504
+ 'src="assets/pptx-claude/textures/1.webp"></div>'
505
+ '</section></deck-stage>',
506
+ )
507
+
508
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
509
+
510
+ self.assertEqual(1, len(problems))
511
+ self.assertIn('不可隐藏', problems[0])
512
+
513
+ def test_zero_sized_fixed_asset_is_hidden(self):
514
+ with tempfile.TemporaryDirectory() as root:
515
+ self.write_texture_pack(root)
516
+ html = self.write_html(
517
+ root,
518
+ '<deck-stage><section data-pptx-layout="cover">'
519
+ '<img data-pptx-asset="texture-1" '
520
+ 'style="position:absolute;left:1142px;top:0;width:0;height:1080px" '
521
+ 'src="assets/pptx-claude/textures/1.webp"></section></deck-stage>',
522
+ )
523
+
524
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
525
+
526
+ self.assertEqual(2, len(problems))
527
+ self.assertTrue(any('不可隐藏' in problem for problem in problems))
528
+ self.assertTrue(any('位置尺寸必须为' in problem for problem in problems))
529
+
530
+ def test_fixed_asset_reference_requires_an_instance_marker(self):
531
+ with tempfile.TemporaryDirectory() as root:
532
+ self.write_texture_pack(root)
533
+ html = self.write_html(
534
+ root,
535
+ '<deck-stage><section data-pptx-layout="cover">'
536
+ '<img style="position:absolute;left:1142px;top:0;width:778px;height:1080px" '
537
+ 'src="assets/pptx-claude/textures/1.webp"></section></deck-stage>',
538
+ )
539
+
540
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
541
+
542
+ self.assertEqual(2, len(problems))
543
+ self.assertTrue(any(
544
+ '缺少 data-pptx-asset' in problem for problem in problems))
545
+ self.assertTrue(any(
546
+ '缺少固定实例 texture-1' in problem for problem in problems))
547
+
548
+ def test_fixed_asset_cannot_hide_a_generated_source_behind_the_original_url(self):
549
+ with tempfile.TemporaryDirectory() as root:
550
+ self.write_texture_pack(root)
551
+ html = self.write_html(
552
+ root,
553
+ '<deck-stage><section data-pptx-layout="cover">'
554
+ '<img data-pptx-asset="texture-1" '
555
+ 'style="position:absolute;left:1142px;top:0;width:778px;height:1080px;'
556
+ 'background-image:url(\'assets/pptx-claude/textures/1.webp\')" '
557
+ 'src="assets/generated/replacement.webp"></section></deck-stage>',
558
+ )
559
+
560
+ problems = validate_layout_assets(root, html, 'assets/pptx-claude')
561
+
562
+ self.assertEqual(1, len(problems))
563
+ self.assertIn('未引用对应的 PPTX 原素材', problems[0])
564
+
565
+ def test_fixed_asset_nested_in_flow_is_checked_at_its_box(self):
566
+ with tempfile.TemporaryDirectory() as root:
567
+ self.write_texture_pack(root)
568
+ with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
569
+ stream.write(
570
+ '---\n'
571
+ 'canvas: 1920x1080\n'
572
+ 'layouts:\n'
573
+ ' cover:\n'
574
+ ' role: cover\n'
575
+ ' flow:\n'
576
+ ' top: 80\n'
577
+ ' margin: [80, 80]\n'
578
+ ' gap: 32\n'
579
+ ' regions:\n'
580
+ ' - kind: free\n'
581
+ ' items:\n'
582
+ ' - {role: texture, type: pic, box: [1142, 0, 778, 1080], asset: texture-1}\n'
583
+ '---\n'
584
+ )
585
+ html = self.write_html(
586
+ root,
587
+ '<deck-stage><section data-pptx-layout="cover">'
588
+ '<img data-pptx-asset="texture-1" '
589
+ 'style="position:absolute;left:1142px;top:0;width:778px;height:1080px" '
590
+ 'src="assets/pptx-claude/textures/1.webp"></section></deck-stage>',
591
+ )
592
+
593
+ self.assertEqual(
594
+ [],
595
+ validate_layout_assets(root, html, 'assets/pptx-claude'),
144
596
  )
145
597
 
146
598