fix(ui): harden resource restore and links

- Restore image resources when metadata exists but bytes are missing
- Limit remote restore scope and sanitize external link handling
- Reuse image resource backup helpers across restore packages
- Extend restore safety coverage for data manager and snapshots
This commit is contained in:
linshen committed 2026-05-09 22:16:11 +08:00
1 parent 9711acb424
commit 8d2c024794
16 files changed
+362 -42

No files matched your search

+100 -17
View File
@@ -653,7 +653,54 @@
/>
</div>
<div class="remote-field remote-restore-strategy">
<div v-if="selectedRemoteBackup" class="remote-field remote-restore-scope">
<NText depth="3" class="remote-field-label">
{{ $t('dataManager.remote.restoreScope') }}
</NText>
<NText
v-if="selectedRemoteBackupMissingAssetsCount > 0"
depth="3"
class="storage-note"
>
{{ $t('dataManager.remote.restoreMissingAssetsHint', { count: selectedRemoteBackupMissingAssetsCount }) }}
</NText>
<div class="local-scope-row">
<label class="local-scope-pill">
<NCheckbox
v-model:checked="remoteRestoreAppData"
:disabled="!availableRemoteRestoreSections.has('appData')"
>
{{ $t('dataManager.sections.appData') }}
</NCheckbox>
</label>
<label class="local-scope-pill">
<NCheckbox
v-model:checked="remoteRestoreAppDataImages"
:disabled="!remoteRestoreAppData || !availableRemoteRestoreSections.has('imageCache')"
>
{{ $t('dataManager.sections.appDataImages') }}
</NCheckbox>
</label>
<label class="local-scope-pill">
<NCheckbox
v-model:checked="remoteRestoreFavorites"
:disabled="!availableRemoteRestoreSections.has('favorites')"
>
{{ $t('dataManager.sections.favoritesBundle') }}
</NCheckbox>
</label>
<label class="local-scope-pill">
<NCheckbox
v-model:checked="remoteRestoreFavoriteImages"
:disabled="!remoteRestoreFavorites || !availableRemoteRestoreSections.has('favoriteImages')"
>
{{ $t('dataManager.sections.favoriteImages') }}
</NCheckbox>
</label>
</div>
</div>
<div v-if="remoteRestoreFavorites" class="remote-field remote-restore-strategy">
<NText depth="3" class="remote-field-label">
{{ $t('dataManager.import.favoriteMergeStrategy') }}
</NText>
@@ -676,7 +723,7 @@
type="success"
block
:loading="isRestoringRemote"
:disabled="!selectedRemoteBackup || isRemoteDataOperationActive"
:disabled="!selectedRemoteBackup || isRemoteDataOperationActive || !hasSelectedRemoteRestoreSection"
@click="handleRemoteRestore"
>
<template #icon>
@@ -776,6 +823,7 @@ import {
type RemoteSnapshotEntry,
} from '../utils/remote-snapshot-backup'
import { recordDataBackupCompleted } from '../utils/data-backup-reminder'
import { openExternalUrl } from '../utils/open-external-url'
interface Props {
show: boolean
@@ -833,6 +881,10 @@ const exportFavorites = ref(true)
const importAppData = ref(true)
const importAppDataImages = ref(true)
const importFavorites = ref(true)
const remoteRestoreAppData = ref(true)
const remoteRestoreAppDataImages = ref(true)
const remoteRestoreFavorites = ref(true)
const remoteRestoreFavoriteImages = ref(true)
const favoriteMergeStrategy = ref<DataManagerFavoritesMergeStrategy>('overwrite')
const storageSummary = ref<StorageBreakdownSummary | null>(null)
@@ -942,6 +994,41 @@ const googleDriveAuthorizationLabel = computed(() =>
const selectedRemoteBackup = computed(() =>
remoteBackups.value.find((entry) => entry.id === selectedRemoteBackupId.value) ?? null
)
const getRemoteBackupIncludedSectionSet = (entry: RemoteSnapshotEntry | null): Set<DataManagerPackageSection> => {
const included = entry?.manifest?.includedSections
if (!Array.isArray(included) || included.length === 0) {
return new Set(allPackageSections)
}
const filtered = included.filter((section): section is DataManagerPackageSection =>
allPackageSections.includes(section as DataManagerPackageSection),
)
return new Set(filtered.length > 0 ? filtered : allPackageSections)
}
const availableRemoteRestoreSections = computed(() =>
getRemoteBackupIncludedSectionSet(selectedRemoteBackup.value)
)
const selectedRemoteBackupMissingAssetsCount = computed(() => {
const missingAssets = selectedRemoteBackup.value?.manifest?.missingAssets
return Array.isArray(missingAssets) ? missingAssets.length : 0
})
const resetRemoteRestoreSelection = () => {
const available = availableRemoteRestoreSections.value
remoteRestoreAppData.value = available.has('appData')
remoteRestoreAppDataImages.value = available.has('imageCache')
remoteRestoreFavorites.value = available.has('favorites')
remoteRestoreFavoriteImages.value = available.has('favoriteImages')
}
const hasSelectedRemoteRestoreSection = computed(() =>
(remoteRestoreAppData.value && availableRemoteRestoreSections.value.has('appData')) ||
(remoteRestoreAppData.value && remoteRestoreAppDataImages.value && availableRemoteRestoreSections.value.has('imageCache')) ||
(remoteRestoreFavorites.value && availableRemoteRestoreSections.value.has('favorites')) ||
(remoteRestoreFavorites.value && remoteRestoreFavoriteImages.value && availableRemoteRestoreSections.value.has('favoriteImages'))
)
watch(selectedRemoteBackup, () => {
resetRemoteRestoreSelection()
})
const cloudflareR2Provider = computed(() =>
remoteSettings.value.provider.kind === 'cloudflare-r2'
@@ -1273,23 +1360,10 @@ const finishRemoteProgress = (messageKey?: string) => {
}
}
const openExternalUrl = async (url: string) => {
if (typeof window !== 'undefined' && window.electronAPI?.shell) {
try {
await window.electronAPI.shell.openExternal(url)
return
} catch (error) {
console.error('[DataManager] Failed to open external URL in Electron:', error)
}
}
window.open(url, '_blank', 'noopener,noreferrer')
}
const openCloudflareR2Link = (
key: 'dashboard' | 'buckets' | 'apiTokens' | 'docs',
) => {
void openExternalUrl(cloudflareR2Links.value[key])
void openExternalUrl(cloudflareR2Links.value[key], { logPrefix: 'DataManager' })
}
const writeClipboardText = async (text: string): Promise<void> => {
@@ -1606,7 +1680,16 @@ const toImportSectionSelection = (): DataManagerPackageSectionSelection => ({
})
const toRemoteRestoreSectionSelection = (): DataManagerPackageSectionSelection => ({
...DEFAULT_DATA_MANAGER_PACKAGE_SECTIONS,
appData: remoteRestoreAppData.value && availableRemoteRestoreSections.value.has('appData'),
favorites: remoteRestoreFavorites.value && availableRemoteRestoreSections.value.has('favorites'),
imageCache:
remoteRestoreAppData.value &&
remoteRestoreAppDataImages.value &&
availableRemoteRestoreSections.value.has('imageCache'),
favoriteImages:
remoteRestoreFavorites.value &&
remoteRestoreFavoriteImages.value &&
availableRemoteRestoreSections.value.has('favoriteImages'),
})
const resetImportSelection = () => {
@@ -283,7 +283,9 @@ const messages = {
"appDataImages": "Related images",
"appDataImagesHint": "Images referenced by history and results; current workspace sessions are not included",
"favoritesBundle": "Favorites",
"favoritesBundleHint": "Favorites, categories, tags, prompt assets, examples, versions, and favorite images"
"favoritesBundleHint": "Favorites, categories, tags, prompt assets, examples, versions, and favorite images",
"favoriteImages": "Favorite images",
"favoriteImagesHint": "Images referenced by favorite prompt assets, examples, and versions"
},
"contexts": {
"title": "Context Collections Management",
@@ -421,6 +423,8 @@ const messages = {
"refreshList": "Refresh Remote Backup List",
"listFailed": "Failed to load remote backup list: {message}",
"restoreFrom": "Restore from remote backup",
"restoreScope": "Restore contents",
"restoreMissingAssetsHint": "This backup was created with {count} missing image resources. Clear image sections to restore only JSON data.",
"noBackups": "No remote backups",
"restoreSelected": "Restore Selected Backup",
"restoreFailed": "Failed to restore remote backup: {message}",
@@ -283,7 +283,9 @@ const messages = {
"appDataImages": "相关图片",
"appDataImagesHint": "历史和结果中引用的图片,不包含当前工作区会话",
"favoritesBundle": "收藏夹",
"favoritesBundleHint": "收藏、分类、标签、提示词资产、示例、版本链和收藏图片"
"favoritesBundleHint": "收藏、分类、标签、提示词资产、示例、版本链和收藏图片",
"favoriteImages": "收藏图片",
"favoriteImagesHint": "收藏提示词资产、示例和版本链引用的图片"
},
"contexts": {
"title": "上下文集合管理",
@@ -421,6 +423,8 @@ const messages = {
"refreshList": "刷新远程备份列表",
"listFailed": "获取远程备份列表失败:{message}",
"restoreFrom": "从远程备份还原",
"restoreScope": "还原内容",
"restoreMissingAssetsHint": "此备份创建时有 {count} 个图片资源缺失,可取消图片内容后只还原 JSON 数据。",
"noBackups": "暂无远程备份",
"restoreSelected": "还原选中备份",
"restoreFailed": "还原远程备份失败:{message}",
@@ -283,7 +283,9 @@ const messages = {
"appDataImages": "相關圖片",
"appDataImagesHint": "歷史和結果中引用的圖片,不包含目前工作區會話",
"favoritesBundle": "收藏",
"favoritesBundleHint": "收藏、分類、標籤、提示詞資產、範例、版本鏈和收藏圖片"
"favoritesBundleHint": "收藏、分類、標籤、提示詞資產、範例、版本鏈和收藏圖片",
"favoriteImages": "收藏圖片",
"favoriteImagesHint": "收藏提示詞資產、範例和版本鏈引用的圖片"
},
"contexts": {
"title": "情境集合管理",
@@ -421,6 +423,8 @@ const messages = {
"refreshList": "重新整理遠端備份列表",
"listFailed": "取得遠端備份列表失敗:{message}",
"restoreFrom": "從遠端備份還原",
"restoreScope": "還原內容",
"restoreMissingAssetsHint": "此備份建立時有 {count} 個圖片資源缺失,可取消圖片內容後只還原 JSON 資料。",
"noBackups": "暫無遠端備份",
"restoreSelected": "還原選中備份",
"restoreFailed": "還原遠端備份失敗:{message}",
@@ -85,8 +85,8 @@ type ExportDataManagerResourcePackageOptions = {
type ImportDataManagerResourcePackageOptions = {
dataManager: Pick<IDataManager, 'importAllData'>
favoriteManager: Pick<IFavoriteManager, 'importFavorites'> | null | undefined
imageStorageService?: Pick<IImageStorageService, 'getMetadata' | 'saveImage'> | null
favoriteImageStorageService?: Pick<IImageStorageService, 'getMetadata' | 'saveImage'> | null
imageStorageService?: Pick<IImageStorageService, 'getImage' | 'saveImage'> | null
favoriteImageStorageService?: Pick<IImageStorageService, 'getImage' | 'saveImage'> | null
sections?: Partial<DataManagerPackageSectionSelection>
favoriteMergeStrategy?: DataManagerFavoritesMergeStrategy
}
@@ -307,7 +307,7 @@ export const readDataManagerResourcePackage = (
const getImportStorageService = (
store: DataManagerImageStoreKey,
options: ImportDataManagerResourcePackageOptions,
): Pick<IImageStorageService, 'getMetadata' | 'saveImage'> | null | undefined =>
): Pick<IImageStorageService, 'getImage' | 'saveImage'> | null | undefined =>
store === 'favoriteImages'
? options.favoriteImageStorageService
: options.imageStorageService
@@ -78,7 +78,7 @@ type ExportFavoriteResourcePackageOptions = {
type ImportFavoriteResourcePackageOptions = {
favoriteManager: Pick<IFavoriteManager, 'importFavorites'>
imageStorageService?: Pick<IImageStorageService, 'getMetadata' | 'saveImage'> | null
imageStorageService?: Pick<IImageStorageService, 'getImage' | 'saveImage'> | null
mergeStrategy?: 'skip' | 'overwrite' | 'merge'
}
@@ -25,7 +25,7 @@ export type ImageResourceRestoreReport<Problem> = {
errors: string[]
}
export type ImageResourceStorage = Pick<IImageStorageService, 'getMetadata' | 'saveImage'>
export type ImageResourceStorage = Pick<IImageStorageService, 'getImage' | 'saveImage'>
export const copyBytes = (bytes: Uint8Array): Uint8Array => {
const out = new globalThis.Uint8Array(bytes.byteLength)
@@ -203,8 +203,8 @@ export const restoreImageResource = async (
bytes: Uint8Array,
storageService: ImageResourceStorage,
): Promise<'restored' | 'skipped'> => {
const existing = await storageService.getMetadata(entry.id)
if (existing) {
const existing = await storageService.getImage(entry.id)
if (existing?.data) {
return 'skipped'
}
+17 -1
View File
@@ -4,12 +4,28 @@ export interface OpenExternalUrlOptions {
logPrefix?: string
}
const SUPPORTED_EXTERNAL_URL_PROTOCOLS = new Set(['http:', 'https:'])
const isSupportedExternalUrl = (url: string): boolean => {
try {
return SUPPORTED_EXTERNAL_URL_PROTOCOLS.has(new URL(url).protocol)
} catch {
return false
}
}
export const openExternalUrl = async (
url: string,
options: OpenExternalUrlOptions = {},
): Promise<boolean> => {
if (!url || typeof window === 'undefined') return false
const prefix = options.logPrefix ?? 'openExternalUrl'
if (!isSupportedExternalUrl(url)) {
console.error(`[${prefix}] Refused to open unsupported external URL:`, url)
return false
}
const target = options.target ?? '_blank'
const features = options.features ?? 'noopener,noreferrer'
@@ -18,8 +34,8 @@ export const openExternalUrl = async (
await window.electronAPI.shell.openExternal(url)
return true
} catch (error) {
const prefix = options.logPrefix ?? 'openExternalUrl'
console.error(`[${prefix}] Failed to open external URL in Electron:`, error)
return false
}
}
@@ -143,8 +143,8 @@ type RestoreRemoteSnapshotOptions = {
snapshotId: string
dataManager: Pick<IDataManager, 'importAllData'>
favoriteManager: Pick<IFavoriteManager, 'importFavorites'> | null | undefined
imageStorageService?: Pick<IImageStorageService, 'getMetadata' | 'saveImage'> | null
favoriteImageStorageService?: Pick<IImageStorageService, 'getMetadata' | 'saveImage'> | null
imageStorageService?: Pick<IImageStorageService, 'getImage' | 'saveImage'> | null
favoriteImageStorageService?: Pick<IImageStorageService, 'getImage' | 'saveImage'> | null
sections?: Partial<DataManagerPackageSectionSelection>
favoriteMergeStrategy?: DataManagerFavoritesMergeStrategy
onProgress?: RemoteSnapshotProgressReporter
@@ -263,7 +263,7 @@ const validateJsonText = (text: string, label: string): void => {
const getImportStorageService = (
store: DataManagerImageStoreKey,
options: RestoreRemoteSnapshotOptions,
): Pick<IImageStorageService, 'getMetadata' | 'saveImage'> | null | undefined =>
): Pick<IImageStorageService, 'getImage' | 'saveImage'> | null | undefined =>
store === 'favoriteImages'
? options.favoriteImageStorageService
: options.imageStorageService
@@ -551,8 +551,8 @@ const prepareRemoteSnapshotRestore = async (
continue
}
const existing = await storageService.getMetadata(asset.id)
if (existing) {
const existing = await storageService.getImage(asset.id)
if (existing?.data) {
report.skipped += 1
continue
}
@@ -575,6 +575,69 @@ describe('DataManager storage breakdown', () => {
}))
})
it('allows remote restore to skip image sections from a partial backup', async () => {
window.localStorage.setItem('prompt-optimizer:remote-backup-settings', JSON.stringify({
provider: {
kind: 'cloudflare-r2',
accountId: 'account-123',
bucket: 'prompt-optimizer-backups',
accessKeyId: 'ak',
secretAccessKey: 'sk',
},
}))
const wrapper = mountComponent()
await flushPromises()
const setupState = (wrapper.vm as any).$?.setupState
setupState.remoteConnectionStates = {
...setupState.remoteConnectionStates,
'cloudflare-r2': {
...setupState.remoteConnectionStates['cloudflare-r2'],
status: 'connected',
},
}
setupState.remoteBackups = [{
id: 'snap-partial',
name: 'snap-partial',
manifestPath: 'v1/snapshots/snap-partial/manifest.json',
updatedAt: '2026-05-06T10:30:00.000Z',
manifest: {
includedSections: ['appData', 'favorites', 'imageCache', 'favoriteImages'],
missingAssets: [
{ store: 'imageCache', id: 'missing-image' },
{ store: 'favoriteImages', id: 'missing-favorite-image' },
],
},
}]
setupState.selectedRemoteBackupId = 'snap-partial'
await flushPromises()
setupState.remoteRestoreAppData = true
setupState.remoteRestoreFavorites = true
setupState.remoteRestoreAppDataImages = false
setupState.remoteRestoreFavoriteImages = false
await flushPromises()
const restoreButton = wrapper.findAll('button').find((button) =>
button.text().includes('Restore Selected Backup')
)
expect(restoreButton).toBeTruthy()
await restoreButton!.trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('missing image resources')
expect(remoteSnapshotMocks.restoreRemoteSnapshotBackup).toHaveBeenCalledWith(expect.objectContaining({
snapshotId: 'snap-partial',
sections: {
appData: true,
favorites: true,
imageCache: false,
favoriteImages: false,
},
}))
})
it('disables remote backup actions while a remote data operation is running', async () => {
window.localStorage.setItem('prompt-optimizer:remote-backup-settings', JSON.stringify({
provider: {
@@ -195,7 +195,7 @@ describe('FavoriteImportPanel', () => {
}),
}
const favoriteImageStorageService = {
getMetadata: vi.fn(async () => null),
getImage: vi.fn(async () => null),
saveImage: vi.fn(async (image: FullImageData) => {
order.push(`save:${image.metadata.id}`)
return image.metadata.id
@@ -140,6 +140,36 @@ describe('dataManagerResourcePackage', () => {
expect(favoriteTarget.store.has('favorite-1')).toBe(true)
})
it('restores package resources when local metadata exists but image bytes are missing', async () => {
const exported = await createDataManagerResourcePackage({
dataManager: {
exportAllData: vi.fn(async () => JSON.stringify({ version: 1, data: {} })),
},
favoriteManager: null,
imageStorageService: createStorage([createImage('session-1', 'session-image')]),
favoriteImageStorageService: createStorage([]),
})
const target = createStorage([])
target.getMetadata.mockResolvedValue(createImage('session-1', 'metadata-only').metadata)
const result = await importDataManagerResourcePackage(await exported.blob.arrayBuffer(), {
dataManager: { importAllData: vi.fn(async () => {}) },
favoriteManager: null,
imageStorageService: target,
favoriteImageStorageService: createStorage([]),
sections: {
appData: false,
favorites: false,
imageCache: true,
favoriteImages: false,
},
})
expect(result.resources.restored).toBe(1)
expect(target.saveImage).toHaveBeenCalledTimes(1)
expect(target.store.has('session-1')).toBe(true)
})
it('exports resource metadata from actual bytes instead of stale image metadata', async () => {
const jpegBytes = [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0xff, 0xd9]
const image: FullImageData = {
@@ -340,6 +340,32 @@ describe('favoriteResourcePackage', () => {
expect(order).toEqual(['save:media-asset', 'import:favorites'])
})
it('restores package resources when local metadata exists but image bytes are missing', async () => {
const favorite = createMediaOnlyFavorite([])
const exported = await createFavoriteResourcePackage({
favoriteManager: {
exportFavorites: vi.fn(async () => JSON.stringify({
version: '1.0',
favorites: [favorite],
})),
},
imageStorageService: createStorage([createImage('cover-asset', 'cover')]),
})
const target = createStorage([])
target.getMetadata.mockResolvedValue(createImage('cover-asset', 'metadata-only').metadata)
const result = await importFavoriteResourcePackage(await exported.blob.arrayBuffer(), {
favoriteManager: {
importFavorites: vi.fn(async () => ({ imported: 1, skipped: 0, errors: [] })),
},
imageStorageService: target,
})
expect(result.resources.restored).toBe(1)
expect(target.saveImage).toHaveBeenCalledTimes(1)
expect(target.store.has('cover-asset')).toBe(true)
})
it('blocks favorites JSON import when package resources are corrupt', async () => {
stubSha256Digest()
@@ -1,8 +1,9 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import {
assertImageResourceRestoreReportSafe,
createFullImageDataFromResource,
restoreImageResource,
safeImageResourceFileName,
validateImageResourceBytes,
} from '../../../src/utils/image-resource-backup'
@@ -62,4 +63,41 @@ describe('image resource backup helpers', () => {
expect(image.metadata.mimeType).toBe('image/jpeg')
expect(image.metadata.source).toBe('generated')
})
it('skips only when local storage can return complete image bytes', async () => {
const bytes = new TextEncoder().encode('backup-image')
const entry = {
kind: 'image' as const,
id: 'asset-3',
path: 'resources/images/asset-3.png',
mimeType: 'image/png',
createdAt: 1,
source: 'uploaded' as const,
}
const saveImage = vi.fn(async () => 'asset-3')
const metadataOnlyStorage = {
getImage: vi.fn(async () => null),
saveImage,
}
await expect(restoreImageResource(entry, bytes, metadataOnlyStorage)).resolves.toBe('restored')
expect(saveImage).toHaveBeenCalledTimes(1)
const completeStorage = {
getImage: vi.fn(async () => ({
metadata: {
id: 'asset-3',
mimeType: 'image/png',
sizeBytes: 1,
createdAt: 1,
source: 'uploaded' as const,
},
data: 'x',
})),
saveImage: vi.fn(async () => 'asset-3'),
}
await expect(restoreImageResource(entry, bytes, completeStorage)).resolves.toBe('skipped')
expect(completeStorage.saveImage).not.toHaveBeenCalled()
})
})
@@ -51,7 +51,7 @@ describe('openExternalUrl', () => {
)
})
it('falls back to window.open when the desktop bridge fails', async () => {
it('does not fall back to window.open when the desktop bridge rejects the URL', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const openExternal = vi.fn().mockRejectedValue(new Error('shell failed'))
Object.defineProperty(window, 'electronAPI', {
@@ -62,16 +62,32 @@ describe('openExternalUrl', () => {
const result = await openExternalUrl('https://example.com', { logPrefix: 'Test' })
expect(result).toBe(true)
expect(result).toBe(false)
expect(openExternal).toHaveBeenCalledWith('https://example.com')
expect(openSpy).toHaveBeenCalledWith(
'https://example.com',
'_blank',
'noopener,noreferrer',
)
expect(openSpy).not.toHaveBeenCalled()
expect(consoleErrorSpy).toHaveBeenCalledWith(
'[Test] Failed to open external URL in Electron:',
expect.any(Error),
)
})
it('rejects unsupported protocols before any runtime opener is called', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const openExternal = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(window, 'electronAPI', {
configurable: true,
writable: true,
value: { shell: { openExternal } },
})
const result = await openExternalUrl('file:///tmp/prompt.txt', { logPrefix: 'Test' })
expect(result).toBe(false)
expect(openExternal).not.toHaveBeenCalled()
expect(openSpy).not.toHaveBeenCalled()
expect(consoleErrorSpy).toHaveBeenCalledWith(
'[Test] Refused to open unsupported external URL:',
'file:///tmp/prompt.txt',
)
})
})
@@ -325,6 +325,42 @@ describe('remote snapshot backup', () => {
])
})
it('restores remote image assets when local metadata exists but image bytes are missing', async () => {
const objectStore = createObjectStore()
const exported = await createRemoteSnapshotBackup({
objectStore,
dataManager: {
exportAllData: vi.fn(async () => JSON.stringify({ version: 1, data: {} })),
},
favoriteManager: null,
imageStorageService: createStorage([createImage('session-1', 'session-image')]),
favoriteImageStorageService: createStorage([]),
sections: {
appData: false,
favorites: false,
imageCache: true,
favoriteImages: false,
},
})
const target = createStorage([])
target.getMetadata.mockResolvedValue(createImage('session-1', 'metadata-only').metadata)
const result = await restoreRemoteSnapshotBackup({
objectStore,
snapshotId: exported.entry.id,
dataManager: {
importAllData: vi.fn(async () => {}),
},
favoriteManager: null,
imageStorageService: target,
favoriteImageStorageService: createStorage([]),
})
expect(result.restored).toBe(1)
expect(target.saveImage).toHaveBeenCalledTimes(1)
expect(target.store.has('session-1')).toBe(true)
})
it('does not restore sections that were not included in the remote snapshot', async () => {
const objectStore = createObjectStore()
const exportAllData = vi.fn(async () => JSON.stringify({ version: 1, data: { history: ['local'] } }))