[bugfix] Use asset_hash for LoadImage node in Cloud mode (#8200)

## Summary
Fix 404 error when adding imported assets to workflow as LoadImage nodes
in Cloud mode.

## Changes
- **What**: Use `asset_hash` (hash-based filename) instead of `name`
(original filename) when creating LoadImage nodes in Cloud mode
- **Files**: `useMediaAssetActions.ts` - modified `addWorkflow` and
`addMultipleToWorkflow` functions
- **Tests**: Added `useMediaAssetActions.test.ts` with Cloud/OSS
filename selection tests

## Review Focus
- Cloud vs OSS branching logic using `isCloud && asset.asset_hash`

🤖 Generated with [Claude Code](https://claude.ai/code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8200-bugfix-Use-asset_hash-for-LoadImage-node-in-Cloud-mode-2ef6d73d365081d785b0d7a94e73c55e)
by [Unito](https://www.unito.io)
This commit is contained in:
Jin Yi
2026-01-21 12:13:52 +09:00
committed by GitHub
parent 47714c2740
commit 327e37db04
2 changed files with 237 additions and 2 deletions

View File

@@ -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> = {}): 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')
})
})
})
})

View File

@@ -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
},