Skip to content

Commit 213f651

Browse files
committed
fix(video-generator): retain empty controls and isolate CI scenarios
1 parent f760903 commit 213f651

4 files changed

Lines changed: 76 additions & 14 deletions

File tree

‎apps/sim/lib/internal/video/client.test.ts‎

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,30 +38,52 @@ describe('Video provider client', () => {
3838
})
3939

4040
it.each([
41-
{ model: 'gen4.5', limit: 1000 },
42-
{ model: 'klingO3_4k', limit: 2500 },
43-
{ model: 'kling_4', limit: 26000 },
41+
{ provider: 'runway' as const, model: 'gen4.5', limit: 1000 },
42+
{ provider: 'runway' as const, model: 'klingO3_4k', limit: 2500 },
43+
{ provider: 'runway' as const, model: 'kling_4', limit: 26000 },
44+
{ provider: 'luma' as const, model: 'ray-3.2', limit: 6000 },
45+
{ provider: 'minimax' as const, model: 'MiniMax-H3', limit: 7000 },
4446
])(
4547
'accepts the published prompt limit for $model and rejects overflow before submission',
46-
async ({ model, limit }) => {
48+
async ({ provider, model, limit }) => {
4749
const mockFetch = vi
4850
.fn<typeof fetch>()
4951
.mockResolvedValueOnce(jsonResponse({ id: 'job' }))
5052
.mockResolvedValueOnce(
51-
jsonResponse({ status: 'SUCCEEDED', output: ['data:video/mp4;base64,dmlkZW8='] })
53+
jsonResponse(
54+
provider === 'luma'
55+
? {
56+
state: 'completed',
57+
output: [{ type: 'video', url: 'data:video/mp4;base64,dmlkZW8=' }],
58+
}
59+
: provider === 'minimax'
60+
? {
61+
task: {
62+
status: 'succeeded',
63+
content: { url: 'data:video/mp4;base64,dmlkZW8=' },
64+
},
65+
}
66+
: { status: 'SUCCEEDED', output: ['data:video/mp4;base64,dmlkZW8='] }
67+
)
5268
)
5369
vi.stubGlobal('fetch', mockFetch)
5470
storageServiceMockFns.mockUploadFile.mockResolvedValue({
5571
path: '/api/files/video.mp4',
5672
size: 5,
5773
})
58-
const input = { provider: 'runway' as const, model, apiKey: 'key', prompt: 'a'.repeat(limit) }
74+
const input = { provider, model, apiKey: 'key', prompt: 'a'.repeat(limit) }
5975
const context = { requestId: 'prompt-boundary', userId: 'user', headers: new Headers() }
6076
const completion = expect(executeVideoOperation(input, context)).resolves.toMatchObject({
6177
model,
6278
})
6379
await Promise.all([completion, vi.advanceTimersByTimeAsync(5000)])
64-
expect(JSON.parse(String(mockFetch.mock.calls[0]?.[1]?.body)).promptText).toBe(input.prompt)
80+
expect(JSON.parse(String(mockFetch.mock.calls[0]?.[1]?.body))).toMatchObject(
81+
provider === 'runway'
82+
? { promptText: input.prompt }
83+
: provider === 'luma'
84+
? { prompt: input.prompt }
85+
: { content: [{ type: 'text', text: input.prompt }] }
86+
)
6587

6688
mockFetch.mockClear()
6789
await expect(
@@ -271,6 +293,32 @@ describe('Video provider client', () => {
271293
})
272294
})
273295

296+
it.each([null, '', false, 0])(
297+
'ignores an empty camera control (%s) when submitting a Luma job',
298+
async (cameraControl) => {
299+
const fetchMock = vi
300+
.fn<typeof fetch>()
301+
.mockResolvedValueOnce(jsonResponse({ id: 'job' }))
302+
.mockResolvedValueOnce(
303+
jsonResponse({ state: 'completed', assets: { video: 'data:video/mp4;base64,dmlkZW8=' } })
304+
)
305+
vi.stubGlobal('fetch', fetchMock)
306+
const result = generateVideo(
307+
{
308+
provider: 'luma',
309+
model: 'ray-2',
310+
apiKey: 'key',
311+
prompt: 'A cinematic sunrise',
312+
cameraControl,
313+
},
314+
{ requestId: 'empty-camera' }
315+
)
316+
const completion = expect(result).resolves.toMatchObject({ buffer: Buffer.from('video') })
317+
await Promise.all([completion, vi.advanceTimersByTimeAsync(5000)])
318+
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).not.toHaveProperty('concepts')
319+
}
320+
)
321+
274322
it('rejects malformed camera controls before submitting a paid Luma job', async () => {
275323
const fetchMock = vi.fn(() => {
276324
throw new Error('Unexpected paid request')

‎apps/sim/lib/internal/video/direct-request.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ export function buildDirectVideoRequest(input: VideoGenerationInput) {
6363
`${model.label} does not support camera controls; describe camera motion in the prompt`
6464
)
6565

66-
const concepts =
67-
typeof input.cameraControl === 'string' ? [{ key: input.cameraControl }] : input.cameraControl
66+
const concepts = input.cameraControl
67+
? typeof input.cameraControl === 'string'
68+
? [{ key: input.cameraControl }]
69+
: input.cameraControl
70+
: undefined
6871
if (
6972
concepts !== undefined &&
7073
(!Array.isArray(concepts) ||

‎apps/sim/lib/media/direct-video-catalog.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const directVideoModels: readonly DirectVideoModel[] = [
4646
label: 'Ray 3.2',
4747
protocol: 'luma',
4848
text: {
49+
maxPromptLength: 6000,
4950
durations: [5, 10],
5051
aspectRatios: ratios,
5152
resolutions: ['360p', '540p', '720p', '1080p'],
@@ -73,6 +74,7 @@ export const directVideoModels: readonly DirectVideoModel[] = [
7374
label: id.replace('MiniMax-', 'MiniMax '),
7475
protocol: 'minimax-v2',
7576
text: {
77+
maxPromptLength: 7000,
7678
durations: range(id === 'MiniMax-H3-Max' ? 5 : 4, 15),
7779
aspectRatios: ratios,
7880
resolutions: id === 'MiniMax-H3' ? ['768P', '2K'] : ['480P', '768P'],

‎apps/sim/scripts/test-browser-extension-e2e.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,15 +1546,24 @@ try {
15461546
if (enabled) await expect(globe).toHaveText('Current page')
15471547
else await expect(globe).not.toHaveText('Current page')
15481548
}
1549-
try {
1549+
const openScenario = async (target: string) => {
1550+
// A rejected send can recover from this tab's session after navigation.
1551+
await shell.close()
1552+
panel = await openPanel()
1553+
shell = panel
15501554
await setPanelViewport(320, 844)
1555+
await shell.goto(`chrome-extension://${extensionId}/sidepanel.html`)
1556+
await observeCaptureReceipts()
1557+
await navigate(base.origin + target)
1558+
await setReference(true)
1559+
}
1560+
try {
15511561
for (const org of [false, true]) {
15521562
const target = org ? `/extension/o/${organizationId}` : `/extension/${alphaId}`
15531563
const input = () =>
15541564
frame().getByRole('textbox', { name: org ? 'Ask Sim' : 'Message', exact: true })
1555-
await navigate(base.origin + target)
1565+
await openScenario(target)
15561566
await expect(input()).toBeVisible()
1557-
await setReference(true)
15581567
await input().fill('Summarize the current page')
15591568
const submit = frame().getByRole('button', {
15601569
name: org ? 'Send' : 'Send message',
@@ -1575,7 +1584,7 @@ try {
15751584
org ? organizationId : alphaId
15761585
)
15771586

1578-
await navigate(base.origin + target)
1587+
await openScenario(target)
15791588
await expect(input()).toBeVisible()
15801589
await capture()
15811590
await second.bringToFront()
@@ -1587,7 +1596,7 @@ try {
15871596
assert.equal(manual.length, 1)
15881597
assert(manual[0].text.includes('ORCHID-482') && !manual[0].text.includes('JADE-231'))
15891598

1590-
await navigate(base.origin + target)
1599+
await openScenario(target)
15911600
await expect(input()).toBeVisible()
15921601
await restricted.bringToFront()
15931602
const draft = 'Keep this draft if access is unavailable'

0 commit comments

Comments
 (0)