import type { AgentErrorCode } from '@/lib/api-error-response'; import { isFeedbackStateStore } from '@/lib/feedback-store'; import { cleanupExpiredWebuiImages } from '@/lib/webui-image-cleanup'; import Database from 'better-sqlite3'; import type { NextRequest } from 'next/server'; import assert from 'node:assert/strict'; import { access, mkdtemp, readdir, rm, utimes } from 'node:fs/promises'; import http from 'node:http'; import type { Socket } from 'node:net'; import os from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, it } from 'node:test'; import { Pool } from 'pg'; const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='; const PNG_CONVERTIBLE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADUlEQVQImWP4z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg=='; let originalEnv: NodeJS.ProcessEnv; let originalCwd = ''; let tempDir = ''; function restoreProcessEnv(snapshot: NodeJS.ProcessEnv) { for (const key of Object.keys(process.env)) { if (!(key in snapshot)) { delete process.env[key]; } } for (const [key, value] of Object.entries(snapshot)) { process.env[key] = value; } } function setPrimaryChannelRequestModes(requestModes: string) { process.env.OPENAI_UPSTREAM_REQUEST_MODES = requestModes; } beforeEach(async () => { originalEnv = { ...process.env }; originalCwd = process.cwd(); tempDir = await mkdtemp(path.join(os.tmpdir(), 'agent-routes-')); process.chdir(tempDir); process.env.AGENT_STATE_BACKEND = 'sqlite'; process.env.AGENT_SQLITE_PATH = path.join(tempDir, 'agent.sqlite'); process.env.NEXT_PUBLIC_IMAGE_STORAGE_MODE = 'fs'; for (const key of Object.keys(process.env)) { if (/^OPENAI_CHANNEL_\d+_/.test(key)) { delete process.env[key]; } } delete process.env.APP_PASSWORD; delete process.env.AGENT_API_TOKEN; delete process.env.AGENT_PUBLIC_BASE_URL; delete process.env.AGENT_REQUEST_TTL_SECONDS; delete process.env.AGENT_ARTIFACT_SHARE_DEFAULT_EXPIRES_MINUTES; delete process.env.AGENT_ARTIFACT_SHARE_MAX_EXPIRES_MINUTES; delete process.env.OPENAI_API_KEY; delete process.env.OPENAI_API_BASE_URL; delete process.env.OPENAI_UPSTREAM_REQUEST_MODES; delete process.env.OPENAI_UPSTREAM_REQUEST_MODE_PRIORITY; delete process.env.OPENAI_UPSTREAM_PROXY_URL; delete process.env.OPENAI_UPSTREAM_USER_AGENT; delete process.env.UPSTREAM_USER_AGENT; delete process.env.OPENAI_CHANNEL_1_ID; delete process.env.OPENAI_CHANNEL_1_API_KEYS; delete process.env.OPENAI_CHANNEL_1_BASE_URL; delete process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE; delete process.env.OPENAI_CHANNEL_1_REQUEST_MODES; delete process.env.OPENAI_CHANNEL_1_REQUEST_MODE_PRIORITY; delete process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID; delete process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET; delete process.env.OPENAI_CHANNEL_1_USER_AGENT; delete process.env.OPENAI_CHANNEL_1_UPSTREAM_HEADERS_JSON; delete process.env.OPENAI_CHANNEL_2_ID; delete process.env.OPENAI_CHANNEL_2_API_KEYS; delete process.env.OPENAI_CHANNEL_2_BASE_URL; delete process.env.OPENAI_CHANNEL_2_UPSTREAM_PROFILE; delete process.env.OPENAI_CHANNEL_2_REQUEST_MODES; delete process.env.OPENAI_CHANNEL_2_REQUEST_MODE_PRIORITY; delete process.env.OPENAI_CHANNEL_2_MATSCA_APP_ID; delete process.env.OPENAI_CHANNEL_2_MATSCA_APP_SECRET; delete process.env.OPENAI_CHANNEL_2_USER_AGENT; delete process.env.OPENAI_CHANNEL_2_UPSTREAM_HEADERS_JSON; delete process.env.OPENAI_CHANNEL_RECOVERY_PROBE_ENABLED; delete process.env.OPENAI_CHANNEL_RECOVERY_PROBE_INTERVAL_MS; delete process.env.OPENAI_CHANNEL_RECOVERY_PROBE_TIMEOUT_MS; delete process.env.OPENAI_CHANNEL_RECOVERY_PROBE_MAX_PER_TICK; delete process.env.OPENAI_CHANNEL_REQUIRE_PROBE_FOR_RECOVERY; delete process.env.OPENAI_ALLOWED_PLAIN_HTTP_API_BASE_URLS; }); afterEach(async () => { const { resetAgentStateStoreForTests, setAgentStateStoreFactoryForTests } = await import('@/lib/agent-state-runtime'); const { resetServerChannelStateForTests } = await import('@/lib/server-channel-router'); restoreProcessEnv(originalEnv); process.chdir(originalCwd); setAgentStateStoreFactoryForTests(undefined); await resetAgentStateStoreForTests(); resetServerChannelStateForTests(); await rm(tempDir, { recursive: true, force: true }); }); describe('Agent route integration', () => { it('reports configured Agent auth without exposing secret values', async () => { const { getCapabilities } = await loadAgentRoutes(); process.env.AGENT_STATE_BACKEND = 'memory'; process.env.AGENT_API_TOKEN = 'capability-token'; process.env.APP_PASSWORD = 'page-access-code'; const response = await getCapabilities(); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.auth.required, true); assert.deepEqual(body.auth.schemes, ['bearer']); assert.equal(JSON.stringify(body).includes('capability-token'), false); assert.equal(JSON.stringify(body).includes('page-access-code'), false); assert.equal(body.defaults.state_backend, 'memory'); }); it('does not report auth as required for blank Agent auth settings', async () => { const { getCapabilities } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = ' '; process.env.APP_PASSWORD = ' '; const response = await getCapabilities(); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.auth.required, false); assert.deepEqual(body.auth.schemes, []); }); it('reports deployed upstream profile limits in capabilities without exposing upstream secrets', async () => { const { getCapabilities } = await loadAgentRoutes(); process.env.OPENAI_CHANNEL_1_ID = 'matsca'; process.env.OPENAI_CHANNEL_1_BASE_URL = 'https://img.matsca.com/v1'; process.env.OPENAI_CHANNEL_1_API_KEYS = 'sk-matsca-secret'; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID = 'matsca-app-id'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET = 'matsca-app-secret'; process.env.OPENAI_CHANNEL_1_USER_AGENT = 'channel-agent-secret'; process.env.OPENAI_CHANNEL_1_UPSTREAM_HEADERS_JSON = JSON.stringify({ 'X-App-Secret': 'channel-header-secret', 'X-Trace-Token': 'trace-token-secret' }); process.env.OPENAI_UPSTREAM_USER_AGENT = 'global-agent-secret'; const response = await getCapabilities(); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.upstream_profile.activeProfile, 'matsca'); assert.deepEqual(body.limits.partial_images, { min: 0, max: 4 }); assert.equal(body.limits.upload_images.max, 8); assert.equal(body.model_limits['gpt-image-2'].allow_transparent_background, true); assert.equal(body.upstream_request_headers.default.user_agent_effective, 'configured'); assert.deepEqual(body.upstream_request_headers.channels, [ { id: 'matsca', upstream_proxy: { configured: false }, request_modes: ['images-non-stream'], request_mode_priority: ['images-non-stream'], request_headers: { user_agent_effective: 'configured', has_extra_headers: true, allowed_header_names: ['user-agent', 'x-app-id', 'x-app-secret'], configured_header_names: ['user-agent', 'x-app-id', 'x-app-secret', 'x-trace-token'] } } ]); const serialized = JSON.stringify(body); assert.equal(serialized.includes('sk-matsca-secret'), false); assert.equal(serialized.includes('matsca-app-id'), false); assert.equal(serialized.includes('matsca-app-secret'), false); assert.equal(serialized.includes('channel-agent-secret'), false); assert.equal(serialized.includes('channel-header-secret'), false); assert.equal(serialized.includes('trace-token-secret'), false); assert.equal(serialized.includes('global-agent-secret'), false); }); it('reports enabled Responses image backend from the deployed runtime environment', async () => { const { getCapabilities } = await loadAgentRoutes(); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; const response = await getCapabilities(); assert.equal(response.status, 200); const body = await response.json(); assert.deepEqual(body.supported.enabled_image_backends, ['images-api', 'responses-image-generation']); assert.equal(body.supported.image_backend_requirements['responses-image-generation'].enabled, true); assert.deepEqual(body.supported.image_backend_requirements['responses-image-generation'].missing_env, []); assert.deepEqual(body.agent_streaming.upstream_sse.enabled_image_backends, [ 'images-api', 'responses-image-generation' ]); assert.deepEqual(body.agent_streaming.upstream_sse.request_fields_by_mode, { generate: ['image_backend', 'stream_mode', 'streaming_strategy', 'partial_images'], edit: ['stream_mode', 'streaming_strategy', 'partial_images'] }); }); it('reports global request mode priority in deployed capabilities', async () => { const { getCapabilities } = await loadAgentRoutes(); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = 'https://upstream.example.com/v1'; process.env.OPENAI_UPSTREAM_REQUEST_MODES = 'images-non-stream,images-sse'; process.env.OPENAI_UPSTREAM_REQUEST_MODE_PRIORITY = 'images-sse,images-non-stream'; const response = await getCapabilities(); assert.equal(response.status, 200); const body = await response.json(); assert.deepEqual(body.upstream_request_headers.channels, [ { id: 'default', upstream_proxy: { configured: false }, request_modes: ['images-non-stream', 'images-sse'], request_mode_priority: ['images-sse', 'images-non-stream'], request_headers: { user_agent_effective: 'visual-journal/2.2.0', has_extra_headers: false, allowed_header_names: ['user-agent', 'x-app-id', 'x-app-secret'], configured_header_names: [] } } ]); }); it('reports global and per-channel upstream proxy summaries without exposing endpoints', async () => { const { getCapabilities } = await loadAgentRoutes(); process.env.OPENAI_UPSTREAM_PROXY_URL = 'https://global-proxy.integration.example:9443'; process.env.OPENAI_CHANNEL_1_ID = 'primary'; process.env.OPENAI_CHANNEL_1_BASE_URL = 'https://primary.example.com/v1'; process.env.OPENAI_CHANNEL_1_API_KEYS = 'primary-secret'; process.env.OPENAI_CHANNEL_2_ID = 'backup'; process.env.OPENAI_CHANNEL_2_BASE_URL = 'https://backup.example.com/v1'; process.env.OPENAI_CHANNEL_2_API_KEYS = 'backup-secret'; process.env.OPENAI_CHANNEL_2_PROXY_URL = 'http://channel-proxy.integration.example:8080'; const response = await getCapabilities(); assert.equal(response.status, 200); const body = await response.json(); assert.deepEqual(body.image_transport.upstream_proxy, { configured: true, protocol: 'https' }); assert.deepEqual( body.upstream_request_headers.channels.map((channel: { id: string; upstream_proxy: unknown }) => ({ id: channel.id, upstream_proxy: channel.upstream_proxy })), [ { id: 'primary', upstream_proxy: { configured: true, protocol: 'https' } }, { id: 'backup', upstream_proxy: { configured: true, protocol: 'http' } } ] ); const serialized = JSON.stringify(body); assert.equal(serialized.includes('global-proxy.integration.example'), false); assert.equal(serialized.includes('channel-proxy.integration.example'), false); assert.equal(serialized.includes('9443'), false); assert.equal(serialized.includes('8080'), false); assert.equal(serialized.includes('primary-secret'), false); assert.equal(serialized.includes('backup-secret'), false); }); it('registers cleanup-managed artifacts for every request mode', async () => { const { generateImage } = await loadAgentRoutes(); const { getAgentStateStore } = await import('@/lib/agent-state-runtime'); const { resetServerChannelStateForTests } = await import('@/lib/server-channel-router'); process.env.AGENT_REQUEST_TTL_SECONDS = String(60 * 24 * 60 * 60); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; const cases = [ { mode: 'images-non-stream', startUpstream: () => startImageUpstream(() => ({ data: [{ b64_json: PNG_BASE64 }] })), request: { prompt: 'cleanup lifecycle images non stream', stream_mode: 'non_stream', streaming_strategy: 'off' } }, { mode: 'images-sse', startUpstream: () => startStreamingImageUpstream(() => [ { event: 'image_generation.completed', data: { type: 'image_generation.completed', b64_json: PNG_BASE64 } } ]), request: { prompt: 'cleanup lifecycle images sse', stream_mode: 'stream', streaming_strategy: 'force-sse', partial_images: 1 } }, { mode: 'responses-non-stream', startUpstream: () => startResponsesImageJsonUpstream(200, { id: 'response-cleanup-json', object: 'response', status: 'completed', output: [ { id: 'image-cleanup-json', type: 'image_generation_call', status: 'completed', result: PNG_BASE64 } ] }), request: { prompt: 'cleanup lifecycle responses non stream', image_backend: 'responses-image-generation', stream_mode: 'non_stream', streaming_strategy: 'off' } }, { mode: 'responses-sse', startUpstream: () => startStreamingResponsesImageUpstream(() => [ { event: 'response.output_item.done', data: { type: 'response.output_item.done', item: { id: 'image-cleanup-sse', type: 'image_generation_call', status: 'completed', result: PNG_BASE64 } } } ]), request: { prompt: 'cleanup lifecycle responses sse', image_backend: 'responses-image-generation', stream_mode: 'stream', streaming_strategy: 'responses-sse', partial_images: 1 } } ] as const; const artifacts: Array<{ id: string; filepath: string }> = []; for (const testCase of cases) { const upstream = await testCase.startUpstream(); process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.OPENAI_UPSTREAM_REQUEST_MODES = testCase.mode; process.env.ENABLE_RESPONSES_IMAGE_BACKEND = testCase.mode.startsWith('responses-') ? 'true' : 'false'; resetServerChannelStateForTests(); try { const response = await generateImage( agentJsonRequest('cleanup-lifecycle-' + testCase.mode, testCase.request) ); assert.equal(response.status, 200, testCase.mode); const body = await response.json(); assert.equal(body.execution.channel_request_mode, testCase.mode); const artifactId = body.images[0].content_url.split('/').at(-2); assert.equal(typeof artifactId, 'string'); const filepath = readStoredArtifactFilepath(artifactId); await access(filepath); artifacts.push({ id: artifactId, filepath }); } finally { await upstream.close(); } } const store = getAgentStateStore(process.env); const protectedPaths = await store.listArtifactFilepaths(); assert.deepEqual(artifacts.map((artifact) => artifact.filepath).sort(), protectedPaths.sort()); const cleanupNow = new Date(); const expiredWebuiTime = new Date(cleanupNow.getTime() - 31 * 24 * 60 * 60 * 1000); await Promise.all(artifacts.map((artifact) => utimes(artifact.filepath, expiredWebuiTime, expiredWebuiTime))); const cleanupResult = await cleanupExpiredWebuiImages({ outputDir: path.dirname(artifacts[0].filepath), retentionDays: 30, protectedArtifactFilepaths: protectedPaths, now: cleanupNow }); assert.equal(cleanupResult.deletedCount, 0); assert.equal(cleanupResult.protectedCount, 4); await Promise.all(artifacts.map((artifact) => access(artifact.filepath))); const purged = await store.purgeExpiredRequests( new Date(cleanupNow.getTime() + 60 * 24 * 60 * 60 * 1000 + 1000) ); assert.equal(purged, 4); for (const artifact of artifacts) { assert.equal(await store.getArtifact(artifact.id), undefined); await assert.rejects(() => access(artifact.filepath)); } }); it('reports Matsca channel limits from the deployed runtime environment without leaking secrets', async () => { const { getCapabilities } = await loadAgentRoutes(); process.env.OPENAI_CHANNEL_1_ID = 'matsca'; process.env.OPENAI_CHANNEL_1_BASE_URL = 'https://img.matsca.com/v1'; process.env.OPENAI_CHANNEL_1_API_KEYS = 'sk-matsca-secret'; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID = 'app-id-secret'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET = 'app-secret-value'; process.env.OPENAI_CHANNEL_1_USER_AGENT = 'channel-agent-secret'; process.env.OPENAI_CHANNEL_1_UPSTREAM_HEADERS_JSON = JSON.stringify({ 'X-App-Secret': 'channel-header-secret', 'X-Trace-Token': 'trace-token-secret' }); process.env.UPSTREAM_USER_AGENT = 'global-agent-secret'; const response = await getCapabilities(); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.limits.max_images, 4); assert.equal(body.limits.upload_images.max, 8); assert.equal(body.limits.max_upload_mb, 10); assert.equal(body.limits.max_total_upload_mb, 80); assert.deepEqual(body.limits.partial_images, { min: 0, max: 4 }); assert.equal(body.model_limits['gpt-image-2'].allow_transparent_background, true); assert.equal(body.model_limits['gpt-image-2'].size_policy, 'positive-integer'); assert.equal(body.upstream_request_headers.default.user_agent_effective, 'configured'); assert.deepEqual(body.upstream_request_headers.channels, [ { id: 'matsca', upstream_proxy: { configured: false }, request_modes: ['images-non-stream'], request_mode_priority: ['images-non-stream'], request_headers: { user_agent_effective: 'configured', has_extra_headers: true, allowed_header_names: ['user-agent', 'x-app-id', 'x-app-secret'], configured_header_names: ['user-agent', 'x-app-id', 'x-app-secret', 'x-trace-token'] } } ]); const serialized = JSON.stringify(body); assert.equal(serialized.includes('sk-matsca-secret'), false); assert.equal(serialized.includes('app-id-secret'), false); assert.equal(serialized.includes('app-secret-value'), false); assert.equal(serialized.includes('channel-agent-secret'), false); assert.equal(serialized.includes('channel-header-secret'), false); assert.equal(serialized.includes('trace-token-secret'), false); assert.equal(serialized.includes('global-agent-secret'), false); }); it('generates through a compatible upstream once and exposes request diagnostics for the same state record', async () => { const { generateImage, getAgentRequestDiagnostics, getArtifactContent, lookupAgentRequestDiagnostics } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }], usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 } }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const first = await generateImage( agentJsonRequest('route-cache-key', { prompt: 'agent route success', output_format: 'webp' }) ); assert.equal(first.status, 200); const firstBody = await first.json(); assert.equal(firstBody.cached, false); assert.equal(firstBody.images.length, 1); assert.ok(firstBody.images[0].content_url); assert.equal('b64_json' in firstBody.images[0], false); assert.equal(firstBody.images[0].output_format, 'webp'); assert.equal(firstBody.images[0].mime_type, 'image/webp'); const artifactId = firstBody.images[0].content_url.split('/').at(-2); assert.equal(typeof artifactId, 'string'); const artifactContent = await getArtifactContent( new Request(`http://localhost/api/agent/artifacts/${artifactId}/content`), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(artifactContent.status, 200); assert.equal(artifactContent.headers.get('content-type'), 'image/webp'); assert.equal(Buffer.from(await artifactContent.arrayBuffer()).toString('ascii', 8, 12), 'WEBP'); assert.equal(firstBody.execution.transport, 'agent_json'); assert.equal(firstBody.execution.endpoint, '/api/agent/images/generate'); assert.equal(firstBody.execution.route_mode, 'agent'); assert.equal(firstBody.execution.operation, 'generate'); assert.equal(firstBody.execution.image_backend, 'images-api'); assert.equal(firstBody.execution.stream_mode, 'non_stream'); assert.equal(firstBody.execution.streaming_strategy, 'auto'); assert.equal(firstBody.execution.channel_request_mode, 'images-non-stream'); assert.equal(firstBody.execution.channel_request_mode_fallback_applied, false); assert.deepEqual(firstBody.execution.route_decision, { requested_backend: 'images-api', candidate_channel_request_modes: ['images-non-stream'], request_mode_priority: ['images-non-stream'], preferred_channel_request_mode: 'images-non-stream', selected_channel_request_mode: 'images-non-stream', fallback_applied: false, selected_channel_id: 'default', upstream_host: new URL(upstream.baseUrl).host }); assert.equal(firstBody.execution.upstream_host, new URL(upstream.baseUrl).host); assert.equal(firstBody.execution.request_headers.user_agent_effective, 'visual-journal/2.2.0'); assert.equal(firstBody.execution.request_headers.has_extra_headers, false); assert.equal(typeof firstBody.timing.elapsed_ms, 'number'); assert.equal(firstBody.timing.elapsed_ms >= 0, true); assert.equal(firstBody.timing.server_elapsed_ms, firstBody.timing.elapsed_ms); assert.equal(typeof firstBody.timing.started_at, 'string'); assert.equal(typeof firstBody.timing.completed_at, 'string'); const second = await generateImage( agentJsonRequest('route-cache-key', { prompt: 'agent route success', output_format: 'webp' }) ); assert.equal(second.status, 200); const secondBody = await second.json(); assert.equal(secondBody.cached, true); assert.equal(second.headers.get('x-idempotent-replay'), 'true'); assert.equal(second.headers.get('x-request-id'), firstBody.request_id); assert.equal(secondBody.request_id, firstBody.request_id); assert.deepEqual(secondBody.execution, firstBody.execution); assert.deepEqual(secondBody.timing, firstBody.timing); assert.equal(upstreamCalls, 1); const byRequestId = await getAgentRequestDiagnostics( new Request(`http://localhost/api/agent/diagnostics/requests/${firstBody.request_id}`), { params: Promise.resolve({ id: firstBody.request_id }) } ); assert.equal(byRequestId.status, 200); const byRequestIdBody = await byRequestId.json(); assert.equal(byRequestIdBody.found, true); assert.equal(byRequestIdBody.diagnostics.request.request_id, firstBody.request_id); assert.equal(byRequestIdBody.diagnostics.request.idempotency_key, 'route-cache-key'); assert.equal(byRequestIdBody.diagnostics.request.status, 'succeeded'); assert.equal(byRequestIdBody.diagnostics.response.image_count, 1); assert.deepEqual(byRequestIdBody.diagnostics.response.timing, firstBody.timing); assert.deepEqual(byRequestIdBody.diagnostics.response.execution, firstBody.execution); assert.deepEqual(byRequestIdBody.diagnostics.response.content_urls, [firstBody.images[0].content_url]); assert.equal(byRequestIdBody.diagnostics.artifacts[0].id, firstBody.images[0].id); assert.equal(byRequestIdBody.diagnostics.state_backend, 'sqlite'); assert.equal(byRequestIdBody.diagnostics.diagnostics_retention.storage, 'agent_state'); assert.equal(byRequestIdBody.diagnostics.diagnostics_boundary.not_page_request_log, true); assert.equal(JSON.stringify(byRequestIdBody).includes(PNG_BASE64), false); const byIdempotencyKey = await lookupAgentRequestDiagnostics( new Request('http://localhost/api/agent/diagnostics/requests?idempotency_key=route-cache-key') ); assert.equal(byIdempotencyKey.status, 200); const byIdempotencyKeyBody = await byIdempotencyKey.json(); assert.equal(byIdempotencyKeyBody.diagnostics.request.request_id, firstBody.request_id); } finally { await upstream.close(); } }); it('retries accepted async image tasks through Agent generate with the same upstream idempotency key', async () => { const { generateImage } = await loadAgentRoutes(); const upstreamIdempotencyKeys: Array = []; let upstreamCalls = 0; const upstream = await startImageUpstream((_body, _url, request, response) => { upstreamCalls += 1; const idempotencyKey = request.headers['idempotency-key']; upstreamIdempotencyKeys.push(Array.isArray(idempotencyKey) ? idempotencyKey.join(',') : idempotencyKey); if (upstreamCalls === 1) { response.setHeader('Retry-After', '1'); return { object: 'image.task', status: 'pending', task_id: 'agent-route-accepted-task', poll_url: '/v1/image-tasks?ids=agent-route-accepted-task' }; } return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await generateImage( agentJsonRequest('agent-route-accepted-task-key', { prompt: 'agent route accepted task', response_mode: 'base64' }) ); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.cached, false); assert.equal(body.images[0].b64_json, PNG_BASE64); assert.deepEqual(upstreamIdempotencyKeys, [ 'agent-route-accepted-task-key', 'agent-route-accepted-task-key' ]); assert.equal(upstreamCalls, 2); } finally { await upstream.close(); } }); it('keeps Agent request diagnostics available when feedback lookup fails', async () => { const { generateImage, getAgentRequestDiagnostics } = await loadAgentRoutes(); const upstream = await startImageUpstream(() => ({ data: [{ b64_json: PNG_BASE64 }] })); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const first = await generateImage( agentJsonRequest('route-feedback-diagnostics-key', { prompt: 'agent route success' }) ); assert.equal(first.status, 200); const firstBody = await first.json(); const { ensureAgentStateStoreReady } = await import('@/lib/agent-state-runtime'); const store = await ensureAgentStateStoreReady(); if (!isFeedbackStateStore(store)) { assert.fail('Agent state store should expose feedback methods for this route test.'); } const originalReadFeedback = store.readFeedback.bind(store); store.readFeedback = async () => { throw new Error('feedback store unavailable'); }; try { const response = await getAgentRequestDiagnostics( new Request(`http://localhost/api/agent/diagnostics/requests/${firstBody.request_id}`), { params: Promise.resolve({ id: firstBody.request_id }) } ); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.found, true); assert.equal(body.diagnostics.request.request_id, firstBody.request_id); assert.equal(body.diagnostics.response.image_count, 1); assert.equal(body.diagnostics.feedback, undefined); } finally { store.readFeedback = originalReadFeedback; } } finally { await upstream.close(); } }); it('rejects Agent generate image counts outside the selected Matsca profile before calling upstream', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'matsca'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID = 'app-id'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET = 'app-secret'; try { const response = await generateImage( agentJsonRequest('agent-matsca-n-limit-key', { prompt: 'agent matsca n limit', n: 5 }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.n, /1 到 4/); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('does not send upstream stream parameters when Agent streaming_strategy is off', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startImageUpstream((body) => { upstreamBody = body; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await generateImage( agentJsonRequest('agent-upstream-stream-off-key', { prompt: 'agent upstream stream off', streaming_strategy: 'off' }) ); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.execution.channel_request_mode, 'images-non-stream'); assert.equal(body.execution.channel_request_mode_fallback_applied, false); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, false); assert.equal(Object.hasOwn(upstreamJson, 'partial_images'), false); } finally { await upstream.close(); } }); it('uses the lower-cost non-streaming channel request mode for Agent auto streaming by default', async () => { const { generateImage } = await loadAgentRoutes(); const { getServerChannelState } = await import('@/lib/server-channel-router'); let upstreamBody = ''; const upstream = await startImageUpstream((body) => { upstreamBody = body; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-non-stream,images-sse'); try { const response = await generateImage( agentJsonRequest('agent-default-auto-stream-key', { prompt: 'agent default auto stream', stream_mode: 'auto' }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); const body = await response.json(); assert.equal(body.images[0].content_url.startsWith('/api/agent/artifacts/'), true); assert.equal(body.execution.channel_request_mode, 'images-non-stream'); assert.equal(body.execution.channel_request_mode_fallback_applied, false); assert.deepEqual(body.execution.route_decision.candidate_channel_request_modes, [ 'images-non-stream', 'images-sse' ]); assert.deepEqual(body.execution.route_decision.request_mode_priority, ['images-non-stream', 'images-sse']); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, false); assert.equal(Object.hasOwn(upstreamJson, 'partial_images'), false); assert.equal(getServerChannelState().streamingAvailability.summary().mark_count, 0); } finally { await upstream.close(); } }); it('uses a non-streaming channel request mode when Agent auto streaming has no SSE channel', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startImageUpstream((body) => { upstreamBody = body; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'json-only'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; try { const response = await generateImage( agentJsonRequest('agent-auto-json-channel-key', { prompt: 'agent auto json channel', stream_mode: 'auto' }) ); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.execution.channel_request_mode, 'images-non-stream'); assert.equal(body.execution.channel_request_mode_fallback_applied, false); assert.deepEqual(body.execution.route_decision, { requested_backend: 'images-api', candidate_channel_request_modes: ['images-non-stream', 'images-sse'], request_mode_priority: ['images-non-stream'], preferred_channel_request_mode: 'images-non-stream', fallback_channel_request_mode: 'images-sse', selected_channel_request_mode: 'images-non-stream', fallback_applied: false, selected_channel_id: 'json-only', upstream_host: new URL(upstream.baseUrl).host }); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, false); assert.equal(Object.hasOwn(upstreamJson, 'partial_images'), false); } finally { await upstream.close(); } }); it('fails explicit Agent stream requests instead of falling back to non-streaming request modes', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'json-only'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_REQUEST_MODES = 'images-non-stream'; try { const response = await generateImage( agentJsonRequest('agent-explicit-stream-no-sse-key', { prompt: 'agent explicit stream no sse', stream_mode: 'stream', streaming_strategy: 'openai-sse', partial_images: 2 }) ); assert.equal(response.status, 503); const body = await response.json(); assert.equal(body.error.code, 'configuration_error'); assert.equal(body.error.diagnostics.channel_request_mode, 'images-sse'); assert.equal(body.error.diagnostics.channel_request_mode_fallback_applied, false); assert.deepEqual(body.error.diagnostics.route_decision, { requested_backend: 'images-api', candidate_channel_request_modes: ['images-sse'], request_mode_priority: ['images-sse'], preferred_channel_request_mode: 'images-sse', selected_channel_request_mode: 'images-sse', fallback_applied: false, no_channel_reason: '当前没有支持 images-sse 的健康渠道凭证。请调整请求策略或 OPENAI_CHANNEL_N_REQUEST_MODES。' }); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('rejects OpenAI-compatible Agent generate profile violations before calling upstream', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.OPENAI_UPSTREAM_PROFILE = 'openai-compatible'; try { const response = await generateImage( agentJsonRequest('agent-generate-profile-violation-key', { prompt: 'transparent object', background: 'transparent', partial_images: 0 }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('does not consume generate idempotency keys for local profile validation failures', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.OPENAI_UPSTREAM_PROFILE = 'openai-compatible'; const idempotencyKey = 'agent-generate-profile-validation-key'; try { const invalid = await generateImage( agentJsonRequest(idempotencyKey, { prompt: 'transparent object', background: 'transparent' }) ); assert.equal(invalid.status, 422); assert.equal(upstreamCalls, 0); const corrected = await generateImage( agentJsonRequest(idempotencyKey, { prompt: 'transparent object', background: 'auto' }) ); assert.equal(corrected.status, 200); assert.equal(upstreamCalls, 1); const body = await corrected.json(); assert.equal(body.idempotency_key, idempotencyKey); } finally { await upstream.close(); } }); it('consumes upstream image SSE internally while keeping the Agent generate response non-streaming', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startStreamingImageUpstream((body) => { upstreamBody = body; return [ { event: 'image_generation.partial_image', data: { type: 'image_generation.partial_image', b64_json: 'agent-partial-base64' } }, { event: 'image_generation.completed', data: { type: 'image_generation.completed', b64_json: PNG_BASE64 } } ]; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-sse'); try { const response = await generateImage( agentJsonRequest('agent-upstream-sse-key', { prompt: 'agent upstream sse', response_mode: 'base64', streaming_strategy: 'newapi-keepalive-sse', partial_images: 2 }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); const body = await response.json(); assert.equal(body.cached, false); assert.equal(body.images[0].b64_json, PNG_BASE64); const upstreamJson = JSON.parse(upstreamBody); assert.equal(upstreamJson.stream, true); assert.equal(upstreamJson.partial_images, 2); } finally { await upstream.close(); } }); it('consumes JSON Images responses returned to Agent stream requests as final results', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startImageUpstream((body) => { upstreamBody = body; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-sse'); try { const response = await generateImage( agentJsonRequest('agent-stream-json-fallback-key', { prompt: 'agent stream json fallback', response_mode: 'base64', streaming_strategy: 'newapi-keepalive-sse', partial_images: 2 }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); const body = await response.json(); assert.equal(body.images[0].b64_json, PNG_BASE64); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, true); assert.equal(upstreamJson.partial_images, 2); } finally { await upstream.close(); } }); it('uses force-sse for Agent upstream image SSE while keeping the final JSON contract', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startStreamingImageUpstream((body) => { upstreamBody = body; return [ { event: 'image_generation.completed', data: { type: 'image_generation.completed', b64_json: PNG_BASE64 } } ]; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-sse'); try { const response = await generateImage( agentJsonRequest('agent-force-sse-key', { prompt: 'agent force sse', response_mode: 'base64', streaming_strategy: 'force-sse', partial_images: 3 }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); const body = await response.json(); assert.equal(body.images[0].b64_json, PNG_BASE64); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, true); assert.equal(upstreamJson.partial_images, 3); } finally { await upstream.close(); } }); it('consumes Responses image_generation SSE internally while keeping the Agent generate response non-streaming', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startStreamingResponsesImageUpstream((body) => { upstreamBody = body; return [ { event: 'response.image_generation_call.partial_image', data: { type: 'response.image_generation_call.partial_image', partial_image_b64: 'agent-responses-partial-base64', partial_image_index: 0 } }, { event: 'response.output_item.done', data: { type: 'response.output_item.done', item: { type: 'image_generation_call', status: 'completed', result: PNG_BASE64 } } } ]; }); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('responses-sse'); try { const response = await generateImage( agentJsonRequest('agent-responses-upstream-sse-key', { prompt: 'agent responses upstream sse', response_mode: 'base64', image_backend: 'responses-image-generation', streaming_strategy: 'responses-sse', partial_images: 2 }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); const body = await response.json(); assert.equal(body.cached, false); assert.equal(body.images[0].b64_json, PNG_BASE64); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.model, 'gpt-5.4'); assert.equal(upstreamJson.stream, true); const tools = upstreamJson.tools as Array>; assert.equal(tools[0].type, 'image_generation'); assert.equal(tools[0].partial_images, 2); } finally { await upstream.close(); } }); it('marks Responses image_generation-disabled 403s as unavailable request modes', async () => { process.env.AGENT_STATE_BACKEND = 'memory'; process.env.OPENAI_CHANNEL_FAILURE_COOLDOWN_ENABLED = 'true'; delete process.env.AGENT_SQLITE_PATH; const { generateImage } = await loadAgentRoutes(); const upstream = await startResponsesImageJsonUpstream(403, { error: { message: 'Image generation is not enabled for this group' } }); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('responses-sse'); try { const response = await generateImage( agentJsonRequest('agent-responses-image-disabled-key', { prompt: 'agent responses disabled', image_backend: 'responses-image-generation', streaming_strategy: 'responses-sse', partial_images: 2 }) ); assert.equal(response.status, 502); const body = await response.json(); assert.equal(body.error.code, 'upstream_unavailable'); assert.equal(body.error.upstream_status, 403); assert.equal(body.error.diagnostics.channel_request_mode, 'responses-sse'); assert.equal(body.error.diagnostics.channel_cooldown_scope, 'channel'); assert.equal(body.error.diagnostics.cooldown_target.channel_id, 'default'); assert.equal(body.error.diagnostics.cooldown_target.request_mode, 'responses-sse'); assert.equal(JSON.stringify(body).includes('test-key'), false); } finally { await upstream.close(); } }); it('uses force-sse for Agent Responses image_generation SSE while keeping the final JSON contract', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startStreamingResponsesImageUpstream((body) => { upstreamBody = body; return [ { event: 'response.output_item.done', data: { type: 'response.output_item.done', item: { type: 'image_generation_call', status: 'completed', result: PNG_BASE64 } } } ]; }); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('responses-sse'); try { const response = await generateImage( agentJsonRequest('agent-responses-force-sse-key', { prompt: 'agent responses force sse', response_mode: 'base64', image_backend: 'responses-image-generation', streaming_strategy: 'force-sse', partial_images: 3 }) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); const body = await response.json(); assert.equal(body.images[0].b64_json, PNG_BASE64); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.model, 'gpt-5.4'); assert.equal(upstreamJson.stream, true); const tools = upstreamJson.tools as Array>; assert.equal(tools[0].type, 'image_generation'); assert.equal(tools[0].partial_images, 3); } finally { await upstream.close(); } }); it('fails Agent Responses upstream SSE requests when partial images arrive without a final image', async () => { const { generateImage } = await loadAgentRoutes(); const upstream = await startStreamingResponsesImageUpstream(() => [ { event: 'response.image_generation_call.partial_image', data: { type: 'response.image_generation_call.partial_image', partial_image_b64: 'agent-responses-partial-only', partial_image_index: 0 } } ]); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('responses-sse'); try { const response = await generateImage( agentJsonRequest('agent-responses-upstream-sse-partial-only-key', { prompt: 'agent responses upstream sse partial only', image_backend: 'responses-image-generation', stream_mode: 'stream', streaming_strategy: 'responses-sse', partial_images: 2 }) ); assert.equal(response.status, 502); const body = await response.json(); assert.equal(body.error.code, 'upstream_unavailable'); assert.match(body.error.message, /最终图片 b64_json/); assert.equal(body.error.diagnostics.upstream_event_type, 'response.image_generation_call.partial_image'); assert.equal(body.error.diagnostics.partial_image_count, 1); assert.equal(body.error.diagnostics.retry_after_seconds, 15); assert.equal(JSON.stringify(body).includes('agent-responses-partial-only'), false); } finally { await upstream.close(); } }); it('rejects Agent Responses upstream SSE partial_images outside the Responses backend contract', async () => { const { generateImage } = await loadAgentRoutes(); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; setPrimaryChannelRequestModes('responses-sse'); const response = await generateImage( agentJsonRequest('agent-responses-partial-range-key', { prompt: 'agent responses partial range', image_backend: 'responses-image-generation', stream_mode: 'stream', streaming_strategy: 'responses-sse', partial_images: 4 }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.partial_images, /1 到 3/); }); it('rejects Agent Images partial_images outside the selected OpenAI-compatible profile before calling upstream', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await generateImage( agentJsonRequest('agent-openai-partial-limit-key', { prompt: 'agent openai partial limit', stream_mode: 'stream', streaming_strategy: 'openai-sse', partial_images: 0 }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.partial_images, /1 到 3/); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('keeps selected upstream image count limits enforced when force_request is enabled', async () => { let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_CHANNEL_1_ID = 'matsca'; process.env.OPENAI_CHANNEL_1_API_KEYS = 'test-key'; process.env.OPENAI_CHANNEL_1_BASE_URL = upstream.baseUrl; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID = 'app-id'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET = 'app-secret'; const { generateImage } = await loadAgentRoutes(); try { const response = await generateImage( agentJsonRequest('agent-force-count-limit-key', { prompt: 'agent force still respects count limits', n: 5, force_request: true }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.n, /1 到 4/); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('fails Agent Responses upstream SSE requests when the image_generation_call fails', async () => { const { generateImage } = await loadAgentRoutes(); const upstream = await startStreamingResponsesImageUpstream(() => [ { event: 'response.output_item.done', data: { type: 'response.output_item.done', item: { type: 'image_generation_call', status: 'failed', error: { code: 'content_policy_violation', message: 'blocked by upstream policy' } } } } ]); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('responses-sse'); try { const response = await generateImage( agentJsonRequest('agent-responses-upstream-sse-failed-call-key', { prompt: 'agent responses upstream sse failed call', image_backend: 'responses-image-generation', stream_mode: 'stream', streaming_strategy: 'responses-sse', partial_images: 2 }) ); assert.equal(response.status, 502); const body = await response.json(); assert.equal(body.error.code, 'upstream_unavailable'); assert.match(body.error.message, /blocked by upstream policy/); } finally { await upstream.close(); } }); it('fails Agent upstream SSE requests when partial images arrive without a final image', async () => { const { generateImage } = await loadAgentRoutes(); const upstream = await startStreamingImageUpstream(() => [ { event: 'image_generation.partial_image', data: { type: 'image_generation.partial_image', b64_json: 'agent-partial-only' } } ]); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-sse'); try { const response = await generateImage( agentJsonRequest('agent-upstream-sse-partial-only-key', { prompt: 'agent upstream sse partial only', stream_mode: 'stream', streaming_strategy: 'newapi-keepalive-sse', partial_images: 2 }) ); assert.equal(response.status, 502); const body = await response.json(); assert.equal(body.error.code, 'upstream_unavailable'); assert.match(body.error.message, /最终图片 b64_json/); assert.equal(body.error.diagnostics.upstream_event_type, 'image_generation.partial_image'); assert.equal(body.error.diagnostics.partial_image_count, 1); } finally { await upstream.close(); } }); it('generates and replays through the memory state backend without creating SQLite state', async () => { process.env.AGENT_STATE_BACKEND = 'memory'; delete process.env.AGENT_SQLITE_PATH; const { generateImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const first = await generateImage( agentJsonRequest('route-memory-cache-key', { prompt: 'agent memory route success' }) ); assert.equal(first.status, 200); const firstBody = await first.json(); assert.equal(firstBody.cached, false); const second = await generateImage( agentJsonRequest('route-memory-cache-key', { prompt: 'agent memory route success' }) ); assert.equal(second.status, 200); const secondBody = await second.json(); assert.equal(secondBody.cached, true); assert.equal(second.headers.get('x-idempotent-replay'), 'true'); assert.equal(secondBody.request_id, firstBody.request_id); assert.equal(upstreamCalls, 1); assert.deepEqual(await listAgentStateFiles(), []); } finally { await upstream.close(); } }); it('returns explicit base64 without storing complete base64 in the request state', async () => { const { generateImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; const first = await generateImage( agentJsonRequest('route-base64-key', { prompt: 'agent route base64', response_mode: 'base64' }) ); assert.equal(first.status, 200); const firstBody = await first.json(); assert.equal(firstBody.cached, false); assert.equal(firstBody.images[0].b64_json, PNG_BASE64); const storedResponse = readStoredResponseJson('route-base64-key'); assert.equal(storedResponse.includes(PNG_BASE64), false); assert.equal(storedResponse.includes('b64_json'), false); const second = await generateImage( agentJsonRequest('route-base64-key', { prompt: 'agent route base64', response_mode: 'base64' }) ); assert.equal(second.status, 200); const secondBody = await second.json(); assert.equal(secondBody.cached, true); assert.equal(secondBody.images[0].b64_json, PNG_BASE64); assert.equal(upstreamCalls, 1); await upstream.close(); }); it('returns field-level errors for invalid generate JSON requests', async () => { const { generateImage } = await loadAgentRoutes(); const response = await generateImage( agentJsonRequest('route-validation-key', { prompt: '', n: 99, response_mode: 'url' }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.prompt, /必填/); assert.match(body.error.details.fields.n, /1 到 10/); assert.match(body.error.details.fields.response_mode, /path/); }); it('returns validation errors for malformed generate JSON requests', async () => { const { generateImage } = await loadAgentRoutes(); const response = await generateImage( new Request('http://localhost/api/agent/images/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'route-malformed-json-key' }, body: '{"prompt":' }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.equal(body.error.retryable, false); }); it('returns sanitized upstream diagnostics for failed generate requests', async () => { process.env.OPENAI_CHANNEL_FAILURE_COOLDOWN_ENABLED = 'true'; const { generateImage, lookupAgentRequestDiagnostics } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; throw new Error('upstream failed'); }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const idempotencyKey = 'route-upstream-diagnostics-key'; const response = await generateImage(agentJsonRequest(idempotencyKey, { prompt: 'diagnostics' })); assert.equal(response.status, 502); const body = await response.json(); assert.equal(body.error.code, 'upstream_unavailable'); assert.equal(body.error.upstream_status, 500); assert.equal(body.error.diagnostics.upstream_status, 500); assert.equal(body.error.diagnostics.selected_channel_id, 'default'); assert.match(body.error.diagnostics.upstream_host, /^127\.0\.0\.1:\d+$/); assert.equal(body.error.diagnostics.channel_cooldown_scope, 'channel'); assert.equal(body.error.diagnostics.cooldown_target.channel_id, 'default'); assert.equal(body.error.diagnostics.cooldown_target.request_mode, 'images-non-stream'); assert.equal(typeof body.error.diagnostics.retry_after_ms, 'number'); assert.equal(typeof body.error.diagnostics.cooldown_until, 'string'); assert.equal(typeof body.error.diagnostics.elapsed_ms, 'number'); assert.equal(JSON.stringify(body).includes('test-key'), false); const upstreamCallsAfterFirstFailure = upstreamCalls; const replay = await generateImage(agentJsonRequest(idempotencyKey, { prompt: 'diagnostics' })); assert.equal(replay.status, 502); assert.equal(replay.headers.get('x-idempotent-replay'), 'true'); assert.equal(replay.headers.get('x-request-id'), body.error.request_id); const replayBody = await replay.json(); assert.equal(replayBody.error.code, 'upstream_unavailable'); assert.equal(replayBody.error.retryable, false); assert.equal(replayBody.error.request_id, body.error.request_id); assert.equal(upstreamCalls, upstreamCallsAfterFirstFailure); const diagnostics = await lookupAgentRequestDiagnostics( new Request(`http://localhost/api/agent/diagnostics/requests?idempotency_key=${idempotencyKey}`) ); assert.equal(diagnostics.status, 200); const diagnosticsBody = await diagnostics.json(); assert.equal(diagnosticsBody.found, true); assert.equal(diagnosticsBody.diagnostics.request.request_id, body.error.request_id); assert.equal(diagnosticsBody.diagnostics.request.status, 'failed'); assert.equal(diagnosticsBody.diagnostics.error.code, 'upstream_unavailable'); assert.equal(diagnosticsBody.diagnostics.error.retryable, true); assert.equal(diagnosticsBody.diagnostics.error.diagnostics.selected_channel_id, 'default'); assert.match(diagnosticsBody.diagnostics.error.diagnostics.upstream_host, /^127\.0\.0\.1:\d+$/); assert.equal(diagnosticsBody.diagnostics.error.diagnostics.cooldown_target.channel_id, 'default'); assert.equal( diagnosticsBody.diagnostics.error.diagnostics.cooldown_target.request_mode, 'images-non-stream' ); assert.equal(typeof diagnosticsBody.diagnostics.error.diagnostics.retry_after_ms, 'number'); assert.equal(JSON.stringify(diagnosticsBody).includes('test-key'), false); } finally { await upstream.close(); } }); it('creates a generate job, exposes running status, and returns the completed result', async () => { const { createGenerateJob, getJob, getJobResult } = await loadAgentRoutes(); let releaseUpstream: (() => void) | undefined; let upstreamCalls = 0; const upstream = await startImageUpstream(async () => { upstreamCalls += 1; await new Promise((resolve) => { releaseUpstream = resolve; }); return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const created = await createGenerateJob( agentJobJsonRequest('route-job-key', { prompt: 'agent route job' }) ); assert.equal(created.status, 202); const createdBody = await created.json(); assert.equal(createdBody.job.state, 'running'); assert.equal(createdBody.job.idempotency_key, 'route-job-key'); assert.equal(createdBody.job.result_url, `/api/agent/jobs/${createdBody.job.id}/result`); await waitFor(() => upstreamCalls === 1); const running = await getJob(new Request(`http://localhost/api/agent/jobs/${createdBody.job.id}`), { params: Promise.resolve({ id: createdBody.job.id }) }); assert.equal(running.status, 200); assert.equal((await running.json()).job.state, 'running'); releaseUpstream?.(); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 200); const resultBody = await result.json(); assert.equal(resultBody.request_id, createdBody.job.id); assert.equal(resultBody.cached, false); assert.equal(resultBody.images.length, 1); assert.equal('b64_json' in resultBody.images[0], false); assert.equal(resultBody.execution.transport, 'agent_job_polling'); assert.equal(resultBody.execution.endpoint, '/api/agent/jobs/images/generate'); assert.equal(resultBody.execution.route_mode, 'job'); assert.equal(resultBody.execution.channel_request_mode, 'images-non-stream'); assert.equal(resultBody.execution.channel_request_mode_fallback_applied, false); assert.equal(typeof resultBody.timing.elapsed_ms, 'number'); assert.equal(resultBody.timing.elapsed_ms >= 0, true); } finally { releaseUpstream?.(); await upstream.close(); } }); it('creates a server-orchestrated image request job and records the orchestration endpoint', async () => { const { createImageRequest, getJobResult } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const created = await createImageRequest( agentImageRequest('route-orchestrated-key', { prompt: 'server orchestrated generate' }) ); assert.equal(created.status, 202); const createdBody = await created.json(); assert.equal(createdBody.job.state, 'running'); assert.equal(createdBody.job.idempotency_key, 'route-orchestrated-key'); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 200); const resultBody = await result.json(); assert.equal(resultBody.request_id, createdBody.job.id); assert.equal(resultBody.cached, false); assert.equal(resultBody.images.length, 1); assert.equal(resultBody.execution.transport, 'agent_job_polling'); assert.equal(resultBody.execution.endpoint, '/api/agent/image-requests'); assert.equal(resultBody.execution.route_mode, 'job'); assert.equal(resultBody.execution.channel_request_mode, 'images-non-stream'); assert.equal(resultBody.execution.channel_request_mode_fallback_applied, false); assert.equal(upstreamCalls, 1); } finally { await upstream.close(); } }); it('does not consume generate job idempotency keys for local profile validation failures', async () => { const { createGenerateJob, getJobResult } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.OPENAI_UPSTREAM_PROFILE = 'openai-compatible'; const idempotencyKey = 'route-job-local-validation-key'; try { const invalid = await createGenerateJob( agentJobJsonRequest(idempotencyKey, { prompt: 'job transparent object', background: 'transparent' }) ); assert.equal(invalid.status, 422); assert.equal((await invalid.json()).error.code, 'validation_error'); assert.equal(upstreamCalls, 0); const corrected = await createGenerateJob( agentJobJsonRequest(idempotencyKey, { prompt: 'job transparent object', background: 'auto' }) ); assert.equal(corrected.status, 202); const correctedBody = await corrected.json(); assert.equal(correctedBody.job.idempotency_key, idempotencyKey); const result = await waitForJobResult(getJobResult, correctedBody.job.id); assert.equal(result.status, 200); assert.equal((await result.json()).idempotency_key, idempotencyKey); assert.equal(upstreamCalls, 1); } finally { await upstream.close(); } }); it('creates a generate job that consumes upstream image SSE and saves the final artifact', async () => { const { createGenerateJob, getJobResult, getArtifactContent } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startStreamingImageUpstream((body) => { upstreamBody = body; return [ { event: 'image_generation.partial_image', data: { type: 'image_generation.partial_image', b64_json: 'job-partial-base64' } }, { event: 'image_generation.completed', data: { type: 'image_generation.completed', b64_json: PNG_BASE64 } } ]; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-sse'); try { const created = await createGenerateJob( agentJobJsonRequest('route-job-upstream-sse-key', { prompt: 'agent job upstream sse', streaming_strategy: 'newapi-keepalive-sse', partial_images: 2 }) ); assert.equal(created.status, 202); const createdBody = await created.json(); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 200); assert.notEqual(result.headers.get('content-type'), 'text/event-stream'); const resultBody = await result.json(); assert.equal(resultBody.request_id, createdBody.job.id); assert.equal(resultBody.cached, false); assert.equal(resultBody.images.length, 1); assert.equal('b64_json' in resultBody.images[0], false); assert.match(resultBody.images[0].content_url, /^\/api\/agent\/artifacts\/[^/]+\/content$/); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.stream, true); assert.equal(upstreamJson.partial_images, 2); const artifactId = resultBody.images[0].content_url.split('/').at(-2); assert.equal(typeof artifactId, 'string'); const content = await getArtifactContent( new Request(`http://localhost/api/agent/artifacts/${artifactId}/content`), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(content.status, 200); assert.equal( Buffer.compare(Buffer.from(await content.arrayBuffer()), Buffer.from(PNG_BASE64, 'base64')), 0 ); } finally { await upstream.close(); } }); it('fails image upstream SSE generate jobs when partial images arrive without a final image', async () => { const { createGenerateJob, getJob, getJobResult } = await loadAgentRoutes(); const upstream = await startStreamingImageUpstream(() => [ { event: 'image_generation.partial_image', data: { type: 'image_generation.partial_image', b64_json: 'job-images-partial-only' } } ]); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-sse'); try { const created = await createGenerateJob( agentJobJsonRequest('route-job-images-missing-final-key', { prompt: 'agent job images partial only', stream_mode: 'stream', streaming_strategy: 'newapi-keepalive-sse', partial_images: 2 }) ); assert.equal(created.status, 202); const createdBody = await created.json(); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 502); const resultBody = await result.json(); assert.equal(resultBody.error.code, 'upstream_unavailable'); assert.match(resultBody.error.message, /最终图片 b64_json/); assert.equal(resultBody.error.retryable, false); assert.equal(resultBody.error.diagnostics.upstream_event_type, 'image_generation.partial_image'); assert.equal(resultBody.error.diagnostics.partial_image_count, 1); assert.equal(resultBody.error.diagnostics.retry_after_seconds, undefined); assert.equal(JSON.stringify(resultBody).includes('job-images-partial-only'), false); const status = await getJob(new Request(`http://localhost/api/agent/jobs/${createdBody.job.id}`), { params: Promise.resolve({ id: createdBody.job.id }) }); assert.equal(status.status, 200); const statusBody = await status.json(); assert.equal(statusBody.job.state, 'failed'); assert.equal(statusBody.job.error.code, 'upstream_unavailable'); assert.match(statusBody.job.error.message, /最终图片 b64_json/); assert.equal(statusBody.job.error.retryable, false); assert.equal(statusBody.job.error.diagnostics.upstream_event_type, 'image_generation.partial_image'); assert.equal(statusBody.job.error.diagnostics.partial_image_count, 1); assert.equal(statusBody.job.error.diagnostics.retry_after_seconds, undefined); assert.equal(JSON.stringify(statusBody).includes('job-images-partial-only'), false); } finally { await upstream.close(); } }); it('creates a generate job that consumes Responses image_generation SSE and saves the final artifact', async () => { const { createGenerateJob, getJobResult, getArtifactContent } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startStreamingResponsesImageUpstream((body) => { upstreamBody = body; return [ { event: 'response.image_generation_call.partial_image', data: { type: 'response.image_generation_call.partial_image', partial_image_b64: 'job-responses-partial-base64', partial_image_index: 0 } }, { event: 'response.output_item.done', data: { type: 'response.output_item.done', item: { type: 'image_generation_call', status: 'completed', result: PNG_BASE64 } } } ]; }); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('responses-sse'); try { const created = await createGenerateJob( agentJobJsonRequest('route-job-responses-upstream-sse-key', { prompt: 'agent job responses upstream sse', image_backend: 'responses-image-generation', streaming_strategy: 'responses-sse', partial_images: 2 }) ); assert.equal(created.status, 202); const createdBody = await created.json(); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 200); assert.notEqual(result.headers.get('content-type'), 'text/event-stream'); const resultBody = await result.json(); assert.equal(resultBody.request_id, createdBody.job.id); assert.equal(resultBody.cached, false); assert.equal(resultBody.images.length, 1); assert.equal('b64_json' in resultBody.images[0], false); assert.match(resultBody.images[0].content_url, /^\/api\/agent\/artifacts\/[^/]+\/content$/); const upstreamJson = JSON.parse(upstreamBody) as Record; assert.equal(upstreamJson.model, 'gpt-5.4'); assert.equal(upstreamJson.stream, true); assert.equal((upstreamJson.tool_choice as Record).type, 'image_generation'); const tools = upstreamJson.tools as Array>; assert.equal(tools[0].type, 'image_generation'); assert.equal(tools[0].partial_images, 2); const artifactId = resultBody.images[0].content_url.split('/').at(-2); assert.equal(typeof artifactId, 'string'); const content = await getArtifactContent( new Request(`http://localhost/api/agent/artifacts/${artifactId}/content`), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(content.status, 200); assert.equal( Buffer.compare(Buffer.from(await content.arrayBuffer()), Buffer.from(PNG_BASE64, 'base64')), 0 ); } finally { await upstream.close(); } }); it('fails Responses upstream SSE generate jobs when partial images arrive without a final image', async () => { const { createGenerateJob, getJob, getJobResult } = await loadAgentRoutes(); const upstream = await startStreamingResponsesImageUpstream(() => [ { event: 'response.image_generation_call.partial_image', data: { type: 'response.image_generation_call.partial_image', partial_image_b64: 'job-responses-partial-only', partial_image_index: 0 } } ]); process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true'; process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4'; process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('responses-sse'); try { const created = await createGenerateJob( agentJobJsonRequest('route-job-responses-missing-final-key', { prompt: 'agent job responses partial only', image_backend: 'responses-image-generation', stream_mode: 'stream', streaming_strategy: 'responses-sse', partial_images: 2 }) ); assert.equal(created.status, 202); const createdBody = await created.json(); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 502); const resultBody = await result.json(); assert.equal(resultBody.error.code, 'upstream_unavailable'); assert.match(resultBody.error.message, /最终图片 b64_json/); assert.equal(resultBody.error.retryable, false); assert.equal( resultBody.error.diagnostics.upstream_event_type, 'response.image_generation_call.partial_image' ); assert.equal(resultBody.error.diagnostics.partial_image_count, 1); assert.equal(resultBody.error.diagnostics.retry_after_seconds, undefined); assert.equal(JSON.stringify(resultBody).includes('job-responses-partial-only'), false); const status = await getJob(new Request(`http://localhost/api/agent/jobs/${createdBody.job.id}`), { params: Promise.resolve({ id: createdBody.job.id }) }); assert.equal(status.status, 200); const statusBody = await status.json(); assert.equal(statusBody.job.state, 'failed'); assert.equal(statusBody.job.error.code, 'upstream_unavailable'); assert.match(statusBody.job.error.message, /最终图片 b64_json/); assert.equal(statusBody.job.error.retryable, false); assert.equal( statusBody.job.error.diagnostics.upstream_event_type, 'response.image_generation_call.partial_image' ); assert.equal(statusBody.job.error.diagnostics.partial_image_count, 1); assert.equal(statusBody.job.error.diagnostics.retry_after_seconds, undefined); assert.equal(JSON.stringify(statusBody).includes('job-responses-partial-only'), false); } finally { await upstream.close(); } }); it('reuses the running generate job for the same idempotency key', async () => { const { createGenerateJob, getJobResult } = await loadAgentRoutes(); let releaseUpstream: (() => void) | undefined; let upstreamCalls = 0; const upstream = await startImageUpstream(async () => { upstreamCalls += 1; await new Promise((resolve) => { releaseUpstream = resolve; }); return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const first = await createGenerateJob(agentJobJsonRequest('route-job-reuse-key', { prompt: 'job reuse' })); assert.equal(first.status, 202); const firstBody = await first.json(); await waitFor(() => upstreamCalls === 1); const second = await createGenerateJob(agentJobJsonRequest('route-job-reuse-key', { prompt: 'job reuse' })); assert.equal(second.status, 202); assert.equal(second.headers.get('x-idempotent-replay'), 'true'); const secondBody = await second.json(); assert.equal(secondBody.job.id, firstBody.job.id); assert.equal(secondBody.job.state, 'running'); assert.equal(upstreamCalls, 1); releaseUpstream?.(); const result = await waitForJobResult(getJobResult, firstBody.job.id); assert.equal(result.status, 200); } finally { releaseUpstream?.(); await upstream.close(); } }); it('rejects generate job idempotency keys reused with a different request body', async () => { const { createGenerateJob, getJobResult } = await loadAgentRoutes(); let releaseUpstream: (() => void) | undefined; let upstreamCalls = 0; const upstream = await startImageUpstream(async () => { upstreamCalls += 1; await new Promise((resolve) => { releaseUpstream = resolve; }); return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const first = await createGenerateJob( agentJobJsonRequest('route-job-conflict-key', { prompt: 'first job body' }) ); assert.equal(first.status, 202); const firstBody = await first.json(); await waitFor(() => upstreamCalls === 1); const conflict = await createGenerateJob( agentJobJsonRequest('route-job-conflict-key', { prompt: 'different job body' }) ); assert.equal(conflict.status, 409); assert.equal((await conflict.json()).error.code, 'idempotency_conflict'); assert.equal(upstreamCalls, 1); releaseUpstream?.(); const result = await waitForJobResult(getJobResult, firstBody.job.id); assert.equal(result.status, 200); } finally { releaseUpstream?.(); await upstream.close(); } }); it('returns stored Agent errors for failed generate jobs', async () => { const { createGenerateJob, getJob, getJobResult } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; throw new Error('job upstream failed'); }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const created = await createGenerateJob( agentJobJsonRequest('route-job-failure-key', { prompt: 'job failure' }) ); assert.equal(created.status, 202); const createdBody = await created.json(); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 502); const resultBody = await result.json(); assert.equal(resultBody.error.code, 'upstream_unavailable'); assert.equal(resultBody.error.retryable, false); assert.equal(resultBody.error.upstream_status, 500); assert.equal(resultBody.error.diagnostics.upstream_status, 500); assert.equal(resultBody.error.request_id, createdBody.job.id); assert.equal(JSON.stringify(resultBody).includes('test-key'), false); const status = await getJob(new Request(`http://localhost/api/agent/jobs/${createdBody.job.id}`), { params: Promise.resolve({ id: createdBody.job.id }) }); assert.equal(status.status, 200); const statusBody = await status.json(); assert.equal(statusBody.job.state, 'failed'); assert.equal(statusBody.job.error.code, 'upstream_unavailable'); assert.equal(statusBody.job.error.retryable, false); assert.equal(statusBody.job.error.upstream_status, 500); assert.equal(statusBody.job.error.diagnostics.upstream_status, 500); assert.equal(upstreamCalls > 0, true); const replay = await createGenerateJob( agentJobJsonRequest('route-job-failure-key', { prompt: 'job failure' }) ); assert.equal(replay.status, 202); assert.equal(replay.headers.get('x-idempotent-replay'), 'true'); const replayBody = await replay.json(); assert.equal(replayBody.job.id, createdBody.job.id); assert.equal(replayBody.job.state, 'failed'); } finally { await upstream.close(); } }); it('marks generate jobs as failed when completion state persistence fails', async () => { const { createGenerateJob, getJob } = await loadAgentRoutes(); const { setAgentStateStoreFactoryForTests } = await import('@/lib/agent-state-runtime'); const requestId = 'job-completion-failure-request'; let failErrorCode: AgentErrorCode | undefined; let saveCalls = 0; const upstream = await startImageUpstream(() => ({ data: [{ b64_json: PNG_BASE64 }] })); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setAgentStateStoreFactoryForTests(() => ({ async init() {}, async recoverExpiredRequests() { return 0; }, async purgeExpiredRequests() { return 0; }, async beginRequest() { return { type: 'acquired', record: { requestId, idempotencyKey: 'job-completion-failure-key', requestHash: 'hash', mode: 'generate', status: 'running', requestJson: { prompt: 'job completion persistence failure' }, createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', expiresAt: '2099-05-13T00:00:00.000Z' } }; }, async refreshRequestLease() { return true; }, async saveArtifacts() { saveCalls += 1; }, async completeRequest() { throw new Error('job completion persistence failed'); }, async failRequest(input: { requestId: string; error: { error: { code: AgentErrorCode } } }) { assert.equal(input.requestId, requestId); failErrorCode = input.error.error.code; }, async getRequest(id: string) { if (id !== requestId || !failErrorCode) return undefined; return { requestId, idempotencyKey: 'job-completion-failure-key', requestHash: 'hash', mode: 'generate', status: 'failed', requestJson: { prompt: 'job completion persistence failure' }, errorJson: { error: { code: failErrorCode, message: '保存请求完成状态失败。', retryable: true, request_id: requestId } }, createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:01.000Z', expiresAt: '2099-05-13T00:00:00.000Z' }; }, async getRequestByIdempotencyKey() { return undefined; }, async getArtifact() { return undefined; }, async listArtifactsForRequest() { return []; }, async listArtifactFilepaths() { return []; }, async deleteArtifact() { return false; } })); const originalConsoleError = console.error; console.error = () => {}; try { const created = await createGenerateJob( agentJobJsonRequest('job-completion-failure-key', { prompt: 'job completion persistence failure' }) ); assert.equal(created.status, 202); await waitFor(() => failErrorCode === 'unexpected_error'); assert.equal(saveCalls, 1); const status = await getJob(new Request(`http://localhost/api/agent/jobs/${requestId}`), { params: Promise.resolve({ id: requestId }) }); assert.equal(status.status, 200); const statusBody = await status.json(); assert.equal(statusBody.job.state, 'failed'); assert.equal(statusBody.job.error.code, 'unexpected_error'); assert.equal(statusBody.job.error.retryable, false); } finally { console.error = originalConsoleError; await upstream.close(); } }); it('keeps a long-running generate job leased while the upstream call is still active', async () => { process.env.AGENT_REQUEST_LEASE_MS = '200'; process.env.AGENT_RECOVERY_INTERVAL_MS = '50'; const { createGenerateJob, getJob, getJobResult } = await loadAgentRoutes(); let releaseUpstream: (() => void) | undefined; let upstreamCalls = 0; const upstream = await startImageUpstream(async () => { upstreamCalls += 1; await new Promise((resolve) => { releaseUpstream = resolve; }); return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const created = await createGenerateJob( agentJobJsonRequest('route-job-lease-key', { prompt: 'job lease' }) ); assert.equal(created.status, 202); const createdBody = await created.json(); await waitFor(() => upstreamCalls === 1); await new Promise((resolve) => setTimeout(resolve, 350)); const status = await getJob(new Request(`http://localhost/api/agent/jobs/${createdBody.job.id}`), { params: Promise.resolve({ id: createdBody.job.id }) }); assert.equal(status.status, 200); const statusBody = await status.json(); assert.equal(statusBody.job.state, 'running'); releaseUpstream?.(); const result = await waitForJobResult(getJobResult, createdBody.job.id); assert.equal(result.status, 200); } finally { releaseUpstream?.(); await upstream.close(); } }); it('returns structured errors for missing and expired jobs', async () => { const { getJob, getJobResult } = await loadAgentRoutes(); const { resetAgentStateStoreForTests, setAgentStateStoreFactoryForTests } = await import('@/lib/agent-state-runtime'); const missing = await getJob(new Request('http://localhost/api/agent/jobs/missing-job'), { params: Promise.resolve({ id: 'missing-job' }) }); assert.equal(missing.status, 404); assert.equal((await missing.json()).error.code, 'job_not_found'); setAgentStateStoreFactoryForTests(() => ({ async init() {}, async recoverExpiredRequests() { return 0; }, async purgeExpiredRequests() { return 0; }, async beginRequest() { throw new Error('not used'); }, async refreshRequestLease() { return false; }, async saveArtifacts() {}, async completeRequest() {}, async failRequest() {}, async getRequest() { return { requestId: 'expired-job', idempotencyKey: 'expired-key', requestHash: 'hash', mode: 'generate', status: 'running', requestJson: { prompt: 'expired' }, createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', expiresAt: '2026-05-12T00:00:01.000Z' }; }, async getRequestByIdempotencyKey() { return undefined; }, async getArtifact() { return undefined; }, async listArtifactsForRequest() { return []; }, async listArtifactFilepaths() { return []; }, async deleteArtifact() { return false; } })); await resetAgentStateStoreForTests(); const expired = await getJobResult(new Request('http://localhost/api/agent/jobs/expired-job/result'), { params: Promise.resolve({ id: 'expired-job' }) }); assert.equal(expired.status, 410); assert.equal((await expired.json()).error.code, 'job_expired'); }); it('edits through multipart input and replays the cached response for the same idempotency key', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; let upstreamBody = ''; const upstream = await startImageUpstream((body) => { upstreamCalls += 1; upstreamBody = body; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; const first = await editImage(agentEditRequest('route-edit-key', 'agent edit success')); assert.equal(first.status, 200); const firstBody = await first.json(); assert.equal(firstBody.cached, false); assert.equal(firstBody.images[0].output_format, 'webp'); assert.equal(firstBody.images[0].mime_type, 'image/webp'); assert.equal(firstBody.images[0].filename.endsWith('.webp'), true); assert.equal('b64_json' in firstBody.images[0], false); assert.doesNotMatch(upstreamBody, /name="output_format"/); assert.doesNotMatch(upstreamBody, /name="response_format"/); const second = await editImage(agentEditRequest('route-edit-key', 'agent edit success')); assert.equal(second.status, 200); const secondBody = await second.json(); assert.equal(secondBody.cached, true); assert.equal(second.headers.get('x-idempotent-replay'), 'true'); assert.equal(second.headers.get('x-request-id'), firstBody.request_id); assert.equal(secondBody.request_id, firstBody.request_id); assert.equal(upstreamCalls, 1); await upstream.close(); }); it('does not consume edit idempotency keys for local input validation failures', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; const idempotencyKey = 'route-edit-local-validation-key'; const invalidFormData = new FormData(); invalidFormData.append('prompt', 'agent edit without image'); invalidFormData.append('model', 'gpt-image-2'); invalidFormData.append('stream_mode', 'non_stream'); try { const invalid = await editImage( new Request('http://localhost/api/agent/images/edit', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey }, body: invalidFormData }) ); assert.equal(invalid.status, 422); assert.equal(upstreamCalls, 0); const corrected = await editImage(agentEditRequest(idempotencyKey, 'agent edit without image')); assert.equal(corrected.status, 200); assert.equal(upstreamCalls, 1); const body = await corrected.json(); assert.equal(body.idempotency_key, idempotencyKey); } finally { await upstream.close(); } }); it('can consume Agent edit upstream SSE internally while returning final JSON', async () => { const { editImage } = await loadAgentRoutes(); let upstreamBody = ''; const upstream = await startStreamingImageUpstream((body) => { upstreamBody = body; return [ { event: 'image_edit.completed', data: { type: 'image_edit.completed', b64_json: PNG_CONVERTIBLE_BASE64 } } ]; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setPrimaryChannelRequestModes('images-sse'); try { const response = await editImage( agentEditRequest( 'route-edit-upstream-sse-key', 'agent edit stream', {}, { stream_mode: 'stream', streaming_strategy: 'openai-sse', partial_images: '2' } ) ); assert.equal(response.status, 200); assert.notEqual(response.headers.get('content-type'), 'text/event-stream'); const body = await response.json(); assert.equal(body.images[0].content_url.startsWith('/api/agent/artifacts/'), true); assert.match(upstreamBody, /name="stream"/); assert.match(upstreamBody, /name="partial_images"/); } finally { await upstream.close(); } }); it('aborts Agent edit upstream calls when the client request signal aborts', async () => { const { editImage } = await loadAgentRoutes(); const { getServerChannelState } = await import('@/lib/server-channel-router'); const upstream = await startHangingImageEditUpstream(); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.OPENAI_UPSTREAM_REQUEST_MODES = 'images-sse'; const abortController = new AbortController(); try { const responsePromise = editImage( agentEditRequest( 'route-edit-abort-key', 'agent edit abort', {}, { stream_mode: 'auto', streaming_strategy: 'openai-sse' }, { signal: abortController.signal } ) ); await waitFor(() => upstream.requests === 1); abortController.abort(); const response = await Promise.race([ responsePromise, new Promise((_, reject) => setTimeout(() => reject(new Error('Agent edit upstream call did not abort')), 1500) ) ]); assert.notEqual(response.status, 200); assert.equal(getServerChannelState().streamingAvailability.summary().mark_count, 0); } finally { abortController.abort(); await upstream.close(); } }); it('returns field-level errors for invalid edit multipart requests', async () => { const { editImage } = await loadAgentRoutes(); const upstream = await startImageUpstream(() => ({ data: [{ b64_json: PNG_BASE64 }] })); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; const response = await editImage( agentEditRequest('route-edit-validation-key', 'agent edit invalid', {}, 'url') ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.response_mode, /path/); await upstream.close(); }); it('rejects Agent edit masks without transparent pixels before contacting upstream', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await editImage( agentEditRequest( 'route-edit-mask-alpha-key', 'agent edit mask alpha', {}, { mask: Buffer.from(PNG_BASE64, 'base64') } ) ); assert.equal(response.status, 422); const body = await response.json(); assert.match(body.error.details.fields.mask, /mask 必须包含透明区域/); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('rejects unsupported fields on Agent edit requests before calling upstream', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await editImage( agentEditRequest( 'route-edit-generate-only-fields-key', 'agent edit invalid fields', {}, { imageBackend: 'responses-image-generation', image_backend: 'responses-image-generation', format: 'webp', outputFormat: 'jpeg', output_format: 'jpeg', outputCompression: '80', output_compression: '80', responsesModel: 'gpt-4.1', responses_model: 'gpt-4.1', background: 'opaque', moderation: 'auto' } ) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.imageBackend, /不接受该字段/); assert.match(body.error.details.fields.image_backend, /不接受该字段/); assert.match(body.error.details.fields.format, /不接受该字段/); assert.match(body.error.details.fields.outputFormat, /不接受该字段/); assert.match(body.error.details.fields.output_format, /不接受该字段/); assert.match(body.error.details.fields.outputCompression, /不接受该字段/); assert.match(body.error.details.fields.output_compression, /不接受该字段/); assert.match(body.error.details.fields.responsesModel, /不接受该字段/); assert.match(body.error.details.fields.responses_model, /不接受该字段/); assert.match(body.error.details.fields.background, /不接受该字段/); assert.match(body.error.details.fields.moderation, /不接受该字段/); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('rejects page-only streaming strategy fields on Agent edit requests before calling upstream', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await editImage( agentEditRequest( 'route-edit-page-streaming-field-key', 'agent edit invalid page streaming field', {}, { image_streaming_strategy: 'force-sse', imageStreamingStrategy: 'force-sse' } ) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.image_streaming_strategy, /streaming_strategy/); assert.match(body.error.details.fields.imageStreamingStrategy, /streaming_strategy/); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('rejects OpenAI-compatible Agent edit partial_images violations before calling upstream', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.OPENAI_UPSTREAM_PROFILE = 'openai-compatible'; setPrimaryChannelRequestModes('images-sse'); try { const response = await editImage( agentEditRequest( 'route-edit-profile-partial-key', 'agent edit invalid profile partial', {}, { stream_mode: 'stream', streaming_strategy: 'force-sse', partial_images: '0' } ) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.details.fields.partial_images, /1 到 3/); assert.equal(upstreamCalls, 0); } finally { await upstream.close(); } }); it('allows high-resolution Agent edit requests as an explicit fallback path', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await editImage( agentEditRequest('route-edit-high-resolution-key', 'high resolution edit', {}, { size: '3072x2048' }) ); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.images[0].output_format, 'webp'); assert.equal(body.images[0].mime_type, 'image/webp'); assert.equal(body.images[0].filename.endsWith('.webp'), true); assert.equal(upstreamCalls, 1); } finally { await upstream.close(); } }); it('allows Agent edit force_request to bypass local fixed-size profile limits', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.OPENAI_UPSTREAM_PROFILE = 'openai-compatible'; try { const rejected = await editImage( agentEditRequest('route-edit-small-size-rejected-key', 'small edit', {}, { size: '512x512' }) ); assert.equal(rejected.status, 422); const rejectedBody = await rejected.json(); assert.equal(rejectedBody.error.code, 'validation_error'); assert.match(rejectedBody.error.details.fields.size, /总像素必须至少/); assert.equal(upstreamCalls, 0); const forced = await editImage( agentEditRequest( 'route-edit-small-size-forced-key', 'small edit', {}, { size: '512x512', force_request: 'true' } ) ); assert.equal(forced.status, 200); assert.equal(upstreamCalls, 1); } finally { await upstream.close(); } }); it('reports missing image files for high-resolution Agent edit before API credentials', async () => { const { editImage } = await loadAgentRoutes(); delete process.env.OPENAI_API_KEY; delete process.env.OPENAI_API_BASE_URL; const formData = new FormData(); formData.append('prompt', 'high resolution edit without file'); formData.append('model', 'gpt-image-2'); formData.append('size', '3072x2048'); formData.append('response_mode', 'path'); const response = await editImage( new Request('http://localhost/api/agent/images/edit', { method: 'POST', headers: { 'Idempotency-Key': 'route-edit-high-resolution-no-file-key' }, body: formData }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.match(body.error.message, /图片文件/); assert.doesNotMatch(body.error.message, /API Key/); }); it('allows auto-size Agent edit when the uploaded source image is high resolution', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const response = await editImage( agentEditRequest( 'route-edit-auto-high-resolution-source-key', 'auto high resolution source', {}, { size: 'auto', image_0: createPngWithDimensions(3072, 2048) } ) ); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.images[0].output_format, 'webp'); assert.equal(body.images[0].mime_type, 'image/webp'); assert.equal(body.images[0].filename.endsWith('.webp'), true); assert.equal(upstreamCalls, 1); } finally { await upstream.close(); } }); it('returns validation errors for non-multipart edit requests', async () => { const { editImage } = await loadAgentRoutes(); const response = await editImage( new Request('http://localhost/api/agent/images/edit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'route-edit-non-multipart-key' }, body: JSON.stringify({ prompt: 'not multipart' }) }) ); assert.equal(response.status, 415); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.equal(body.error.retryable, false); }); it('returns validation errors for malformed edit multipart requests', async () => { const { editImage } = await loadAgentRoutes(); const response = await editImage( new Request('http://localhost/api/agent/images/edit', { method: 'POST', headers: { 'Content-Type': 'multipart/form-data; boundary=broken-boundary', 'Idempotency-Key': 'route-edit-malformed-multipart-key' }, body: '--not-the-declared-boundary\r\n' }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.equal(body.error.retryable, false); }); it('returns sanitized upstream diagnostics for failed edit requests', async () => { const { editImage } = await loadAgentRoutes(); let upstreamCalls = 0; const upstream = await startImageUpstream(() => { upstreamCalls += 1; throw new Error('edit upstream failed'); }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const idempotencyKey = 'route-edit-upstream-diagnostics-key'; const response = await editImage(agentEditRequest(idempotencyKey, 'edit diagnostics')); assert.equal(response.status, 502); const body = await response.json(); assert.equal(body.error.code, 'upstream_unavailable'); assert.equal(body.error.upstream_status, 500); assert.equal(body.error.diagnostics.upstream_status, 500); assert.equal(body.error.diagnostics.selected_channel_id, 'default'); assert.match(body.error.diagnostics.upstream_host, /^127\.0\.0\.1:\d+$/); assert.equal(body.error.diagnostics.channel_cooldown_scope, 'channel'); assert.equal(typeof body.error.diagnostics.elapsed_ms, 'number'); assert.equal(JSON.stringify(body).includes('test-key'), false); const upstreamCallsAfterFirstFailure = upstreamCalls; const replay = await editImage(agentEditRequest(idempotencyKey, 'edit diagnostics')); assert.equal(replay.status, 502); assert.equal(replay.headers.get('x-idempotent-replay'), 'true'); assert.equal(replay.headers.get('x-request-id'), body.error.request_id); const replayBody = await replay.json(); assert.equal(replayBody.error.code, 'upstream_unavailable'); assert.equal(replayBody.error.retryable, false); assert.equal(replayBody.error.request_id, body.error.request_id); assert.equal(upstreamCalls, upstreamCallsAfterFirstFailure); } finally { await upstream.close(); } }); it('does not mark a real upstream success as failed when edit state completion fails', async () => { const { editImage } = await loadAgentRoutes(); const { setAgentStateStoreFactoryForTests } = await import('@/lib/agent-state-runtime'); let upstreamCalls = 0; let failCalls = 0; let saveCalls = 0; const requestId = 'edit-completion-failure-request'; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_CONVERTIBLE_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setAgentStateStoreFactoryForTests(() => ({ async init() {}, async recoverExpiredRequests() { return 0; }, async purgeExpiredRequests() { return 0; }, async beginRequest() { return { type: 'acquired', record: { requestId, idempotencyKey: 'edit-completion-failure-key', requestHash: 'hash', mode: 'edit', status: 'running', requestJson: { fields: { prompt: 'state completion failure' } }, createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', expiresAt: '2026-05-13T00:00:00.000Z' } }; }, async refreshRequestLease() { return false; }, async saveArtifacts() { saveCalls += 1; }, async completeRequest() { throw new Error('state completion failed'); }, async failRequest() { failCalls += 1; }, async getRequest() { return undefined; }, async getRequestByIdempotencyKey() { return undefined; }, async getArtifact() { return undefined; }, async listArtifactsForRequest() { return []; }, async listArtifactFilepaths() { return []; }, async deleteArtifact() { return false; } })); const originalConsoleError = console.error; console.error = () => {}; try { const response = await editImage( agentEditRequest('edit-completion-failure-key', 'state completion failure') ); assert.equal(response.status, 500); const body = await response.json(); assert.equal(body.error.code, 'unexpected_error'); assert.equal(body.error.retryable, true); assert.equal(body.error.request_id, requestId); assert.equal(upstreamCalls, 1); assert.equal(saveCalls, 1); assert.equal(failCalls, 0); } finally { console.error = originalConsoleError; await upstream.close(); } }); it('requires artifact content authorization and returns image bytes when authorized', async () => { const { generateImage, getArtifact, getArtifactContent, createArtifactShare, getShareContent, deleteArtifact } = await loadAgentRoutes(); const upstream = await startImageUpstream(() => ({ data: [{ b64_json: PNG_BASE64 }] })); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; process.env.AGENT_API_TOKEN = 'artifact-token'; const generated = await generateImage( agentJsonRequest( 'artifact-auth-key', { prompt: 'artifact auth' }, { Authorization: 'Bearer artifact-token' } ) ); const body = await generated.json(); const artifactId = body.images[0].id; const denied = await getArtifactContent( new Request(`http://localhost/api/agent/artifacts/${artifactId}/content`), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(denied.status, 401); assert.equal((await denied.json()).error.code, 'unauthorized'); const allowed = await getArtifactContent( new Request(`http://localhost/api/agent/artifacts/${artifactId}/content`, { headers: { Authorization: 'Bearer artifact-token' } }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(allowed.status, 200); assert.equal(allowed.headers.get('content-type'), 'image/png'); assert.ok((await allowed.arrayBuffer()).byteLength > 0); const invalidContentTypeShare = await createArtifactShare( new Request(`http://internal.local/api/agent/artifacts/${artifactId}/share`, { method: 'POST', headers: { Authorization: 'Bearer artifact-token' }, body: JSON.stringify({ access_code: '12345678' }) }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(invalidContentTypeShare.status, 400); assert.equal((await invalidContentTypeShare.json()).error.code, 'validation_error'); const deniedShare = await createArtifactShare( new Request(`http://localhost/api/agent/artifacts/${artifactId}/share`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expires_in_minutes: 60 }) }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(deniedShare.status, 401); assert.equal((await deniedShare.json()).error.code, 'unauthorized'); const relativeShare = await createArtifactShare( new Request(`http://spoofed.local/api/agent/artifacts/${artifactId}/share`, { method: 'POST', headers: { Authorization: 'Bearer artifact-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ expires_in_minutes: 60 }) }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(relativeShare.status, 201); const relativeShareBody = await relativeShare.json(); assert.equal(relativeShareBody.share_url, `/share/${relativeShareBody.token}`); assert.equal(relativeShareBody.direct_content_url, `/api/shares/${relativeShareBody.token}/content`); process.env.AGENT_PUBLIC_BASE_URL = 'https://public.example.test'; const publicShare = await createArtifactShare( new Request(`http://internal.local/api/agent/artifacts/${artifactId}/share`, { method: 'POST', headers: { Authorization: 'Bearer artifact-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ expires_in_minutes: 60 }) }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(publicShare.status, 201); const publicShareBody = await publicShare.json(); assert.equal(publicShareBody.artifact_id, artifactId); assert.match(publicShareBody.token, /^[a-f0-9]{24}$/); assert.equal(publicShareBody.share_url, `https://public.example.test/share/${publicShareBody.token}`); assert.equal( publicShareBody.direct_content_url, `https://public.example.test/api/shares/${publicShareBody.token}/content` ); assert.equal(publicShareBody.access_code_required, false); assert.equal(typeof publicShareBody.expires_at, 'string'); process.env.AGENT_PUBLIC_BASE_URL = 'https://public.example.test/playground'; const publicShareWithPath = await createArtifactShare( new Request(`http://internal.local/api/agent/artifacts/${artifactId}/share`, { method: 'POST', headers: { Authorization: 'Bearer artifact-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ expires_in_minutes: 60 }) }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(publicShareWithPath.status, 201); const publicShareWithPathBody = await publicShareWithPath.json(); assert.equal( publicShareWithPathBody.share_url, `https://public.example.test/playground/share/${publicShareWithPathBody.token}` ); assert.equal( publicShareWithPathBody.direct_content_url, `https://public.example.test/playground/api/shares/${publicShareWithPathBody.token}/content` ); const publicShareContent = await getShareContent(new Request(publicShareBody.direct_content_url), { params: Promise.resolve({ token: publicShareBody.token }) }); assert.equal(publicShareContent.status, 200); assert.equal(publicShareContent.headers.get('content-type'), 'image/png'); assert.ok((await publicShareContent.arrayBuffer()).byteLength > 0); const protectedShare = await createArtifactShare( new Request(`http://localhost/api/agent/artifacts/${artifactId}/share`, { method: 'POST', headers: { Authorization: 'Bearer artifact-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ access_code: '12345678', expires_in_minutes: null }) }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(protectedShare.status, 201); const protectedShareBody = await protectedShare.json(); assert.equal(protectedShareBody.access_code_required, true); assert.equal(protectedShareBody.expires_at, null); const protectedGet = await getShareContent(new Request(protectedShareBody.direct_content_url), { params: Promise.resolve({ token: protectedShareBody.token }) }); assert.equal(protectedGet.status, 401); assert.equal((await protectedGet.json()).code, 'share_access_code_required'); process.env.AGENT_ARTIFACT_SHARE_MAX_EXPIRES_MINUTES = '30'; const defaultExpiryShare = await createArtifactShare( new Request(`http://internal.local/api/agent/artifacts/${artifactId}/share`, { method: 'POST', headers: { Authorization: 'Bearer artifact-token' } }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(defaultExpiryShare.status, 201); const defaultExpiryShareBody = await defaultExpiryShare.json(); const defaultExpiryMs = new Date(defaultExpiryShareBody.expires_at).getTime() - Date.now(); assert.ok(defaultExpiryMs > 0); assert.ok(defaultExpiryMs <= 30 * 60 * 1000); const metadata = await getArtifact( new Request(`http://localhost/api/agent/artifacts/${artifactId}`, { headers: { Authorization: 'Bearer artifact-token' } }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(metadata.status, 200); const metadataBody = await metadata.json(); assert.equal(metadataBody.artifact.id, artifactId); assert.equal(metadataBody.artifact.output_format, 'png'); assert.equal(metadataBody.artifact.mime_type, 'image/png'); assert.equal('filepath' in metadataBody.artifact, false); const deleted = await deleteArtifact( new Request(`http://localhost/api/agent/artifacts/${artifactId}`, { method: 'DELETE', headers: { Authorization: 'Bearer artifact-token' } }), { params: Promise.resolve({ id: artifactId }) } ); assert.equal(deleted.status, 200); assert.equal((await deleted.json()).deleted, true); const replayAfterDelete = await generateImage( agentJsonRequest( 'artifact-auth-key', { prompt: 'artifact auth' }, { Authorization: 'Bearer artifact-token' } ) ); assert.equal(replayAfterDelete.status, 404); assert.equal((await replayAfterDelete.json()).error.code, 'artifact_not_found'); await upstream.close(); }); it('returns not found when artifact metadata exists but content file is missing', async () => { const { generateImage, getArtifactContent } = await loadAgentRoutes(); const upstream = await startImageUpstream(() => ({ data: [{ b64_json: PNG_BASE64 }] })); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { const generated = await generateImage( agentJsonRequest('artifact-missing-content-key', { prompt: 'missing content' }) ); const body = await generated.json(); const image = body.images[0]; await rm(readStoredArtifactFilepath(image.id), { force: true }); const missing = await getArtifactContent( new Request(`http://localhost/api/agent/artifacts/${image.id}/content`), { params: Promise.resolve({ id: image.id }) } ); assert.equal(missing.status, 404); assert.equal((await missing.json()).error.code, 'artifact_not_found'); } finally { await upstream.close(); } }); it('persists page feedback and lets Agent clients read it by page request id', async () => { const { putFeedback, getPageRequestFeedback, getPageRequestFeedbackBatch } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'feedback-token'; const saved = await putFeedback( new Request('http://localhost/api/feedback', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-agent-route', filename: 'feedback.png' }], value: 'needs_revision', note: 'text overlaps subject', updatedAt: '2026-05-12T00:00:00.000Z' }) }) ); assert.equal(saved.status, 200); const savedBody = await saved.json(); assert.equal(savedBody.feedback.length, 1); const response = await getPageRequestFeedback( new Request('http://localhost/api/agent/page-requests/web-feedback-agent-route/feedback', { headers: { Authorization: 'Bearer feedback-token' } }), { params: Promise.resolve({ id: 'web-feedback-agent-route' }) } ); assert.equal(response.status, 200); const body = await response.json(); assert.deepEqual(body.feedback, { target_type: 'page_request', target_id: 'web-feedback-agent-route', value: 'needs_revision', note: 'text overlaps subject', source: 'webui', updated_at: '2026-05-12T00:00:00.000Z' }); const batchResponse = await getPageRequestFeedbackBatch( new Request('http://localhost/api/agent/page-requests/feedback', { method: 'POST', headers: { Authorization: 'Bearer feedback-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: ['web-feedback-agent-route', 'web-feedback-missing'] }) }) ); assert.equal(batchResponse.status, 200); const batchBody = await batchResponse.json(); assert.deepEqual(batchBody.targets, [ { type: 'page_request', id: 'web-feedback-agent-route' }, { type: 'page_request', id: 'web-feedback-missing' } ]); assert.deepEqual(batchBody.feedback, [body.feedback]); }); it('returns validation errors for malformed page feedback batch JSON requests', async () => { const { getPageRequestFeedbackBatch } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'feedback-malformed-json-token'; const response = await getPageRequestFeedbackBatch( new Request('http://localhost/api/agent/page-requests/feedback', { method: 'POST', headers: { Authorization: 'Bearer feedback-malformed-json-token', 'Content-Type': 'application/json' }, body: '{"ids":' }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.equal(body.error.retryable, false); }); it('returns validation errors for non-string page feedback batch ids', async () => { const { getPageRequestFeedbackBatch } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'feedback-invalid-ids-token'; const response = await getPageRequestFeedbackBatch( new Request('http://localhost/api/agent/page-requests/feedback', { method: 'POST', headers: { Authorization: 'Bearer feedback-invalid-ids-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: ['web-feedback-id', 123] }) }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.equal(body.error.message, 'ids 数组必须只包含字符串 ID。'); }); it('deduplicates page feedback batch ids before applying the max-id limit', async () => { const { getPageRequestFeedbackBatch } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'feedback-id-dedupe-token'; const response = await getPageRequestFeedbackBatch( new Request('http://localhost/api/agent/page-requests/feedback', { method: 'POST', headers: { Authorization: 'Bearer feedback-id-dedupe-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: Array.from({ length: 60 }, () => 'web-feedback-same-id') }) }) ); assert.equal(response.status, 200); const body = await response.json(); assert.deepEqual(body.targets, [{ type: 'page_request', id: 'web-feedback-same-id' }]); assert.deepEqual(body.feedback, []); }); it('deduplicates repeated page feedback targets before persisting them', async () => { const { putFeedback, getPageRequestFeedback } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'feedback-dedupe-token'; const saved = await putFeedback( new Request('http://localhost/api/feedback', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [ { type: 'page_request', id: 'web-feedback-duplicate' }, { type: 'page_request', id: 'web-feedback-duplicate' } ], value: 'usable', updatedAt: '2026-05-12T00:00:00.000Z' }) }) ); assert.equal(saved.status, 200); const savedBody = await saved.json(); assert.deepEqual( savedBody.feedback.map((item: { target_id: string }) => item.target_id), ['web-feedback-duplicate'] ); const response = await getPageRequestFeedback( new Request('http://localhost/api/agent/page-requests/web-feedback-duplicate/feedback', { headers: { Authorization: 'Bearer feedback-dedupe-token' } }), { params: Promise.resolve({ id: 'web-feedback-duplicate' }) } ); assert.equal(response.status, 200); assert.equal((await response.json()).feedback.value, 'usable'); }); it('returns validation errors for non-serializable WebUI feedback timestamps', async () => { const { putFeedback, deleteFeedback } = await loadAgentRoutes(); const invalidUpdatedAt = await putFeedback( new Request('http://localhost/api/feedback', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-invalid-updated-at' }], value: 'usable', updatedAt: 1e20 }) }) ); assert.equal(invalidUpdatedAt.status, 400); const invalidUpdatedAtBody = await invalidUpdatedAt.json(); assert.equal(invalidUpdatedAtBody.code, 'invalid_feedback_request'); assert.equal(invalidUpdatedAtBody.error, '反馈更新时间无效。'); const invalidDeletedAt = await deleteFeedback( new Request('http://localhost/api/feedback', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-invalid-deleted-at' }], deletedAt: 1e20 }) }) ); assert.equal(invalidDeletedAt.status, 400); const invalidDeletedAtBody = await invalidDeletedAt.json(); assert.equal(invalidDeletedAtBody.code, 'invalid_feedback_delete_request'); assert.equal(invalidDeletedAtBody.error, '反馈删除时间无效。'); }); it('deletes page feedback when the WebUI clears matching page request targets', async () => { const { putFeedback, deleteFeedback, getPageRequestFeedback } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'feedback-delete-token'; const saved = await putFeedback( new Request('http://localhost/api/feedback', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-delete-route' }], value: 'usable', updatedAt: '2026-05-12T00:00:00.000Z' }) }) ); assert.equal(saved.status, 200); const deleted = await deleteFeedback( new Request('http://localhost/api/feedback', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-delete-route' }] }) }) ); assert.equal(deleted.status, 200); assert.equal((await deleted.json()).deleted, 1); const response = await getPageRequestFeedback( new Request('http://localhost/api/agent/page-requests/web-feedback-delete-route/feedback', { headers: { Authorization: 'Bearer feedback-delete-token' } }), { params: Promise.resolve({ id: 'web-feedback-delete-route' }) } ); assert.equal(response.status, 200); assert.equal((await response.json()).feedback, null); }); it('does not let stale WebUI feedback deletes remove newer feedback', async () => { const { putFeedback, deleteFeedback, getPageRequestFeedback } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'feedback-delete-stale-token'; const saved = await putFeedback( new Request('http://localhost/api/feedback', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-delete-stale-route' }], value: 'needs_revision', note: 'latest feedback', updatedAt: '2026-05-12T00:02:00.000Z' }) }) ); assert.equal(saved.status, 200); const staleDeleted = await deleteFeedback( new Request('http://localhost/api/feedback', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-delete-stale-route' }], deletedAt: '2026-05-12T00:01:00.000Z' }) }) ); assert.equal(staleDeleted.status, 200); assert.equal((await staleDeleted.json()).deleted, 0); const retained = await getPageRequestFeedback( new Request('http://localhost/api/agent/page-requests/web-feedback-delete-stale-route/feedback', { headers: { Authorization: 'Bearer feedback-delete-stale-token' } }), { params: Promise.resolve({ id: 'web-feedback-delete-stale-route' }) } ); assert.equal(retained.status, 200); assert.equal((await retained.json()).feedback.note, 'latest feedback'); const currentDeleted = await deleteFeedback( new Request('http://localhost/api/feedback', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targets: [{ type: 'page_request', id: 'web-feedback-delete-stale-route' }], deletedAt: '2026-05-12T00:03:00.000Z' }) }) ); assert.equal(currentDeleted.status, 200); assert.equal((await currentDeleted.json()).deleted, 1); const cleared = await getPageRequestFeedback( new Request('http://localhost/api/agent/page-requests/web-feedback-delete-stale-route/feedback', { headers: { Authorization: 'Bearer feedback-delete-stale-token' } }), { params: Promise.resolve({ id: 'web-feedback-delete-stale-route' }) } ); assert.equal(cleared.status, 200); assert.equal((await cleared.json()).feedback, null); }); it('returns machine-readable Agent diagnostics for page request logs', async () => { const { getPageRequestDiagnostics, getPageRequestDiagnosticsBatch } = await loadAgentRoutes(); const { appLogger, clearAppLogEntriesForTest } = await import('@/lib/app-logger'); process.env.AGENT_API_TOKEN = 'diagnostics-token'; clearAppLogEntriesForTest(); appLogger.info('流式生成完成。', { clientRequestId: 'web-diagnostics-request', filenames: ['diagnostic.png'], providerDialect: 'sdk_parsed_fallback', normalizedEventCount: 1, reason: 'json_final_fallback', image_backend: 'images-api', operation: 'generate' }); appLogger.warn('流式生成回退。', { clientRequestId: 'web-diagnostics-request-2', filenames: ['diagnostic-2.png'], reason: 'json_final_fallback', image_backend: 'images-api', operation: 'generate' }); const response = await getPageRequestDiagnostics( new Request('http://localhost/api/agent/diagnostics/page-requests/web-diagnostics-request', { headers: { Authorization: 'Bearer diagnostics-token' } }), { params: Promise.resolve({ id: 'web-diagnostics-request' }) } ); assert.equal(response.status, 200); const body = await response.json(); assert.equal(body.scope.request_ids[0], 'web-diagnostics-request'); assert.equal(body.matched_log_count, 1); assert.equal(body.events[0].client_request_id, 'web-diagnostics-request'); assert.equal(body.events[0].diagnostics.providerDialect, 'sdk_parsed_fallback'); assert.equal(body.events[0].diagnostics.normalizedEventCount, 1); assert.equal(body.events[0].diagnostics.reason, 'json_final_fallback'); assert.equal(body.diagnostics_retention.storage, 'bounded_local_jsonl'); assert.equal(body.diagnostics_retention.configured_by, 'APP_LOG_MAX_ENTRIES'); const missingResponse = await getPageRequestDiagnostics( new Request('http://localhost/api/agent/diagnostics/page-requests/web-diagnostics-missing', { headers: { Authorization: 'Bearer diagnostics-token' } }), { params: Promise.resolve({ id: 'web-diagnostics-missing' }) } ); assert.equal(missingResponse.status, 200); const missingBody = await missingResponse.json(); assert.equal(missingBody.matched_log_count, 0); assert.equal(missingBody.diagnostics_note.code, 'no_matching_logs_in_retention_window'); assert.equal(missingBody.diagnostics_note.retention.max_entries, missingBody.diagnostics_retention.max_entries); const batchResponse = await getPageRequestDiagnosticsBatch( new Request('http://localhost/api/agent/diagnostics/page-requests', { method: 'POST', headers: { Authorization: 'Bearer diagnostics-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: ['web-diagnostics-request', 'web-diagnostics-request-2'], filenames: ['diagnostic-2.png'] }) }) ); assert.equal(batchResponse.status, 200); const batchBody = await batchResponse.json(); assert.deepEqual(batchBody.targets, [ { type: 'page_request', id: 'web-diagnostics-request' }, { type: 'page_request', id: 'web-diagnostics-request-2' } ]); assert.deepEqual( batchBody.diagnostics.map((item: { client_request_id: string }) => item.client_request_id), ['web-diagnostics-request', 'web-diagnostics-request-2'] ); assert.equal(batchBody.diagnostics[0].matched_log_count, 2); assert.deepEqual(batchBody.diagnostics[0].scope.filename_matched_request_ids, ['web-diagnostics-request-2']); assert.equal(batchBody.diagnostics[1].matched_log_count, 1); assert.equal(batchBody.diagnostics_retention.storage, 'bounded_local_jsonl'); assert.equal( batchBody.diagnostics[0].diagnostics_retention.max_entries, batchBody.diagnostics_retention.max_entries ); }); it('returns validation errors for malformed page diagnostics batch JSON requests', async () => { const { getPageRequestDiagnosticsBatch } = await loadAgentRoutes(); process.env.AGENT_API_TOKEN = 'diagnostics-malformed-json-token'; const response = await getPageRequestDiagnosticsBatch( new Request('http://localhost/api/agent/diagnostics/page-requests', { method: 'POST', headers: { Authorization: 'Bearer diagnostics-malformed-json-token', 'Content-Type': 'application/json' }, body: '{"ids":' }) ); assert.equal(response.status, 422); const body = await response.json(); assert.equal(body.error.code, 'validation_error'); assert.equal(body.error.retryable, false); }); it('does not mark a real upstream success as failed when state completion fails', async () => { const { generateImage } = await loadAgentRoutes(); const { setAgentStateStoreFactoryForTests } = await import('@/lib/agent-state-runtime'); let upstreamCalls = 0; let failCalls = 0; let saveCalls = 0; const requestId = 'completion-failure-request'; const upstream = await startImageUpstream(() => { upstreamCalls += 1; return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setAgentStateStoreFactoryForTests(() => ({ async init() {}, async recoverExpiredRequests() { return 0; }, async purgeExpiredRequests() { return 0; }, async beginRequest() { return { type: 'acquired', record: { requestId, idempotencyKey: 'completion-failure-key', requestHash: 'hash', mode: 'generate', status: 'running', requestJson: { prompt: 'state completion failure' }, createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', expiresAt: '2026-05-13T00:00:00.000Z' } }; }, async refreshRequestLease() { return false; }, async saveArtifacts() { saveCalls += 1; }, async completeRequest() { throw new Error('state completion failed'); }, async failRequest() { failCalls += 1; }, async getRequest() { return undefined; }, async getRequestByIdempotencyKey() { return undefined; }, async getArtifact() { return undefined; }, async listArtifactsForRequest() { return []; }, async listArtifactFilepaths() { return []; }, async deleteArtifact() { return false; } })); const originalConsoleError = console.error; console.error = () => {}; try { const response = await generateImage( agentJsonRequest('completion-failure-key', { prompt: 'state completion failure' }) ); assert.equal(response.status, 500); const body = await response.json(); assert.equal(body.error.code, 'unexpected_error'); assert.equal(body.error.retryable, true); assert.equal(body.error.request_id, requestId); assert.equal(upstreamCalls, 1); assert.equal(saveCalls, 1); assert.equal(failCalls, 0); } finally { console.error = originalConsoleError; } await upstream.close(); }); it('does not return artifact URLs when artifact metadata persistence fails', async () => { const { generateImage } = await loadAgentRoutes(); const { setAgentStateStoreFactoryForTests } = await import('@/lib/agent-state-runtime'); const requestId = 'artifact-save-failure-request'; const upstream = await startImageUpstream(() => ({ data: [{ b64_json: PNG_BASE64 }] })); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; setAgentStateStoreFactoryForTests(() => ({ async init() {}, async recoverExpiredRequests() { return 0; }, async purgeExpiredRequests() { return 0; }, async beginRequest() { return { type: 'acquired', record: { requestId, idempotencyKey: 'artifact-save-failure-key', requestHash: 'hash', mode: 'generate', status: 'running', requestJson: { prompt: 'artifact save failure' }, createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', expiresAt: '2026-05-13T00:00:00.000Z' } }; }, async refreshRequestLease() { return false; }, async saveArtifacts() { throw new Error('artifact metadata save failed'); }, async completeRequest() {}, async failRequest(input: { requestId: string; error: { error: { retryable: boolean } } }) { assert.equal(input.requestId, requestId); assert.equal(input.error.error.retryable, true); }, async getRequest() { return undefined; }, async getRequestByIdempotencyKey() { return undefined; }, async getArtifact() { return undefined; }, async listArtifactsForRequest() { return []; }, async listArtifactFilepaths() { return []; }, async deleteArtifact() { return false; } })); const originalConsoleError = console.error; console.error = () => {}; try { const response = await generateImage( agentJsonRequest('artifact-save-failure-key', { prompt: 'artifact save failure' }) ); assert.equal(response.status, 500); const body = await response.json(); assert.equal(body.error.code, 'unexpected_error'); assert.equal(body.error.retryable, true); assert.equal(body.error.request_id, requestId); assert.deepEqual(await listGeneratedImageFiles(), []); } finally { console.error = originalConsoleError; await upstream.close(); } }); }); const livePostgresUrl = process.env.AGENT_POSTGRES_TEST_DATABASE_URL; describe( 'Agent route PostgreSQL integration', { skip: livePostgresUrl ? false : 'AGENT_POSTGRES_TEST_DATABASE_URL is not set' }, () => { it('allows only one upstream winner for concurrent identical idempotency requests', async () => { assert.ok(livePostgresUrl); const { generateImage } = await loadAgentRoutes(); const schemaName = `agent_route_${Date.now().toString(36)}`; const pool = new Pool({ connectionString: livePostgresUrl }); process.env.AGENT_STATE_BACKEND = 'postgres'; process.env.AGENT_DATABASE_URL = `${livePostgresUrl}${livePostgresUrl.includes('?') ? '&' : '?'}options=-c%20search_path%3D${schemaName}`; process.env.AGENT_REQUEST_LEASE_MS = '60000'; const admin = await pool.connect(); let releaseUpstream: (() => void) | undefined; let upstreamCalls = 0; const upstream = await startImageUpstream(async () => { upstreamCalls += 1; await new Promise((resolve) => { releaseUpstream = resolve; }); return { data: [{ b64_json: PNG_BASE64 }] }; }); process.env.OPENAI_API_KEY = 'test-key'; process.env.OPENAI_API_BASE_URL = upstream.baseUrl; try { await admin.query(`CREATE SCHEMA "${schemaName}"`); const firstRequest = generateImage( agentJsonRequest('pg-route-concurrent-key', { prompt: 'pg concurrent' }) ); await waitFor(() => upstreamCalls === 1); const second = await generateImage( agentJsonRequest('pg-route-concurrent-key', { prompt: 'pg concurrent' }) ); assert.equal(second.status, 409); const secondBody = await second.json(); assert.equal(secondBody.error.code, 'request_in_progress'); assert.equal(secondBody.error.retryable, true); assert.equal(second.headers.has('retry-after'), true); releaseUpstream?.(); const first = await firstRequest; assert.equal(first.status, 200); const replay = await generateImage( agentJsonRequest('pg-route-concurrent-key', { prompt: 'pg concurrent' }) ); assert.equal(replay.status, 200); assert.equal((await replay.json()).cached, true); assert.equal(upstreamCalls, 1); } finally { releaseUpstream?.(); await upstream.close(); await admin.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`); admin.release(); await pool.end(); } }); } ); async function loadAgentRoutes() { const { resetAgentStateStoreForTests } = await import('@/lib/agent-state-runtime'); const { resetServerChannelStateForTests } = await import('@/lib/server-channel-router'); await resetAgentStateStoreForTests(); resetServerChannelStateForTests(); const generateRoute = await import('./images/generate/route'); const editRoute = await import('./images/edit/route'); const artifactRoute = await import('./artifacts/[id]/route'); const artifactContentRoute = await import('./artifacts/[id]/content/route'); const artifactShareRoute = await import('./artifacts/[id]/share/route'); const capabilitiesRoute = await import('./capabilities/route'); const imageRequestRoute = await import('./image-requests/route'); const createGenerateJobRoute = await import('./jobs/images/generate/route'); const jobRoute = await import('./jobs/[id]/route'); const jobResultRoute = await import('./jobs/[id]/result/route'); const feedbackRoute = await import('../feedback/route'); const pageRequestFeedbackBatchRoute = await import('./page-requests/feedback/route'); const pageRequestFeedbackRoute = await import('./page-requests/[id]/feedback/route'); const agentRequestDiagnosticsLookupRoute = await import('./diagnostics/requests/route'); const agentRequestDiagnosticsRoute = await import('./diagnostics/requests/[id]/route'); const pageRequestDiagnosticsBatchRoute = await import('./diagnostics/page-requests/route'); const pageRequestDiagnosticsRoute = await import('./diagnostics/page-requests/[id]/route'); const shareContentRoute = await import('../shares/[token]/content/route'); return { getCapabilities: () => capabilitiesRoute.GET(), generateImage: (request: Request) => generateRoute.POST(asNextRequest(request)), editImage: (request: Request) => editRoute.POST(asNextRequest(request)), createImageRequest: (request: Request) => imageRequestRoute.POST(asNextRequest(request)), createGenerateJob: (request: Request) => createGenerateJobRoute.POST(asNextRequest(request)), getJob: (request: Request, context: AgentRouteContext) => jobRoute.GET(asNextRequest(request), context), getJobResult: (request: Request, context: AgentRouteContext) => jobResultRoute.GET(asNextRequest(request), context), getArtifact: (request: Request, context: AgentRouteContext) => artifactRoute.GET(asNextRequest(request), context), deleteArtifact: (request: Request, context: AgentRouteContext) => artifactRoute.DELETE(asNextRequest(request), context), getArtifactContent: (request: Request, context: AgentRouteContext) => artifactContentRoute.GET(asNextRequest(request), context), createArtifactShare: (request: Request, context: AgentRouteContext) => artifactShareRoute.POST(asNextRequest(request), context), getShareContent: (request: Request, context: { params: Promise<{ token: string }> }) => shareContentRoute.GET(asNextRequest(request), context), putFeedback: (request: Request) => feedbackRoute.PUT(asNextRequest(request)), deleteFeedback: (request: Request) => feedbackRoute.DELETE(asNextRequest(request)), getPageRequestFeedbackBatch: (request: Request) => pageRequestFeedbackBatchRoute.POST(asNextRequest(request)), getPageRequestFeedback: (request: Request, context: AgentRouteContext) => pageRequestFeedbackRoute.GET(asNextRequest(request), context), lookupAgentRequestDiagnostics: (request: Request) => agentRequestDiagnosticsLookupRoute.GET(asNextRequest(request)), getAgentRequestDiagnostics: (request: Request, context: AgentRouteContext) => agentRequestDiagnosticsRoute.GET(asNextRequest(request), context), getPageRequestDiagnosticsBatch: (request: Request) => pageRequestDiagnosticsBatchRoute.POST(asNextRequest(request)), getPageRequestDiagnostics: (request: Request, context: AgentRouteContext) => pageRequestDiagnosticsRoute.GET(asNextRequest(request), context) }; } type AgentRouteContext = { params: Promise<{ id: string }> }; function asNextRequest(request: Request): NextRequest { return request as unknown as NextRequest; } function agentJsonRequest(idempotencyKey: string, body: Record, headers: Record = {}) { const requestBody = buildAgentGenerateRequestBody(body); return new Request('http://localhost/api/agent/images/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, ...headers }, body: JSON.stringify(requestBody) }); } function agentJobJsonRequest( idempotencyKey: string, body: Record, headers: Record = {} ) { const requestBody = buildAgentGenerateRequestBody(body); return new Request('http://localhost/api/agent/jobs/images/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, ...headers }, body: JSON.stringify(requestBody) }); } function agentImageRequest( idempotencyKey: string, body: Record, headers: Record = {} ) { const requestBody = buildAgentGenerateRequestBody(body); return new Request('http://localhost/api/agent/image-requests', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, ...headers }, body: JSON.stringify(requestBody) }); } function buildAgentGenerateRequestBody(body: Record): Record { return { ...body, ...('stream_mode' in body || 'streaming_strategy' in body ? {} : { stream_mode: 'non_stream' }), ...('output_format' in body ? {} : { output_format: 'png' }) }; } type AgentEditRequestFields = { image_0?: Buffer; mask?: Buffer; [field: string]: string | Buffer | undefined; }; function agentEditRequest( idempotencyKey: string, prompt: string, headers: Record = {}, responseModeOrFields: string | AgentEditRequestFields = 'path', options: { signal?: AbortSignal } = {} ) { const fields: AgentEditRequestFields = typeof responseModeOrFields === 'string' ? { response_mode: responseModeOrFields } : { response_mode: 'path', ...responseModeOrFields }; if (!('stream_mode' in fields) && !('streaming_strategy' in fields)) { fields.stream_mode = 'non_stream'; } const imageBuffer = fields.image_0 ?? Buffer.from(PNG_BASE64, 'base64'); const formData = new FormData(); formData.append('prompt', prompt); formData.append('model', 'gpt-image-2'); for (const [key, value] of Object.entries(fields)) { if (key === 'image_0' || key === 'mask') continue; if (typeof value !== 'string') { throw new TypeError(`Agent edit test field ${key} must be a string.`); } formData.append(key, value); } formData.append('image_0', new File([imageBuffer], 'input.png', { type: 'image/png' })); if (fields.mask) { formData.append('mask', new File([fields.mask], 'mask.png', { type: 'image/png' })); } return new Request('http://localhost/api/agent/images/edit', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey, ...headers }, body: formData, signal: options.signal }); } function createPngWithDimensions(width: number, height: number): Buffer { const buffer = Buffer.from(PNG_BASE64, 'base64'); buffer.writeUInt32BE(width, 16); buffer.writeUInt32BE(height, 20); return buffer; } async function startImageUpstream( handler: ( body: string, url: string, request: http.IncomingMessage, response: http.ServerResponse ) => unknown | Promise ): Promise<{ baseUrl: string; close: () => Promise }> { const server = http.createServer(async (request, response) => { if ( request.method !== 'POST' || (!request.url?.endsWith('/images/generations') && !request.url?.endsWith('/images/edits')) ) { response.writeHead(404, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ error: { message: 'not found' } })); return; } const chunks: Buffer[] = []; request.on('data', (chunk: Buffer) => chunks.push(chunk)); await new Promise((resolve) => request.on('end', resolve)); try { const body = await handler(Buffer.concat(chunks).toString('utf8'), request.url || '', request, response); response.writeHead(200, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(body)); } catch (error) { response.writeHead(500, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : '上游失败' } })); } }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); assert.ok(address && typeof address === 'object'); const baseUrl = `http://127.0.0.1:${address.port}/v1`; return { baseUrl, close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))) }; } async function startHangingImageEditUpstream(): Promise<{ baseUrl: string; readonly requests: number; close: () => Promise; }> { let requests = 0; const sockets = new Set(); const server = http.createServer(async (request, response) => { if (request.method === 'POST' && request.url?.endsWith('/images/edits')) { requests += 1; request.resume(); return; } request.resume(); response.writeHead(404, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ error: { message: 'not found' } })); }); server.on('connection', (socket) => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); assert.ok(address && typeof address === 'object'); return { baseUrl: `http://127.0.0.1:${address.port}/v1`, get requests() { return requests; }, close: () => new Promise((resolve, reject) => { for (const socket of sockets) socket.destroy(); server.close((error) => (error ? reject(error) : resolve())); }) }; } async function startStreamingImageUpstream( handler: ( body: string ) => Array<{ event?: string; data: unknown }> | Promise> ): Promise<{ baseUrl: string; close: () => Promise }> { const server = http.createServer(async (request, response) => { const isImageStreamPath = request.url?.endsWith('/images/generations') || request.url?.endsWith('/images/edits'); if (request.method !== 'POST' || !isImageStreamPath) { response.writeHead(404, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ error: { message: 'not found' } })); return; } const chunks: Buffer[] = []; request.on('data', (chunk: Buffer) => chunks.push(chunk)); await new Promise((resolve) => request.on('end', resolve)); const events = await handler(Buffer.concat(chunks).toString('utf8')); response.writeHead(200, { 'Content-Type': 'text/event-stream' }); for (const event of events) { if (event.event) { response.write(`event: ${event.event}\n`); } response.write(`data: ${JSON.stringify(event.data)}\n\n`); } response.write('data: [DONE]\n\n'); response.end(); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); assert.ok(address && typeof address === 'object'); return { baseUrl: `http://127.0.0.1:${address.port}/v1`, close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))) }; } async function startStreamingResponsesImageUpstream( handler: ( body: string ) => Array<{ event?: string; data: unknown }> | Promise> ): Promise<{ baseUrl: string; close: () => Promise }> { const server = http.createServer(async (request, response) => { if (request.method !== 'POST' || !request.url?.endsWith('/responses')) { response.writeHead(404, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ error: { message: 'not found' } })); return; } const chunks: Buffer[] = []; request.on('data', (chunk: Buffer) => chunks.push(chunk)); await new Promise((resolve) => request.on('end', resolve)); const events = await handler(Buffer.concat(chunks).toString('utf8')); response.writeHead(200, { 'Content-Type': 'text/event-stream' }); for (const event of events) { if (event.event) { response.write(`event: ${event.event}\n`); } response.write(`data: ${JSON.stringify(event.data)}\n\n`); } response.write('data: [DONE]\n\n'); response.end(); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); assert.ok(address && typeof address === 'object'); return { baseUrl: `http://127.0.0.1:${address.port}/v1`, close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))) }; } async function startResponsesImageJsonUpstream( status: number, body: unknown ): Promise<{ baseUrl: string; close: () => Promise }> { const server = http.createServer((request, response) => { if (request.method !== 'POST' || !request.url?.endsWith('/responses')) { response.writeHead(404, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ error: { message: 'not found' } })); return; } request.resume(); response.writeHead(status, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(body)); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); assert.ok(address && typeof address === 'object'); return { baseUrl: `http://127.0.0.1:${address.port}/v1`, close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))) }; } function readStoredResponseJson(idempotencyKey: string): string { const db = new Database(path.join(tempDir, 'agent.sqlite'), { readonly: true }); try { const row = db .prepare('SELECT response_json FROM agent_requests WHERE idempotency_key = ?') .get(idempotencyKey) as { response_json: string } | undefined; assert.ok(row); return row.response_json; } finally { db.close(); } } function readStoredArtifactFilepath(id: string): string { const db = new Database(path.join(tempDir, 'agent.sqlite'), { readonly: true }); try { const row = db.prepare('SELECT filepath FROM agent_artifacts WHERE id = ?').get(id) as { filepath: string } | undefined; assert.ok(row); return row.filepath; } finally { db.close(); } } async function listGeneratedImageFiles(): Promise { try { return (await readdir(path.join(tempDir, 'generated-images'))).filter((entry) => /\.(png|jpe?g|webp)$/i.test(entry) ); } catch { return []; } } async function listAgentStateFiles(): Promise { try { return await readdir(path.join(tempDir, 'generated-images', '.agent-state')); } catch { return []; } } async function waitFor(predicate: () => boolean): Promise { for (let attempt = 0; attempt < 50; attempt += 1) { if (predicate()) return; await new Promise((resolve) => setTimeout(resolve, 20)); } throw new Error('等待条件超时'); } async function waitForJobResult( getJobResult: (request: Request, context: { params: Promise<{ id: string }> }) => Promise, id: string ): Promise { for (let attempt = 0; attempt < 200; attempt += 1) { const response = await getJobResult(new Request(`http://localhost/api/agent/jobs/${id}/result`), { params: Promise.resolve({ id }) }); if (response.status !== 409) { return response; } await new Promise((resolve) => setTimeout(resolve, 20)); } throw new Error('等待 job result 超时'); }