@@ -19,6 +19,8 @@ const mockIsCloud = vi.hoisted(() => ({ value: true }))
1919const mockShowUploadDialog = vi . hoisted ( ( ) => vi . fn ( ) )
2020const mockCopyToClipboard = vi . hoisted ( ( ) => vi . fn ( ) )
2121const mockDownloadModel = vi . hoisted ( ( ) => vi . fn ( ) )
22+ const mockFetchModelMetadata = vi . hoisted ( ( ) => vi . fn ( ) )
23+ const mockOpenGatedRepoPage = vi . hoisted ( ( ) => vi . fn ( ) )
2224const mockRootGraph = vi . hoisted < {
2325 value : Record < string , never > | null
2426} > ( ( ) => ( { value : null } ) )
@@ -104,10 +106,8 @@ vi.mock('@/platform/missingModel/missingModelDownload', async () => {
104106 return {
105107 ...actual ,
106108 downloadModel : mockDownloadModel ,
107- fetchModelMetadata : vi . fn ( ) . mockResolvedValue ( {
108- fileSize : null ,
109- gatedRepoUrl : null
110- } )
109+ fetchModelMetadata : mockFetchModelMetadata ,
110+ openGatedRepoPage : mockOpenGatedRepoPage
111111 }
112112} )
113113
@@ -177,12 +177,17 @@ function renderRow(
177177describe ( 'MissingModelRow' , ( ) => {
178178 beforeEach ( ( ) => {
179179 vi . clearAllMocks ( )
180+ delete window . __comfyDesktop2
180181 mockIsCloud . value = true
181182 mockRootGraph . value = null
182183 mockApiListeners . clear ( )
183184 mockGetNodeByExecutionId . mockReset ( )
184185 mockUploadContext . resolver = undefined
185186 mockUploadCallbacks . onUploadSuccess = undefined
187+ mockFetchModelMetadata . mockResolvedValue ( {
188+ fileSize : null ,
189+ gatedRepoUrl : null
190+ } )
186191 } )
187192
188193 it ( 'opens the model import dialog from the cloud row' , async ( ) => {
@@ -401,6 +406,96 @@ describe('MissingModelRow', () => {
401406 )
402407 } )
403408
409+ it ( 'shows a gated HuggingFace access action without replacing download' , async ( ) => {
410+ mockIsCloud . value = false
411+ const model = makeModel ( [ { nodeId : '1' , widgetName : 'ckpt_name' } ] )
412+ model . representative . url =
413+ 'https://huggingface.co/bfl/FLUX.1/resolve/main/model.safetensors'
414+ mockFetchModelMetadata . mockResolvedValueOnce ( {
415+ fileSize : null ,
416+ gatedRepoUrl : 'https://huggingface.co/bfl/FLUX.1'
417+ } )
418+
419+ renderRow ( model , vi . fn ( ) , false )
420+ const store = useMissingModelStore ( )
421+
422+ await waitFor ( ( ) => {
423+ expect ( store . gatedRepoUrls [ model . representative . url ! ] ) . toBe (
424+ 'https://huggingface.co/bfl/FLUX.1'
425+ )
426+ } )
427+
428+ const gatedModelTooltip =
429+ 'This model is gated and requires you to be logged in to Hugging Face and to accept its license agreement.'
430+ expect ( screen . getByTestId ( 'missing-model-gated-access' ) ) . toHaveAttribute (
431+ 'title' ,
432+ gatedModelTooltip
433+ )
434+ expect ( screen . getByTestId ( 'missing-model-download' ) ) . toHaveAttribute (
435+ 'title' ,
436+ gatedModelTooltip
437+ )
438+ } )
439+
440+ it ( 'opens gated repo action separately from the download action' , async ( ) => {
441+ mockIsCloud . value = false
442+ const user = userEvent . setup ( )
443+ const model = makeModel ( [ { nodeId : '1' , widgetName : 'ckpt_name' } ] )
444+ model . representative . url =
445+ 'https://huggingface.co/bfl/FLUX.1/resolve/main/model.safetensors'
446+
447+ renderRow ( model , vi . fn ( ) , false )
448+ const store = useMissingModelStore ( )
449+ store . setGatedRepoUrl (
450+ model . representative . url ,
451+ 'https://huggingface.co/bfl/FLUX.1'
452+ )
453+ await nextTick ( )
454+
455+ await user . click ( screen . getByTestId ( 'missing-model-gated-access' ) )
456+ expect ( mockOpenGatedRepoPage ) . toHaveBeenCalledWith (
457+ 'https://huggingface.co/bfl/FLUX.1'
458+ )
459+
460+ await user . click ( screen . getByTestId ( 'missing-model-download' ) )
461+ expect ( mockDownloadModel ) . toHaveBeenCalledWith (
462+ {
463+ name : 'model.safetensors' ,
464+ url : 'https://huggingface.co/bfl/FLUX.1/resolve/main/model.safetensors' ,
465+ directory : 'checkpoints'
466+ } ,
467+ { }
468+ )
469+ } )
470+
471+ it ( 'delegates the gated repo action to the Desktop bridge when available' , async ( ) => {
472+ mockIsCloud . value = false
473+ const user = userEvent . setup ( )
474+ const openModelAccessPage = vi . fn ( ) . mockResolvedValue ( true )
475+ window . __comfyDesktop2 = {
476+ isRemote : ( ) => false ,
477+ openModelAccessPage
478+ }
479+ const model = makeModel ( [ { nodeId : '1' , widgetName : 'ckpt_name' } ] )
480+ model . representative . url =
481+ 'https://huggingface.co/bfl/FLUX.1/resolve/main/model.safetensors'
482+
483+ renderRow ( model , vi . fn ( ) , false )
484+ const store = useMissingModelStore ( )
485+ store . setGatedRepoUrl (
486+ model . representative . url ,
487+ 'https://huggingface.co/bfl/FLUX.1'
488+ )
489+ await nextTick ( )
490+
491+ await user . click ( screen . getByTestId ( 'missing-model-gated-access' ) )
492+
493+ expect ( openModelAccessPage ) . toHaveBeenCalledWith (
494+ 'https://huggingface.co/bfl/FLUX.1'
495+ )
496+ expect ( mockOpenGatedRepoPage ) . not . toHaveBeenCalled ( )
497+ } )
498+
404499 it ( 'shows unknown category metadata for models without a directory' , ( ) => {
405500 renderRow (
406501 makeModel ( [ { nodeId : '1' , widgetName : 'ckpt_name' } ] ) ,
0 commit comments