mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 18:02:17 +03:00
test(api): add route-level coverage for Adobe Firefly /v1/images/edits + fix typecheck/file-size drift
Covers the referenceBlobs upload path, the 4-reference cap error, and the credentials/rate-limit branches added to the /v1/images/edits route for adobe-firefly (#8510). Also fixes a Buffer/BodyInit typecheck mismatch in uploadAdobeFireflyImage and corrects the adobeFireflyClient.ts file-size baseline entry to match the gate's actual LOC count (it counts the trailing newline, so the frozen value is 2317, not 2316), plus a testFrozen entry for adobe-firefly.test.ts's own +159 line growth from this PR. Moves the handleAdobeFireflyImageGeneration re-export out of the middle of the import block in imageGeneration.ts for readability. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -196,8 +196,8 @@
|
||||
"_rebaseline_2026_07_23_8252_combo_400_advance": "#8252 (@RaviTharuma) own growth: accountFallback.ts 1932->1940 (+8) + combo.ts 3604->3630 (+26) — advance combo on model-scoped 400s wrapped as invalid/Bad-Request. Irreducible wiring at existing account-fallback + combo dispatch chokepoints. Covered by combo-model-scoped-400-advance.test.ts.",
|
||||
"_rebaseline_2026_07_23_8247_8248_model_unhealthy": "#8247+#8248 own growth: accountFallback.ts 1940->1941 (+1, irreducible import statement only — the substantive #8248 DEGRADED-pattern classifier was extracted into open-sse/config/errorConfig.ts, which has ample headroom, instead of growing this frozen file; #8247's fix is a single existing-line condition change, net zero lines). Scoping the credits-exhausted 403/429 branch to isCompatibleProvider() (per-model-quota openai/anthropic-compatible-* nicknames) so it stays model-scoped instead of terminalling the whole connection, and classifying NVIDIA NIM 'Function ... DEGRADED' 400 bodies as model-access-denied instead of a raw passthrough 400. Covered by tests/unit/8247-accountfallback-model-unhealthy.test.ts and tests/unit/8248-accountfallback-nvidia-degraded.test.ts.",
|
||||
"open-sse/services/accountFallback.ts": 1966,
|
||||
"open-sse/services/adobeFireflyClient.ts": 2316,
|
||||
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2316 (+upload helpers, extract sources, resolve blob ids).",
|
||||
"open-sse/services/adobeFireflyClient.ts": 2317,
|
||||
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\"\\n\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
|
||||
"open-sse/services/batchProcessor.ts": 915,
|
||||
"open-sse/services/browserBackedChat.ts": 850,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
@@ -342,6 +342,8 @@
|
||||
},
|
||||
"testCap": 800,
|
||||
"testFrozen": {
|
||||
"_rebaseline_2026_07_25_8510_adobe_firefly_reference_images_tests": "#8510 (artickc, feat/adobe-firefly-reference-images) own test growth: tests/unit/adobe-firefly.test.ts 711->871 (+159, entirely this PR's diff — new referenceBlobs upload/dispatch coverage for handleAdobeFireflyImageGeneration, resolveAdobeSourceImageIds, and the storage-upload wire contract). Route-level /v1/images/edits coverage (credentials/rate-limit/4-ref-cap branches added to route.ts) lives in the new tests/unit/8510-adobe-firefly-edits-route.test.ts instead of growing this file further.",
|
||||
"tests/unit/adobe-firefly.test.ts": 871,
|
||||
"_rebaseline_2026_07_09_6126_clinepass_dualauth": "#6126 (ClinePass dual-auth) own test growth: oauth-providers-config.test.ts 842->845 (+3: clinepass key/config/required-fields entries reusing the Cline WorkOS flow config, needed after registering clinepass in the oauth.ts PROVIDERS enum).",
|
||||
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
|
||||
"_rebaseline_2026_07_02_5928_base_red": "web-cookie-providers-new.test.ts 845->850: #5928 (test(security) Kimi Web URL host parse, CodeQL #689) grew the file +5 lines and merged into release/v3.8.44 WITHOUT rebaselining, leaving a fast-gates base-red that blocked every subsequent PR->release. Test growth is legitimate (a security regression test); maintainer absorbs the drift here. Frozen at 850.",
|
||||
|
||||
@@ -74,14 +74,15 @@ import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmin
|
||||
import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts";
|
||||
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
|
||||
import { handleAdobeFireflyImageGeneration } from "./imageGeneration/providers/adobeFirefly.ts";
|
||||
// Re-export so /v1/images/edits can dispatch Firefly reference-image edits.
|
||||
export { handleAdobeFireflyImageGeneration };
|
||||
import { handleAlibabaImageGeneration } from "./imageGeneration/providers/alibabaImage.ts";
|
||||
import {
|
||||
applyPollinationsAnonymousFallback,
|
||||
reportPollinationsAnonOutcome,
|
||||
} from "./imageGeneration/pollinationsAnonAuth.ts";
|
||||
|
||||
// Re-export so /v1/images/edits can dispatch Firefly reference-image edits.
|
||||
export { handleAdobeFireflyImageGeneration };
|
||||
|
||||
interface KieImageOptions {
|
||||
model: string;
|
||||
provider: string;
|
||||
|
||||
@@ -1245,7 +1245,7 @@ export async function uploadAdobeFireflyImage(opts: {
|
||||
cookie: cookieHeader || undefined,
|
||||
prompt: opts.prompt || "upload",
|
||||
}),
|
||||
body: buffer,
|
||||
body: buffer as unknown as BodyInit,
|
||||
});
|
||||
|
||||
const text = await resp.text().catch(() => "");
|
||||
|
||||
230
tests/unit/8510-adobe-firefly-edits-route.test.ts
Normal file
230
tests/unit/8510-adobe-firefly-edits-route.test.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
// #8510 (artickc, feat/adobe-firefly-reference-images): route-level coverage for the Adobe
|
||||
// Firefly branch that /v1/images/edits gained in this PR. Exercises the actual
|
||||
// POST(request) handler (not the inner handleAdobeFireflyImageGeneration helper directly,
|
||||
// which tests/unit/adobe-firefly.test.ts already covers) so the credentials /
|
||||
// rate-limit / 4-reference-cap branches added to route.ts itself are proven, not just the
|
||||
// downstream service call.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-adobe-firefly-edits-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "adobe-firefly-edits-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const imageEditRoute = await import("../../src/app/api/v1/images/edits/route.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
const {
|
||||
ADOBE_FIREFLY_IMAGE_UPLOAD_URL,
|
||||
ADOBE_FIREFLY_IMAGE_SUBMIT_URL,
|
||||
} = await import("../../open-sse/services/adobeFireflyClient.ts");
|
||||
|
||||
interface ErrorResponseBody {
|
||||
error: { message: string; code?: string };
|
||||
}
|
||||
|
||||
interface ImageResponseBody {
|
||||
data: Array<{ b64_json?: string; url?: string }>;
|
||||
}
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
async function resetStorage() {
|
||||
globalThis.fetch = originalFetch;
|
||||
apiKeysDb.resetApiKeyState();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
|
||||
}
|
||||
|
||||
async function seedAdobeFireflyConnection(
|
||||
overrides: { apiKey?: string; rateLimitedUntil?: string | null } = {}
|
||||
) {
|
||||
return providersDb.createProviderConnection({
|
||||
provider: "adobe-firefly",
|
||||
authType: "apikey",
|
||||
name: "adobe-firefly-test",
|
||||
apiKey: overrides.apiKey ?? userImsJwt(),
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
rateLimitedUntil: overrides.rateLimitedUntil ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// Mirrors tests/unit/adobe-firefly.test.ts's userImsJwt() helper — a synthetic,
|
||||
// non-guest IMS access token shape so resolveAdobeAccessToken() accepts it directly
|
||||
// without needing a live cookie->token exchange call.
|
||||
function userImsJwt(userId = "0EB@AdobeID"): string {
|
||||
return (
|
||||
`eyJhbGciOiJSUzI1NiJ9.` +
|
||||
Buffer.from(
|
||||
JSON.stringify({ user_id: userId, type: "access_token", client_id: "clio-playground-web" })
|
||||
).toString("base64url") +
|
||||
`.` +
|
||||
"sig".padEnd(40, "x")
|
||||
);
|
||||
}
|
||||
|
||||
function dataUrlPng(bytes: number[]): string {
|
||||
return `data:image/png;base64,${Buffer.from(bytes).toString("base64")}`;
|
||||
}
|
||||
|
||||
const REF_A = dataUrlPng([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1]);
|
||||
const REF_B = dataUrlPng([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 2]);
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
apiKeysDb.resetApiKeyState();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#8510 v1 image edit POST uploads Adobe Firefly reference images and dispatches referenceBlobs", async () => {
|
||||
await seedAdobeFireflyConnection();
|
||||
|
||||
const uploadedIds: string[] = [];
|
||||
let submitBody: Record<string, unknown> | null = null;
|
||||
|
||||
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === ADOBE_FIREFLY_IMAGE_UPLOAD_URL) {
|
||||
const id = `blob-${uploadedIds.length + 1}`;
|
||||
uploadedIds.push(id);
|
||||
return new Response(JSON.stringify({ images: [{ id }] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (stringUrl === ADOBE_FIREFLY_IMAGE_SUBMIT_URL) {
|
||||
submitBody = JSON.parse(String(init.body || "{}"));
|
||||
return new Response(JSON.stringify({ links: { result: "https://poll.example/job/img1" } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (stringUrl === "https://poll.example/job/img1") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status: "COMPLETED",
|
||||
outputs: [{ image: { presignedUrl: "https://cdn.example/edited-out.png" } }],
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
};
|
||||
|
||||
const response = await imageEditRoute.POST(
|
||||
new Request("http://localhost/api/v1/images/edits", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "adobe-firefly/nano-banana-pro",
|
||||
prompt: "combine these two references",
|
||||
images: [REF_A, REF_B],
|
||||
}),
|
||||
})
|
||||
);
|
||||
const body = (await response.json()) as ImageResponseBody;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.data[0].url, "https://cdn.example/edited-out.png");
|
||||
|
||||
// referenceBlobs upload path: both distinct reference images must have been uploaded
|
||||
// to Firefly storage and forwarded as referenceBlobs on the generate-async dispatch.
|
||||
assert.equal(uploadedIds.length, 2, "both reference images must be uploaded individually");
|
||||
assert.ok(submitBody, "generate-async must have been called");
|
||||
const referenceBlobs = (submitBody as Record<string, unknown>).referenceBlobs as Array<{
|
||||
id: string;
|
||||
}>;
|
||||
assert.ok(Array.isArray(referenceBlobs), "generate-async payload must carry referenceBlobs");
|
||||
assert.deepEqual(
|
||||
referenceBlobs.map((r) => r.id).sort(),
|
||||
[...uploadedIds].sort()
|
||||
);
|
||||
});
|
||||
|
||||
test("#8510 v1 image edit POST rejects more than 4 Adobe Firefly reference images", async () => {
|
||||
await seedAdobeFireflyConnection();
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("Over-cap Adobe Firefly reference sets must not reach upstream");
|
||||
};
|
||||
|
||||
const images = Array.from({ length: 5 }, (_, i) => dataUrlPng([0x89, 0x50, 0x4e, 0x47, i + 1]));
|
||||
const response = await imageEditRoute.POST(
|
||||
new Request("http://localhost/api/v1/images/edits", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "adobe-firefly/nano-banana-pro",
|
||||
prompt: "combine these references",
|
||||
images,
|
||||
}),
|
||||
})
|
||||
);
|
||||
const body = (await response.json()) as ErrorResponseBody;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(body.error.message, /Adobe Firefly image edit supports at most 4 reference images/);
|
||||
// Hard Rule #12 — every error response routes through buildErrorBody/sanitizeErrorMessage
|
||||
// and must never leak a raw stack trace.
|
||||
assert.ok(!body.error.message.includes("at /"));
|
||||
});
|
||||
|
||||
test("#8510 v1 image edit POST surfaces missing Adobe Firefly credentials", async () => {
|
||||
// No adobe-firefly connection seeded at all.
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("Missing-credentials path must not reach upstream");
|
||||
};
|
||||
|
||||
const response = await imageEditRoute.POST(
|
||||
new Request("http://localhost/api/v1/images/edits", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "adobe-firefly/nano-banana-pro",
|
||||
prompt: "edit this",
|
||||
images: [REF_A],
|
||||
}),
|
||||
})
|
||||
);
|
||||
const body = (await response.json()) as ErrorResponseBody;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.match(body.error.message, /No credentials for provider: adobe-firefly/);
|
||||
assert.ok(!body.error.message.includes("at /"));
|
||||
});
|
||||
|
||||
test("#8510 v1 image edit POST surfaces Adobe Firefly rate-limit sentinel", async () => {
|
||||
await seedAdobeFireflyConnection({ rateLimitedUntil: new Date(Date.now() + 60_000).toISOString() });
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("Rate-limited path must not reach upstream");
|
||||
};
|
||||
|
||||
const response = await imageEditRoute.POST(
|
||||
new Request("http://localhost/api/v1/images/edits", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "adobe-firefly/nano-banana-pro",
|
||||
prompt: "edit this",
|
||||
images: [REF_A],
|
||||
}),
|
||||
})
|
||||
);
|
||||
const body = (await response.json()) as ErrorResponseBody;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.match(body.error.message, /All accounts rate limited/);
|
||||
assert.ok(!body.error.message.includes("at /"));
|
||||
});
|
||||
Reference in New Issue
Block a user