Spaces:
Running
Running
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, string> }; | |
| 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<string, unknown> = 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; | |
| } | |
| } | |