Files
Genarrative/server-node/src/services/sceneImageService.test.ts
高物 09d4c0c31b
Some checks failed
CI / verify (push) Has been cancelled
11
2026-04-16 21:47:20 +08:00

300 lines
8.7 KiB
TypeScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { type AppConfig } from '../config.js';
import type { AppContext } from '../context.js';
import { generateSceneImage } from './sceneImageService.js';
const PNG_BUFFER = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+7aQAAAAASUVORK5CYII=',
'base64',
);
function createTestConfig(
projectRoot: string,
dashScopeBaseUrl: string,
): AppConfig {
return {
projectRoot,
publicDir: path.join(projectRoot, 'public'),
dashScope: {
baseUrl: dashScopeBaseUrl,
apiKey: 'test-dashscope-key',
imageModel: 'wan2.2-t2i-flash',
requestTimeoutMs: 5_000,
},
} as AppConfig;
}
function readRequestBody(req: IncomingMessage) {
return new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', (chunk) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
function sendJson(res: ServerResponse, payload: unknown) {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(payload));
}
async function withHttpServer<T>(
buildHandler: (baseUrl: string) => (
req: IncomingMessage,
res: ServerResponse,
) => void | Promise<void>,
run: (baseUrl: string) => Promise<T>,
) {
let handler: (
req: IncomingMessage,
res: ServerResponse,
) => void | Promise<void> = () => undefined;
const server = createServer((req, res) => {
Promise.resolve(handler(req, res)).catch((error) => {
res.statusCode = 500;
res.end(error instanceof Error ? error.stack : String(error));
});
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => resolve());
});
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('failed to resolve test server address');
}
const baseUrl = `http://127.0.0.1:${address.port}`;
handler = buildHandler(baseUrl);
try {
return await run(baseUrl);
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
}
test('generateSceneImage uses wan2.2-t2i-flash text-to-image payload and saves the generated scene', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genarrative-scene-image-'));
const capturedRequests: Array<{
pathname: string;
bodyText?: string;
}> = [];
await withHttpServer(
(baseUrl) => async (req, res) => {
const url = new URL(req.url || '/', baseUrl);
const bodyText =
req.method === 'POST' ? (await readRequestBody(req)).toString('utf8') : undefined;
capturedRequests.push({
pathname: url.pathname,
bodyText,
});
if (
req.method === 'POST' &&
url.pathname === '/api/v1/services/aigc/image-generation/generation'
) {
sendJson(res, {
output: {
task_id: 'scene-task-1',
},
});
return;
}
if (req.method === 'GET' && url.pathname === '/api/v1/tasks/scene-task-1') {
sendJson(res, {
output: {
task_status: 'SUCCEEDED',
results: [
{
url: `${baseUrl}/downloads/scene.png`,
actual_prompt: '整理后的场景提示词',
},
],
},
});
return;
}
if (req.method === 'GET' && url.pathname === '/downloads/scene.png') {
res.statusCode = 200;
res.setHeader('Content-Type', 'image/png');
res.end(PNG_BUFFER);
return;
}
res.statusCode = 404;
res.end('not found');
},
async (dashScopeBaseUrl) => {
const context = {
config: createTestConfig(tempRoot, `${dashScopeBaseUrl}/api/v1`),
} as AppContext;
const result = await generateSceneImage(context, {
prompt: '海雾港口像素风场景',
negativePrompt: '模糊',
size: '1280*720',
model: 'wan2.7-image',
worldName: '潮雾群岛',
profileId: 'world-1',
landmarkName: '旧港灯塔',
landmarkId: 'landmark-1',
});
assert.equal(result.ok, true);
assert.match(result.imageSrc, /^\/generated-custom-world-scenes\//u);
assert.equal(result.actualPrompt, '整理后的场景提示词');
const createRequest = capturedRequests.find(
(entry) => entry.pathname === '/api/v1/services/aigc/image-generation/generation',
);
assert.ok(createRequest?.bodyText);
const createPayload = JSON.parse(createRequest.bodyText) as {
model: string;
input: {
messages: Array<{
content: Array<{ text?: string; image?: string }>;
}>;
};
parameters: {
negative_prompt?: string;
};
};
const content = createPayload.input.messages[0]?.content ?? [];
assert.equal(createPayload.model, 'wan2.2-t2i-flash');
assert.equal(content[0]?.text, '海雾港口像素风场景');
assert.equal(content.length, 1);
assert.equal(createPayload.parameters.negative_prompt, '模糊');
const savedImagePath = path.join(tempRoot, 'public', result.imageSrc.slice(1));
assert.equal(fs.existsSync(savedImagePath), true);
},
);
});
test('generateSceneImage uses qwen-image-2.0 edit flow when a reference image is provided', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genarrative-scene-image-'));
const publicDir = path.join(tempRoot, 'public');
fs.mkdirSync(path.join(publicDir, 'scene_bg'), { recursive: true });
fs.writeFileSync(path.join(publicDir, 'scene_bg', 'reference-layout.png'), PNG_BUFFER);
const capturedRequests: Array<{
pathname: string;
bodyText?: string;
}> = [];
await withHttpServer(
(baseUrl) => async (req, res) => {
const url = new URL(req.url || '/', baseUrl);
const bodyText =
req.method === 'POST' ? (await readRequestBody(req)).toString('utf8') : undefined;
capturedRequests.push({
pathname: url.pathname,
bodyText,
});
if (
req.method === 'POST' &&
url.pathname === '/api/v1/services/aigc/multimodal-generation/generation'
) {
sendJson(res, {
output: {
choices: [
{
message: {
content: [
{
image: `${baseUrl}/downloads/reference-scene.png`,
},
],
},
},
],
},
});
return;
}
if (req.method === 'GET' && url.pathname === '/downloads/reference-scene.png') {
res.statusCode = 200;
res.setHeader('Content-Type', 'image/png');
res.end(PNG_BUFFER);
return;
}
res.statusCode = 404;
res.end('not found');
},
async (dashScopeBaseUrl) => {
const context = {
config: createTestConfig(tempRoot, `${dashScopeBaseUrl}/api/v1`),
} as AppContext;
const result = await generateSceneImage(context, {
prompt: '废墟月台像素风场景',
negativePrompt: '模糊',
size: '1280*720',
worldName: '碎轨边境',
profileId: 'world-2',
landmarkName: '裂轨月台',
landmarkId: 'landmark-2',
referenceImageSrc: '/scene_bg/reference-layout.png',
});
assert.equal(result.ok, true);
assert.equal(result.model, 'qwen-image-2.0');
assert.match(result.taskId, /^scene-edit-/u);
assert.equal(
capturedRequests.some(
(entry) => entry.pathname === '/api/v1/tasks/scene-task-1',
),
false,
);
const createRequest = capturedRequests.find(
(entry) =>
entry.pathname === '/api/v1/services/aigc/multimodal-generation/generation',
);
assert.ok(createRequest?.bodyText);
const createPayload = JSON.parse(createRequest.bodyText) as {
model: string;
input: {
messages: Array<{
content: Array<{ text?: string; image?: string }>;
}>;
};
};
const content = createPayload.input.messages[0]?.content ?? [];
assert.equal(createPayload.model, 'qwen-image-2.0');
assert.match(content[0]?.image ?? '', /^data:image\/png;base64,/u);
assert.equal(content[1]?.text, '废墟月台像素风场景');
},
);
});