diff --git a/src/platform/assets/composables/useMediaAssetActions.test.ts b/src/platform/assets/composables/useMediaAssetActions.test.ts new file mode 100644 index 000000000..ef7ccf8ee --- /dev/null +++ b/src/platform/assets/composables/useMediaAssetActions.test.ts @@ -0,0 +1,223 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' + +// Use vi.hoisted to create a mutable reference for isCloud +const mockIsCloud = vi.hoisted(() => ({ value: false })) + +// Track the filename passed to createAnnotatedPath +const capturedFilenames = vi.hoisted(() => ({ values: [] as string[] })) + +vi.mock('@/platform/distribution/types', () => ({ + get isCloud() { + return mockIsCloud.value + } +})) + +vi.mock('primevue/usetoast', () => ({ + useToast: () => ({ + add: vi.fn() + }) +})) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string) => key + }), + createI18n: () => ({ + global: { + t: (key: string) => key + } + }) +})) + +vi.mock('@/stores/dialogStore', () => ({ + useDialogStore: () => ({ + showDialog: vi.fn() + }) +})) + +vi.mock('@/composables/useCopyToClipboard', () => ({ + useCopyToClipboard: () => ({ + copyToClipboard: vi.fn() + }) +})) + +vi.mock('@/platform/workflow/core/services/workflowActionsService', () => ({ + useWorkflowActionsService: () => ({ + openWorkflowAction: vi.fn(), + exportWorkflowAction: vi.fn() + }) +})) + +vi.mock('@/services/litegraphService', () => ({ + useLitegraphService: () => ({ + addNodeOnGraph: vi.fn().mockReturnValue({ + widgets: [{ name: 'image', value: '', callback: vi.fn() }], + graph: { setDirtyCanvas: vi.fn() } + } as unknown as LGraphNode), + getCanvasCenter: vi.fn().mockReturnValue([100, 100]) + }) +})) + +vi.mock('@/stores/nodeDefStore', () => ({ + useNodeDefStore: () => ({ + nodeDefsByName: { + LoadImage: { + name: 'LoadImage', + display_name: 'Load Image' + } + } + }) +})) + +vi.mock('@/utils/createAnnotatedPath', () => ({ + createAnnotatedPath: vi.fn((item: { filename: string }) => { + capturedFilenames.values.push(item.filename) + return item.filename + }) +})) + +vi.mock('@/utils/loaderNodeUtil', () => ({ + detectNodeTypeFromFilename: vi.fn().mockReturnValue({ + nodeType: 'LoadImage', + widgetName: 'image' + }) +})) + +vi.mock('@/utils/typeGuardUtil', () => ({ + isResultItemType: vi.fn().mockReturnValue(true) +})) + +vi.mock('@/platform/assets/utils/assetTypeUtil', () => ({ + getAssetType: vi.fn().mockReturnValue('input') +})) + +vi.mock('../schemas/assetMetadataSchema', () => ({ + getOutputAssetMetadata: vi.fn().mockReturnValue(null) +})) + +function createMockAsset(overrides: Partial = {}): AssetItem { + return { + id: 'test-asset-id', + name: 'original-name.jpeg', + size: 1024, + created_at: '2025-01-01T00:00:00Z', + tags: ['input'], + ...overrides + } +} + +describe('useMediaAssetActions', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + capturedFilenames.values = [] + mockIsCloud.value = false + }) + + describe('addWorkflow', () => { + describe('OSS mode (isCloud = false)', () => { + beforeEach(() => { + mockIsCloud.value = false + }) + + it('should use asset.name as filename', async () => { + const { useMediaAssetActions } = await import('./useMediaAssetActions') + const actions = useMediaAssetActions() + + const asset = createMockAsset({ + name: 'my-image.jpeg', + asset_hash: 'hash123.jpeg' + }) + + await actions.addWorkflow(asset) + + expect(capturedFilenames.values).toContain('my-image.jpeg') + }) + }) + + describe('Cloud mode (isCloud = true)', () => { + beforeEach(() => { + mockIsCloud.value = true + }) + + it('should use asset_hash as filename when available', async () => { + const { useMediaAssetActions } = await import('./useMediaAssetActions') + const actions = useMediaAssetActions() + + const asset = createMockAsset({ + name: 'original.jpeg', + asset_hash: 'abc123hash.jpeg' + }) + + await actions.addWorkflow(asset) + + expect(capturedFilenames.values).toContain('abc123hash.jpeg') + }) + + it('should fall back to asset.name when asset_hash is not available', async () => { + const { useMediaAssetActions } = await import('./useMediaAssetActions') + const actions = useMediaAssetActions() + + const asset = createMockAsset({ + name: 'fallback-name.jpeg', + asset_hash: undefined + }) + + await actions.addWorkflow(asset) + + expect(capturedFilenames.values).toContain('fallback-name.jpeg') + }) + + it('should fall back to asset.name when asset_hash is null', async () => { + const { useMediaAssetActions } = await import('./useMediaAssetActions') + const actions = useMediaAssetActions() + + const asset = createMockAsset({ + name: 'fallback-null.jpeg', + asset_hash: null + }) + + await actions.addWorkflow(asset) + + expect(capturedFilenames.values).toContain('fallback-null.jpeg') + }) + }) + }) + + describe('addMultipleToWorkflow', () => { + describe('Cloud mode (isCloud = true)', () => { + beforeEach(() => { + mockIsCloud.value = true + }) + + it('should use asset_hash for each asset', async () => { + const { useMediaAssetActions } = await import('./useMediaAssetActions') + const actions = useMediaAssetActions() + + const assets = [ + createMockAsset({ + id: '1', + name: 'file1.jpeg', + asset_hash: 'hash1.jpeg' + }), + createMockAsset({ + id: '2', + name: 'file2.jpeg', + asset_hash: 'hash2.jpeg' + }) + ] + + await actions.addMultipleToWorkflow(assets) + + expect(capturedFilenames.values).toContain('hash1.jpeg') + expect(capturedFilenames.values).toContain('hash2.jpeg') + expect(capturedFilenames.values).not.toContain('file1.jpeg') + expect(capturedFilenames.values).not.toContain('file2.jpeg') + }) + }) + }) +}) diff --git a/src/platform/assets/composables/useMediaAssetActions.ts b/src/platform/assets/composables/useMediaAssetActions.ts index 395fae23a..40636b851 100644 --- a/src/platform/assets/composables/useMediaAssetActions.ts +++ b/src/platform/assets/composables/useMediaAssetActions.ts @@ -272,10 +272,17 @@ export function useMediaAssetActions() { const metadata = getOutputAssetMetadata(targetAsset.user_metadata) const assetType = getAssetType(targetAsset, 'input') + // In Cloud mode, use asset_hash (the actual stored filename) + // In OSS mode, use the original name + const filename = + isCloud && targetAsset.asset_hash + ? targetAsset.asset_hash + : targetAsset.name + // Create annotated path for the asset const annotated = createAnnotatedPath( { - filename: targetAsset.name, + filename, subfolder: metadata?.subfolder || '', type: isResultItemType(assetType) ? assetType : undefined }, @@ -407,9 +414,14 @@ export function useMediaAssetActions() { const metadata = getOutputAssetMetadata(asset.user_metadata) const assetType = getAssetType(asset, 'input') + // In Cloud mode, use asset_hash (the actual stored filename) + // In OSS mode, use the original name + const filename = + isCloud && asset.asset_hash ? asset.asset_hash : asset.name + const annotated = createAnnotatedPath( { - filename: asset.name, + filename, subfolder: metadata?.subfolder || '', type: isResultItemType(assetType) ? assetType : undefined },