vargai 0.4.0-alpha70 → 0.4.0-alpha71

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.
package/package.json CHANGED
@@ -71,7 +71,7 @@
71
71
  "zod": "^4.2.1"
72
72
  },
73
73
  "sideEffects": false,
74
- "version": "0.4.0-alpha70",
74
+ "version": "0.4.0-alpha71",
75
75
  "exports": {
76
76
  ".": "./src/index.ts",
77
77
  "./ai": "./src/ai-sdk/index.ts",
@@ -583,14 +583,7 @@ class FalVideoModel implements VideoModelV3 {
583
583
  }
584
584
  } else if (isKlingV3 || isKlingV26) {
585
585
  // Duration must be string for Kling v2.6+ and O3 (v3)
586
- // Kling v3 (O3) supports 3-15s; v2.6 supports 5 or 10
587
- const raw = duration ?? 5;
588
- const clamped = isKlingV3
589
- ? Math.max(3, Math.min(15, raw))
590
- : raw <= 5
591
- ? 5
592
- : 10;
593
- input.duration = String(clamped);
586
+ input.duration = String(duration ?? 5);
594
587
  } else if (isGrokImagine) {
595
588
  // Grok Imagine: duration 1-15 seconds (default 6)
596
589
  input.duration = duration ?? 6;
@@ -33,6 +33,20 @@ async function detectDefaultModels(): Promise<DefaultModels | undefined> {
33
33
  return Object.keys(defaults).length > 0 ? defaults : undefined;
34
34
  }
35
35
 
36
+ /**
37
+ * Resolve the default export of a module.
38
+ * Handles async function exports: if mod.default is a function, call it and await.
39
+ */
40
+ async function resolveDefaultExport(mod: {
41
+ default: unknown;
42
+ }): Promise<VargElement> {
43
+ let component = mod.default;
44
+ if (typeof component === "function") {
45
+ component = await (component as () => unknown)();
46
+ }
47
+ return component as VargElement;
48
+ }
49
+
36
50
  async function loadComponent(filePath: string): Promise<VargElement> {
37
51
  const resolvedPath = resolve(filePath);
38
52
  const source = await Bun.file(resolvedPath).text();
@@ -54,7 +68,7 @@ async function loadComponent(filePath: string): Promise<VargElement> {
54
68
 
55
69
  if (hasRelativeImport) {
56
70
  const mod = await import(resolvedPath);
57
- return mod.default;
71
+ return resolveDefaultExport(mod);
58
72
  }
59
73
 
60
74
  if (hasVargaiImport) {
@@ -63,7 +77,7 @@ async function loadComponent(filePath: string): Promise<VargElement> {
63
77
 
64
78
  try {
65
79
  const mod = await import(tmpFile);
66
- return mod.default;
80
+ return resolveDefaultExport(mod);
67
81
  } finally {
68
82
  (await Bun.file(tmpFile).exists()) && (await Bun.write(tmpFile, ""));
69
83
  }
@@ -72,7 +86,7 @@ async function loadComponent(filePath: string): Promise<VargElement> {
72
86
  const hasAnyImport = source.includes(" from ");
73
87
  if (hasAnyImport) {
74
88
  const mod = await import(resolvedPath);
75
- return mod.default;
89
+ return resolveDefaultExport(mod);
76
90
  }
77
91
 
78
92
  const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
@@ -80,7 +94,7 @@ async function loadComponent(filePath: string): Promise<VargElement> {
80
94
 
81
95
  try {
82
96
  const mod = await import(tmpFile);
83
- return mod.default;
97
+ return resolveDefaultExport(mod);
84
98
  } finally {
85
99
  (await Bun.file(tmpFile).exists()) && (await Bun.write(tmpFile, ""));
86
100
  }
@@ -0,0 +1,618 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import { File } from "../ai-sdk/file";
3
+ import {
4
+ Captions,
5
+ Clip,
6
+ Image,
7
+ Music,
8
+ Render,
9
+ Speech,
10
+ Video,
11
+ } from "./elements";
12
+ import { resolveLazy } from "./renderers/resolve-lazy";
13
+ import { ResolvedElement } from "./resolved-element";
14
+ import type { VargElement, VargNode } from "./types";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helper: create a ResolvedElement manually (bypasses AI generation)
18
+ // ---------------------------------------------------------------------------
19
+ function makeResolved<T extends VargElement["type"]>(
20
+ element: VargElement<T>,
21
+ duration: number,
22
+ ): ResolvedElement<T> {
23
+ const file = File.fromGenerated({
24
+ uint8Array: new Uint8Array([0, 1, 2, 3]),
25
+ mediaType: "audio/mpeg",
26
+ });
27
+ return new ResolvedElement(element, { file, duration });
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Phase 1: ResolvedElement class
32
+ // ---------------------------------------------------------------------------
33
+ describe("ResolvedElement", () => {
34
+ test("wraps a VargElement with meta (file + duration)", () => {
35
+ const speech = Speech({ voice: "adam", children: "hello" });
36
+ const resolved = makeResolved(speech, 3.8);
37
+
38
+ expect(resolved.type).toBe("speech");
39
+ expect(resolved.props.voice).toBe("adam");
40
+ expect(resolved.children).toEqual(["hello"]);
41
+ expect(resolved.duration).toBe(3.8);
42
+ expect(resolved.meta.duration).toBe(3.8);
43
+ expect(resolved.meta.file).toBeInstanceOf(File);
44
+ });
45
+
46
+ test("instanceof check works", () => {
47
+ const speech = Speech({ voice: "adam", children: "hello" });
48
+ const resolved = makeResolved(speech, 2.5);
49
+
50
+ expect(resolved instanceof ResolvedElement).toBe(true);
51
+ // A plain VargElement is NOT a ResolvedElement
52
+ expect(speech instanceof ResolvedElement).toBe(false);
53
+ });
54
+
55
+ test("satisfies VargElement shape (can be used as child)", () => {
56
+ const speech = Speech({ voice: "adam", children: "hello" });
57
+ const resolved = makeResolved(speech, 4.0);
58
+
59
+ // Can be used as a Clip child
60
+ const clip = Clip({
61
+ duration: resolved.duration,
62
+ children: [resolved as unknown as VargNode],
63
+ });
64
+
65
+ expect(clip.type).toBe("clip");
66
+ expect(clip.props.duration).toBe(4.0);
67
+ expect(clip.children.length).toBe(1);
68
+ });
69
+
70
+ test("duration getter reads from meta", () => {
71
+ const music = Music({ prompt: "chill beats" });
72
+ const resolved = makeResolved(music, 30.5);
73
+
74
+ expect(resolved.duration).toBe(30.5);
75
+ expect(resolved.meta.duration).toBe(30.5);
76
+ });
77
+
78
+ test("file getter reads from meta", () => {
79
+ const image = Image({ prompt: "sunset" });
80
+ const resolved = makeResolved(image, 0);
81
+
82
+ expect(resolved.file).toBeInstanceOf(File);
83
+ expect(resolved.duration).toBe(0); // images have 0 duration
84
+ });
85
+
86
+ test("aspectRatio on meta", () => {
87
+ const image = Image({ prompt: "sunset", aspectRatio: "9:16" });
88
+ const file = File.fromGenerated({
89
+ uint8Array: new Uint8Array([0]),
90
+ mediaType: "image/png",
91
+ });
92
+ const resolved = new ResolvedElement(image, {
93
+ file,
94
+ duration: 0,
95
+ aspectRatio: "9:16",
96
+ });
97
+
98
+ expect(resolved.aspectRatio).toBe("9:16");
99
+ });
100
+ });
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Phase 1: Element factories are thenable
104
+ // ---------------------------------------------------------------------------
105
+ describe("element factories are thenable", () => {
106
+ test("Speech() returns a VargElement with a .then method", () => {
107
+ const speech = Speech({ voice: "adam", children: "hello" });
108
+
109
+ expect(speech.type).toBe("speech");
110
+ expect(typeof (speech as any).then).toBe("function");
111
+ });
112
+
113
+ test("Video() returns a VargElement with a .then method", () => {
114
+ const video = Video({ prompt: "sunset" });
115
+
116
+ expect(video.type).toBe("video");
117
+ expect(typeof (video as any).then).toBe("function");
118
+ });
119
+
120
+ test("Image() returns a VargElement with a .then method", () => {
121
+ const image = Image({ prompt: "cat" });
122
+
123
+ expect(image.type).toBe("image");
124
+ expect(typeof (image as any).then).toBe("function");
125
+ });
126
+
127
+ test("Music() returns a VargElement with a .then method", () => {
128
+ const music = Music({ prompt: "chill" });
129
+
130
+ expect(music.type).toBe("music");
131
+ expect(typeof (music as any).then).toBe("function");
132
+ });
133
+
134
+ test("thenable elements still have .type (not treated as pure Promises)", () => {
135
+ // This is critical: the JSX runtime uses .type to distinguish thenable
136
+ // VargElements from async component Promises
137
+ const speech = Speech({ voice: "adam", children: "test" });
138
+ expect(speech.type).toBe("speech");
139
+ expect("type" in speech).toBe(true);
140
+ });
141
+
142
+ test("Render, Clip, Captions are NOT thenable (no .then)", () => {
143
+ const render = Render({ width: 1080, height: 1920 });
144
+ const clip = Clip({ duration: 5 });
145
+ const captions = Captions({ style: "tiktok" });
146
+
147
+ expect((render as any).then).toBeUndefined();
148
+ expect((clip as any).then).toBeUndefined();
149
+ expect((captions as any).then).toBeUndefined();
150
+ });
151
+ });
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Phase 1: ResolvedElement in composition tree
155
+ // ---------------------------------------------------------------------------
156
+ describe("ResolvedElement in composition tree", () => {
157
+ test("audio.duration drives Clip duration", () => {
158
+ const speech = Speech({ voice: "adam", children: "hello world" });
159
+ const audio = makeResolved(speech, 3.8);
160
+
161
+ const tree = Render({
162
+ width: 1080,
163
+ height: 1920,
164
+ children: [
165
+ Clip({
166
+ duration: audio.duration,
167
+ children: [audio as unknown as VargNode],
168
+ }),
169
+ ],
170
+ });
171
+
172
+ expect(tree.type).toBe("render");
173
+ const clip = tree.children[0] as VargElement<"clip">;
174
+ expect(clip.type).toBe("clip");
175
+ expect(clip.props.duration).toBe(3.8);
176
+ });
177
+
178
+ test("audio.duration in template literal", () => {
179
+ const speech = Speech({ voice: "adam", children: "hello" });
180
+ const audio = makeResolved(speech, 3.8);
181
+
182
+ const prompt = `This video is exactly ${audio.duration} seconds long.`;
183
+ expect(prompt).toBe("This video is exactly 3.8 seconds long.");
184
+ });
185
+
186
+ test("resolved speech as Captions src", () => {
187
+ const speech = Speech({ voice: "adam", children: "hello" });
188
+ const audio = makeResolved(speech, 3.8);
189
+
190
+ const captions = Captions({
191
+ src: audio as unknown as VargElement<"speech">,
192
+ style: "tiktok",
193
+ });
194
+
195
+ expect(captions.type).toBe("captions");
196
+ expect(captions.props.src).toBe(audio);
197
+ });
198
+
199
+ test("two resolved audios in same tree", () => {
200
+ const s1 = makeResolved(
201
+ Speech({ voice: "rachel", children: "part one" }),
202
+ 2.5,
203
+ );
204
+ const s2 = makeResolved(
205
+ Speech({ voice: "rachel", children: "part two" }),
206
+ 5.3,
207
+ );
208
+
209
+ const tree = Render({
210
+ width: 1080,
211
+ height: 1920,
212
+ children: [
213
+ Clip({
214
+ duration: s1.duration,
215
+ children: [s1 as unknown as VargNode],
216
+ }),
217
+ Clip({
218
+ duration: s2.duration,
219
+ children: [s2 as unknown as VargNode],
220
+ }),
221
+ ],
222
+ });
223
+
224
+ const clips = tree.children as VargElement[];
225
+ expect((clips[0] as VargElement<"clip">).props.duration).toBe(2.5);
226
+ expect((clips[1] as VargElement<"clip">).props.duration).toBe(5.3);
227
+ });
228
+
229
+ test("dynamic clip count from audio.duration", () => {
230
+ const speech = Speech({ voice: "adam", children: "long narration..." });
231
+ const audio = makeResolved(speech, 12.4);
232
+
233
+ const clipDuration = 2;
234
+ const numClips = Math.ceil(audio.duration / clipDuration);
235
+
236
+ expect(numClips).toBe(7); // ceil(12.4 / 2) = 7
237
+
238
+ const clips = Array.from({ length: numClips }, (_, i) =>
239
+ Clip({
240
+ duration: Math.min(clipDuration, audio.duration - i * clipDuration),
241
+ key: i,
242
+ }),
243
+ );
244
+
245
+ expect(clips.length).toBe(7);
246
+ expect(clips[0]!.props.duration).toBe(2);
247
+ expect(clips[6]!.props.duration).toBeCloseTo(0.4); // 12.4 - 6*2 = 0.4
248
+ });
249
+ });
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Phase 2: JSX runtime lazy wrapping
253
+ // ---------------------------------------------------------------------------
254
+ describe("JSX runtime lazy wrapping", () => {
255
+ // Import the jsx function directly
256
+ const { jsx } = require("./runtime/jsx-runtime");
257
+
258
+ test("sync component returns VargElement directly", () => {
259
+ function SyncScene(props: Record<string, unknown>) {
260
+ return Clip({ duration: 5 });
261
+ }
262
+
263
+ const result = jsx(SyncScene, {});
264
+ expect(result.type).toBe("clip");
265
+ });
266
+
267
+ test("async component is wrapped as __lazy element", () => {
268
+ async function AsyncScene(props: Record<string, unknown>) {
269
+ return Clip({ duration: 5 });
270
+ }
271
+
272
+ const result = jsx(AsyncScene, {});
273
+ expect(result.type).toBe("__lazy");
274
+ expect(result.props._promise).toBeInstanceOf(Promise);
275
+ });
276
+
277
+ test("thenable VargElement (Speech) is NOT wrapped as __lazy", () => {
278
+ // Speech() returns an object with both .type and .then
279
+ // The JSX runtime should recognize this as a VargElement, not a Promise
280
+ const result = jsx(Speech, { voice: "adam", children: "test" });
281
+ expect(result.type).toBe("speech");
282
+ expect(result.type).not.toBe("__lazy");
283
+ });
284
+ });
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Phase 2: resolveLazy
288
+ // ---------------------------------------------------------------------------
289
+ describe("resolveLazy", () => {
290
+ test("passes through plain VargElements unchanged", async () => {
291
+ const element = Render({
292
+ width: 1080,
293
+ height: 1920,
294
+ children: [Clip({ duration: 5 })],
295
+ });
296
+
297
+ const resolved = (await resolveLazy(element)) as VargElement;
298
+ expect(resolved.type).toBe("render");
299
+ expect((resolved.children[0] as VargElement).type).toBe("clip");
300
+ });
301
+
302
+ test("passes through primitives and nulls", async () => {
303
+ expect(await resolveLazy(null)).toBe(null);
304
+ expect(await resolveLazy(undefined)).toBe(undefined);
305
+ expect(await resolveLazy("hello")).toBe("hello");
306
+ expect(await resolveLazy(42)).toBe(42);
307
+ });
308
+
309
+ test("resolves a __lazy element", async () => {
310
+ const lazyElement: VargElement<"__lazy"> = {
311
+ type: "__lazy",
312
+ props: {
313
+ _promise: Promise.resolve(Clip({ duration: 10 })),
314
+ },
315
+ children: [],
316
+ };
317
+
318
+ const resolved = (await resolveLazy(lazyElement)) as VargElement;
319
+ expect(resolved.type).toBe("clip");
320
+ expect(resolved.props.duration).toBe(10);
321
+ });
322
+
323
+ test("resolves __lazy that returns an array (Fragment)", async () => {
324
+ const lazyElement: VargElement<"__lazy"> = {
325
+ type: "__lazy",
326
+ props: {
327
+ _promise: Promise.resolve([
328
+ Clip({ duration: 3 }),
329
+ Clip({ duration: 5 }),
330
+ ]),
331
+ },
332
+ children: [],
333
+ };
334
+
335
+ const resolved = await resolveLazy(lazyElement);
336
+ expect(Array.isArray(resolved)).toBe(true);
337
+ const arr = resolved as VargNode[];
338
+ expect(arr.length).toBe(2);
339
+ expect((arr[0] as VargElement).type).toBe("clip");
340
+ expect((arr[1] as VargElement).type).toBe("clip");
341
+ });
342
+
343
+ test("resolves nested __lazy elements", async () => {
344
+ const innerLazy: VargElement<"__lazy"> = {
345
+ type: "__lazy",
346
+ props: { _promise: Promise.resolve(Clip({ duration: 7 })) },
347
+ children: [],
348
+ };
349
+
350
+ const outerLazy: VargElement<"__lazy"> = {
351
+ type: "__lazy",
352
+ props: { _promise: Promise.resolve(innerLazy) },
353
+ children: [],
354
+ };
355
+
356
+ const resolved = (await resolveLazy(outerLazy)) as VargElement;
357
+ expect(resolved.type).toBe("clip");
358
+ expect(resolved.props.duration).toBe(7);
359
+ });
360
+
361
+ test("resolves __lazy inside Render children", async () => {
362
+ const tree = Render({
363
+ width: 1080,
364
+ height: 1920,
365
+ children: [
366
+ Clip({ duration: 3 }),
367
+ {
368
+ type: "__lazy" as const,
369
+ props: {
370
+ _promise: Promise.resolve([
371
+ Clip({ duration: 5 }),
372
+ Clip({ duration: 4 }),
373
+ ]),
374
+ },
375
+ children: [],
376
+ } as VargElement<"__lazy">,
377
+ Clip({ duration: 2 }),
378
+ ],
379
+ });
380
+
381
+ const resolved = (await resolveLazy(tree)) as VargElement<"render">;
382
+ expect(resolved.type).toBe("render");
383
+
384
+ // Should have 4 children: the original Clip(3), the two from the lazy, and Clip(2)
385
+ const children = resolved.children as VargElement[];
386
+ expect(children.length).toBe(4);
387
+ expect(children[0]!.props.duration).toBe(3);
388
+ expect(children[1]!.props.duration).toBe(5);
389
+ expect(children[2]!.props.duration).toBe(4);
390
+ expect(children[3]!.props.duration).toBe(2);
391
+ });
392
+
393
+ test("resolves async Scene-like component pattern", async () => {
394
+ // Simulates the strawberry-vs-chocolate example
395
+ async function Scene(props: { text: string; clipCount: number }) {
396
+ // In real code this would be: const audio = await Speech({...})
397
+ // Here we simulate it with a resolved element
398
+ const clips = Array.from({ length: props.clipCount }, (_, i) =>
399
+ Clip({ duration: 2, key: i }),
400
+ );
401
+ return clips; // Fragment-like return (array)
402
+ }
403
+
404
+ const { jsx } = require("./runtime/jsx-runtime");
405
+
406
+ const tree = Render({
407
+ width: 1080,
408
+ height: 1920,
409
+ children: [
410
+ jsx(Scene, { text: "strawberry", clipCount: 3 }),
411
+ jsx(Scene, { text: "chocolate", clipCount: 2 }),
412
+ ],
413
+ });
414
+
415
+ // Before resolution: children contain __lazy elements
416
+ expect((tree.children[0] as VargElement).type).toBe("__lazy");
417
+ expect((tree.children[1] as VargElement).type).toBe("__lazy");
418
+
419
+ // After resolution: lazy elements are replaced with clip arrays
420
+ const resolved = (await resolveLazy(tree)) as VargElement<"render">;
421
+ const children = resolved.children as VargElement[];
422
+
423
+ // 3 clips from strawberry + 2 from chocolate = 5 total
424
+ expect(children.length).toBe(5);
425
+ for (const child of children) {
426
+ expect(child.type).toBe("clip");
427
+ expect(child.props.duration).toBe(2);
428
+ }
429
+ });
430
+ });
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // Nested clips (container clip pattern)
434
+ // ---------------------------------------------------------------------------
435
+ describe("nested clips (container clip pattern)", () => {
436
+ test("container clip with child clips produces correct structure", () => {
437
+ // Pattern 1: shared audio across sub-clips
438
+ const audio = makeResolved(
439
+ Speech({ voice: "adam", children: "hello world" }),
440
+ 4.0,
441
+ );
442
+
443
+ const tree = Render({
444
+ width: 1080,
445
+ height: 1920,
446
+ children: [
447
+ Clip({
448
+ duration: audio.duration,
449
+ children: [
450
+ Clip({
451
+ duration: 2,
452
+ children: [Image({ prompt: "black dog" })],
453
+ }),
454
+ Clip({
455
+ duration: 2,
456
+ children: [Image({ prompt: "orange cat" })],
457
+ }),
458
+ Captions({
459
+ src: audio as unknown as VargElement<"speech">,
460
+ style: "tiktok",
461
+ }),
462
+ ],
463
+ }),
464
+ ],
465
+ });
466
+
467
+ expect(tree.type).toBe("render");
468
+ // The outer Clip is a container — it has child clips
469
+ const containerClip = tree.children[0] as VargElement<"clip">;
470
+ expect(containerClip.type).toBe("clip");
471
+ expect(containerClip.props.duration).toBe(4.0);
472
+
473
+ // Container has 3 children: 2 inner clips + captions
474
+ expect(containerClip.children.length).toBe(3);
475
+ expect((containerClip.children[0] as VargElement).type).toBe("clip");
476
+ expect((containerClip.children[1] as VargElement).type).toBe("clip");
477
+ expect((containerClip.children[2] as VargElement).type).toBe("captions");
478
+ });
479
+
480
+ test("pattern 2: per-clip audio with auto-duration parent", () => {
481
+ const s1 = makeResolved(
482
+ Speech({ voice: "adam", children: "this is a black dog" }),
483
+ 2.5,
484
+ );
485
+ const s2 = makeResolved(
486
+ Speech({ voice: "adam", children: "this is an orange cat" }),
487
+ 3.0,
488
+ );
489
+
490
+ const tree = Render({
491
+ width: 1080,
492
+ height: 1920,
493
+ children: [
494
+ Clip({
495
+ children: [
496
+ Clip({
497
+ duration: s1.duration,
498
+ children: [
499
+ Image({ prompt: "black dog" }),
500
+ s1 as unknown as VargNode,
501
+ ],
502
+ }),
503
+ Clip({
504
+ duration: s2.duration,
505
+ children: [
506
+ Image({ prompt: "orange cat" }),
507
+ s2 as unknown as VargNode,
508
+ ],
509
+ }),
510
+ ],
511
+ }),
512
+ ],
513
+ });
514
+
515
+ // Outer clip has no explicit duration — children define it
516
+ const containerClip = tree.children[0] as VargElement<"clip">;
517
+ expect(containerClip.type).toBe("clip");
518
+ expect(containerClip.props.duration).toBeUndefined();
519
+
520
+ // Inner clips have durations matching their speech
521
+ const inner0 = containerClip.children[0] as VargElement<"clip">;
522
+ const inner1 = containerClip.children[1] as VargElement<"clip">;
523
+ expect(inner0.props.duration).toBe(2.5);
524
+ expect(inner1.props.duration).toBe(3.0);
525
+ });
526
+
527
+ test("3-level nesting produces correct flat structure", () => {
528
+ // Scene > Acts > Clips
529
+ const tree = Render({
530
+ width: 1080,
531
+ height: 1920,
532
+ children: [
533
+ Clip({
534
+ children: [
535
+ Clip({
536
+ children: [
537
+ Clip({ duration: 3, children: [Image({ prompt: "a" })] }),
538
+ Clip({ duration: 3, children: [Image({ prompt: "b" })] }),
539
+ ],
540
+ }),
541
+ Clip({
542
+ duration: 4,
543
+ children: [Image({ prompt: "c" })],
544
+ }),
545
+ ],
546
+ }),
547
+ ],
548
+ });
549
+
550
+ // Verify the structure exists — the flattening happens inside renderRoot,
551
+ // which we can't easily unit test without mocking the whole render pipeline.
552
+ // But we can verify the element tree is valid.
553
+ const scene = tree.children[0] as VargElement<"clip">;
554
+ expect(scene.type).toBe("clip");
555
+ const act1 = scene.children[0] as VargElement<"clip">;
556
+ const act2 = scene.children[1] as VargElement<"clip">;
557
+ expect(act1.type).toBe("clip");
558
+ expect(act2.type).toBe("clip");
559
+ expect(act2.props.duration).toBe(4);
560
+
561
+ const clip1 = act1.children[0] as VargElement<"clip">;
562
+ const clip2 = act1.children[1] as VargElement<"clip">;
563
+ expect(clip1.props.duration).toBe(3);
564
+ expect(clip2.props.duration).toBe(3);
565
+ });
566
+
567
+ test("async Scene component with container clip pattern", async () => {
568
+ const { jsx } = require("./runtime/jsx-runtime");
569
+
570
+ async function Scene(props: { clipCount: number }) {
571
+ const audio = makeResolved(
572
+ Speech({ voice: "adam", children: "narration" }),
573
+ props.clipCount * 2,
574
+ );
575
+
576
+ return Clip({
577
+ duration: audio.duration,
578
+ children: [
579
+ ...Array.from({ length: props.clipCount }, (_, i) =>
580
+ Clip({
581
+ duration: 2,
582
+ children: [Image({ prompt: `image ${i}` })],
583
+ }),
584
+ ),
585
+ Captions({
586
+ src: audio as unknown as VargElement<"speech">,
587
+ style: "tiktok",
588
+ }),
589
+ ],
590
+ });
591
+ }
592
+
593
+ const tree = Render({
594
+ width: 1080,
595
+ height: 1920,
596
+ children: [jsx(Scene, { clipCount: 3 }), jsx(Scene, { clipCount: 2 })],
597
+ });
598
+
599
+ // Before resolution: lazy elements
600
+ expect((tree.children[0] as VargElement).type).toBe("__lazy");
601
+ expect((tree.children[1] as VargElement).type).toBe("__lazy");
602
+
603
+ // After resolution: container clips
604
+ const resolved = (await resolveLazy(tree)) as VargElement<"render">;
605
+ const children = resolved.children as VargElement[];
606
+
607
+ expect(children.length).toBe(2);
608
+ expect(children[0]!.type).toBe("clip");
609
+ expect(children[0]!.props.duration).toBe(6); // 3 clips * 2s
610
+ expect(children[1]!.type).toBe("clip");
611
+ expect(children[1]!.props.duration).toBe(4); // 2 clips * 2s
612
+
613
+ // First container has 3 inner clips + 1 captions = 4 children
614
+ expect(children[0]!.children.length).toBe(4);
615
+ // Second container has 2 inner clips + 1 captions = 3 children
616
+ expect(children[1]!.children.length).toBe(3);
617
+ });
618
+ });