Files
Genarrative/scripts/check-external-v1-scene-generation-smoke.mjs
T
kdletters eb7c00658a
Project CI / AI game creator shell Rust crates (push) Successful in 1m27s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m49s
Project CI / Backend tests (push) Successful in 5m9s
Project CI / Native shell tests (push) Successful in 6m14s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m6s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m12s
Project CI / Frontend tests (push) Successful in 2m42s
Project CI / AI game creator shell web tests (push) Successful in 1m41s
Project CI / Repository checks (push) Successful in 2m16s
补齐 External v1 场景生成路由的本地真实栈受理 smoke
- 新增 scripts/check-external-v1-scene-generation-smoke.mjs:dev 账号现建外部 API Key,校验非法 Key 401、缺幂等键/空 sceneContent/custom 缺 customStyle 各自 400、合法请求 202 且返回 operationId、同键重放同一 operationId、同键换请求体 409
- 轮询生成状态得到 completed 与 assetObjectId,并用 external read-url 换签名地址下载成功(200 image/png),证明 Provider 出图在本机可用
- 里程碑最后一条验收改为已勾选,并把先前「Provider 出图未验证」的环境结论更新为已实测
2026-09-28 19:52:16 +08:00

255 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// External v1 场景生成路由的本地真实栈受理 smoke。
//
// 用法:
// E2E_API_BASE 默认 http://127.0.0.1:4198
// E2E_EXTERNAL_API_KEY 可选;不给时脚本在 dev 环境注册一个账号并通过
// /api/profile/api-keys 现建一个 Key(dev 密码登录会自动注册)。
//
// 覆盖:非法 Key 401 → 缺幂等键 400 → 空 sceneContent 400 → custom 缺 customStyle 400 →
// 合法请求 202 且返回 operationId → 同键重放返回同一 operationId → 同键换请求体 409 →
// 按 operationId 查一次生成状态(Provider 出图按环境可用性如实记录,不作为受理判据)。
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:4198';
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
const DEV_PASSWORD = 'GenE2e123!';
let failures = 0;
function check(name, ok, detail = '') {
if (!ok) failures += 1;
console.log(
`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` :: ${detail}` : ''}`,
);
}
async function api(pathname, options = {}) {
const { method = 'GET', token, body, headers = {}, externalKey } = options;
const finalHeaders = { ...ENVELOPE, ...headers };
if (token) finalHeaders.Authorization = `Bearer ${token}`;
if (externalKey) finalHeaders.Authorization = `Bearer ${externalKey}`;
let finalBody;
if (body !== undefined) {
finalHeaders['Content-Type'] = 'application/json';
finalBody = JSON.stringify(body);
}
const response = await fetch(`${API}${pathname}`, {
method,
headers: finalHeaders,
body: finalBody,
});
const text = await response.text();
let json = null;
try {
json = JSON.parse(text);
} catch {
json = null;
}
return {
status: response.status,
text,
json,
data: json?.data,
error: json?.error,
};
}
async function resolveExternalKey() {
const provided = (process.env.E2E_EXTERNAL_API_KEY ?? '').trim();
if (provided) return provided;
const phone = `133${String(Date.now()).slice(-8)}`;
const registered = await api('/api/auth/entry', {
method: 'POST',
body: { purePhoneNumber: phone, password: DEV_PASSWORD },
});
const token = registered.data?.token;
check(
'dev 账号注册/登录成功',
registered.status === 200 && Boolean(token),
`status=${registered.status}`,
);
if (!token) process.exit(1);
const created = await api('/api/profile/api-keys', {
method: 'POST',
token,
body: { name: 'E2E 场景路由 smoke' },
});
const apiKey = created.data?.apiKey;
check(
'创建外部 API Key 成功',
created.status === 200 && Boolean(apiKey),
`status=${created.status}`,
);
if (!apiKey) process.exit(1);
return apiKey;
}
const SCENE_PATH = '/api/external/v1/editor/scenes/generations';
async function main() {
const apiKey = await resolveExternalKey();
const unauthorized = await api(SCENE_PATH, {
method: 'POST',
externalKey: 'tnr_sk_invalid_probe',
headers: { 'Idempotency-Key': `scene-smoke-unauthorized-${Date.now()}` },
body: { sceneContent: '夕阳下的海边小镇', stylePreset: 'flat' },
});
check(
'非法 API Key 被拒(401)',
unauthorized.status === 401,
`status=${unauthorized.status}`,
);
const stamp = Date.now();
const missingKey = await api(SCENE_PATH, {
method: 'POST',
externalKey: apiKey,
body: { sceneContent: '夕阳下的海边小镇', stylePreset: 'flat' },
});
check(
'缺 Idempotency-Key 被拒(400)',
missingKey.status === 400,
`status=${missingKey.status}`,
);
const blankScene = await api(SCENE_PATH, {
method: 'POST',
externalKey: apiKey,
headers: { 'Idempotency-Key': `scene-smoke-blank-${stamp}` },
body: { sceneContent: ' ', stylePreset: 'flat' },
});
check(
'空 sceneContent 被拒(400)',
blankScene.status === 400,
`status=${blankScene.status}`,
);
const customWithoutStyle = await api(SCENE_PATH, {
method: 'POST',
externalKey: apiKey,
headers: { 'Idempotency-Key': `scene-smoke-custom-${stamp}` },
body: { sceneContent: '夕阳下的海边小镇', stylePreset: 'custom' },
});
check(
'custom 风格缺 customStyle 被拒(400)',
customWithoutStyle.status === 400,
`status=${customWithoutStyle.status}`,
);
const idempotencyKey = `scene-smoke-${stamp}`;
const body = {
sceneContent: '夕阳下的海边小镇,远处有灯塔',
stylePreset: 'flat',
aspectRatio: '16:9',
imageSize: '1K',
generationInputs: { source: 'ai-game-creator-client' },
};
const accepted = await api(SCENE_PATH, {
method: 'POST',
externalKey: apiKey,
headers: { 'Idempotency-Key': idempotencyKey },
body,
});
const operationId = accepted.data?.operationId ?? accepted.data?.operation_id;
check(
'合法场景请求被受理(202 + operationId)',
accepted.status === 202 && Boolean(operationId),
`status=${accepted.status} operationId=${operationId ?? ''} msg=${accepted.error?.message ?? ''}`,
);
if (!operationId) process.exit(1);
const replayed = await api(SCENE_PATH, {
method: 'POST',
externalKey: apiKey,
headers: { 'Idempotency-Key': idempotencyKey },
body,
});
check(
'同键同请求重放返回同一 operationId',
replayed.status === 202 &&
(replayed.data?.operationId ?? replayed.data?.operation_id) ===
operationId,
`status=${replayed.status} operationId=${replayed.data?.operationId ?? ''}`,
);
const conflicting = await api(SCENE_PATH, {
method: 'POST',
externalKey: apiKey,
headers: { 'Idempotency-Key': idempotencyKey },
body: { ...body, sceneContent: '同一个幂等键换了画面内容' },
});
check(
'同键换请求体被拒(409)',
conflicting.status === 409,
`status=${conflicting.status} code=${conflicting.error?.code ?? ''}`,
);
let generation = null;
for (let attempt = 0; attempt < 12; attempt += 1) {
generation = await api(`/api/external/v1/generations/${operationId}`, {
externalKey: apiKey,
});
const status = generation.data?.status ?? '';
if (
status === 'completed' ||
status === 'succeeded' ||
status === 'failed' ||
status === 'cancelled'
) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 5_000));
}
const status = generation?.data?.status ?? 'unknown';
check(
'按 operationId 能查到生成状态',
generation?.status === 200 && status !== 'unknown',
`status=${generation?.status} generationStatus=${status}`,
);
const result = generation?.data?.result ?? {};
const assetObjectId = result.assetObjectId ?? result.asset?.assetObjectId;
const objectKey = result.asset?.objectKey ?? result.objectKey ?? '';
const providerProducedImage =
status === 'completed' && Boolean(assetObjectId);
check(
'Provider 出图成功并返回素材引用(completed + assetObjectId)',
providerProducedImage,
`generationStatus=${status} assetObjectId=${assetObjectId ?? ''}`,
);
if (providerProducedImage && objectKey) {
const readUrl = await api(
`/api/external/v1/assets/read-url?objectKey=${encodeURIComponent(objectKey)}`,
{ externalKey: apiKey },
);
const signedUrl = readUrl.data?.read?.signedUrl ?? readUrl.data?.signedUrl;
const imageResponse = signedUrl ? await fetch(signedUrl) : null;
const contentType = imageResponse?.headers.get('content-type') ?? '';
check(
'出图素材能通过 external read-url 换签名地址下载',
readUrl.status === 200 &&
Boolean(signedUrl) &&
imageResponse?.status === 200 &&
contentType.startsWith('image/'),
`readUrl=${readUrl.status} objectKey=${objectKey} download=${imageResponse?.status ?? 'n/a'} type=${contentType}`,
);
}
console.log(
`\nProvider 出图:${
providerProducedImage
? `已验证(assetObjectId=${assetObjectId}${objectKey ? ` objectKey=${objectKey}` : ''})`
: `未验证(当前 generationStatus=${status},本机图像 Provider 不可用时属环境限制)`
}`,
);
console.log(`生成状态原始响应:${generation?.text?.slice(0, 400) ?? ''}`);
console.log(`\n${failures === 0 ? '全部通过' : `${failures} 项失败`}`);
process.exit(failures === 0 ? 0 : 1);
}
main().catch((error) => {
console.error(
`[check-external-v1-scene-generation-smoke] 运行失败:${error}`,
);
process.exit(1);
});