import { AGENT_PAGE_REQUEST_DIAGNOSTICS_NO_MATCH_HINT, AGENT_PAGE_SSE_AGENT_USAGE, AGENT_SCHEMA_VERSION, PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH, buildAgentAuthCapabilities, buildAgentCapabilities, buildPageRequestDiagnosticsCapabilities, readAgentLeaseMs, readAgentPublicBaseUrl, readAgentRecoveryIntervalMs, readAgentRequestTtlSeconds, validateAgentGenerateRequest } from './agent-api-contracts'; import { AGENT_ENDPOINTS, AGENT_JOB_ENDPOINTS } from './agent-api-paths.mjs'; import { buildAgentOpenApiDocument } from './agent-openapi'; import { RequestValidationError } from './image-request-utils'; import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; function restoreEnv(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; } } describe('validateAgentGenerateRequest', () => { it('accepts a minimal JSON generate request and applies Agent defaults', () => { assert.deepEqual(validateAgentGenerateRequest({ prompt: 'draw a stable test image' }), { model: 'gpt-image-2', prompt: 'draw a stable test image', n: 1, size: '1024x1024', quality: 'high', output_format: 'webp', output_compression: 100, background: 'auto', moderation: 'auto', response_mode: 'path', image_backend: 'images-api', stream_mode: 'auto', streaming_strategy: 'auto', partial_images: 2 }); }); it('accepts explicit Agent upstream streaming strategy fields', () => { assert.deepEqual( validateAgentGenerateRequest({ prompt: 'draw a stable streaming image', image_backend: 'images', streaming_strategy: 'newapi-keepalive-sse', partial_images: 3 }), { model: 'gpt-image-2', prompt: 'draw a stable streaming image', n: 1, size: '1024x1024', quality: 'high', output_format: 'webp', output_compression: 100, background: 'auto', moderation: 'auto', response_mode: 'path', image_backend: 'images-api', stream_mode: 'auto', streaming_strategy: 'newapi-keepalive-sse', partial_images: 3 } ); }); it('accepts Agent generate response controls as service-owned intent fields', () => { assert.deepEqual( validateAgentGenerateRequest({ prompt: 'draw a stable generate request', image_backend: 'responses-image-generation', responsesModel: 'gpt-5.4-mini', thinking: 'medium', promptOptimization: false, force_web: true }), { model: 'gpt-image-2', prompt: 'draw a stable generate request', n: 1, size: '1024x1024', quality: 'high', output_format: 'webp', output_compression: 100, background: 'auto', moderation: 'auto', response_mode: 'path', image_backend: 'responses-image-generation', stream_mode: 'auto', streaming_strategy: 'auto', partial_images: 2, responsesModel: 'gpt-5.4-mini', thinking: 'medium', promptOptimization: false, force_web: true } ); }); it('rejects Responses backend partial image counts outside the backend contract', () => { assert.throws( () => validateAgentGenerateRequest({ prompt: 'draw a responses image', image_backend: 'responses-image-generation', partial_images: 0 }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.partial_images, /1 到 3/); return true; } ); }); it('uses deployed upstream profile limits when validating Agent partial image counts', () => { const originalEnv = { ...process.env }; try { 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 = 'configured'; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_ID = 'configured'; process.env.OPENAI_CHANNEL_1_MATSCA_APP_SECRET = 'configured'; const imagesRequest = validateAgentGenerateRequest({ prompt: 'draw through matsca images api', image_backend: 'images-api', partial_images: 4 }); assert.equal(imagesRequest.partial_images, 4); assert.throws( () => validateAgentGenerateRequest({ prompt: 'draw through matsca responses', image_backend: 'responses-image-generation', partial_images: 4 }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.partial_images, /1 到 3/); return true; } ); } finally { restoreEnv(originalEnv); } }); it('rejects non-string Agent upstream strategy fields instead of defaulting them', () => { assert.throws( () => validateAgentGenerateRequest({ prompt: 'bad upstream strategy fields', image_backend: 123, streaming_strategy: true }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.image_backend, /字符串/); assert.match(details.fields.streaming_strategy, /字符串/); return true; } ); }); it('returns field-level validation errors for agent-correctable inputs', () => { assert.throws( () => validateAgentGenerateRequest({ prompt: '', n: 99, output_format: 'gif', response_mode: 'url' }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.prompt, /必填/); assert.match(details.fields.n, /1 到 10/); assert.match(details.fields.output_format, /png/); assert.match(details.fields.response_mode, /path/); return true; } ); }); it('rejects incompatible Agent image backend and streaming strategy combinations', () => { assert.throws( () => validateAgentGenerateRequest({ prompt: 'bad strategy', image_backend: 'images-api', streaming_strategy: 'responses-sse' }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.streaming_strategy, /Images API/); return true; } ); }); it('accepts Matsca-compatible gpt-image-2 fields before upstream execution', () => { const originalEnv = { ...process.env }; try { 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 = 'configured'; process.env.OPENAI_CHANNEL_1_UPSTREAM_PROFILE = 'matsca'; const request = validateAgentGenerateRequest({ prompt: 'transparent object', model: 'gpt-image-2', size: '123x456', background: 'transparent', partial_images: 0 }); assert.equal(request.size, '123x456'); assert.equal(request.background, 'transparent'); assert.equal(request.partial_images, 0); } finally { restoreProcessEnv(originalEnv); } }); it('rejects OpenAI-compatible gpt-image-2 sizes that violate the active profile contract', () => { assert.throws( () => validateAgentGenerateRequest({ prompt: 'invalid official size', model: 'gpt-image-2', size: '123x456' }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.size, /16/); return true; } ); }); it('accepts explicit force_request for locally unsupported gpt-image-2 sizes', () => { const request = validateAgentGenerateRequest({ prompt: 'force a small upstream request', model: 'gpt-image-2', size: '512x512', force_request: true }); assert.equal(request.size, '512x512'); assert.equal(request.force_request, true); }); it('rejects force_request sizes outside non gpt-image-2 model allowlists', () => { assert.throws( () => validateAgentGenerateRequest({ prompt: 'force a legacy model size', model: 'gpt-image-1', size: '512x512', force_request: true }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.size, /gpt-image-1.*1024x1024/); return true; } ); }); it('rejects malformed force_request sizes without returning the bad value', () => { assert.throws( () => validateAgentGenerateRequest({ prompt: 'force a malformed upstream request', model: 'gpt-image-2', size: 'wide', force_request: true }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.size, /WxH/); return true; } ); }); it('rejects gpt-image-2 sizes that are not positive integer dimensions', () => { for (const { size, pattern } of [ { size: '0x512', pattern: /正数/ }, { size: 'abc', pattern: /WIDTH|WxH|auto/ } ]) { assert.throws( () => validateAgentGenerateRequest({ prompt: 'out of range image', model: 'gpt-image-2', size }), (error) => { assert.ok(error instanceof RequestValidationError); assert.equal(error.status, 422); const details = JSON.parse(error.message) as { fields: Record }; assert.match(details.fields.size, pattern); return true; } ); } }); }); describe('Agent numeric configuration', () => { it('uses defaults when optional numeric env values are absent', () => { assert.equal(readAgentRequestTtlSeconds({}), 86400); assert.equal(readAgentLeaseMs({}), 600000); assert.equal(readAgentRecoveryIntervalMs({}), 30000); }); it('fails explicitly when numeric env values are invalid', () => { assert.throws( () => readAgentRequestTtlSeconds({ AGENT_REQUEST_TTL_SECONDS: 'abc' }), /AGENT_REQUEST_TTL_SECONDS/ ); assert.throws(() => readAgentLeaseMs({ AGENT_REQUEST_LEASE_MS: '0' }), /AGENT_REQUEST_LEASE_MS/); assert.throws( () => readAgentRecoveryIntervalMs({ AGENT_RECOVERY_INTERVAL_MS: '-1' }), /AGENT_RECOVERY_INTERVAL_MS/ ); }); it('validates the public OpenAPI server URL', () => { assert.equal(readAgentPublicBaseUrl({}), '/'); assert.equal( readAgentPublicBaseUrl({ AGENT_PUBLIC_BASE_URL: 'https://images.example.test/' }), 'https://images.example.test' ); assert.equal( readAgentPublicBaseUrl({ AGENT_PUBLIC_BASE_URL: 'http://localhost:4783' }), 'http://localhost:4783' ); assert.throws(() => readAgentPublicBaseUrl({ AGENT_PUBLIC_BASE_URL: 'not a url' }), /AGENT_PUBLIC_BASE_URL/); assert.throws( () => readAgentPublicBaseUrl({ AGENT_PUBLIC_BASE_URL: 'javascript:alert(1)' }), /AGENT_PUBLIC_BASE_URL/ ); assert.throws( () => readAgentPublicBaseUrl({ AGENT_PUBLIC_BASE_URL: 'https://user:pass@images.example.test' }), /AGENT_PUBLIC_BASE_URL/ ); assert.throws( () => readAgentPublicBaseUrl({ AGENT_PUBLIC_BASE_URL: 'https://images.example.test?token=secret' }), /AGENT_PUBLIC_BASE_URL/ ); }); }); describe('buildAgentCapabilities', () => { it('exposes machine-readable defaults, limits, auth, and storage metadata', () => { const capabilities = buildAgentCapabilities({ AGENT_STATE_BACKEND: 'postgres', AGENT_DATABASE_URL: 'postgres://example', AGENT_API_TOKEN: 'token', NEXT_PUBLIC_IMAGE_STORAGE_MODE: 'fs' }); assert.equal(capabilities.defaults.state_backend, 'postgres'); assert.equal(capabilities.schema_version, AGENT_SCHEMA_VERSION); assert.deepEqual(capabilities.image_transport, { upstream_timeout_ms: 900_000, stream_data_interval_timeout_ms: 900_000, upstream_max_retries: 0, upstream_proxy: { configured: false } }); assert.equal(capabilities.defaults.image_backend, 'images-api'); assert.equal(capabilities.defaults.stream_mode, 'auto'); assert.equal(capabilities.defaults.streaming_strategy, 'auto'); assert.equal(capabilities.defaults.partial_images, 2); assert.equal(capabilities.upstream_profile.activeProfile, 'openai-compatible'); assert.equal(capabilities.upstream_profile.serverProfile, 'openai-compatible'); assert.equal(capabilities.upstream_profile.serverProfileMixed, false); assert.equal(capabilities.upstream_profile.requestProfile, 'openai-compatible'); assert.equal(capabilities.upstream_profile.activeConstraints.id, 'openai-compatible'); assert.deepEqual(capabilities.limits.partial_images, { min: 1, max: 3 }); assert.deepEqual(capabilities.limits.partial_images_by_backend, { 'images-api': { min: 1, max: 3 }, 'responses-image-generation': { min: 1, max: 3 } }); assert.equal(capabilities.limits.max_images, 10); assert.deepEqual(capabilities.limits.generate_images, { min: 1, max: 10 }); assert.deepEqual(capabilities.limits.edit_images, { min: 1, max: 10 }); assert.equal(capabilities.limits.upload_images.max, 10); assert.equal(capabilities.limits.max_upload_mb, 25); assert.equal(capabilities.limits.upstream_profile, 'openai-compatible'); assert.equal(capabilities.limits.upstream_profile_mixed, false); assert.equal(capabilities.auth.required, true); assert.deepEqual(capabilities.auth.schemes, ['bearer']); assert.equal(capabilities.storage.postgres_configured, true); assert.equal('sqlite_path' in capabilities.storage, false); assert.deepEqual(capabilities.page_request_diagnostics.endpoints, { single: AGENT_ENDPOINTS.page_request_diagnostics, batch: AGENT_ENDPOINTS.page_request_diagnostics_batch }); assert.equal(capabilities.page_request_diagnostics.supported, true); assert.equal(capabilities.page_request_diagnostics.source, 'app_log'); assert.equal(capabilities.page_request_diagnostics.retention.storage, 'bounded_local_jsonl'); assert.equal(capabilities.page_request_diagnostics.retention.max_entries, 300); assert.equal(capabilities.page_request_diagnostics.retention.default_max_entries, 300); assert.equal(capabilities.page_request_diagnostics.retention.min_entries, 100); assert.equal(capabilities.page_request_diagnostics.retention.max_configured_entries, 5000); assert.equal(capabilities.page_request_diagnostics.retention.configured_by, 'APP_LOG_MAX_ENTRIES'); assert.equal(capabilities.page_request_diagnostics.retention.persisted_across_process_restart, true); assert.equal(capabilities.page_request_diagnostics.retention.bounded, true); assert.equal(capabilities.page_request_diagnostics.retention.not_agent_state_backend, true); assert.deepEqual(capabilities.page_request_diagnostics.retention.loss_modes, [ 'entry_evicted_by_max_entries', 'log_level_filter', 'local_log_file_missing_or_cleared' ]); assert.equal(capabilities.page_request_diagnostics.no_match_hint, AGENT_PAGE_REQUEST_DIAGNOSTICS_NO_MATCH_HINT); assert.equal(capabilities.agent_request_diagnostics.supported, true); assert.equal(capabilities.agent_request_diagnostics.source, 'agent_state'); assert.deepEqual(capabilities.agent_request_diagnostics.endpoints, { lookup: AGENT_ENDPOINTS.agent_request_diagnostics_lookup, single: AGENT_ENDPOINTS.agent_request_diagnostics }); assert.deepEqual(capabilities.agent_request_diagnostics.lookup, { by_request_id: true, by_idempotency_key: true }); assert.deepEqual(capabilities.agent_request_diagnostics.retention, { storage: 'agent_state', ttl_seconds: 86400, bounded: true, loss_modes: ['request_expired_by_ttl', 'artifact_deleted_or_purged', 'state_backend_reset'] }); assert.equal(capabilities.idempotency.header, 'Idempotency-Key'); assert.ok(capabilities.supported.models.includes('gpt-image-2')); assert.equal(capabilities.model_limits['gpt-image-2'].max_edge, 3840); assert.equal(capabilities.model_limits['gpt-image-2'].edge_multiple, 16); assert.equal(capabilities.model_limits['gpt-image-2'].max_pixels, 8294400); assert.equal(capabilities.model_limits['gpt-image-2'].min_pixels, 655360); assert.equal(capabilities.model_limits['gpt-image-2'].max_aspect, 3); assert.equal(capabilities.model_limits['gpt-image-2'].size_policy, 'openai-compatible'); assert.equal(capabilities.model_limits['gpt-image-2'].allow_transparent_background, false); assert.deepEqual(capabilities.force_request_controls, { field: 'force_request', cli_flag: '--force-request', default: false, effect: 'skip_local_upstream_profile_size_and_background_validation', still_enforced: [ 'authentication', 'idempotency_key', 'billable_confirmation', 'api_base_url_safety', 'channel_request_mode_whitelist', 'image_count_limits', 'partial_image_limits', 'non_gpt_image2_size_allowlist', 'positive_integer_size_syntax', 'upload_file_size_and_type', 'mask_integrity_checks' ], intended_for: [ 'upstream_compatibility_probe', 'administrator_confirmed_provider_contract', 'requests_where_real_upstream_should_decide' ] }); assert.deepEqual(capabilities.model_limits['gpt-image-2'].large_image_risk.applies_to, [ 'max_edge>2048', 'long_running_upstream' ]); assert.match(capabilities.model_limits['gpt-image-2'].large_image_risk.guidance, /大尺寸/); assert.equal(capabilities.agent_streaming.generate.supported, false); assert.equal(capabilities.agent_streaming.generate.mode, 'non_streaming_only'); assert.equal(capabilities.agent_streaming.upstream_sse.supported, true); assert.equal(capabilities.agent_streaming.upstream_sse.mode, 'internal_upstream_sse'); assert.deepEqual(capabilities.agent_streaming.upstream_sse.request_fields, [ 'image_backend', 'stream_mode', 'streaming_strategy', 'partial_images' ]); assert.deepEqual(capabilities.agent_streaming.upstream_sse.request_fields_by_mode, { generate: ['image_backend', 'stream_mode', 'streaming_strategy', 'partial_images'], edit: ['stream_mode', 'streaming_strategy', 'partial_images'] }); assert.deepEqual(capabilities.agent_streaming.upstream_sse.image_backends, [ 'images-api', 'responses-image-generation' ]); assert.deepEqual(capabilities.agent_streaming.upstream_sse.enabled_image_backends, ['images-api']); assert.deepEqual(capabilities.agent_streaming.upstream_sse.streaming_strategies, [ 'off', 'auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse' ]); assert.deepEqual(capabilities.agent_streaming.upstream_sse.activation_strategies, [ 'auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse' ]); assert.deepEqual(capabilities.agent_streaming.upstream_sse.stream_modes, ['auto', 'stream', 'non_stream']); assert.equal(capabilities.agent_streaming.upstream_sse.final_response_contract, 'AgentImageResponse'); assert.equal(capabilities.agent_streaming.page_sse.endpoint, '/api/images'); assert.equal(capabilities.agent_streaming.page_sse.contract, 'page_ui_only'); assert.equal(capabilities.agent_streaming.page_sse.transport_contract, 'page_form_data_sse'); assert.deepEqual(capabilities.agent_streaming.page_sse.auth, { required: false, schemes: [], form_field: 'passwordHash' }); assert.deepEqual(capabilities.agent_streaming.page_sse.client_request_id, { form_field: 'clientRequestId', source_header: 'Idempotency-Key', max_length: PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH }); assert.equal(capabilities.agent_streaming.page_sse.agent_usage, AGENT_PAGE_SSE_AGENT_USAGE); assert.deepEqual(capabilities.upstream_request_headers.default, { 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: [] }); assert.deepEqual(capabilities.upstream_request_headers.channels, []); assert.deepEqual(capabilities.routing_rules.high_resolution_edit.when, ['operation=edit', 'max_edge>2048']); assert.deepEqual(capabilities.routing_rules.high_resolution_edit.conditions, { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } }); assert.equal(capabilities.routing_rules.high_resolution_edit.endpoint, '/api/images'); assert.equal(capabilities.routing_rules.high_resolution_edit.transport, 'page_sse'); assert.equal(capabilities.routing_rules.high_resolution_edit.strength, 'default'); assert.deepEqual(capabilities.routing_rules.high_resolution_edit.action, { endpoint: '/api/images', transport: 'page_sse', strength: 'default', fallback_endpoint: AGENT_ENDPOINTS.edit, fallback_mode: 'manual_after_diagnosis', requires_new_idempotency_key_on_retry: true, no_automatic_fallback: true }); assert.equal(capabilities.routing_rules.complex_ui_batch.endpoint, '/api/images'); assert.deepEqual(capabilities.routing_rules.complex_ui_batch.conditions, { operation: 'generate_or_edit', complex_ui: true, batch: true }); assert.equal(capabilities.routing_rules.complex_ui_batch.strength, 'recommended'); assert.equal(capabilities.routing_rules.long_image_recovery.endpoint, '/api/images'); assert.equal(capabilities.routing_rules.long_image_recovery.transport, 'page_sse'); assert.deepEqual(capabilities.routing_rules.long_image_recovery.conditions, { operation: 'generate_or_edit', long_image: true, resume_or_recover: true }); assert.equal(capabilities.routing_rules.agent_generate_small_smoke.endpoint, AGENT_ENDPOINTS.generate); assert.equal(capabilities.routing_rules.agent_generate_small_smoke.strength, 'explicit'); assert.deepEqual(capabilities.routing_rules.agent_generate_small_smoke.action, { endpoint: AGENT_ENDPOINTS.generate, transport: 'agent_json', strength: 'explicit', requires_new_idempotency_key_on_retry: true, no_automatic_fallback: true }); assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.endpoint, '/api/images'); assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.transport, 'page_sse'); assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.strength, 'explicit'); assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.action.strength, 'explicit'); assert.equal( capabilities.routing_rules.page_sse_generate_diagnostics.action.fallback_endpoint, AGENT_ENDPOINTS.create_image_request ); assert.deepEqual(capabilities.routing_rules.page_sse_generate_diagnostics.conditions, { operation: 'generate', explicit_page_sse: true, single_request: true }); assert.equal(capabilities.routing_rules.retry_recovery.reuse_failed_idempotency_key, false); assert.match(capabilities.routing_rules.retry_recovery.new_attempt_guidance, /new Idempotency-Key/); assert.equal(capabilities.orchestration.supported, true); assert.equal(capabilities.orchestration.policy, 'server_orchestrated_generate_v1'); assert.equal(capabilities.orchestration.endpoint, AGENT_ENDPOINTS.create_image_request); assert.equal(capabilities.orchestration.client_contract, 'intent_only'); assert.equal(capabilities.orchestration.transport_selection, 'server_owned'); assert.equal(capabilities.orchestration.result_mode, 'job_polling'); assert.deepEqual(capabilities.orchestration.diagnostics, { job_result: AGENT_ENDPOINTS.job_result, request_lookup: AGENT_ENDPOINTS.agent_request_diagnostics_lookup }); assert.match(capabilities.orchestration.current_guidance, /只提交生成意图/); assert.deepEqual(capabilities.supported.image_backends, ['images-api', 'responses-image-generation']); assert.deepEqual(capabilities.supported.enabled_image_backends, ['images-api']); assert.deepEqual(capabilities.supported.image_backend_requirements['images-api'], { supported: true, enabled: true, required_env: [], missing_env: [] }); assert.deepEqual(capabilities.supported.image_backend_requirements['responses-image-generation'], { supported: true, enabled: false, required_env: ['ENABLE_RESPONSES_IMAGE_BACKEND', 'OPENAI_RESPONSES_API_MODEL'], missing_env: ['ENABLE_RESPONSES_IMAGE_BACKEND', 'OPENAI_RESPONSES_API_MODEL'] }); assert.deepEqual(capabilities.supported.request_modes, [ 'images-non-stream', 'images-sse', 'responses-non-stream', 'responses-sse' ]); assert.deepEqual(capabilities.request_mode_controls, { source: 'admin_env_whitelist', global_env: 'OPENAI_UPSTREAM_REQUEST_MODES', channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES', global_priority_env: 'OPENAI_UPSTREAM_REQUEST_MODE_PRIORITY', channel_priority_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODE_PRIORITY', default_priority: ['images-non-stream', 'images-sse', 'responses-non-stream', 'responses-sse'], default_priority_policy: 'lowest_cost_first', mutable_at_runtime: false, agent_client_policy: 'diagnostics_only', final_gate_command: 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --require-independent-targets --allow-billable', smoke_gate_commands: { 'images-non-stream': [ 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --case original-images-json --allow-billable' ], 'images-sse': [ 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --case sub2api-images-sse --allow-billable' ], 'responses-non-stream': [ 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --case sub2api-responses-json --allow-billable' ], 'responses-sse': [ 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --case gpt2image-responses-sse --allow-billable' ] } }); assert.deepEqual(capabilities.supported.streaming_strategies, [ 'off', 'auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse' ]); assert.deepEqual(capabilities.supported.stream_modes, ['auto', 'stream', 'non_stream']); assert.equal(capabilities.endpoints.create_generate_job, AGENT_ENDPOINTS.create_generate_job); assert.equal(capabilities.endpoints.create_image_request, AGENT_ENDPOINTS.create_image_request); assert.equal(capabilities.endpoints.page_request_feedback_batch, AGENT_ENDPOINTS.page_request_feedback_batch); assert.equal(capabilities.endpoints.page_request_feedback, AGENT_ENDPOINTS.page_request_feedback); assert.equal( capabilities.endpoints.page_request_diagnostics_batch, AGENT_ENDPOINTS.page_request_diagnostics_batch ); assert.equal(capabilities.endpoints.page_request_diagnostics, AGENT_ENDPOINTS.page_request_diagnostics); assert.equal( capabilities.endpoints.agent_request_diagnostics_lookup, AGENT_ENDPOINTS.agent_request_diagnostics_lookup ); assert.equal(capabilities.endpoints.agent_request_diagnostics, AGENT_ENDPOINTS.agent_request_diagnostics); assert.equal(capabilities.agent_jobs.supported, true); assert.equal(capabilities.agent_jobs.mode, 'job_polling'); assert.deepEqual(capabilities.agent_jobs.intended_for, [ 'explicit_job_route', 'manual_after_diagnosis', 'long_running_upstream_when_page_sse_not_selected' ]); assert.equal(capabilities.agent_jobs.endpoints.create_generate_job, AGENT_JOB_ENDPOINTS.create_generate_job); assert.deepEqual(capabilities.agent_jobs.states, ['queued', 'running', 'succeeded', 'failed', 'expired']); assert.match(capabilities.agent_jobs.current_guidance, /orchestration\.endpoint/); assert.match(capabilities.agent_jobs.current_guidance, /job/); }); it('reports configured server-channel request modes in Agent capabilities', () => { const capabilities = buildAgentCapabilities({ OPENAI_CHANNEL_1_ID: 'images', OPENAI_CHANNEL_1_BASE_URL: 'https://images.example.com/v1', OPENAI_CHANNEL_1_API_KEYS: 'configured', OPENAI_CHANNEL_1_REQUEST_MODES: 'images-json,images-sse' }); assert.deepEqual(capabilities.upstream_request_headers.channels, [ { id: 'images', upstream_proxy: { configured: false }, request_modes: ['images-non-stream', 'images-sse'], request_mode_priority: ['images-non-stream', 'images-sse'], 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', () => { const capabilities = buildAgentCapabilities({ OPENAI_UPSTREAM_PROXY_URL: 'https://global-proxy.internal.example:9443', OPENAI_CHANNEL_1_ID: 'primary', OPENAI_CHANNEL_1_BASE_URL: 'https://primary.example.com/v1', OPENAI_CHANNEL_1_API_KEYS: 'configured', OPENAI_CHANNEL_2_ID: 'backup', OPENAI_CHANNEL_2_BASE_URL: 'https://backup.example.com/v1', OPENAI_CHANNEL_2_API_KEYS: 'configured', OPENAI_CHANNEL_2_PROXY_URL: 'http://channel-proxy.internal.example:8080' }); assert.deepEqual(capabilities.image_transport.upstream_proxy, { configured: true, protocol: 'https' }); assert.deepEqual( capabilities.upstream_request_headers.channels.map((channel) => ({ 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(capabilities); assert.equal(serialized.includes('global-proxy.internal.example'), false); assert.equal(serialized.includes('channel-proxy.internal.example'), false); assert.equal(serialized.includes('9443'), false); assert.equal(serialized.includes('8080'), false); }); it('reports Matsca server-channel upload and image-count limits in Agent capabilities', () => { const capabilities = buildAgentCapabilities({ OPENAI_CHANNEL_1_ID: 'matsca', OPENAI_CHANNEL_1_BASE_URL: 'https://img.matsca.com/v1', OPENAI_CHANNEL_1_API_KEYS: 'configured', OPENAI_CHANNEL_1_UPSTREAM_PROFILE: 'matsca', OPENAI_CHANNEL_1_MATSCA_APP_ID: 'configured', OPENAI_CHANNEL_1_MATSCA_APP_SECRET: 'configured' }); assert.equal(capabilities.upstream_profile.activeProfile, 'matsca'); assert.equal(capabilities.upstream_profile.serverProfile, 'matsca'); assert.equal(capabilities.upstream_profile.serverProfileMixed, false); assert.equal(capabilities.upstream_profile.requestProfile, 'openai-compatible'); assert.equal(capabilities.upstream_profile.activeConstraints.id, 'matsca'); assert.equal(capabilities.limits.max_images, 4); assert.deepEqual(capabilities.limits.generate_images, { min: 1, max: 4 }); assert.deepEqual(capabilities.limits.edit_images, { min: 1, max: 4 }); assert.equal(capabilities.limits.upload_images.max, 8); assert.equal(capabilities.limits.max_upload_mb, 10); assert.equal(capabilities.limits.max_total_upload_mb, 80); assert.deepEqual(capabilities.limits.partial_images, { min: 0, max: 4 }); assert.deepEqual(capabilities.limits.partial_images_by_backend, { 'images-api': { min: 0, max: 4 }, 'responses-image-generation': { min: 1, max: 3 } }); assert.equal(capabilities.limits.upstream_profile, 'matsca'); assert.equal(capabilities.limits.upstream_profile_mixed, false); assert.equal(capabilities.model_limits['gpt-image-2'].size_policy, 'positive-integer'); assert.equal(capabilities.model_limits['gpt-image-2'].allow_transparent_background, true); }); it('reports provider manifest constraints in Agent capabilities', () => { const capabilities = buildAgentCapabilities({ OPENAI_CHANNEL_1_ID: 'custom', OPENAI_CHANNEL_1_BASE_URL: 'https://custom.example.com/v1', OPENAI_CHANNEL_1_API_KEYS: 'configured', OPENAI_CHANNEL_1_PROVIDER_MANIFEST: JSON.stringify({ id: 'custom_provider', base_profile: 'openai-compatible', base_url: 'https://provider.internal.example/v1', modes: { generate: { submit: { path: '/images/generations' } } }, constraints: { generate_count: { min: 1, max: 2 }, edit_count: { min: 1, max: 1 }, partial_images: { min: 1, max: 2 }, upload: { max_images: 3, max_single_bytes: 10485760 } } }) }); assert.equal(capabilities.upstream_profile.activeConstraints.providerManifest?.id, 'custom_provider'); assert.equal(JSON.stringify(capabilities).includes('provider.internal.example'), false); assert.deepEqual(capabilities.limits.generate_images, { min: 1, max: 2 }); assert.deepEqual(capabilities.limits.edit_images, { min: 1, max: 1 }); assert.equal(capabilities.limits.max_images, 1); assert.equal(capabilities.limits.upload_images.max, 3); assert.equal(capabilities.limits.max_upload_mb, 10); assert.deepEqual(capabilities.limits.partial_images, { min: 1, max: 2 }); }); it('reports mixed Agent limits when only one OpenAI-compatible channel has provider constraints', () => { const capabilities = buildAgentCapabilities({ OPENAI_CHANNEL_1_ID: 'custom', OPENAI_CHANNEL_1_BASE_URL: 'https://custom.example.com/v1', OPENAI_CHANNEL_1_API_KEYS: 'configured', OPENAI_CHANNEL_1_PROVIDER_MANIFEST: JSON.stringify({ id: 'custom_provider', base_profile: 'openai-compatible', modes: { generate: { submit: { path: '/images/generations' } } }, constraints: { generate_count: { min: 1, max: 2 }, upload: { max_images: 3, max_single_bytes: 10485760 } } }), OPENAI_CHANNEL_2_ID: 'official', OPENAI_CHANNEL_2_BASE_URL: 'https://api.openai.com/v1', OPENAI_CHANNEL_2_API_KEYS: 'configured', OPENAI_CHANNEL_2_UPSTREAM_PROFILE: 'openai-compatible' }); assert.equal(capabilities.upstream_profile.serverProfileMixed, true); assert.equal(capabilities.limits.upstream_profile_mixed, true); assert.deepEqual(capabilities.limits.generate_images, { min: 1, max: 2 }); assert.equal(capabilities.limits.upload_images.max, 3); assert.equal(capabilities.limits.max_upload_mb, 10); }); it('uses conservative Agent limits for mixed upstream profiles', () => { const capabilities = buildAgentCapabilities({ OPENAI_CHANNEL_1_ID: 'matsca', OPENAI_CHANNEL_1_BASE_URL: 'https://img.matsca.com/v1', OPENAI_CHANNEL_1_API_KEYS: 'configured', OPENAI_CHANNEL_1_UPSTREAM_PROFILE: 'matsca', OPENAI_CHANNEL_1_MATSCA_APP_ID: 'configured', OPENAI_CHANNEL_1_MATSCA_APP_SECRET: 'configured', OPENAI_CHANNEL_2_ID: 'official', OPENAI_CHANNEL_2_BASE_URL: 'https://api.openai.com/v1', OPENAI_CHANNEL_2_API_KEYS: 'configured', OPENAI_CHANNEL_2_UPSTREAM_PROFILE: 'openai-compatible' }); assert.equal(capabilities.upstream_profile.activeProfile, 'openai-compatible'); assert.equal(capabilities.upstream_profile.serverProfile, 'openai-compatible'); assert.equal(capabilities.upstream_profile.serverProfileMixed, true); assert.equal(capabilities.upstream_profile.requestProfile, 'openai-compatible'); assert.equal(capabilities.upstream_profile.activeConstraints.generateCount.max, 4); assert.equal(capabilities.upstream_profile.activeConstraints.upload.maxImages, 8); assert.deepEqual(capabilities.limits.partial_images, { min: 1, max: 3 }); assert.deepEqual(capabilities.limits.partial_images_by_backend, { 'images-api': { min: 1, max: 3 }, 'responses-image-generation': { min: 1, max: 3 } }); assert.equal(capabilities.limits.max_images, 4); assert.deepEqual(capabilities.limits.generate_images, { min: 1, max: 4 }); assert.deepEqual(capabilities.limits.edit_images, { min: 1, max: 4 }); assert.equal(capabilities.limits.upload_images.max, 8); assert.equal(capabilities.limits.max_upload_mb, 10); assert.equal(capabilities.limits.max_total_upload_mb, 80); assert.equal(capabilities.limits.upstream_profile_mixed, true); assert.equal(capabilities.model_limits['gpt-image-2'].allow_transparent_background, false); }); it('exposes only the runtime-accepted bearer auth scheme when Agent token is configured', () => { assert.deepEqual( buildAgentAuthCapabilities({ AGENT_API_TOKEN: 'token', APP_PASSWORD: 'page-access-code' }), { required: true, schemes: ['bearer'] } ); }); it('marks Responses image backend runtime-enabled only when its feature flag and model are configured', () => { const missingModel = buildAgentCapabilities({ ENABLE_RESPONSES_IMAGE_BACKEND: 'true' }); assert.deepEqual(missingModel.supported.enabled_image_backends, ['images-api']); assert.deepEqual(missingModel.supported.image_backend_requirements['responses-image-generation'].missing_env, [ 'OPENAI_RESPONSES_API_MODEL' ]); const enabled = buildAgentCapabilities({ ENABLE_RESPONSES_IMAGE_BACKEND: 'true', OPENAI_RESPONSES_API_MODEL: 'gpt-4.1' }); assert.deepEqual(enabled.agent_streaming.upstream_sse.enabled_image_backends, [ 'images-api', 'responses-image-generation' ]); assert.deepEqual(enabled.supported.enabled_image_backends, ['images-api', 'responses-image-generation']); assert.deepEqual(enabled.supported.image_backend_requirements['responses-image-generation'], { supported: true, enabled: true, required_env: ['ENABLE_RESPONSES_IMAGE_BACKEND', 'OPENAI_RESPONSES_API_MODEL'], missing_env: [] }); }); it('exposes page SSE form password auth separately from Agent bearer auth', () => { const capabilities = buildAgentCapabilities({ AGENT_API_TOKEN: 'token', APP_PASSWORD: 'page-access-code' }); assert.deepEqual(capabilities.auth, { required: true, schemes: ['bearer'] }); assert.deepEqual(capabilities.agent_streaming.page_sse.auth, { required: true, schemes: ['form-password-hash'], form_field: 'passwordHash' }); }); it('propagates the configured app log retention window into Agent diagnostics capabilities', () => { assert.deepEqual(buildPageRequestDiagnosticsCapabilities({ APP_LOG_MAX_ENTRIES: '450' }).retention, { storage: 'bounded_local_jsonl', max_entries: 450, default_max_entries: 300, min_entries: 100, max_configured_entries: 5000, configured_by: 'APP_LOG_MAX_ENTRIES', persisted_across_process_restart: true, loss_modes: ['entry_evicted_by_max_entries', 'log_level_filter', 'local_log_file_missing_or_cleared'], bounded: true, not_agent_state_backend: true }); }); it('exposes access-code hash auth only when no Agent token is configured', () => { assert.deepEqual(buildAgentAuthCapabilities({ APP_PASSWORD: 'page-access-code' }), { required: true, schemes: ['x-app-password-hash'] }); }); it('marks Agent auth as optional when no auth env is configured', () => { assert.deepEqual( buildAgentAuthCapabilities({ AGENT_API_TOKEN: ' ', APP_PASSWORD: ' ' }), { required: false, schemes: [] } ); }); it('exposes memory state backend for ephemeral deployments', () => { const capabilities = buildAgentCapabilities({ AGENT_STATE_BACKEND: 'memory', NEXT_PUBLIC_IMAGE_STORAGE_MODE: 'fs' }); assert.equal(capabilities.defaults.state_backend, 'memory'); assert.equal(capabilities.storage.postgres_configured, false); }); it('generates an OpenAPI document with the Agent generate endpoint', () => { const document = buildAgentOpenApiDocument({ AGENT_PUBLIC_BASE_URL: 'https://images.example.test' }); assert.equal(document.openapi, '3.1.0'); assert.equal(document.info.title, 'Visual Journal Agent API'); assert.deepEqual(document.servers, [{ url: 'https://images.example.test' }]); assert.ok(AGENT_ENDPOINTS.openapi in document.paths); assert.ok(AGENT_ENDPOINTS.create_image_request in document.paths); assert.ok(AGENT_ENDPOINTS.generate in document.paths); assert.ok(AGENT_ENDPOINTS.create_generate_job in document.paths); assert.ok(AGENT_ENDPOINTS.job in document.paths); assert.ok(AGENT_ENDPOINTS.job_result in document.paths); assert.ok(AGENT_ENDPOINTS.artifact_share in document.paths); assert.ok(AGENT_ENDPOINTS.page_request_feedback_batch in document.paths); assert.ok(AGENT_ENDPOINTS.page_request_feedback in document.paths); assert.ok(AGENT_ENDPOINTS.page_request_diagnostics_batch in document.paths); assert.ok(AGENT_ENDPOINTS.page_request_diagnostics in document.paths); assert.ok(AGENT_ENDPOINTS.agent_request_diagnostics_lookup in document.paths); assert.ok(AGENT_ENDPOINTS.agent_request_diagnostics in document.paths); assert.ok('AgentCapabilities' in document.components.schemas); assert.ok('AgentImageResponse' in document.components.schemas); assert.ok('AgentJobStatusResponse' in document.components.schemas); assert.ok('AgentArtifact' in document.components.schemas); assert.ok('CreateArtifactShareRequest' in document.components.schemas); assert.ok('ArtifactShareResponse' in document.components.schemas); assert.ok('ResultFeedback' in document.components.schemas); assert.ok('FeedbackTarget' in document.components.schemas); assert.ok('PageRequestFeedbackBatchRequest' in document.components.schemas); assert.ok('PageRequestFeedbackBatchResponse' in document.components.schemas); assert.ok('PageRequestFeedbackResponse' in document.components.schemas); assert.ok('PageRequestDiagnosticsBatchRequest' in document.components.schemas); assert.ok('PageRequestDiagnosticsBatchItem' in document.components.schemas); assert.ok('PageRequestDiagnosticsBatchResponse' in document.components.schemas); assert.ok('PageRequestDiagnosticsResponse' in document.components.schemas); assert.ok('PageRequestDiagnosticsNote' in document.components.schemas); assert.ok('PageRequestDiagnosticContext' in document.components.schemas); assert.ok('EditRequest' in document.components.schemas); assert.ok('AgentError' in document.components.schemas); assert.ok('AgentModelLimits' in document.components.schemas); assert.ok('ImageUpstreamProfile' in document.components.schemas); assert.ok('AgentStreamingCapabilities' in document.components.schemas); assert.ok('AgentPageRequestDiagnosticsCapabilities' in document.components.schemas); assert.ok('AppLogRetentionMetadata' in document.components.schemas); assert.ok('AgentJobCapabilities' in document.components.schemas); assert.ok('AgentOrchestrationCapabilities' in document.components.schemas); assert.ok('AgentRoutingRules' in document.components.schemas); assert.ok('AgentRoutingRule' in document.components.schemas); assert.ok('AgentErrorDiagnostics' in document.components.schemas); assert.ok('AgentImageResponseTiming' in document.components.schemas); assert.ok('AgentImageResponseExecution' in document.components.schemas); assert.ok('ChannelRequestModeDecision' in document.components.schemas); assert.ok('UpstreamProxySummary' in document.components.schemas); assert.deepEqual(document.components.schemas.AgentImageResponseExecution.properties.channel_request_mode.enum, [ 'images-non-stream', 'images-sse', 'responses-non-stream', 'responses-sse' ]); assert.equal( document.components.schemas.AgentImageResponseExecution.properties.channel_request_mode_fallback_applied .type, 'boolean' ); assert.equal( document.components.schemas.AgentImageResponseExecution.properties.route_decision.$ref, '#/components/schemas/ChannelRequestModeDecision' ); assert.equal( document.components.schemas.ChannelRequestModeDecision.properties.requested_backend.enum.includes( 'images-api' ), true ); assert.ok('UpstreamRequestHeaderSummary' in document.components.schemas); assert.ok('AgentRequestDiagnosticsCapabilities' in document.components.schemas); assert.ok('AgentRequestDiagnosticsRetention' in document.components.schemas); assert.ok('AgentRequestDiagnosticsLookupResponse' in document.components.schemas); assert.ok('AgentRequestDiagnostics' in document.components.schemas); assert.deepEqual(document.components.schemas.CreateArtifactShareRequest.properties.access_code.anyOf, [ { type: 'string', pattern: '^\\s*$' }, { type: 'string', minLength: 8, maxLength: 128 } ]); assert.ok(document.paths[AGENT_ENDPOINTS.generate].post.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.generate].post.responses['403']); assert.ok(document.paths[AGENT_ENDPOINTS.generate].post.responses['429']); assert.ok(document.paths[AGENT_ENDPOINTS.generate].post.responses['422']); assert.ok(document.paths[AGENT_ENDPOINTS.create_image_request].post.responses['202']); assert.ok(document.paths[AGENT_ENDPOINTS.create_image_request].post.responses['409']); assert.ok(document.paths[AGENT_ENDPOINTS.create_generate_job].post.responses['202']); assert.ok(document.paths[AGENT_ENDPOINTS.job_result].get.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.job_result].get.responses['409']); assert.ok(document.paths[AGENT_ENDPOINTS.job_result].get.responses['422']); assert.ok(document.paths[AGENT_ENDPOINTS.job_result].get.responses['429']); assert.ok(document.paths[AGENT_ENDPOINTS.job_result].get.responses['502']); assert.ok(document.paths[AGENT_ENDPOINTS.artifact_share].post.responses['201']); assert.ok(document.paths[AGENT_ENDPOINTS.artifact_share].post.responses['400']); assert.ok(document.paths[AGENT_ENDPOINTS.page_request_feedback_batch].post.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.page_request_feedback].get.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.page_request_diagnostics_batch].post.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.page_request_diagnostics].get.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.agent_request_diagnostics_lookup].get.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.agent_request_diagnostics_lookup].get.responses['404']); assert.ok(document.paths[AGENT_ENDPOINTS.agent_request_diagnostics].get.responses['200']); assert.ok(document.paths[AGENT_ENDPOINTS.agent_request_diagnostics].get.responses['404']); const generateProperties = document.components.schemas.GenerateRequest.properties; assert.deepEqual(generateProperties.image_backend.enum, ['images-api', 'responses-image-generation']); assert.deepEqual(generateProperties.streaming_strategy.enum, [ 'off', 'auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse' ]); assert.deepEqual(generateProperties.stream_mode.enum, ['auto', 'stream', 'non_stream']); assert.deepEqual(generateProperties.thinking.enum, ['minimal', 'none', 'low', 'medium', 'high', 'xhigh']); assert.equal(generateProperties.responsesModel.maxLength, 128); assert.equal(generateProperties.promptOptimization.type, 'boolean'); assert.equal(generateProperties.force_web.type, 'boolean'); assert.equal(generateProperties.force_request.type, 'boolean'); assert.deepEqual(generateProperties.n, { type: 'integer', minimum: 1, maximum: 10 }); assert.deepEqual(generateProperties.partial_images, { type: 'integer', minimum: 1, maximum: 3, default: 2 }); assert.deepEqual(document.components.schemas.GenerateRequest.allOf[0].then.properties.partial_images, { type: 'integer', minimum: 1, maximum: 3, default: 2 }); assert.deepEqual(generateProperties.background.enum, ['transparent', 'opaque', 'auto']); assert.deepEqual(document.components.schemas.GenerateRequest.allOf[1], { if: { not: { properties: { force_request: { const: true } }, required: ['force_request'] } }, then: { properties: { background: { type: 'string', enum: ['opaque', 'auto'] } } } }); const editProperties: Record = document.components.schemas.EditRequest.properties; assert.equal('image_backend' in editProperties, false); assert.equal('output_format' in editProperties, false); assert.equal('output_compression' in editProperties, false); assert.equal('background' in editProperties, false); assert.equal('moderation' in editProperties, false); const forceRequestProperty = editProperties.force_request; assert.ok(forceRequestProperty && typeof forceRequestProperty === 'object' && 'type' in forceRequestProperty); assert.equal(forceRequestProperty.type, 'boolean'); assert.deepEqual(editProperties.n, { type: 'integer', minimum: 1, maximum: 10 }); assert.deepEqual(editProperties.partial_images, { type: 'integer', minimum: 1, maximum: 3, default: 2 }); assert.deepEqual(document.components.schemas.EditRequest.required, ['prompt']); assert.match(document.components.schemas.EditRequest.description, /至少提供一个 image_0\.\.image_9/); assert.deepEqual( document.components.schemas.EditRequest.anyOf, Array.from({ length: 10 }, (_, index) => ({ required: [`image_${index}`] })) ); for (let index = 0; index < 10; index += 1) { assert.deepEqual(editProperties[`image_${index}`], { type: 'string', format: 'binary' }); } const capabilityProperties = document.components.schemas.AgentCapabilities.properties; assert.equal(capabilityProperties.routing_rules.$ref, '#/components/schemas/AgentRoutingRules'); assert.equal(capabilityProperties.image_transport.$ref, '#/components/schemas/ImageTransportCapabilities'); assert.equal( document.components.schemas.ImageTransportCapabilities.properties.upstream_timeout_ms.const, 900000 ); assert.equal( document.components.schemas.ImageTransportCapabilities.properties.upstream_proxy.$ref, '#/components/schemas/UpstreamProxySummary' ); assert.deepEqual(document.components.schemas.UpstreamProxySummary.required, ['configured']); assert.equal( capabilityProperties.upstream_request_headers.properties.default.$ref, '#/components/schemas/UpstreamRequestHeaderSummary' ); assert.equal(capabilityProperties.upstream_profile.properties.serverProfile.const, 'openai-compatible'); assert.equal( capabilityProperties.upstream_profile.properties.activeConstraints.$ref, '#/components/schemas/ImageUpstreamProfile' ); assert.equal(capabilityProperties.limits.properties.partial_images.properties.min.const, 1); assert.equal(capabilityProperties.limits.properties.partial_images.properties.max.const, 3); assert.equal( capabilityProperties.limits.properties.partial_images_by_backend.properties['responses-image-generation'] .properties.max.const, 3 ); assert.equal( capabilityProperties.page_request_diagnostics.$ref, '#/components/schemas/AgentPageRequestDiagnosticsCapabilities' ); assert.equal( document.components.schemas.AgentPageRequestDiagnosticsCapabilities.properties.retention.$ref, '#/components/schemas/AppLogRetentionMetadata' ); assert.equal( document.components.schemas.AgentPageRequestDiagnosticsCapabilities.properties.endpoints.properties.single .const, AGENT_ENDPOINTS.page_request_diagnostics ); assert.equal( document.components.schemas.AgentPageRequestDiagnosticsCapabilities.properties.endpoints.properties.batch .const, AGENT_ENDPOINTS.page_request_diagnostics_batch ); assert.equal(document.components.schemas.AppLogRetentionMetadata.properties.max_entries.const, 300); assert.equal( document.components.schemas.AppLogRetentionMetadata.properties.configured_by.const, 'APP_LOG_MAX_ENTRIES' ); assert.deepEqual(document.components.schemas.AppLogRetentionMetadata.properties.loss_modes.const, [ 'entry_evicted_by_max_entries', 'log_level_filter', 'local_log_file_missing_or_cleared' ]); assert.equal( document.components.schemas.PageRequestDiagnosticsResponse.required.includes('diagnostics_retention'), true ); assert.equal( document.components.schemas.PageRequestDiagnosticsResponse.properties.diagnostics_retention.$ref, '#/components/schemas/AppLogRetentionMetadata' ); assert.equal( document.components.schemas.PageRequestDiagnosticsResponse.properties.diagnostics_note.$ref, '#/components/schemas/PageRequestDiagnosticsNote' ); assert.equal( document.components.schemas.PageRequestDiagnosticsBatchResponse.required.includes('diagnostics_retention'), true ); assert.equal( document.components.schemas.PageRequestDiagnosticsBatchResponse.properties.diagnostics_retention.$ref, '#/components/schemas/AppLogRetentionMetadata' ); assert.equal( document.components.schemas.PageRequestDiagnosticsNote.properties.retention.$ref, '#/components/schemas/AppLogRetentionMetadata' ); assert.equal( document.components.schemas.PageRequestDiagnosticEvent.properties.diagnostics.$ref, '#/components/schemas/PageRequestDiagnosticContext' ); assert.equal(document.components.schemas.PageRequestDiagnosticContext.additionalProperties, false); assert.deepEqual(document.components.schemas.PageRequestDiagnosticContext.properties.transport_error, { type: 'boolean' }); assert.deepEqual(document.components.schemas.PageRequestDiagnosticContext.properties.partial_images, { type: 'number' }); const routingRuleProperties = document.components.schemas.AgentRoutingRule.properties; assert.equal(routingRuleProperties.conditions.$ref, '#/components/schemas/AgentRoutingCondition'); assert.equal(routingRuleProperties.action.$ref, '#/components/schemas/AgentRoutingAction'); assert.deepEqual(document.components.schemas.AgentRoutingCondition.properties.operation.enum, [ 'generate', 'edit', 'generate_or_edit' ]); assert.deepEqual( document.components.schemas.AgentRoutingCondition.properties.max_edge.properties.operator.enum, ['gt', 'lte'] ); assert.equal(document.components.schemas.AgentRoutingCondition.properties.explicit_page_sse.type, 'boolean'); assert.equal( document.components.schemas.AgentRoutingAction.properties.requires_new_idempotency_key_on_retry.type, 'boolean' ); assert.equal(document.components.schemas.AgentRoutingAction.properties.no_automatic_fallback.type, 'boolean'); assert.deepEqual(capabilityProperties.defaults.properties.image_backend.enum, [ 'images-api', 'responses-image-generation' ]); assert.deepEqual(capabilityProperties.supported.properties.image_backends.items.enum, [ 'images-api', 'responses-image-generation' ]); assert.deepEqual(capabilityProperties.supported.properties.enabled_image_backends.items.enum, [ 'images-api', 'responses-image-generation' ]); assert.ok(capabilityProperties.supported.properties.image_backend_requirements); assert.deepEqual(capabilityProperties.supported.properties.request_modes.items.enum, [ 'images-non-stream', 'images-sse', 'responses-non-stream', 'responses-sse' ]); assert.equal( document.components.schemas.AgentRequestModeControls.properties.agent_client_policy.const, 'diagnostics_only' ); assert.equal( document.components.schemas.AgentRequestModeControls.properties.channel_env_pattern.type, 'string' ); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.upstream_sse.properties.request_fields .const, ['image_backend', 'stream_mode', 'streaming_strategy', 'partial_images'] ); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.upstream_sse.properties .request_fields_by_mode.properties.generate.const, ['image_backend', 'stream_mode', 'streaming_strategy', 'partial_images'] ); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.upstream_sse.properties .request_fields_by_mode.properties.edit.const, ['stream_mode', 'streaming_strategy', 'partial_images'] ); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.upstream_sse.properties .enabled_image_backends.items.enum, ['images-api', 'responses-image-generation'] ); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.upstream_sse.properties .streaming_strategies.items.enum, ['off', 'auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse'] ); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.upstream_sse.properties .activation_strategies.items.enum, ['auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse'] ); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.upstream_sse.properties.stream_modes.items .enum, ['auto', 'stream', 'non_stream'] ); assert.equal( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.contract.enum[0], 'page_ui_only' ); assert.equal( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.transport_contract .enum[0], 'page_form_data_sse' ); assert.equal( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.auth.properties .form_field.const, 'passwordHash' ); assert.equal( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.client_request_id .properties.max_length.const, PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH ); assert.equal( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.client_request_id .properties.source_header.const, 'Idempotency-Key' ); assert.equal(document.components.schemas.AgentRoutingRules.required.includes('high_resolution_edit'), true); assert.equal(document.components.schemas.AgentRoutingRules.required.includes('long_image_recovery'), true); assert.match(document.components.schemas.EditRequest.description, /\/api\/images/); assert.ok('upstream_event_type' in document.components.schemas.AgentErrorDiagnostics.properties); assert.deepEqual(document.components.schemas.AgentErrorDiagnostics.properties.channel_request_mode.enum, [ 'images-non-stream', 'images-sse', 'responses-non-stream', 'responses-sse' ]); assert.equal( document.components.schemas.AgentErrorDiagnostics.properties.channel_request_mode_fallback_applied.type, 'boolean' ); assert.equal( document.components.schemas.AgentErrorDiagnostics.properties.route_decision.$ref, '#/components/schemas/ChannelRequestModeDecision' ); assert.ok('partial_image_count' in document.components.schemas.AgentErrorDiagnostics.properties); assert.ok('transport_error_kind' in document.components.schemas.AgentErrorDiagnostics.properties); assert.ok('retry_after_ms' in document.components.schemas.AgentErrorDiagnostics.properties); assert.ok('cooldown_until' in document.components.schemas.AgentErrorDiagnostics.properties); assert.ok('cooldown_target' in document.components.schemas.AgentErrorDiagnostics.properties); assert.deepEqual( document.components.schemas.AgentErrorDiagnostics.properties.cooldown_target.properties.request_mode.enum, ['images-non-stream', 'images-sse', 'responses-non-stream', 'responses-sse'] ); assert.equal(document.components.schemas.ResultFeedback.properties.note.maxLength, 500); }); it('describes Matsca server-channel limits in OpenAPI request schemas', () => { const document = buildAgentOpenApiDocument({ OPENAI_CHANNEL_1_ID: 'matsca', OPENAI_CHANNEL_1_BASE_URL: 'https://img.matsca.com/v1', OPENAI_CHANNEL_1_API_KEYS: 'configured', OPENAI_CHANNEL_1_UPSTREAM_PROFILE: 'matsca', OPENAI_CHANNEL_1_MATSCA_APP_ID: 'configured', OPENAI_CHANNEL_1_MATSCA_APP_SECRET: 'configured' }); assert.equal(document.components.schemas.GenerateRequest.properties.n.maximum, 4); assert.equal(document.components.schemas.EditRequest.properties.n.maximum, 4); assert.equal(document.components.schemas.GenerateRequest.properties.partial_images.minimum, 0); assert.equal(document.components.schemas.GenerateRequest.properties.partial_images.maximum, 4); assert.deepEqual(document.components.schemas.GenerateRequest.allOf[0].then.properties.partial_images, { type: 'integer', minimum: 1, maximum: 3, default: 2 }); assert.equal(document.components.schemas.EditRequest.properties.partial_images.minimum, 0); assert.equal(document.components.schemas.EditRequest.properties.partial_images.maximum, 4); assert.match(document.components.schemas.EditRequest.description, /image_0\.\.image_7/); assert.deepEqual( document.components.schemas.EditRequest.anyOf, Array.from({ length: 8 }, (_, index) => ({ required: [`image_${index}`] })) ); assert.equal('image_8' in document.components.schemas.EditRequest.properties, false); }); it('describes public capabilities without server-local SQLite paths', () => { const document = buildAgentOpenApiDocument({}); const storageSchema = document.components.schemas.AgentCapabilities.properties.storage; assert.ok(storageSchema.properties.image_storage_mode); assert.ok(storageSchema.properties.postgres_configured); assert.equal('sqlite_path' in storageSchema.properties, false); }); it('describes bearer authentication and common runtime failures in OpenAPI', () => { const document = buildAgentOpenApiDocument({ AGENT_API_TOKEN: 'token', APP_PASSWORD: 'page-access-code' }); assert.ok(document.components.securitySchemes.BearerAuth); assert.equal('AppPasswordHash' in document.components.securitySchemes, false); assert.deepEqual(document.components.schemas.AgentCapabilities.properties.auth.properties.schemes.const, [ 'bearer' ]); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.auth.properties .schemes.const, ['form-password-hash'] ); assert.equal( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.auth.properties .required.const, true ); assert.deepEqual(document.paths['/api/agent/images/generate'].post.security, [{ BearerAuth: [] }]); assert.ok(document.paths['/api/agent/images/generate'].post.responses['401']); assert.ok(document.paths['/api/agent/images/generate'].post.responses['415']); assert.ok(document.paths['/api/agent/images/generate'].post.responses['502']); assert.ok(document.paths['/api/agent/images/edit'].post.responses['401']); assert.ok(document.paths['/api/agent/images/edit'].post.responses['409'].headers['Retry-After']); assert.ok(document.paths['/api/agent/images/edit'].post.responses['415']); assert.ok(document.paths['/api/agent/images/edit'].post.responses['502']); }); it('describes access-code hash authentication in OpenAPI when no Agent token is configured', () => { const document = buildAgentOpenApiDocument({ APP_PASSWORD: 'page-access-code' }); assert.equal('BearerAuth' in document.components.securitySchemes, false); assert.ok(document.components.securitySchemes.AppPasswordHash); assert.deepEqual(document.components.schemas.AgentCapabilities.properties.auth.properties.schemes.const, [ 'x-app-password-hash' ]); assert.deepEqual(document.paths['/api/agent/images/generate'].post.security, [{ AppPasswordHash: [] }]); }); it('does not require Agent authentication in OpenAPI when no auth env is configured', () => { const document = buildAgentOpenApiDocument({}); assert.deepEqual(document.components.securitySchemes, {}); assert.deepEqual(document.components.schemas.AgentCapabilities.properties.auth.properties.schemes.const, []); assert.deepEqual( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.auth.properties .schemes.const, [] ); assert.equal( document.components.schemas.AgentStreamingCapabilities.properties.page_sse.properties.auth.properties .required.const, false ); assert.deepEqual(document.paths['/api/agent/images/generate'].post.security, []); }); it('marks artifact routes as authenticated in OpenAPI', () => { const document = buildAgentOpenApiDocument({ AGENT_API_TOKEN: 'token' }); const expectedSecurity = [{ BearerAuth: [] }]; assert.deepEqual(document.paths['/api/agent/artifacts/{id}'].get.security, expectedSecurity); assert.deepEqual(document.paths['/api/agent/artifacts/{id}'].delete.security, expectedSecurity); assert.deepEqual(document.paths['/api/agent/artifacts/{id}/content'].get.security, expectedSecurity); assert.deepEqual(document.paths['/api/agent/artifacts/{id}/share'].post.security, expectedSecurity); assert.deepEqual(document.paths['/api/agent/page-requests/{id}/feedback'].get.security, expectedSecurity); assert.deepEqual(document.paths['/api/agent/diagnostics/requests'].get.security, expectedSecurity); assert.deepEqual(document.paths['/api/agent/diagnostics/requests/{id}'].get.security, expectedSecurity); assert.deepEqual(document.paths['/api/agent/diagnostics/page-requests/{id}'].get.security, expectedSecurity); }); it('describes artifact metadata responses without server file paths', () => { const document = buildAgentOpenApiDocument({}); const responseSchema = document.components.schemas.ArtifactMetadataResponse; const schema = document.components.schemas.AgentArtifact; assert.equal(responseSchema.properties.artifact.$ref, '#/components/schemas/AgentArtifact'); assert.equal('AgentArtifactRecord' in document.components.schemas, false); assert.equal('filepath' in schema.properties, false); assert.ok('content_url' in schema.properties); assert.ok('metadata_url' in schema.properties); assert.ok('output_format' in schema.properties); assert.ok('mime_type' in schema.properties); assert.ok('size_bytes' in schema.properties); }); }); function restoreProcessEnv(snapshot: NodeJS.ProcessEnv): void { 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; } }