Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "LoadVideo",
"pos": [50, 120],
"size": [400, 200],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadVideo"
},
"widgets_values": ["video/cloud-video-hash.mp4 [output]", "image"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { expect, mergeTests } from '@playwright/test'
import type { Page, Route } from '@playwright/test'
import type { Asset, ListAssetsResponse } from '@comfyorg/ingest-types'
import type {
Asset,
GetAllSettingsResponse,
GetSettingByIdResponse,
ListAssetsResponse
} from '@comfyorg/ingest-types'

import {
assetRequestIncludesTag,
createCloudAssetsFixture
} from '@e2e/fixtures/assetApiFixture'
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import type { WorkspaceStore } from '@e2e/types/globals'
import {
routeObjectInfoFromSetupApi,
setComboInputOptions
Expand All @@ -23,10 +29,11 @@ import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
const ossTest = mergeTests(comfyPageFixture, jobsRouteFixture)
const outputHash =
'147257c95a3e957e0deee73a077cfec89da2d906dd086ca70a2b0c897a9591d6e.png'
const outputVideoHash = 'cloud-video-hash.mp4'
const plainVideoFileName = 'plain_video.mp4'
const graphDropPosition = { x: 500, y: 300 }
const missingMediaUploadObservationMs = 1_000
const missingMediaUploadPollMs = 100
const missingMediaObservationMs = 1_000
const missingMediaPollMs = 100
const emptyMediaLoaderNodes = [
{
nodeType: 'LoadImage',
Expand Down Expand Up @@ -60,6 +67,18 @@ const cloudOutputAsset: Asset & { hash?: string } = {
last_access_time: '2026-05-01T00:00:00Z'
}

const cloudOutputVideoAsset: Asset & { hash?: string } = {
id: 'test-output-video-hash-001',
name: 'ComfyUI_00001_.mp4',
hash: outputVideoHash,
size: 4_194_304,
mime_type: 'video/mp4',
tags: ['output'],
created_at: '2026-05-01T00:00:00Z',
updated_at: '2026-05-01T00:00:00Z',
last_access_time: '2026-05-01T00:00:00Z'
}

const cloudUploadedVideoAsset: Asset & { hash?: string } = {
id: 'test-uploaded-video-001',
name: plainVideoFileName,
Expand Down Expand Up @@ -92,10 +111,21 @@ interface CloudUploadAssetState {

async function routeCloudBootstrapApis(page: Page) {
await page.route('**/api/settings**', async (route) => {
const completedSurveySetting: GetSettingByIdResponse = {
value: { usage: 'personal' }
}
const allSettings: GetAllSettingsResponse = {}
const body = route
.request()
.url()
.includes('/api/settings/onboarding_survey')
? completedSurveySetting
: allSettings

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({})
body: JSON.stringify(body)
})
})
await page.route('**/api/userdata**', async (route) => {
Expand All @@ -121,7 +151,10 @@ async function routeCloudBootstrapApis(page: Page) {
})
}

const cloudOutputTest = createCloudAssetsFixture([cloudOutputAsset]).extend({
const cloudOutputTest = createCloudAssetsFixture([
cloudOutputAsset,
cloudOutputVideoAsset
]).extend({
page: async ({ page }, use) => {
await routeCloudBootstrapApis(page)
const unrouteObjectInfo = await routeObjectInfoFromSetupApi(page)
Expand Down Expand Up @@ -225,6 +258,33 @@ function getErrorOverlay(comfyPage: ComfyPage) {
return comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
}

function isOutputAssetsRequest(url: string) {
return url.includes('/api/assets') && assetRequestIncludesTag(url, 'output')
}

async function waitForOutputAssetsResponse(comfyPage: ComfyPage) {
await comfyPage.page.waitForResponse(
(response) =>
response.status() === 200 && isOutputAssetsRequest(response.url())
)
}

async function getCachedMissingMediaWarningNames(
comfyPage: ComfyPage
): Promise<string[] | null> {
return await comfyPage.page.evaluate(() => {
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow
if (!workflow) return null

return (
workflow.pendingWarnings?.missingMediaCandidates?.map(
(candidate) => candidate.name
) ?? []
)
})
}

async function expectNoErrorsTab(comfyPage: ComfyPage) {
await expect(getErrorOverlay(comfyPage)).toBeHidden()

Expand Down Expand Up @@ -327,25 +387,31 @@ async function expectLoadVideoUploading(comfyPage: ComfyPage) {
.toBe(true)
}

async function expectNoMissingMediaDuringUpload(comfyPage: ComfyPage) {
async function expectNoMissingMediaForObservationWindow(comfyPage: ComfyPage) {
await comfyPage.nextFrame()
await comfyPage.nextFrame()

let sawErrorOverlay = false
let sawCachedMissingMedia = false
const startedAt = Date.now()
await expect
.poll(
async () => {
const cachedMissingMedia =
await getCachedMissingMediaWarningNames(comfyPage)
sawCachedMissingMedia =
sawCachedMissingMedia || !!cachedMissingMedia?.length
sawErrorOverlay =
sawErrorOverlay || (await getErrorOverlay(comfyPage).isVisible())
return (
!sawErrorOverlay &&
Date.now() - startedAt >= missingMediaUploadObservationMs
!sawCachedMissingMedia &&
Date.now() - startedAt >= missingMediaObservationMs
)
},
{
timeout: missingMediaUploadObservationMs + missingMediaUploadPollMs * 5,
intervals: [missingMediaUploadPollMs]
timeout: missingMediaObservationMs + missingMediaPollMs * 5,
intervals: [missingMediaPollMs]
}
)
.toBe(true)
Expand Down Expand Up @@ -424,7 +490,7 @@ ossTest.describe(
})

await expectLoadVideoUploading(comfyPage)
await expectNoMissingMediaDuringUpload(comfyPage)
await expectNoMissingMediaForObservationWindow(comfyPage)

await delayedUpload.finishUpload()
await expect(getErrorOverlay(comfyPage)).toBeHidden()
Expand Down Expand Up @@ -482,18 +548,30 @@ cloudOutputTest.describe(

cloudOutputTest(
'resolves compact annotated output media from output assets',
async ({ cloudAssetRequests, comfyPage }) => {
async ({ comfyPage }) => {
const outputAssetsResponse = waitForOutputAssetsResponse(comfyPage)

await comfyPage.workflow.loadWorkflow(
'missing/missing_media_cloud_output_annotation'
)

await expect
.poll(() =>
cloudAssetRequests.some((url) =>
assetRequestIncludesTag(url, 'output')
)
)
.toBe(true)
await outputAssetsResponse
await expectNoMissingMediaForObservationWindow(comfyPage)
await expectNoErrorsTab(comfyPage)
}
)

cloudOutputTest(
'resolves subfoldered output video media from flat output asset hashes',
async ({ comfyPage }) => {
const outputAssetsResponse = waitForOutputAssetsResponse(comfyPage)

await comfyPage.workflow.loadWorkflow(
'missing/missing_media_cloud_output_video_subfolder'
)

await outputAssetsResponse
await expectNoMissingMediaForObservationWindow(comfyPage)
await expectNoErrorsTab(comfyPage)
}
)
Expand Down Expand Up @@ -529,7 +607,7 @@ cloudUploadRaceTest.describe(
})

await expectLoadVideoUploading(comfyPage)
await expectNoMissingMediaDuringUpload(comfyPage)
await expectNoMissingMediaForObservationWindow(comfyPage)

markUploadedCloudAssetAvailable()
await delayedUpload.finishUpload()
Expand Down
54 changes: 52 additions & 2 deletions src/platform/missingMedia/missingMediaAssetResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,12 @@ describe('resolveMissingMediaAssetSources', () => {

it('stops reading cloud output asset pages once all requested names are found', async () => {
const target = 'target-output.png'
const outputAsset = makeAsset('ComfyUI_00001_.png', target)
mockGetAssetsPageByTag.mockResolvedValueOnce(
makeAssetPage([makeAsset(target)], { hasMore: true, total: 501 })
makeAssetPage([outputAsset], {
hasMore: true,
total: 501
})
)

const result = await resolveMissingMediaAssetSources({
Expand All @@ -163,10 +167,56 @@ describe('resolveMissingMediaAssetSources', () => {
allowCompactSuffix: true
})

expect(result.generatedAssets).toEqual([makeAsset(target)])
expect(result.generatedAssets).toEqual([outputAsset])
expect(mockGetAssetsPageByTag).toHaveBeenCalledOnce()
})

it('stops reading cloud output asset pages when a flat target matches by name', async () => {
const target = 'ComfyUI_00001_.mp4'
const outputAsset = makeAsset(target, 'different-output-hash.mp4')
mockGetAssetsPageByTag.mockResolvedValueOnce(
makeAssetPage([outputAsset], {
hasMore: true,
total: 501
})
)

const result = await resolveMissingMediaAssetSources({
isCloud: true,
includeGeneratedAssets: true,
generatedMatchNames: new Set([target]),
allowCompactSuffix: true
})

expect(result.generatedAssets).toEqual([outputAsset])
expect(mockGetAssetsPageByTag).toHaveBeenCalledOnce()
})

it('does not stop cloud output asset paging on a flat asset name collision', async () => {
const target = 'target-output.mp4'
const collidingNameAsset = makeAsset(target)
const matchingHashAsset = makeAsset('ComfyUI_00001_.mp4', target)
mockGetAssetsPageByTag
.mockResolvedValueOnce(
makeAssetPage([collidingNameAsset], { hasMore: true, total: 501 })
)
.mockResolvedValueOnce(makeAssetPage([matchingHashAsset]))

const result = await resolveMissingMediaAssetSources({
isCloud: true,
includeGeneratedAssets: true,
generatedMatchNames: new Set([target]),
generatedHashRequiredNames: new Set([target]),
allowCompactSuffix: true
})

expect(result.generatedAssets).toEqual([
collidingNameAsset,
matchingHashAsset
])
expect(mockGetAssetsPageByTag).toHaveBeenCalledTimes(2)
})

it('aborts cloud output asset loading when input asset loading fails', async () => {
const inputError = new Error('input failed')
let rejectInputAssets!: (err: Error) => void
Expand Down
Loading
Loading