@weirdfingers/boards 0.2.0 → 0.3.0

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.
@@ -0,0 +1,576 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type { JSONSchema7 } from "json-schema";
3
+ import {
4
+ parseGeneratorSchema,
5
+ isArtifactReference,
6
+ getArtifactType,
7
+ parseArtifactSlot,
8
+ parseSettingsField,
9
+ } from "../schemaParser";
10
+
11
+ describe("schemaParser", () => {
12
+ describe("isArtifactReference", () => {
13
+ it("should identify direct artifact references", () => {
14
+ const property: JSONSchema7 = {
15
+ $ref: "#/$defs/AudioArtifact",
16
+ };
17
+ expect(isArtifactReference(property)).toBe(true);
18
+ });
19
+
20
+ it("should identify array artifact references", () => {
21
+ const property: JSONSchema7 = {
22
+ type: "array",
23
+ items: {
24
+ $ref: "#/$defs/ImageArtifact",
25
+ },
26
+ };
27
+ expect(isArtifactReference(property)).toBe(true);
28
+ });
29
+
30
+ it("should return false for non-artifact references", () => {
31
+ const property: JSONSchema7 = {
32
+ type: "string",
33
+ };
34
+ expect(isArtifactReference(property)).toBe(false);
35
+ });
36
+
37
+ it("should return false for undefined property", () => {
38
+ expect(isArtifactReference(undefined)).toBe(false);
39
+ });
40
+
41
+ it("should return false for boolean schema", () => {
42
+ expect(isArtifactReference(true)).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe("getArtifactType", () => {
47
+ it("should extract audio type", () => {
48
+ expect(getArtifactType("#/$defs/AudioArtifact")).toBe("audio");
49
+ });
50
+
51
+ it("should extract video type", () => {
52
+ expect(getArtifactType("#/$defs/VideoArtifact")).toBe("video");
53
+ });
54
+
55
+ it("should extract image type", () => {
56
+ expect(getArtifactType("#/$defs/ImageArtifact")).toBe("image");
57
+ });
58
+
59
+ it("should extract text type", () => {
60
+ expect(getArtifactType("#/$defs/TextArtifact")).toBe("text");
61
+ });
62
+
63
+ it("should fallback to image for unknown patterns", () => {
64
+ expect(getArtifactType("#/$defs/UnknownType")).toBe("image");
65
+ });
66
+ });
67
+
68
+ describe("parseArtifactSlot", () => {
69
+ it("should parse single artifact slot", () => {
70
+ const property: JSONSchema7 = {
71
+ $ref: "#/$defs/AudioArtifact",
72
+ title: "Audio Source",
73
+ description: "Audio file to process",
74
+ };
75
+
76
+ const slot = parseArtifactSlot("audio_source", property, true);
77
+
78
+ expect(slot).toEqual({
79
+ name: "Audio Source",
80
+ fieldName: "audio_source",
81
+ artifactType: "audio",
82
+ required: true,
83
+ description: "Audio file to process",
84
+ isArray: false,
85
+ });
86
+ });
87
+
88
+ it("should parse array artifact slot", () => {
89
+ const property: JSONSchema7 = {
90
+ type: "array",
91
+ items: {
92
+ $ref: "#/$defs/ImageArtifact",
93
+ },
94
+ title: "Reference Images",
95
+ description: "Images for style transfer",
96
+ minItems: 1,
97
+ maxItems: 5,
98
+ };
99
+
100
+ const slot = parseArtifactSlot("reference_images", property, false);
101
+
102
+ expect(slot).toEqual({
103
+ name: "Reference Images",
104
+ fieldName: "reference_images",
105
+ artifactType: "image",
106
+ required: false,
107
+ description: "Images for style transfer",
108
+ isArray: true,
109
+ minItems: 1,
110
+ maxItems: 5,
111
+ });
112
+ });
113
+
114
+ it("should use fieldName as name if title is missing", () => {
115
+ const property: JSONSchema7 = {
116
+ $ref: "#/$defs/VideoArtifact",
117
+ };
118
+
119
+ const slot = parseArtifactSlot("video_input", property, false);
120
+
121
+ expect(slot.name).toBe("video_input");
122
+ expect(slot.fieldName).toBe("video_input");
123
+ });
124
+ });
125
+
126
+ describe("parseSettingsField", () => {
127
+ it("should parse slider field (float)", () => {
128
+ const property: JSONSchema7 = {
129
+ type: "number",
130
+ title: "Strength",
131
+ description: "Effect strength",
132
+ minimum: 0.0,
133
+ maximum: 1.0,
134
+ default: 0.75,
135
+ };
136
+
137
+ const field = parseSettingsField("strength", property);
138
+
139
+ expect(field).toEqual({
140
+ type: "slider",
141
+ fieldName: "strength",
142
+ title: "Strength",
143
+ description: "Effect strength",
144
+ min: 0.0,
145
+ max: 1.0,
146
+ default: 0.75,
147
+ isInteger: false,
148
+ });
149
+ });
150
+
151
+ it("should parse slider field (integer)", () => {
152
+ const property: JSONSchema7 = {
153
+ type: "integer",
154
+ title: "Steps",
155
+ description: "Number of steps",
156
+ minimum: 1,
157
+ maximum: 100,
158
+ default: 50,
159
+ multipleOf: 5,
160
+ };
161
+
162
+ const field = parseSettingsField("steps", property);
163
+
164
+ expect(field).toEqual({
165
+ type: "slider",
166
+ fieldName: "steps",
167
+ title: "Steps",
168
+ description: "Number of steps",
169
+ min: 1,
170
+ max: 100,
171
+ step: 5,
172
+ default: 50,
173
+ isInteger: true,
174
+ });
175
+ });
176
+
177
+ it("should parse dropdown field", () => {
178
+ const property: JSONSchema7 = {
179
+ type: "string",
180
+ title: "Style",
181
+ description: "Art style",
182
+ enum: ["realistic", "anime", "abstract"],
183
+ default: "realistic",
184
+ };
185
+
186
+ const field = parseSettingsField("style", property);
187
+
188
+ expect(field).toEqual({
189
+ type: "dropdown",
190
+ fieldName: "style",
191
+ title: "Style",
192
+ description: "Art style",
193
+ options: ["realistic", "anime", "abstract"],
194
+ default: "realistic",
195
+ });
196
+ });
197
+
198
+ it("should parse text input field", () => {
199
+ const property: JSONSchema7 = {
200
+ type: "string",
201
+ title: "Negative Prompt",
202
+ description: "What to avoid",
203
+ default: "",
204
+ pattern: "^[a-zA-Z0-9\\s,]+$",
205
+ };
206
+
207
+ const field = parseSettingsField("negative_prompt", property);
208
+
209
+ expect(field).toEqual({
210
+ type: "text",
211
+ fieldName: "negative_prompt",
212
+ title: "Negative Prompt",
213
+ description: "What to avoid",
214
+ default: "",
215
+ pattern: "^[a-zA-Z0-9\\s,]+$",
216
+ });
217
+ });
218
+
219
+ it("should parse number input field", () => {
220
+ const property: JSONSchema7 = {
221
+ type: "integer",
222
+ title: "Seed",
223
+ description: "Random seed",
224
+ default: -1,
225
+ };
226
+
227
+ const field = parseSettingsField("seed", property);
228
+
229
+ expect(field).toEqual({
230
+ type: "number",
231
+ fieldName: "seed",
232
+ title: "Seed",
233
+ description: "Random seed",
234
+ default: -1,
235
+ isInteger: true,
236
+ });
237
+ });
238
+
239
+ it("should use fieldName as title if missing", () => {
240
+ const property: JSONSchema7 = {
241
+ type: "string",
242
+ enum: ["option1", "option2"],
243
+ };
244
+
245
+ const field = parseSettingsField("my_field", property);
246
+
247
+ expect(field?.title).toBe("my_field");
248
+ });
249
+
250
+ it("should return null for unsupported types", () => {
251
+ const property: JSONSchema7 = {
252
+ type: "object",
253
+ };
254
+
255
+ const field = parseSettingsField("complex", property);
256
+
257
+ expect(field).toBeNull();
258
+ });
259
+ });
260
+
261
+ describe("parseGeneratorSchema", () => {
262
+ it("should parse complete lipsync schema", () => {
263
+ const schema: JSONSchema7 = {
264
+ $defs: {
265
+ AudioArtifact: {
266
+ type: "object",
267
+ properties: {
268
+ generation_id: { type: "string" },
269
+ storage_url: { type: "string" },
270
+ format: { type: "string" },
271
+ },
272
+ required: ["generation_id", "storage_url", "format"],
273
+ },
274
+ VideoArtifact: {
275
+ type: "object",
276
+ properties: {
277
+ generation_id: { type: "string" },
278
+ storage_url: { type: "string" },
279
+ format: { type: "string" },
280
+ },
281
+ required: ["generation_id", "storage_url", "format"],
282
+ },
283
+ },
284
+ type: "object",
285
+ properties: {
286
+ audio_source: {
287
+ $ref: "#/$defs/AudioArtifact",
288
+ description: "Audio track for lip sync",
289
+ },
290
+ video_source: {
291
+ $ref: "#/$defs/VideoArtifact",
292
+ description: "Video to sync lips in",
293
+ },
294
+ prompt: {
295
+ type: "string",
296
+ description: "Optional prompt for generation",
297
+ default: "",
298
+ },
299
+ },
300
+ required: ["audio_source", "video_source"],
301
+ };
302
+
303
+ const parsed = parseGeneratorSchema(schema);
304
+
305
+ expect(parsed.artifactSlots).toHaveLength(2);
306
+ expect(parsed.artifactSlots[0]).toMatchObject({
307
+ fieldName: "audio_source",
308
+ artifactType: "audio",
309
+ required: true,
310
+ isArray: false,
311
+ });
312
+ expect(parsed.artifactSlots[1]).toMatchObject({
313
+ fieldName: "video_source",
314
+ artifactType: "video",
315
+ required: true,
316
+ isArray: false,
317
+ });
318
+
319
+ expect(parsed.promptField).toMatchObject({
320
+ fieldName: "prompt",
321
+ description: "Optional prompt for generation",
322
+ required: false,
323
+ default: "",
324
+ });
325
+
326
+ expect(parsed.settingsFields).toHaveLength(0);
327
+ });
328
+
329
+ it("should parse FLUX Pro schema with settings", () => {
330
+ const schema: JSONSchema7 = {
331
+ type: "object",
332
+ description: "Input schema for FLUX.1.1 Pro image generation.",
333
+ properties: {
334
+ prompt: {
335
+ type: "string",
336
+ title: "Prompt",
337
+ description: "Text prompt for image generation",
338
+ },
339
+ aspect_ratio: {
340
+ type: "string",
341
+ title: "Aspect Ratio",
342
+ description: "Image aspect ratio",
343
+ default: "1:1",
344
+ enum: ["1:1", "16:9", "21:9", "2:3", "3:2", "4:5", "5:4", "9:16", "9:21"],
345
+ },
346
+ safety_tolerance: {
347
+ type: "integer",
348
+ title: "Safety Tolerance",
349
+ description: "Safety tolerance level (1-5)",
350
+ default: 2,
351
+ minimum: 1,
352
+ maximum: 5,
353
+ },
354
+ },
355
+ required: ["prompt"],
356
+ };
357
+
358
+ const parsed = parseGeneratorSchema(schema);
359
+
360
+ expect(parsed.artifactSlots).toHaveLength(0);
361
+
362
+ expect(parsed.promptField).toMatchObject({
363
+ fieldName: "prompt",
364
+ description: "Text prompt for image generation",
365
+ required: true,
366
+ });
367
+
368
+ expect(parsed.settingsFields).toHaveLength(2);
369
+ expect(parsed.settingsFields[0]).toMatchObject({
370
+ type: "dropdown",
371
+ fieldName: "aspect_ratio",
372
+ title: "Aspect Ratio",
373
+ options: ["1:1", "16:9", "21:9", "2:3", "3:2", "4:5", "5:4", "9:16", "9:21"],
374
+ default: "1:1",
375
+ });
376
+ expect(parsed.settingsFields[1]).toMatchObject({
377
+ type: "slider",
378
+ fieldName: "safety_tolerance",
379
+ title: "Safety Tolerance",
380
+ min: 1,
381
+ max: 5,
382
+ default: 2,
383
+ isInteger: true,
384
+ });
385
+ });
386
+
387
+ it("should parse schema with array artifacts", () => {
388
+ const schema: JSONSchema7 = {
389
+ $defs: {
390
+ ImageArtifact: {
391
+ type: "object",
392
+ properties: {
393
+ generation_id: { type: "string" },
394
+ storage_url: { type: "string" },
395
+ },
396
+ },
397
+ },
398
+ type: "object",
399
+ properties: {
400
+ reference_images: {
401
+ type: "array",
402
+ items: {
403
+ $ref: "#/$defs/ImageArtifact",
404
+ },
405
+ title: "Reference Images",
406
+ description: "Style reference images",
407
+ minItems: 1,
408
+ maxItems: 3,
409
+ },
410
+ prompt: {
411
+ type: "string",
412
+ description: "Generation prompt",
413
+ },
414
+ strength: {
415
+ type: "number",
416
+ title: "Strength",
417
+ minimum: 0.0,
418
+ maximum: 1.0,
419
+ default: 0.8,
420
+ },
421
+ },
422
+ required: ["reference_images", "prompt"],
423
+ };
424
+
425
+ const parsed = parseGeneratorSchema(schema);
426
+
427
+ expect(parsed.artifactSlots).toHaveLength(1);
428
+ expect(parsed.artifactSlots[0]).toMatchObject({
429
+ fieldName: "reference_images",
430
+ artifactType: "image",
431
+ required: true,
432
+ isArray: true,
433
+ minItems: 1,
434
+ maxItems: 3,
435
+ });
436
+
437
+ expect(parsed.promptField).toMatchObject({
438
+ fieldName: "prompt",
439
+ required: true,
440
+ });
441
+
442
+ expect(parsed.settingsFields).toHaveLength(1);
443
+ expect(parsed.settingsFields[0]).toMatchObject({
444
+ type: "slider",
445
+ fieldName: "strength",
446
+ min: 0.0,
447
+ max: 1.0,
448
+ });
449
+ });
450
+
451
+ it("should handle schema with no properties", () => {
452
+ const schema: JSONSchema7 = {
453
+ type: "object",
454
+ };
455
+
456
+ const parsed = parseGeneratorSchema(schema);
457
+
458
+ expect(parsed.artifactSlots).toHaveLength(0);
459
+ expect(parsed.promptField).toBeNull();
460
+ expect(parsed.settingsFields).toHaveLength(0);
461
+ });
462
+
463
+ it("should handle mixed field types", () => {
464
+ const schema: JSONSchema7 = {
465
+ $defs: {
466
+ AudioArtifact: {
467
+ type: "object",
468
+ properties: {
469
+ generation_id: { type: "string" },
470
+ },
471
+ },
472
+ },
473
+ type: "object",
474
+ properties: {
475
+ audio_input: {
476
+ $ref: "#/$defs/AudioArtifact",
477
+ description: "Audio to process",
478
+ },
479
+ prompt: {
480
+ type: "string",
481
+ description: "Processing instructions",
482
+ },
483
+ language: {
484
+ type: "string",
485
+ title: "Language",
486
+ default: "en",
487
+ },
488
+ temperature: {
489
+ type: "number",
490
+ title: "Temperature",
491
+ minimum: 0.0,
492
+ maximum: 2.0,
493
+ default: 1.0,
494
+ },
495
+ format: {
496
+ type: "string",
497
+ title: "Output Format",
498
+ enum: ["json", "text", "srt"],
499
+ default: "text",
500
+ },
501
+ seed: {
502
+ type: "integer",
503
+ title: "Seed",
504
+ default: -1,
505
+ },
506
+ },
507
+ required: ["audio_input", "prompt"],
508
+ };
509
+
510
+ const parsed = parseGeneratorSchema(schema);
511
+
512
+ // Should have 1 artifact slot
513
+ expect(parsed.artifactSlots).toHaveLength(1);
514
+ expect(parsed.artifactSlots[0].fieldName).toBe("audio_input");
515
+
516
+ // Should have prompt field
517
+ expect(parsed.promptField).not.toBeNull();
518
+ expect(parsed.promptField?.fieldName).toBe("prompt");
519
+
520
+ // Should have 4 settings fields
521
+ expect(parsed.settingsFields).toHaveLength(4);
522
+ const fieldsByName = Object.fromEntries(
523
+ parsed.settingsFields.map((f) => [f.fieldName, f])
524
+ );
525
+
526
+ expect(fieldsByName.language).toMatchObject({
527
+ type: "text",
528
+ fieldName: "language",
529
+ });
530
+ expect(fieldsByName.temperature).toMatchObject({
531
+ type: "slider",
532
+ fieldName: "temperature",
533
+ });
534
+ expect(fieldsByName.format).toMatchObject({
535
+ type: "dropdown",
536
+ fieldName: "format",
537
+ });
538
+ expect(fieldsByName.seed).toMatchObject({
539
+ type: "number",
540
+ fieldName: "seed",
541
+ });
542
+ });
543
+
544
+ it("should handle optional vs required fields", () => {
545
+ const schema: JSONSchema7 = {
546
+ $defs: {
547
+ ImageArtifact: {
548
+ type: "object",
549
+ properties: {
550
+ generation_id: { type: "string" },
551
+ },
552
+ },
553
+ },
554
+ type: "object",
555
+ properties: {
556
+ required_image: {
557
+ $ref: "#/$defs/ImageArtifact",
558
+ },
559
+ optional_image: {
560
+ $ref: "#/$defs/ImageArtifact",
561
+ },
562
+ prompt: {
563
+ type: "string",
564
+ },
565
+ },
566
+ required: ["required_image", "prompt"],
567
+ };
568
+
569
+ const parsed = parseGeneratorSchema(schema);
570
+
571
+ expect(parsed.artifactSlots[0].required).toBe(true);
572
+ expect(parsed.artifactSlots[1].required).toBe(false);
573
+ expect(parsed.promptField?.required).toBe(true);
574
+ });
575
+ });
576
+ });