Files
MiragenFlow/server/tests/provider-images.test.ts
T

384 lines
22 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { encryptSecret } from "../src/shared/auth.ts";
import { createStore } from "../src/store.ts";
import { invokeProvider, probeProviderModels, queryProvider } from "../src/adapters/provider.ts";
const png = Buffer.from("89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489", "hex");
function channel(store: ReturnType<typeof createStore>) {
return {
...store.channels[0],
providerType: "openai-images" as const,
baseUrl: "https://1.1.1.1/v1",
models: [
{ requestModelId: "base-model", source: "manual" as const, enabled: true },
{ requestModelId: "mapped-2k", source: "manual" as const, enabled: true },
{ requestModelId: "供应商模型", source: "manual" as const, enabled: true },
],
modelMappings: [],
secretRef: encryptSecret("1234567890123456", store.channelEncryptionKey),
};
}
test("OpenAI image generation uses the mapped model and normalized v1 endpoint", async () => {
const store = createStore();
const requests: Array<{ url: string; body: unknown; headers: Headers }> = [];
const previousFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => {
requests.push({ url: String(input), body: init?.body, headers: new Headers(init?.headers) });
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json", "x-request-id": "request-123" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-1", taskType: "image", requestModelId: "base-model", prompt: "一只猫", count: 1, params: { resolution: "2K" }, platformIdempotencyKey: "idem-1" });
assert.equal(result.status, "succeeded");
assert.equal(result.providerRequestId, "request-123");
assert.equal(result.outputs?.[0]?.mimeType, "image/png");
assert.equal(requests[0]?.url, "https://1.1.1.1/v1/images/generations");
assert.equal(requests[0]?.headers.get("authorization"), "Bearer 1234567890123456");
assert.equal(requests[0]?.headers.get("idempotency-key"), "idem-1");
assert.equal((JSON.parse(String(requests[0]?.body)) as Record<string, unknown>).model, "base-model");
} finally {
globalThis.fetch = previousFetch;
}
});
test("display model IDs resolve to distinct upstream request model IDs", async () => {
const store = createStore();
const configured = { ...channel(store), modelMappings: [{ displayModelId: "平台模型", requestModelId: "供应商模型" }] };
const previousFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
assert.equal((JSON.parse(String(init?.body)) as Record<string, unknown>).model, "供应商模型");
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, configured, { taskId: "mapping-task", taskType: "image", publicModelId: "平台模型", prompt: "测试", params: { resolution: "2K" }, platformIdempotencyKey: "mapping-idem" });
assert.equal(result.status, "succeeded");
} finally {
globalThis.fetch = previousFetch;
}
});
test("OpenAI image edits preserve reference order with image[] fields", async () => {
const store = createStore();
for (const [id, color] of [["ref-a", 0x11], ["ref-b", 0x22]] as const) {
const bytes = Buffer.from(png);
bytes[bytes.length - 1] ^= color;
store.objects.set(id, { id, ownerId: "user-1", mimeType: "image/png", data: bytes.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
}
const previousFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
const form = init?.body as FormData;
const names = [...form.getAll("image[]")].map((value) => (value instanceof File ? value.name : ""));
assert.deepEqual(names, ["reference-0.png", "reference-1.png"]);
assert.equal(form.get("model"), "base-model");
assert.equal(new Headers(init?.headers).get("idempotency-key"), "idem-2");
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-2", taskType: "image", requestModelId: "base-model", prompt: "@图片一和@图片二", count: 1, referenceImages: [{ objectId: "ref-b", seq: 2, name: "图片二" }, { objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-2" });
assert.equal(result.status, "succeeded");
} finally {
globalThis.fetch = previousFetch;
}
});
test("invalid image base64 and empty model probes fail closed", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
globalThis.fetch = async (input) => {
if (String(input).endsWith("/models")) return new Response(JSON.stringify({ data: [] }), { status: 200, headers: { "content-type": "application/json" } });
return new Response(JSON.stringify({ data: [{ b64_json: "not-base64" }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-3", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: "idem-3" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
await assert.rejects(() => probeProviderModels("https://1.1.1.1/v1", "1234567890123456"));
} finally {
globalThis.fetch = previousFetch;
}
});
test("duplicate references are uploaded once and URL outputs are validated", async () => {
const store = createStore();
store.objects.set("ref-a", { id: "ref-a", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
const previousFetch = globalThis.fetch;
const urls: string[] = [];
globalThis.fetch = async (input, init) => {
urls.push(String(input));
if (String(input).endsWith("/images/edits")) {
const form = init?.body as FormData;
assert.equal(form.getAll("image[]").length, 1);
assert.equal(form.get("prompt"), "使用第一张图");
return new Response(JSON.stringify({ data: [{ url: "https://1.1.1.1/result.png" }] }), { status: 200, headers: { "content-type": "application/json" } });
}
return new Response(png, { status: 200, headers: { "content-type": "image/png" } });
};
try {
const result = await invokeProvider(store, channel(store), {
taskId: "task-4", requestModelId: "base-model",
taskType: "image",
prompt: "使用@图片一",
count: 1,
referenceImages: [
{ objectId: "ref-a", seq: 1, name: "图片一" },
{ objectId: "ref-a", seq: 2, name: "图片二" },
],
platformIdempotencyKey: "idem-4",
});
assert.equal(result.status, "succeeded");
assert.equal(result.outputs?.[0]?.mimeType, "image/png");
assert.deepEqual(urls, ["https://1.1.1.1/v1/images/edits", "https://1.1.1.1/result.png"]);
} finally {
globalThis.fetch = previousFetch;
}
});
test("unavailable reference objects fail without retrying the provider", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
let called = false;
globalThis.fetch = async () => {
called = true;
return new Response(JSON.stringify({ data: [] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), {
taskId: "task-5", requestModelId: "base-model",
taskType: "image",
prompt: "使用第一张图",
count: 1,
referenceImages: [{ objectId: "missing", seq: 1, name: "图片一" }],
platformIdempotencyKey: "idem-5",
});
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
assert.equal(called, false);
} finally {
globalThis.fetch = previousFetch;
}
});
test("expired, revoked, and missing staging references fail deterministically", async () => {
const store = createStore();
store.objects.set("expired", { id: "expired", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() - 1).toISOString() });
store.objects.set("revoked", { id: "revoked", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString(), revoked: true });
store.objects.set("staging-missing", { id: "staging-missing", ownerId: "user-1", mimeType: "image/png", stagingKey: "missing/reference.png", expiresAt: new Date(Date.now() + 60_000).toISOString() });
const previousFetch = globalThis.fetch;
let called = false;
globalThis.fetch = async () => { called = true; return new Response(JSON.stringify({ data: [] }), { status: 200 }); };
try {
for (const [index, objectId] of ["expired", "revoked", "staging-missing"].entries()) {
const result = await invokeProvider(store, channel(store), { taskId: `reference-unavailable-${index}`, taskType: "image", requestModelId: "base-model", prompt: "使用第一张图", count: 1, referenceImages: [{ objectId, seq: 1, name: "图片一" }], platformIdempotencyKey: `reference-unavailable-${index}` });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
assert.equal(result.retryable, false);
}
assert.equal(called, false);
} finally {
globalThis.fetch = previousFetch;
}
});
test("invalid image output URLs are non-retryable provider failures", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
const outputs = [
"not a url",
"https://127.0.0.1/result.png",
"https://1.1.1.1/result.png",
];
try {
for (const [index, outputUrl] of outputs.entries()) {
globalThis.fetch = async (input) => {
if (String(input).endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ url: outputUrl }] }), { status: 200, headers: { "content-type": "application/json" } });
return new Response(index === 2 ? png : "not an image", { status: index === 2 ? 404 : 200, headers: { "content-type": "image/png" } });
};
const result = await invokeProvider(store, channel(store), { taskId: `invalid-output-${index}`, taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: `invalid-output-${index}` });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
assert.equal(result.retryable, false);
}
} finally {
globalThis.fetch = previousFetch;
}
});
test("redirected image output URLs are rejected as invalid output", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
let downloadRedirected = false;
globalThis.fetch = async (input) => {
if (String(input).endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ url: "https://1.1.1.1/result.png" }] }), { status: 200, headers: { "content-type": "application/json" } });
downloadRedirected = true;
return new Response(null, { status: 302, headers: { location: "https://example.com/private.png" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "redirect-output", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: "redirect-output" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_OUTPUT");
assert.equal(result.retryable, false);
assert.equal(downloadRedirected, true);
} finally {
globalThis.fetch = previousFetch;
}
});
test("image output metadata prefers dimensions decoded from the returned bytes", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
globalThis.fetch = async (input) => {
if (String(input).endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64"), size: "999x999" }] }), { status: 200, headers: { "content-type": "application/json" } });
return new Response(null, { status: 500 });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "metadata-bytes", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, platformIdempotencyKey: "metadata-bytes" });
assert.equal(result.status, "succeeded");
assert.deepEqual(result.outputs?.[0]?.metadata, { width: 1, height: 1, format: "png", size: "1x1", revisedPrompt: undefined, usage: undefined, source: "base64" });
} finally {
globalThis.fetch = previousFetch;
}
});
test("image references validate mentions and add a default reference instruction", async () => {
const store = createStore();
store.objects.set("ref-a", { id: "ref-a", ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
const previousFetch = globalThis.fetch;
const prompts: string[] = [];
globalThis.fetch = async (_input, init) => {
const form = init?.body as FormData;
prompts.push(String(form.get("prompt")));
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const result = await invokeProvider(store, channel(store), { taskId: "task-6", taskType: "image", requestModelId: "base-model", prompt: "请保持主体", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-6" });
assert.equal(result.status, "succeeded");
assert.equal(prompts[0], "请保持主体\n基于以下参考图生成。");
const missing = await invokeProvider(store, channel(store), { taskId: "task-7", taskType: "image", requestModelId: "base-model", prompt: "使用@图片二", count: 1, referenceImages: [{ objectId: "ref-a", seq: 1, name: "图片一" }], platformIdempotencyKey: "idem-7" });
assert.equal(missing.status, "failed");
assert.equal(missing.errorCode, "PROVIDER_REFERENCE_UNAVAILABLE");
} finally {
globalThis.fetch = previousFetch;
}
});
test("invalid provider reference sequences fail closed", async () => {
const store = createStore();
const result = await invokeProvider(store, channel(store), { taskId: "task-8", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, referenceImages: [{ objectId: "ref-a", seq: 17, name: "图片十七" }], platformIdempotencyKey: "idem-8" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_INVALID");
});
test("duplicate reference sequences are rejected instead of producing ambiguous 图片 names", async () => {
const store = createStore();
for (const objectId of ["duplicate-seq-a", "duplicate-seq-b"]) store.objects.set(objectId, { id: objectId, ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
const result = await invokeProvider(store, channel(store), { taskId: "duplicate-seq", taskType: "image", requestModelId: "base-model", prompt: "测试", count: 1, referenceImages: [{ objectId: "duplicate-seq-a", seq: 1, name: "图片一" }, { objectId: "duplicate-seq-b", seq: 1, name: "图片一" }], platformIdempotencyKey: "duplicate-seq" });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_REFERENCE_INVALID");
});
test("provider image requests reject non-positive or fractional counts", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
let called = false;
globalThis.fetch = async () => { called = true; return new Response(JSON.stringify({ data: [] }), { status: 200 }); };
try {
for (const count of [0, -1, 1.5]) {
const result = await invokeProvider(store, channel(store), { taskId: `invalid-count-${count}`, taskType: "image", requestModelId: "base-model", prompt: "测试", count, platformIdempotencyKey: `invalid-count-${String(count).replace(".", "-")}` });
assert.equal(result.status, "failed");
assert.equal(result.errorCode, "PROVIDER_INVALID_REQUEST");
}
assert.equal(called, false);
} finally {
globalThis.fetch = previousFetch;
}
});
test("OpenAI image edits accept the full sixteen-reference boundary and reject the seventeenth", async () => {
const store = createStore();
const references = Array.from({ length: 16 }, (_, index) => {
const objectId = `boundary-ref-${index + 1}`;
store.objects.set(objectId, { id: objectId, ownerId: "user-1", mimeType: "image/png", data: png.toString("base64"), expiresAt: new Date(Date.now() + 60_000).toISOString() });
return { objectId, seq: index + 1, name: `图片${index + 1}` };
});
const previousFetch = globalThis.fetch;
globalThis.fetch = async (_input, init) => {
const form = init?.body as FormData;
assert.equal(form.getAll("image[]").length, 16);
return new Response(JSON.stringify({ data: [{ b64_json: png.toString("base64") }] }), { status: 200, headers: { "content-type": "application/json" } });
};
try {
const accepted = await invokeProvider(store, channel(store), { taskId: "sixteen-references", taskType: "image", requestModelId: "base-model", prompt: "组合参考图", count: 1, referenceImages: references, platformIdempotencyKey: "sixteen-references" });
assert.equal(accepted.status, "succeeded");
const rejected = await invokeProvider(store, channel(store), { taskId: "seventeen-references", taskType: "image", requestModelId: "base-model", prompt: "组合参考图", count: 1, referenceImages: [...references, { objectId: "boundary-ref-1", seq: 17, name: "图片十七" }], platformIdempotencyKey: "seventeen-references" });
assert.equal(rejected.status, "failed");
assert.equal(rejected.errorCode, "PROVIDER_REFERENCE_LIMIT");
} finally {
globalThis.fetch = previousFetch;
}
});
test("provider output metadata detects JPEG and WebP bytes instead of trusting a declared format", async () => {
const store = createStore();
const jpeg = Buffer.from("ffd8ffc00011080001000101011100021100031100ffd9", "hex");
const webp = Buffer.alloc(30);
webp.write("RIFF", 0, "ascii"); webp.writeUInt32LE(22, 4); webp.write("WEBP", 8, "ascii"); webp.write("VP8X", 12, "ascii"); webp.writeUInt32LE(10, 16);
const previousFetch = globalThis.fetch;
let requestedFormat = "jpeg";
globalThis.fetch = async (input) => {
const url = String(input);
if (url.endsWith("/images/generations")) return new Response(JSON.stringify({ data: [{ url: `https://1.1.1.1/result.${requestedFormat}` }] }), { status: 200, headers: { "content-type": "application/json" } });
const bytes = requestedFormat === "jpeg" ? jpeg : webp;
return new Response(bytes, { status: 200, headers: { "content-type": requestedFormat === "jpeg" ? "image/jpg" : "image/webp" } });
};
try {
for (const [index, suffix] of ["jpeg", "webp"].entries()) {
requestedFormat = suffix;
const result = await invokeProvider(store, channel(store), { taskId: `format-${suffix}`, taskType: "image", requestModelId: "base-model", prompt: "格式测试", count: 1, params: { output_format: suffix }, platformIdempotencyKey: `format-${index}` });
assert.equal(result.status, "succeeded");
assert.equal(result.outputs?.[0]?.mimeType, `image/${suffix}`);
assert.deepEqual(result.outputs?.[0]?.metadata && { width: result.outputs[0].metadata.width, height: result.outputs[0].metadata.height, format: result.outputs[0].metadata.format }, { width: 1, height: 1, format: suffix });
}
} finally {
globalThis.fetch = previousFetch;
}
});
test("provider probes use GET and Bearer authentication, and transport failures become unknown", async () => {
const store = createStore();
const previousFetch = globalThis.fetch;
const probeRequests: Array<{ method: string; authorization: string | null }> = [];
globalThis.fetch = async (input, init) => {
if (String(input).endsWith("/models")) {
probeRequests.push({ method: init?.method || "GET", authorization: new Headers(init?.headers).get("authorization") });
return new Response(JSON.stringify({ data: [{ id: "gpt-image-2" }] }), { status: 200, headers: { "content-type": "application/json" } });
}
throw new Error("socket closed");
};
try {
const probe = await probeProviderModels("https://1.1.1.1/v1", "1234567890123456");
assert.deepEqual(probe, { healthy: true, models: ["gpt-image-2"] });
assert.deepEqual(probeRequests, [{ method: "GET", authorization: "Bearer 1234567890123456" }]);
const unknown = await invokeProvider(store, channel(store), { taskId: "transport-unknown", taskType: "image", requestModelId: "base-model", prompt: "网络中断", count: 1, platformIdempotencyKey: "transport-unknown" });
assert.equal(unknown.status, "unknown");
assert.equal(unknown.errorCode, "PROVIDER_TRANSPORT_UNKNOWN");
const abortedFetch = globalThis.fetch;
globalThis.fetch = async () => { throw Object.assign(new Error("timeout"), { name: "AbortError" }); };
const timeout = await invokeProvider(store, channel(store), { taskId: "timeout-unknown", taskType: "image", requestModelId: "base-model", prompt: "超时", count: 1, platformIdempotencyKey: "timeout-unknown" });
assert.equal(timeout.status, "unknown");
assert.equal(timeout.errorCode, "PROVIDER_TIMEOUT_UNKNOWN");
globalThis.fetch = async () => { throw new Error("probe transport failed"); };
await assert.rejects(() => probeProviderModels("https://1.1.1.1/v1", "1234567890123456"));
globalThis.fetch = abortedFetch;
} finally {
globalThis.fetch = previousFetch;
}
});
test("OpenAI image channels stay in manual reconciliation because no query endpoint is assumed", async () => {
const store = createStore();
const result = await queryProvider(store, channel(store), "provider-request-1");
assert.deepEqual(result, { status: "unknown", providerRequestId: "provider-request-1", errorCode: "PROVIDER_QUERY_UNAVAILABLE" });
});