@lark-apaas/coding-steering 0.1.38 → 0.1.39-alpha.20260824122813

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.
Files changed (25) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/pptx-style-extract/SKILL.md +19 -28
  3. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +2 -8
  4. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +190 -1181
  5. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +10 -229
  6. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -18
  7. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +40 -643
  8. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +3 -19
  9. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +2 -4
  10. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +1 -308
  11. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +0 -14
  12. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +7 -157
  13. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +2 -1598
  14. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +4 -151
  15. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +1 -1
  16. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
  17. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +0 -11
  18. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +4 -67
  19. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +1 -0
  20. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -75
  21. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +7 -5
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +0 -556
  23. package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +0 -479
  24. package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +0 -400
  25. package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +0 -12
@@ -1,556 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Integration regression tests for model-decided image roles."""
3
- import json
4
- import os
5
- import sys
6
- import tempfile
7
- import unittest
8
-
9
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
-
11
- from check_v2 import Pack, RULES # noqa: E402
12
- from package import (Fail, apply_asset_decisions, bind_asset_vision_group_scopes,
13
- load_layout_blocks, main as package_main, # noqa: E402
14
- validate_asset_vision_groups)
15
-
16
-
17
- class AssetJudgmentPackageTest(unittest.TestCase):
18
- def test_asset_vision_index_requires_selected_candidates(self):
19
- with tempfile.TemporaryDirectory() as lout:
20
- with open(os.path.join(lout, 'asset-vision-groups.json'),
21
- 'w', encoding='utf-8') as stream:
22
- json.dump({'version': 2}, stream)
23
-
24
- with self.assertRaisesRegex(Fail, '缺少 selected'):
25
- validate_asset_vision_groups(lout, {'asset_vision_groups': []})
26
-
27
- def test_asset_vision_groups_must_keep_every_extracted_candidate(self):
28
- with tempfile.TemporaryDirectory() as lout:
29
- with open(os.path.join(lout, 'asset-vision-groups.json'),
30
- 'w', encoding='utf-8') as stream:
31
- json.dump({
32
- 'version': 2,
33
- 'selected': [{
34
- 'candidates': [{
35
- 'id': 'asset-cover',
36
- 'source_media': 'cover.png',
37
- 'placements': [{
38
- 'box': [100, 200, 300, 400],
39
- 'archetype': 'cover',
40
- }],
41
- }, {
42
- 'id': 'asset-ornament',
43
- 'source_media': 'ornament.png',
44
- 'placements': [{
45
- 'box': [10, 20, 30, 40],
46
- 'archetype': 'content',
47
- }],
48
- }],
49
- }],
50
- }, stream)
51
- manifest = {
52
- 'asset_vision_groups': [{
53
- 'id': 'asset-cover',
54
- 'source_media': 'cover.png',
55
- 'layout': 'cover',
56
- 'box': '[100, 200, 300, 400]',
57
- 'visual_kind': 'illustration',
58
- }, {
59
- 'id': 'asset-ornament',
60
- 'source_media': 'ornament.png',
61
- 'layout': 'content',
62
- 'box': '[10, 20, 30, 40]',
63
- 'visual_kind': 'decorative',
64
- }],
65
- }
66
-
67
- validate_asset_vision_groups(lout, manifest)
68
- manifest['asset_vision_groups'].pop()
69
-
70
- with self.assertRaisesRegex(Fail, '逐项保留抽取候选.*asset-ornament'):
71
- validate_asset_vision_groups(lout, manifest)
72
-
73
- def test_asset_vision_groups_reject_rewritten_extracted_candidate_scope(self):
74
- with tempfile.TemporaryDirectory() as lout:
75
- with open(os.path.join(lout, 'asset-vision-groups.json'),
76
- 'w', encoding='utf-8') as stream:
77
- json.dump({
78
- 'version': 2,
79
- 'selected': [{
80
- 'candidates': [{
81
- 'id': 'asset-ornament',
82
- 'source_media': 'ornament.png',
83
- 'placements': [{
84
- 'box': [10, 20, 30, 40],
85
- 'archetype': None,
86
- }],
87
- }],
88
- }],
89
- }, stream)
90
- manifest = {
91
- 'asset_vision_groups': [{
92
- 'id': 'asset-ornament',
93
- 'source_media': 'other.png',
94
- 'box': '[10, 20, 30, 40]',
95
- 'visual_kind': 'decorative',
96
- }],
97
- }
98
-
99
- with self.assertRaisesRegex(Fail, '改写了抽取候选的图片或位置'):
100
- validate_asset_vision_groups(lout, manifest)
101
-
102
- def test_v2_candidate_ids_resolve_same_box_on_different_layouts(self):
103
- with tempfile.TemporaryDirectory() as lout:
104
- with open(os.path.join(lout, 'asset-vision-groups.json'),
105
- 'w', encoding='utf-8') as stream:
106
- json.dump({
107
- 'version': 2,
108
- 'selected': [{
109
- 'candidates': [{
110
- 'id': 'asset-cover-logo',
111
- 'source_media': 'shared.png',
112
- 'placements': [{
113
- 'box': [100, 200, 80, 80],
114
- 'archetype': 'cover',
115
- }],
116
- }, {
117
- 'id': 'asset-content-ornament',
118
- 'source_media': 'shared.png',
119
- 'placements': [{
120
- 'box': [100, 200, 80, 80],
121
- 'archetype': 'content',
122
- }],
123
- }],
124
- }],
125
- }, stream)
126
- manifest = {
127
- 'assets': [],
128
- 'asset_vision_groups': [{
129
- 'id': 'asset-cover-logo',
130
- 'source_media': 'shared.png',
131
- 'box': '[100, 200, 80, 80]',
132
- 'visual_kind': 'logo',
133
- }, {
134
- 'id': 'asset-content-ornament',
135
- 'source_media': 'shared.png',
136
- 'box': '[100, 200, 80, 80]',
137
- 'visual_kind': 'decorative',
138
- }],
139
- }
140
-
141
- bind_asset_vision_group_scopes(lout, manifest)
142
- decisions, _ = apply_asset_decisions(
143
- manifest,
144
- bound_sources={'shared.png'},
145
- bound_slots={
146
- 'shared.png': {
147
- ('cover', (100, 200, 80, 80)),
148
- ('content', (100, 200, 80, 80)),
149
- },
150
- },
151
- )
152
-
153
- self.assertEqual(
154
- decisions.for_scope('shared.png', 'cover', (100, 200, 80, 80)),
155
- 'logo',
156
- )
157
- self.assertEqual(
158
- decisions.for_scope('shared.png', 'content', (100, 200, 80, 80)),
159
- 'texture',
160
- )
161
-
162
- def test_legacy_asset_vision_index_accepts_a_group_default(self):
163
- with tempfile.TemporaryDirectory() as lout:
164
- with open(os.path.join(lout, 'asset-vision-groups.json'),
165
- 'w', encoding='utf-8') as stream:
166
- json.dump({
167
- 'version': 1,
168
- 'selected': [{
169
- 'candidates': [{
170
- 'id': 'asset-cover',
171
- 'source_media': 'cover.png',
172
- 'placements': [{'box': [0, 0, 100, 100]}],
173
- }, {
174
- 'id': 'asset-ornament',
175
- 'source_media': 'ornament.png',
176
- 'placements': [{'box': [10, 20, 30, 40]}],
177
- }],
178
- }],
179
- }, stream)
180
- manifest = {
181
- 'asset_vision_groups': [{
182
- 'id': 'vision-1',
183
- 'source_media': '[cover.png, ornament.png]',
184
- 'visual_kind': 'decorative',
185
- }],
186
- }
187
-
188
- validate_asset_vision_groups(lout, manifest)
189
-
190
- def test_layout_controls_override_legacy_judgment_without_reading_layout_todos(self):
191
- with tempfile.TemporaryDirectory() as root:
192
- lout = os.path.join(root, 'l-out')
193
- os.makedirs(lout)
194
- with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
195
- stream.write(
196
- 'names:\n'
197
- ' content: TODO中文名\n'
198
- 'roles:\n'
199
- ' content: TODO角色\n'
200
- 'layouts:\n'
201
- ' content:\n'
202
- ' slots:\n'
203
- ' - {role: body, box: [10, 20, 30, 40], type: body}\n'
204
- ' confidence: high\n'
205
- )
206
- with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
207
- stream.write(
208
- 'names:\n'
209
- ' content: "内容页"\n'
210
- 'roles:\n'
211
- ' content: content\n'
212
- )
213
-
214
- blocks = load_layout_blocks(lout)
215
-
216
- self.assertEqual([key for key, _, _ in blocks],
217
- ['names', 'roles', 'layouts'])
218
- self.assertIn('内容页', blocks[0][2][0])
219
- self.assertIn('content', blocks[1][2][0])
220
- self.assertNotIn('TODO', '\n'.join(
221
- line for _, _, lines in blocks for line in lines
222
- ))
223
-
224
- def test_layout_controls_reject_non_judgment_block(self):
225
- with tempfile.TemporaryDirectory() as root:
226
- lout = os.path.join(root, 'l-out')
227
- os.makedirs(lout)
228
- with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
229
- stream.write('layouts:\n content:\n role: content\n')
230
- with open(os.path.join(lout, 'layout-controls.yaml'), 'w', encoding='utf-8') as stream:
231
- stream.write('layouts:\n cover:\n role: cover\n')
232
-
233
- with self.assertRaisesRegex(Fail, '只能包含'):
234
- load_layout_blocks(lout)
235
-
236
- def test_layout_controls_must_cover_all_legacy_judgment_blocks(self):
237
- with tempfile.TemporaryDirectory() as root:
238
- lout = os.path.join(root, 'l-out')
239
- os.makedirs(lout)
240
- with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
241
- stream.write(
242
- 'names:\n'
243
- ' content: "内容页"\n'
244
- 'roles:\n'
245
- ' content: content\n'
246
- 'layouts:\n'
247
- ' content:\n'
248
- ' role: content\n'
249
- )
250
- with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
251
- encoding='utf-8') as stream:
252
- stream.write('roles:\n content: content\n')
253
-
254
- with self.assertRaisesRegex(Fail, '缺:names'):
255
- load_layout_blocks(lout)
256
-
257
- def test_layout_controls_must_keep_each_legacy_judgment_entry(self):
258
- with tempfile.TemporaryDirectory() as root:
259
- lout = os.path.join(root, 'l-out')
260
- os.makedirs(lout)
261
- with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
262
- stream.write(
263
- 'names:\n'
264
- ' cover: "封面"\n'
265
- ' content: "内容页"\n'
266
- 'roles:\n'
267
- ' cover: cover\n'
268
- ' content: content\n'
269
- 'layouts:\n'
270
- ' cover:\n'
271
- ' role: cover\n'
272
- ' content:\n'
273
- ' role: content\n'
274
- )
275
- with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
276
- encoding='utf-8') as stream:
277
- stream.write(
278
- 'names:\n'
279
- ' cover: "封面"\n'
280
- 'roles:\n'
281
- ' cover: cover\n'
282
- ' content: content\n'
283
- )
284
-
285
- with self.assertRaisesRegex(Fail, 'names 段缺少旧判断条目:content'):
286
- load_layout_blocks(lout)
287
-
288
- def test_package_strips_judgment_fields_and_preserves_content_slot(self):
289
- try:
290
- from PIL import Image
291
- except ImportError:
292
- self.skipTest('Pillow unavailable')
293
-
294
- with tempfile.TemporaryDirectory() as root:
295
- stage1 = os.path.join(root, 'stage1')
296
- lout = os.path.join(stage1, 'l-out')
297
- pack = os.path.join(root, 'pack')
298
- media = os.path.join(stage1, 'media-out')
299
- ref = os.path.join(stage1, 'ref')
300
- os.makedirs(lout)
301
- os.makedirs(media)
302
- os.makedirs(ref)
303
-
304
- Image.new('RGBA', (40, 20), (20, 40, 60, 255)).save(
305
- os.path.join(media, 'partner-logo.png'))
306
- Image.new('RGBA', (24, 24), (60, 40, 20, 255)).save(
307
- os.path.join(media, 'ornament.png'))
308
-
309
- box_rows = [
310
- ('ppt/media/image1.png', 'partner-logo.png', [100, 200, 400, 200]),
311
- ('ppt/media/image2.png', 'ornament.png', [40, 40, 120, 120]),
312
- ]
313
- extract = {
314
- 'source': {'filename': 'fixture.pptx'},
315
- 'canvas': {
316
- 'px': [1920, 1080],
317
- 'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
318
- },
319
- 'media': [
320
- {
321
- 'media': source,
322
- 'out': 'media-out/' + output,
323
- 'source_px': [40, 20] if output == 'partner-logo.png' else [24, 24],
324
- }
325
- for source, output, _ in box_rows
326
- ],
327
- 'images': [
328
- {
329
- 'media': source,
330
- 'boxes': [{
331
- 'count': 1,
332
- 'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
333
- 'parts': ['ppt/slides/slide1.xml'],
334
- }],
335
- }
336
- for source, _, box in box_rows
337
- ],
338
- 'color_freq': [],
339
- 'text_scale': [],
340
- }
341
- with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
342
- json.dump(extract, stream)
343
- with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
344
- json.dump({
345
- 'shapes': [
346
- {
347
- 'part': 'ppt/slides/slide1.xml',
348
- 'kind': 'pic',
349
- 'name': output,
350
- 'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
351
- }
352
- for _, output, box in box_rows
353
- ],
354
- }, stream)
355
-
356
- with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
357
- stream.write(
358
- 'version: alpha\n'
359
- 'name: asset-judgment-fixture\n'
360
- 'description: >\n'
361
- ' A restrained fixture with explicit image-role judgments and traced layout slots.\n'
362
- 'assets:\n'
363
- ' - id: logo-primary\n'
364
- ' source_media: partner-logo.png\n'
365
- ' kind: logo\n'
366
- ' on-bg: light\n'
367
- 'asset_vision_groups:\n'
368
- ' - id: vision-1\n'
369
- ' source_media: [partner-logo.png, ornament.png]\n'
370
- ' visual_kind: content-image\n'
371
- 'asset_decisions:\n'
372
- ' - source_media: ornament.png\n'
373
- ' visual_kind: decorative\n'
374
- )
375
- with open(os.path.join(lout, 'frontmatter.yaml'), 'w', encoding='utf-8') as stream:
376
- stream.write('')
377
- with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
378
- stream.write(
379
- 'roles:\n'
380
- ' content: TODO角色\n'
381
- 'layouts:\n'
382
- ' content:\n'
383
- ' name: "内容页"\n'
384
- ' role: content\n'
385
- ' slots:\n'
386
- ' - {role: logo, box: [100, 200, 400, 200], type: pic, '
387
- 'asset: logo-primary, source_media: partner-logo.png}\n'
388
- ' - {role: asset-candidate, box: [40, 40, 120, 120], type: pic, '
389
- 'source_media: ornament.png}\n'
390
- ' confidence: high\n'
391
- )
392
- with open(os.path.join(lout, 'layout-controls.yaml'), 'w',
393
- encoding='utf-8') as stream:
394
- stream.write('roles:\n content: content\n')
395
- with open(os.path.join(lout, 'body.md'), 'w', encoding='utf-8') as stream:
396
- stream.write(
397
- '## Usage\n\n'
398
- 'Read `layouts.md` before composing a slide.\n\n'
399
- '## Assets\n\n'
400
- '{{ASSET_TABLE}}\n\n'
401
- '## Hard Rules\n\n'
402
- '{{LOGO_RULES}}\n'
403
- )
404
- check_v1 = os.path.join(root, 'check_v1.py')
405
- with open(check_v1, 'w', encoding='utf-8') as stream:
406
- stream.write('raise SystemExit(0)\n')
407
-
408
- rc = package_main([stage1, lout, pack, '--check-v1', check_v1])
409
-
410
- self.assertEqual(rc, 0)
411
- with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
412
- design = stream.read()
413
- with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
414
- layouts = stream.read()
415
- self.assertNotIn('asset_decisions', design)
416
- self.assertNotIn('asset_vision_groups', design)
417
- self.assertNotIn('visual_kind', design)
418
- self.assertNotIn('source_media', design)
419
- self.assertNotIn('source_media', layouts)
420
- self.assertNotIn('TODO', layouts)
421
- self.assertNotIn('{{LOGO_RULES}}', design)
422
- self.assertNotIn('logo-primary', design)
423
- self.assertNotIn('logo-primary', layouts)
424
- self.assertIn(
425
- '- {role: pic, box: [100, 200, 400, 200], type: pic}',
426
- layouts,
427
- )
428
- self.assertIn('texture-1:', design)
429
- self.assertIn('kind: texture', design)
430
- self.assertIn(
431
- '- {role: texture, box: [40, 40, 120, 120], type: pic, asset: texture-1}',
432
- layouts,
433
- )
434
- gate = Pack(pack)
435
- self.assertFalse([
436
- result
437
- for result in (rule(gate) for rule in RULES)
438
- if result.fails
439
- ])
440
-
441
- def test_package_places_a_rendered_picture_instance_as_a_logo_asset(self):
442
- try:
443
- from PIL import Image
444
- except ImportError:
445
- self.skipTest('Pillow unavailable')
446
-
447
- with tempfile.TemporaryDirectory() as root:
448
- stage1 = os.path.join(root, 'stage1')
449
- lout = os.path.join(stage1, 'l-out')
450
- pack = os.path.join(root, 'pack')
451
- media = os.path.join(stage1, 'media-out')
452
- ref = os.path.join(stage1, 'ref')
453
- os.makedirs(lout)
454
- os.makedirs(media)
455
- os.makedirs(ref)
456
-
457
- source = 'generated/instance/picture-directory-logo.webp'
458
- output = 'picture-directory-logo.webp'
459
- box = [1813, 18, 59, 228]
460
- Image.new('RGBA', (59, 228), (20, 60, 100, 255)).save(
461
- os.path.join(media, output), 'WEBP')
462
- extract = {
463
- 'source': {'filename': 'fixture.pptx'},
464
- 'canvas': {
465
- 'px': [1920, 1080],
466
- 'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
467
- },
468
- 'media': [{
469
- 'media': source,
470
- 'out': 'media-out/' + output,
471
- 'generated': True,
472
- 'rendered_from': 'ppt/media/original-logo.png',
473
- }],
474
- 'images': [{
475
- 'media': source,
476
- 'boxes': [{
477
- 'count': 1,
478
- 'box': dict(zip(('x', 'y', 'w', 'h'), box)),
479
- 'parts': ['ppt/slides/slide2.xml'],
480
- }],
481
- }],
482
- 'color_freq': [],
483
- 'text_scale': [],
484
- }
485
- with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
486
- json.dump(extract, stream)
487
- with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
488
- json.dump({'shapes': [{
489
- 'part': 'ppt/slides/slide2.xml',
490
- 'kind': 'pic',
491
- 'box': dict(zip(('x', 'y', 'w', 'h'), box)),
492
- }]}, stream)
493
- with open(os.path.join(lout, 'asset-vision-groups.json'),
494
- 'w', encoding='utf-8') as stream:
495
- json.dump({
496
- 'version': 2,
497
- 'selected': [{
498
- 'candidates': [{
499
- 'id': 'asset-directory-logo',
500
- 'source_media': output,
501
- 'placements': [{
502
- 'slide': 2,
503
- 'archetype': 'content',
504
- 'box': box,
505
- }],
506
- }],
507
- }],
508
- 'omitted': [],
509
- }, stream)
510
- with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
511
- stream.write(
512
- 'version: alpha\n'
513
- 'name: rendered-instance-fixture\n'
514
- 'description: Rendered local PPT image instances remain reusable assets.\n'
515
- 'asset_vision_groups:\n'
516
- ' - id: asset-directory-logo\n'
517
- ' source_media: picture-directory-logo.webp\n'
518
- ' box: [1813, 18, 59, 228]\n'
519
- ' visual_kind: logo\n'
520
- )
521
- for name, content in (
522
- ('frontmatter.yaml', ''),
523
- ('body.md', 'Read `layouts.md` before composing a slide.\n\n{{ASSET_TABLE}}\n'),
524
- ('layouts.yaml',
525
- 'layouts:\n'
526
- ' content:\n'
527
- ' name: "目录"\n'
528
- ' role: content\n'
529
- ' slots:\n'
530
- ' - {role: asset-candidate, box: [1813, 18, 59, 228], type: pic, '
531
- 'source_media: picture-directory-logo.webp}\n'
532
- ' confidence: high\n'),
533
- ):
534
- with open(os.path.join(lout, name), 'w', encoding='utf-8') as stream:
535
- stream.write(content)
536
- check_v1 = os.path.join(root, 'check_v1.py')
537
- with open(check_v1, 'w', encoding='utf-8') as stream:
538
- stream.write('raise SystemExit(0)\n')
539
-
540
- self.assertEqual(package_main([stage1, lout, pack, '--check-v1', check_v1]), 0)
541
-
542
- with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
543
- design = stream.read()
544
- with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
545
- layouts = stream.read()
546
- self.assertTrue(os.path.isfile(os.path.join(pack, 'assets', 'logos', '1.webp')))
547
- self.assertIn('logo-1:', design)
548
- self.assertIn('assets/logos/1.webp', design)
549
- self.assertIn(
550
- '- {role: logo, box: [1813, 18, 59, 228], type: pic, asset: logo-1}',
551
- layouts,
552
- )
553
-
554
-
555
- if __name__ == '__main__':
556
- unittest.main()