Merge pull request #345 from ArkciaTheDragon/fix/template-import-message-content
fix(template): preserve advanced template imports
This commit is contained in:
7 files changed
+370
-103
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
DataInvalidFormatError,
|
||||
DataInvalidJsonError,
|
||||
} from './errors';
|
||||
import { toErrorWithCode } from '../../utils/error';
|
||||
import { stripErrorCodeMarkers, toErrorWithCode } from '../../utils/error';
|
||||
|
||||
/**
|
||||
* 数据导入导出管理器
|
||||
@@ -142,7 +142,11 @@ export class DataManager implements IDataManager {
|
||||
await service.importData(dataToImport[dataKey]);
|
||||
console.log(`Successfully imported ${dataKey}`);
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to import ${dataKey}: ${error instanceof Error ? error.message : String(error)}`;
|
||||
// Nested structured errors may contain markers outside the leading position.
|
||||
const detail = stripErrorCodeMarkers(
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
const errorMessage = `Failed to import ${dataKey}: ${detail}`;
|
||||
errors.push(errorMessage);
|
||||
console.error(errorMessage, error);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BuiltinTemplateLanguage, ITemplateLanguageService } from './languageSer
|
||||
import { CORE_SERVICE_KEYS } from '../../constants/storage-keys';
|
||||
import { ImportExportError } from '../../interfaces/import-export';
|
||||
import { IMPORT_EXPORT_ERROR_CODES, TEMPLATE_ERROR_CODES } from '../../constants/error-codes';
|
||||
import { stripErrorCodeMarkers } from '../../utils/error';
|
||||
|
||||
|
||||
|
||||
@@ -332,11 +333,67 @@ export class TemplateManager implements ITemplateManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize and validate a template from imported data.
|
||||
*/
|
||||
private normalizeImportedTemplate(
|
||||
item: unknown,
|
||||
builtinTemplateIds: ReadonlySet<string> = new Set<string>()
|
||||
): Template {
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
throw new TemplateValidationError('Template must be an object');
|
||||
}
|
||||
|
||||
const template = item as Partial<Template>;
|
||||
if (typeof template.id !== 'string' || typeof template.name !== 'string') {
|
||||
throw new TemplateValidationError('Template id and name must be strings');
|
||||
}
|
||||
|
||||
// Reject corrupt shapes before applying defaults to legacy metadata.
|
||||
const rawMetadata = template.metadata;
|
||||
if (
|
||||
rawMetadata !== undefined &&
|
||||
(typeof rawMetadata !== 'object' || rawMetadata === null || Array.isArray(rawMetadata))
|
||||
) {
|
||||
throw new TemplateValidationError('Template metadata must be a non-array object when present');
|
||||
}
|
||||
const metadata: Partial<Template['metadata']> = rawMetadata ?? {};
|
||||
|
||||
let finalTemplateId = template.id;
|
||||
let finalTemplateName = template.name;
|
||||
|
||||
if (builtinTemplateIds.has(finalTemplateId)) {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
finalTemplateId = `user-${finalTemplateId}-${timestamp}-${random}`;
|
||||
finalTemplateName = `${finalTemplateName} (Imported copy)`;
|
||||
console.warn(`Detected conflict with built-in template ID: ${template.id}, renamed to: ${finalTemplateId}`);
|
||||
}
|
||||
|
||||
const userTemplate: Template = {
|
||||
...template,
|
||||
id: finalTemplateId,
|
||||
name: finalTemplateName,
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: metadata.version ?? '1.0.0',
|
||||
lastModified: Date.now(),
|
||||
templateType: metadata.templateType ?? 'optimize',
|
||||
author: metadata.author ?? 'User',
|
||||
...(metadata.description !== undefined && { description: metadata.description }),
|
||||
...(metadata.language !== undefined && { language: metadata.language })
|
||||
}
|
||||
} as Template;
|
||||
|
||||
this.validateTemplateSchema(userTemplate);
|
||||
this.validateTemplateId(userTemplate.id);
|
||||
return userTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入用户模板
|
||||
*/
|
||||
async importData(data: any): Promise<void> {
|
||||
// 基本格式验证:必须是数组
|
||||
if (!Array.isArray(data)) {
|
||||
throw new ImportExportError(
|
||||
'Invalid template data format: data must be an array of template objects',
|
||||
@@ -346,77 +403,44 @@ export class TemplateManager implements ITemplateManager {
|
||||
);
|
||||
}
|
||||
|
||||
const templates = data as Template[];
|
||||
|
||||
// Get existing user templates to clean up (替换模式)
|
||||
const existingTemplates = await this.listTemplates();
|
||||
const userTemplateIds = existingTemplates
|
||||
.filter(template => !template.isBuiltin)
|
||||
.map(template => template.id);
|
||||
const builtinTemplateIds = new Set(
|
||||
existingTemplates
|
||||
.filter(template => template.isBuiltin)
|
||||
.map(template => template.id)
|
||||
);
|
||||
const importedTemplates: Template[] = [];
|
||||
|
||||
// Delete all existing user templates
|
||||
for (const id of userTemplateIds) {
|
||||
// Validate and normalize the complete replacement before mutating storage.
|
||||
// This prevents malformed backups from deleting existing user templates.
|
||||
for (const [index, item] of data.entries()) {
|
||||
try {
|
||||
await this.deleteTemplate(id);
|
||||
const userTemplate = this.normalizeImportedTemplate(item, builtinTemplateIds);
|
||||
|
||||
// Match saveTemplate's duplicate-ID behavior: the last entry wins.
|
||||
const duplicateIndex = importedTemplates.findIndex(
|
||||
importedTemplate => importedTemplate.id === userTemplate.id
|
||||
);
|
||||
if (duplicateIndex >= 0) {
|
||||
importedTemplates[duplicateIndex] = userTemplate;
|
||||
} else {
|
||||
importedTemplates.push(userTemplate);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete template ${id}:`, error);
|
||||
const message = stripErrorCodeMarkers(
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
throw new ImportExportError(
|
||||
`Invalid template data at index ${index}: ${message}`,
|
||||
await this.getDataType(),
|
||||
error as Error,
|
||||
IMPORT_EXPORT_ERROR_CODES.VALIDATION_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const failedTemplates: { template: Template; error: Error }[] = [];
|
||||
|
||||
// Import each template individually, capturing failures
|
||||
for (const template of templates) {
|
||||
try {
|
||||
// 使用 validateData 验证单个模板
|
||||
if (!this.validateSingleTemplate(template)) {
|
||||
console.warn(`Skipping invalid template configuration:`, template);
|
||||
failedTemplates.push({ template, error: new Error('Invalid template configuration') });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否与内置模板ID冲突
|
||||
const builtinTemplate = existingTemplates.find(t => t.id === template.id && t.isBuiltin);
|
||||
let finalTemplateId = template.id;
|
||||
let finalTemplateName = template.name;
|
||||
|
||||
if (builtinTemplate) {
|
||||
// 为冲突的模板生成新的ID和名称
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substr(2, 6);
|
||||
finalTemplateId = `user-${template.id}-${timestamp}-${random}`;
|
||||
finalTemplateName = `${template.name} (Imported copy)`;
|
||||
console.warn(`Detected conflict with built-in template ID: ${template.id}, renamed to: ${finalTemplateId}`);
|
||||
}
|
||||
|
||||
// 确保导入的模板标记为用户模板,并为缺失字段提供默认值
|
||||
const userTemplate: Template = {
|
||||
...template,
|
||||
id: finalTemplateId,
|
||||
name: finalTemplateName,
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: template.metadata?.version || '1.0.0',
|
||||
lastModified: Date.now(), // 更新为当前时间
|
||||
templateType: template.metadata?.templateType || 'optimize', // 为旧版本数据提供默认类型
|
||||
author: template.metadata?.author || 'User', // 导入的模板标记为用户创建
|
||||
...(template.metadata?.description && { description: template.metadata.description }),
|
||||
...(template.metadata?.language && { language: template.metadata.language }) // 只在原本有language字段时才保留
|
||||
}
|
||||
};
|
||||
|
||||
await this.saveTemplate(userTemplate);
|
||||
console.log(`Imported template: ${finalTemplateId} (${finalTemplateName})`);
|
||||
} catch (error) {
|
||||
console.warn('Failed to import template:', error);
|
||||
failedTemplates.push({ template, error: error as Error });
|
||||
}
|
||||
}
|
||||
|
||||
if (failedTemplates.length > 0) {
|
||||
console.warn(`Failed to import ${failedTemplates.length} templates`);
|
||||
// 不抛出错误,允许部分成功的导入
|
||||
}
|
||||
// User templates are stored under one key, so replace them in one write.
|
||||
await this.persistUserTemplates(importedTemplates);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,14 +465,12 @@ export class TemplateManager implements ITemplateManager {
|
||||
* 验证单个模板配置
|
||||
*/
|
||||
private validateSingleTemplate(item: any): boolean {
|
||||
return typeof item === 'object' &&
|
||||
item !== null &&
|
||||
typeof item.id === 'string' &&
|
||||
typeof item.name === 'string' &&
|
||||
typeof item.content === 'string' &&
|
||||
typeof item.isBuiltin === 'boolean' &&
|
||||
typeof item.metadata === 'object' &&
|
||||
item.metadata !== null;
|
||||
try {
|
||||
this.normalizeImportedTemplate(item);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ export function isStructuredErrorLike(value: unknown): value is StructuredErrorL
|
||||
return isRecord(value) && typeof value.code === 'string'
|
||||
}
|
||||
|
||||
const ERROR_CODE_MARKER_PATTERN = /\[error\.[^\]]+\]\s*/g
|
||||
|
||||
/** Remove structured error-code markers before nesting a message in user-facing details. */
|
||||
export function stripErrorCodeMarkers(message: string): string {
|
||||
return message.replace(ERROR_CODE_MARKER_PATTERN, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize unknown values into an Error instance while preserving `{ code, params }`
|
||||
* when available (e.g., errors crossing IPC boundaries).
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IPreferenceService } from '../../../src/services/preference/types';
|
||||
import { ContextRepo } from '../../../src/services/context/types';
|
||||
import { MemoryStorageProvider } from '../../../src/services/storage/memoryStorageProvider';
|
||||
import { DATA_ERROR_CODES } from '../../../src/constants/error-codes';
|
||||
import { TemplateStorageError, TemplateValidationError } from '../../../src/services/template/errors';
|
||||
|
||||
describe('DataManager', () => {
|
||||
let dataManager: DataManager;
|
||||
@@ -214,6 +215,38 @@ describe('DataManager', () => {
|
||||
.rejects.toMatchObject({ code: DATA_ERROR_CODES.INVALID_JSON });
|
||||
});
|
||||
|
||||
it('should not leak inner error-code placeholders into the aggregate details', async () => {
|
||||
// Cover markers at both the leading and nested positions.
|
||||
const leadingMarker = new TemplateStorageError('Failed to save user templates: Storage error');
|
||||
expect(leadingMarker.message).toMatch(/^\[error\.template\.storage\]/);
|
||||
|
||||
const nestedMarker = new Error(
|
||||
`Invalid template data at index 0: ${new TemplateValidationError('Template validation failed: content: Too small').message}`
|
||||
);
|
||||
expect(nestedMarker.message).toContain('[error.template.validation]');
|
||||
expect(nestedMarker.message).not.toMatch(/^\[error\./);
|
||||
|
||||
const importData = {
|
||||
version: 1,
|
||||
data: {
|
||||
userTemplates: [{ id: 'tpl1', name: 'T', content: 'c', isBuiltin: false, metadata: { templateType: 'optimize', version: '1.0', lastModified: 0 } }],
|
||||
},
|
||||
};
|
||||
const payload = JSON.stringify(importData);
|
||||
|
||||
for (const innerError of [leadingMarker, nestedMarker]) {
|
||||
vi.mocked(mockTemplateManager.importData).mockRejectedValue(innerError);
|
||||
|
||||
const error = await dataManager.importAllData(payload).catch((reason: unknown) => reason) as {
|
||||
code?: string;
|
||||
params?: { details?: string };
|
||||
};
|
||||
expect(error.code).toBe(DATA_ERROR_CODES.IMPORT_PARTIAL_FAILED);
|
||||
expect(error.params?.details).toContain('Failed to import userTemplates');
|
||||
expect(error.params?.details).not.toContain('[error.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw an error for data without a "data" property in new format', async () => {
|
||||
await expect(dataManager.importAllData(JSON.stringify({ version: 1 })))
|
||||
.rejects.toMatchObject({ code: DATA_ERROR_CODES.INVALID_FORMAT });
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Template } from '../../../src/services/template/types';
|
||||
import { MemoryStorageProvider } from '../../../src/services/storage/memoryStorageProvider';
|
||||
import { TemplateLanguageService } from '../../../src/services/template/languageService';
|
||||
import { PreferenceService } from '../../../src/services/preference/service';
|
||||
import { IMPORT_EXPORT_ERROR_CODES } from '../../../src/constants/error-codes';
|
||||
|
||||
describe('TemplateManager Import/Export', () => {
|
||||
let templateManager: TemplateManager;
|
||||
@@ -244,12 +245,164 @@ describe('TemplateManager Import/Export', () => {
|
||||
expect(importedTemplate?.metadata.lastModified).toBeGreaterThan(0); // 应该被设置
|
||||
});
|
||||
|
||||
it('should skip invalid templates', async () => {
|
||||
it('should reject corrupt metadata types instead of silently defaulting them', async () => {
|
||||
// Invalid metadata containers must not be normalized into valid defaults.
|
||||
const existingTemplate: Template = {
|
||||
id: 'existing-template',
|
||||
name: 'Existing Template',
|
||||
content: 'Existing content',
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: Date.now(),
|
||||
templateType: 'optimize',
|
||||
author: 'User'
|
||||
}
|
||||
};
|
||||
await templateManager.saveTemplate(existingTemplate);
|
||||
|
||||
const corruptShapes: Array<[string, unknown]> = [
|
||||
['null', null],
|
||||
['string', 'corrupt'],
|
||||
['array', []],
|
||||
['number', 42],
|
||||
['boolean', true],
|
||||
];
|
||||
|
||||
for (const [label, metadata] of corruptShapes) {
|
||||
const item = {
|
||||
id: 'corrupt-template',
|
||||
name: `Corrupt metadata (${label})`,
|
||||
content: 'Corrupt content',
|
||||
isBuiltin: false,
|
||||
metadata
|
||||
};
|
||||
|
||||
expect(await templateManager.validateData([item])).toBe(false);
|
||||
await expect(templateManager.importData([item])).rejects.toMatchObject({
|
||||
code: IMPORT_EXPORT_ERROR_CODES.VALIDATION_ERROR,
|
||||
message: expect.stringContaining(
|
||||
'Template metadata must be a non-array object when present'
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// The corrupt batches must not have replaced the existing user templates.
|
||||
const userTemplates = (await templateManager.listTemplates())
|
||||
.filter(template => !template.isBuiltin);
|
||||
expect(userTemplates).toHaveLength(1);
|
||||
expect(userTemplates[0].id).toBe('existing-template');
|
||||
});
|
||||
|
||||
it('should treat absent metadata exactly like an empty metadata object', async () => {
|
||||
const absent = { id: 'absent-metadata', name: 'Absent', content: 'Content', isBuiltin: false };
|
||||
const empty = {
|
||||
id: 'empty-metadata',
|
||||
name: 'Empty',
|
||||
content: 'Content',
|
||||
isBuiltin: false,
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
expect(await templateManager.validateData([absent])).toBe(true);
|
||||
expect(await templateManager.validateData([empty])).toBe(true);
|
||||
|
||||
await templateManager.importData([absent, empty]);
|
||||
|
||||
const imported = (await templateManager.listTemplates())
|
||||
.filter(template => !template.isBuiltin);
|
||||
expect(imported).toHaveLength(2);
|
||||
|
||||
const [a, e] = ['absent-metadata', 'empty-metadata']
|
||||
.map(id => imported.find(template => template.id === id));
|
||||
for (const template of [a, e]) {
|
||||
expect(template?.metadata.version).toBe('1.0.0');
|
||||
expect(template?.metadata.templateType).toBe('optimize');
|
||||
expect(template?.metadata.author).toBe('User');
|
||||
expect(template?.metadata.lastModified).toBeGreaterThan(0);
|
||||
}
|
||||
expect(Object.keys(a!.metadata).sort()).toEqual(Object.keys(e!.metadata).sort());
|
||||
});
|
||||
|
||||
it('should align preflight validation with import normalization and ID rules', async () => {
|
||||
const incompleteMetadata = [
|
||||
{
|
||||
id: 'legacy-template',
|
||||
name: 'Legacy Template',
|
||||
content: 'Legacy content',
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
author: 'User'
|
||||
}
|
||||
}
|
||||
];
|
||||
const invalidId = [
|
||||
{
|
||||
id: 'INVALID-ID',
|
||||
name: 'Invalid ID Template',
|
||||
content: 'Invalid content',
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: Date.now(),
|
||||
templateType: 'optimize',
|
||||
author: 'User'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
expect(await templateManager.validateData(incompleteMetadata)).toBe(true);
|
||||
expect(await templateManager.validateData(invalidId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should import advanced templates with message-array content', async () => {
|
||||
const messages: Template['content'] = [
|
||||
{ role: 'system', content: 'You are a prompt optimizer.' },
|
||||
{ role: 'user', content: '{{originalPrompt}}' }
|
||||
];
|
||||
const importData: Template[] = [
|
||||
{
|
||||
id: 'advanced-template',
|
||||
name: 'Advanced Template',
|
||||
content: messages,
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: Date.now(),
|
||||
templateType: 'optimize',
|
||||
author: 'User'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
expect(await templateManager.validateData(importData)).toBe(true);
|
||||
await templateManager.importData(importData);
|
||||
|
||||
const importedTemplate = (await templateManager.listTemplates())
|
||||
.find(template => template.id === 'advanced-template');
|
||||
expect(importedTemplate?.content).toEqual(messages);
|
||||
});
|
||||
|
||||
it('should reject invalid templates without deleting existing templates', async () => {
|
||||
const existingTemplate: Template = {
|
||||
id: 'existing-template',
|
||||
name: 'Existing Template',
|
||||
content: 'Existing content',
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: Date.now(),
|
||||
templateType: 'optimize',
|
||||
author: 'User'
|
||||
}
|
||||
};
|
||||
await templateManager.saveTemplate(existingTemplate);
|
||||
|
||||
const importData = [
|
||||
{
|
||||
// 缺少id字段
|
||||
name: 'Invalid Template 1',
|
||||
content: 'Invalid content 1',
|
||||
// Missing id field.
|
||||
name: 'Invalid Template',
|
||||
content: 'Invalid content',
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
@@ -272,22 +425,21 @@ describe('TemplateManager Import/Export', () => {
|
||||
}
|
||||
];
|
||||
|
||||
// 应该不抛出错误,只是跳过无效模板
|
||||
await expect(templateManager.importData(importData)).resolves.not.toThrow();
|
||||
await expect(templateManager.importData(importData))
|
||||
.rejects.toThrow('Invalid template data at index 0');
|
||||
|
||||
// 验证有效模板被导入
|
||||
const afterImport = await templateManager.listTemplates();
|
||||
const userTemplates = afterImport.filter(t => !t.isBuiltin);
|
||||
expect(userTemplates.length).toBe(1);
|
||||
expect(userTemplates[0].id).toBe('valid-template');
|
||||
const userTemplates = (await templateManager.listTemplates())
|
||||
.filter(template => !template.isBuiltin);
|
||||
expect(userTemplates).toHaveLength(1);
|
||||
expect(userTemplates[0].id).toBe('existing-template');
|
||||
});
|
||||
|
||||
it('should handle import errors gracefully', async () => {
|
||||
const importData: Template[] = [
|
||||
it('should not embed inner error-code markers in the rejection details', async () => {
|
||||
const importData = [
|
||||
{
|
||||
id: 'error-template',
|
||||
name: 'Error Template',
|
||||
content: 'Error content',
|
||||
id: 'schema-fail',
|
||||
name: 'Schema Fail',
|
||||
content: '', // fails templateSchema: content must be non-empty
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
@@ -298,11 +450,59 @@ describe('TemplateManager Import/Export', () => {
|
||||
}
|
||||
];
|
||||
|
||||
// 模拟saveTemplate错误
|
||||
vi.spyOn(templateManager, 'saveTemplate').mockRejectedValue(new Error('Save template error'));
|
||||
const error = await templateManager.importData(importData)
|
||||
.catch((reason: unknown) => reason) as {
|
||||
code?: string;
|
||||
message?: string;
|
||||
params?: { details?: string };
|
||||
};
|
||||
expect(error.code).toBe(IMPORT_EXPORT_ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Invalid template data at index 0');
|
||||
expect(error.message).toContain('Template validation failed');
|
||||
expect(error.message).not.toContain('[error.');
|
||||
expect(error.params?.details).not.toContain('[error.');
|
||||
});
|
||||
|
||||
// 应该不抛出错误,只是记录失败
|
||||
await expect(templateManager.importData(importData)).resolves.not.toThrow();
|
||||
it('should preserve existing templates when the replacement write fails', async () => {
|
||||
const existingTemplate: Template = {
|
||||
id: 'existing-template',
|
||||
name: 'Existing Template',
|
||||
content: 'Existing content',
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: Date.now(),
|
||||
templateType: 'optimize',
|
||||
author: 'User'
|
||||
}
|
||||
};
|
||||
await templateManager.saveTemplate(existingTemplate);
|
||||
|
||||
const importData: Template[] = [
|
||||
{
|
||||
id: 'replacement-template',
|
||||
name: 'Replacement Template',
|
||||
content: 'Replacement content',
|
||||
isBuiltin: false,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: Date.now(),
|
||||
templateType: 'optimize',
|
||||
author: 'User'
|
||||
}
|
||||
}
|
||||
];
|
||||
const setItemSpy = vi.spyOn(storageProvider, 'setItem')
|
||||
.mockRejectedValueOnce(new Error('Storage error'));
|
||||
|
||||
await expect(templateManager.importData(importData))
|
||||
.rejects.toThrow('Failed to save user templates');
|
||||
setItemSpy.mockRestore();
|
||||
|
||||
const userTemplates = (await templateManager.listTemplates())
|
||||
.filter(template => !template.isBuiltin);
|
||||
expect(userTemplates).toHaveLength(1);
|
||||
expect(userTemplates[0].id).toBe('existing-template');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ function setupPreferenceHandlers() {
|
||||
|
||||
ipcMain.handle('preference-getDataType', async (event) => {
|
||||
try {
|
||||
const result = preferenceService.getDataType();
|
||||
const result = await preferenceService.getDataType();
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return createErrorResponse(error);
|
||||
@@ -1453,7 +1453,7 @@ function setupIPC() {
|
||||
|
||||
ipcMain.handle('model-getDataType', async (event) => {
|
||||
try {
|
||||
const result = modelManager.getDataType();
|
||||
const result = await modelManager.getDataType();
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return createErrorResponse(error);
|
||||
@@ -1575,7 +1575,7 @@ function setupIPC() {
|
||||
|
||||
ipcMain.handle('template-getDataType', async (event) => {
|
||||
try {
|
||||
const result = templateManager.getDataType();
|
||||
const result = await templateManager.getDataType();
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return createErrorResponse(error);
|
||||
@@ -1586,7 +1586,7 @@ function setupIPC() {
|
||||
try {
|
||||
// 清理Vue响应式对象,防止IPC序列化错误
|
||||
const safeData = safeSerialize(data);
|
||||
const result = templateManager.validateData(safeData);
|
||||
const result = await templateManager.validateData(safeData);
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return createErrorResponse(error);
|
||||
@@ -1623,7 +1623,7 @@ function setupIPC() {
|
||||
|
||||
ipcMain.handle('template-getSupportedLanguages', async (event, template) => {
|
||||
try {
|
||||
const result = templateManager.getSupportedLanguages(template);
|
||||
const result = await templateManager.getSupportedLanguages(template);
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return createErrorResponse(error);
|
||||
@@ -1751,7 +1751,7 @@ function setupIPC() {
|
||||
|
||||
ipcMain.handle('history-getDataType', async (event) => {
|
||||
try {
|
||||
const result = historyManager.getDataType();
|
||||
const result = await historyManager.getDataType();
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return createErrorResponse(error);
|
||||
@@ -1904,7 +1904,7 @@ function setupIPC() {
|
||||
|
||||
ipcMain.handle('context-getDataType', async (event) => {
|
||||
try {
|
||||
const result = contextRepo.getDataType();
|
||||
const result = await contextRepo.getDataType();
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return createErrorResponse(error);
|
||||
|
||||
@@ -826,6 +826,7 @@ import {
|
||||
} from '../utils/remote-snapshot-backup'
|
||||
import { recordDataBackupCompleted } from '../utils/data-backup-reminder'
|
||||
import { openExternalUrl } from '../utils/open-external-url'
|
||||
import { formatErrorSummary } from '../utils/error'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
@@ -1949,7 +1950,7 @@ const handleImport = async () => {
|
||||
clearSelectedFile()
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error)
|
||||
toast.error(`${t('dataManager.import.failed')}: ${(error as Error).message}`)
|
||||
toast.error(formatErrorSummary(t('dataManager.import.failed'), error))
|
||||
} finally {
|
||||
isImporting.value = false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user