UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

559 lines (489 loc) • 18 kB
// @vitest-environment node import { PromptBuilder } from '@saintno/comfyui-sdk'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TEST_CUSTOM_SD, TEST_SD35_MODELS, TEST_SDXL_MODELS, } from '@/server/services/comfyui/__tests__/fixtures/testModels'; import { mockContext } from '@/server/services/comfyui/__tests__/helpers/mockContext'; import { setupAllMocks } from '@/server/services/comfyui/__tests__/setup/unifiedMocks'; import { buildSimpleSDWorkflow } from '@/server/services/comfyui/workflows/simple-sd'; // Setup basic mocks vi.mock('@lobechat/utils', () => ({ generateUniqueSeeds: vi.fn(() => ({ seed: 123456, noiseSeed: 654321 })), })); vi.mock('../utils/workflowUtils', () => ({ getWorkflowFilenamePrefix: vi.fn(() => 'simple-sd'), })); vi.mock('../utils/staticModelLookup', () => ({ getModelConfig: vi.fn((modelName: string) => { // Mock model configuration mapping if (modelName.includes('sd3.5') || modelName.includes('sd35')) { return { modelFamily: 'SD3', variant: 'medium', }; } if (modelName.includes('sdxl') || modelName.includes('xl')) { return { modelFamily: 'SDXL', variant: 'base', }; } if (modelName.includes('sd1') || modelName.includes('v1')) { return { modelFamily: 'SD1', variant: '5', }; } if (modelName === TEST_CUSTOM_SD) { return { modelFamily: 'SD1', variant: 'custom', }; } return null; }), })); const { inputCalls } = setupAllMocks(); // Extended mock context for SD testing const createSDMockContext = () => ({ ...mockContext, modelResolverService: { ...mockContext.modelResolverService, getAvailableVAEFiles: vi .fn() .mockResolvedValue([ 'vae-ft-mse-840000-ema-pruned.safetensors', 'sdxl_vae_fp16fix.safetensors', 'custom_sd_lobe_vae.safetensors', ]), getOptimalComponent: vi.fn().mockImplementation((type: string, modelFamily: string) => { if (type === 'vae') { if (modelFamily === 'SDXL') { return Promise.resolve('sdxl_vae_fp16fix.safetensors'); } if (modelFamily === 'SD1') { return Promise.resolve('vae-ft-mse-840000-ema-pruned.safetensors'); } // SD3 models have built-in VAE return Promise.resolve(undefined); } return Promise.resolve(null); }), }, }); describe('buildSimpleSDWorkflow - Universal SD Support', () => { let sdMockContext: any; beforeEach(() => { vi.clearAllMocks(); sdMockContext = createSDMockContext(); }); describe('Model Family Detection Tests', () => { it('should detect SD3.5 model family', async () => { const modelName = TEST_SD35_MODELS.MEDIUM; const params = { cfg: 4.0, height: 1024, prompt: 'SD3.5 family test', steps: 28, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // SD3 models shouldn't need external VAE }); it('should detect SDXL model family', async () => { const modelName = TEST_SDXL_MODELS.BASE; const params = { cfg: 7.5, height: 1024, prompt: 'SDXL family test', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // SDXL models should use external VAE expect(sdMockContext.modelResolverService.getOptimalComponent).toHaveBeenCalledWith( 'vae', 'SDXL', ); }); it('should handle custom SD model', async () => { const modelName = TEST_CUSTOM_SD; const params = { cfg: 7.5, height: 512, prompt: 'Custom SD model test', steps: 20, width: 512, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // Custom models should check for custom VAE expect(sdMockContext.modelResolverService.getAvailableVAEFiles).toHaveBeenCalled(); }); }); describe('Smart VAE Selection Tests', () => { it('should not attach VAE for SD3 models (built-in VAE)', async () => { const modelName = TEST_SD35_MODELS.LARGE; const params = { cfg: 4.0, height: 1024, prompt: 'SD3 built-in VAE test', steps: 28, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // SD3 should not request external VAE }); it('should attach optimal VAE for SDXL models', async () => { const modelName = TEST_SDXL_MODELS.BASE; const params = { cfg: 7.5, height: 1024, prompt: 'SDXL VAE selection test', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); expect(sdMockContext.modelResolverService.getOptimalComponent).toHaveBeenCalledWith( 'vae', 'SDXL', ); }); it('should handle custom VAE for custom models', async () => { const modelName = TEST_CUSTOM_SD; const params = { cfg: 7.5, height: 512, prompt: 'Custom VAE test', steps: 20, width: 512, }; // Mock custom VAE availability sdMockContext.modelResolverService.getAvailableVAEFiles.mockResolvedValue([ 'custom_sd_lobe_vae.safetensors', 'vae-ft-mse-840000-ema-pruned.safetensors', ]); const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); expect(sdMockContext.modelResolverService.getAvailableVAEFiles).toHaveBeenCalled(); }); it('should fallback to built-in VAE when custom VAE unavailable', async () => { const modelName = TEST_CUSTOM_SD; const params = { cfg: 7.5, height: 512, prompt: 'VAE fallback test', steps: 20, width: 512, }; // Mock custom VAE not available sdMockContext.modelResolverService.getAvailableVAEFiles.mockResolvedValue([ 'vae-ft-mse-840000-ema-pruned.safetensors', ]); const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // Should fall back to built-in VAE }); }); describe('Dual Mode Support Tests', () => { it('should build text-to-image workflow', async () => { const modelName = TEST_SDXL_MODELS.BASE; const params = { cfg: 7.5, height: 1024, prompt: 'Text to image test', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // Should be in t2i mode (no input image) }); it('should build image-to-image workflow', async () => { const modelName = TEST_SDXL_MODELS.BASE; const params = { cfg: 7.5, denoise: 0.75, height: 1024, imageUrl: 'https://example.com/input.jpg', prompt: 'Image to image test', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // Should be in i2i mode with input image }); it('should handle strength parameter mapping to denoise', async () => { const modelName = TEST_SDXL_MODELS.BASE; const params = { cfg: 7.5, height: 1024, imageUrl: 'https://example.com/input.jpg', prompt: 'Strength mapping test', steps: 20, strength: 0.8, // Frontend parameter width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // Strength should be mapped to denoise internally }); it('should handle imageUrls array parameter', async () => { const modelName = TEST_SDXL_MODELS.BASE; const params = { cfg: 7.5, height: 1024, imageUrls: ['https://example.com/input1.jpg', 'https://example.com/input2.jpg'], prompt: 'Multiple images test', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // Should use first image from array }); }); describe('Parameter Validation Tests', () => { it('should handle different CFG values by model family', async () => { const testCases = [ { model: TEST_SD35_MODELS.MEDIUM, cfg: 4.0, family: 'SD3' }, { model: TEST_SDXL_MODELS.BASE, cfg: 7.5, family: 'SDXL' }, { model: TEST_CUSTOM_SD, cfg: 7.5, family: 'SD1' }, ]; for (const testCase of testCases) { const params = { cfg: testCase.cfg, height: 1024, prompt: `CFG test for ${testCase.family}`, steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(testCase.model, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); } }); it('should handle different schedulers by model family', async () => { const testCases = [ { model: TEST_SD35_MODELS.MEDIUM, scheduler: 'sgm_uniform' }, { model: TEST_SDXL_MODELS.BASE, scheduler: 'normal' }, { model: TEST_CUSTOM_SD, scheduler: 'normal' }, ]; for (const testCase of testCases) { const params = { cfg: 7.5, height: 1024, prompt: `Scheduler test: ${testCase.scheduler}`, scheduler: testCase.scheduler, steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(testCase.model, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); } }); it('should handle various image dimensions', async () => { const dimensions = [ { width: 512, height: 512 }, // SD1.5 default { width: 1024, height: 1024 }, // SDXL default { width: 768, height: 1024 }, // Portrait { width: 1344, height: 768 }, // Landscape ]; for (const dim of dimensions) { const params = { cfg: 7.5, height: dim.height, prompt: `Dimension test ${dim.width}x${dim.height}`, steps: 20, width: dim.width, }; const result = await buildSimpleSDWorkflow(TEST_SDXL_MODELS.BASE, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); } }); }); describe('Error Handling Tests', () => { it('should handle unknown model gracefully', async () => { const modelName = 'unknown-model.safetensors'; const params = { cfg: 7.5, height: 1024, prompt: 'Unknown model test', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(modelName, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); // Should work with default configuration }); it('should handle VAE resolution failure', async () => { const failingContext = { ...sdMockContext, modelResolverService: { ...sdMockContext.modelResolverService, getOptimalComponent: vi.fn().mockRejectedValue(new Error('VAE not found')), }, }; const params = { cfg: 7.5, height: 1024, prompt: 'VAE failure test', steps: 20, width: 1024, }; // Should not throw for SD3 models (built-in VAE) const result = await buildSimpleSDWorkflow(TEST_SD35_MODELS.MEDIUM, params, failingContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); }); it('should handle empty prompt', async () => { const params = { cfg: 7.5, height: 1024, prompt: '', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(TEST_SDXL_MODELS.BASE, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); }); }); describe('Advanced Features Tests', () => { it('should support negative prompts', async () => { const params = { cfg: 7.5, height: 1024, negativePrompt: 'low quality, blurry', prompt: 'High quality image', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow(TEST_SDXL_MODELS.BASE, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); }); it('should handle seed generation', async () => { const { generateUniqueSeeds } = await import('@lobechat/utils'); const params = { cfg: 7.5, height: 1024, prompt: 'Seed test', steps: 20, width: 1024, }; await buildSimpleSDWorkflow(TEST_SDXL_MODELS.BASE, params, sdMockContext); expect(generateUniqueSeeds).toHaveBeenCalled(); }); it('should support custom sampler settings', async () => { const params = { cfg: 7.5, height: 1024, prompt: 'Custom sampler test', samplerName: 'dpmpp_2m_sde', scheduler: 'karras', steps: 25, width: 1024, }; const result = await buildSimpleSDWorkflow(TEST_SDXL_MODELS.BASE, params, sdMockContext); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); }); }); describe('Backward Compatibility Tests', () => { it('should maintain API compatibility with existing calls', async () => { // Test with minimal parameters (existing API pattern) const minimalParams = { prompt: 'Backward compatibility test', }; const result = await buildSimpleSDWorkflow( TEST_SDXL_MODELS.BASE, minimalParams, sdMockContext, ); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); }); it('should handle legacy parameter names', async () => { const legacyParams = { cfg: 7.5, height: 1024, inputImage: 'https://example.com/legacy.jpg', // Legacy parameter prompt: 'Legacy parameter test', steps: 20, width: 1024, }; const result = await buildSimpleSDWorkflow( TEST_SDXL_MODELS.BASE, legacyParams, sdMockContext, ); expect(result).toHaveProperty('input'); expect(result).toHaveProperty('setInputNode'); expect(result).toHaveProperty('setOutputNode'); expect(result).toHaveProperty('workflow'); }); }); });